From e6a3cba68c15dd6235e8ac42882ec3ef2c85786f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 19 Dec 2025 10:13:12 +0100 Subject: [PATCH 001/930] Start development of Spring Boot 4.1.x --- gradle.properties | 2 +- platform/spring-boot-dependencies/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 5d6cfb719326..3c4835e585e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=4.0.2-SNAPSHOT +version=4.1.0-SNAPSHOT latestVersion=true spring.build-type=oss diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e78528e44274..370c2629ec90 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -23,7 +23,7 @@ description = "Spring Boot Dependencies" bom { upgrade { - policy = "same-minor-version" + policy = "same-major-version" gitHub { issueLabels = ["type: dependency-upgrade"] } From 458f7f6ac9517d9b14f79a7e8aa4bad7b210b3b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 19 Dec 2025 10:31:22 +0100 Subject: [PATCH 002/930] Polish "Start development of Spring Boot 4.1.x" --- .../jar-layered-custom/jar/src/layers.xml | 2 +- .../war-layered-custom/war/src/layers.xml | 2 +- .../src/main/xsd/layers-4.1.xsd | 100 ++++++++++++++++++ .../dependencies-layer-no-filter.xml | 2 +- .../src/test/resources/layers.xml | 2 +- .../resources/resource-layer-no-filter.xml | 2 +- 6 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 build-plugin/spring-boot-maven-plugin/src/main/xsd/layers-4.1.xsd diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml index a7a3dc82f48b..77bfa58e8d47 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/layers/layers-4.1.xsd"> **/application*.* diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml index a7a3dc82f48b..77bfa58e8d47 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml +++ b/build-plugin/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-4.1.xsd"> **/application*.* diff --git a/build-plugin/spring-boot-maven-plugin/src/main/xsd/layers-4.1.xsd b/build-plugin/spring-boot-maven-plugin/src/main/xsd/layers-4.1.xsd new file mode 100644 index 000000000000..20219b9bd8b1 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/main/xsd/layers-4.1.xsd @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build-plugin/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml b/build-plugin/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml index 32808c3122fa..abd81ae6adb8 100644 --- a/build-plugin/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml +++ b/build-plugin/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-4.1.xsd"> diff --git a/build-plugin/spring-boot-maven-plugin/src/test/resources/layers.xml b/build-plugin/spring-boot-maven-plugin/src/test/resources/layers.xml index b91c1f65d8e8..f4921f54f107 100644 --- a/build-plugin/spring-boot-maven-plugin/src/test/resources/layers.xml +++ b/build-plugin/spring-boot-maven-plugin/src/test/resources/layers.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-4.1.xsd"> META-INF/resources/** diff --git a/build-plugin/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml b/build-plugin/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml index 2669650cfc76..e256884d6f07 100644 --- a/build-plugin/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml +++ b/build-plugin/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-4.1.xsd"> From b2b0fed285d1fe82513df65f3baf79109d7edbad Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Sat, 15 Nov 2025 14:17:25 +0100 Subject: [PATCH 003/930] Decorate all Assert implementations with @CheckReturnValue See gh-46766 Signed-off-by: Stefano Cordio --- .../AbstractArchiveIntegrationTests.java | 3 ++ .../build/architecture/ArchitectureCheck.java | 3 ++ .../build/architecture/ArchitectureRules.java | 22 +++++++++++ .../architecture/ArchitectureCheckTests.java | 24 +++++++++++- .../WithCheckReturnValue.java | 39 +++++++++++++++++++ .../NoCheckReturnValue.java | 31 +++++++++++++++ config/checkstyle/import-control.xml | 3 ++ .../assertj/ApplicationContextAssert.java | 26 ++++++++----- .../boot/test/json/JsonContentAssert.java | 7 ++++ .../boot/test/json/ObjectContentAssert.java | 4 ++ .../assertions/ContainerConfigAssert.java | 6 +++ .../boot/image/assertions/ImageAssert.java | 2 + .../ScheduledExecutorServiceAssert.java | 2 + .../SimpleAsyncTaskExecutorAssert.java | 2 + 14 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/architecture/assertj/checkReturnValue/WithCheckReturnValue.java create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/architecture/assertj/noCheckReturnValue/NoCheckReturnValue.java diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java index 4b9e7657906d..0bfe5a0fbf48 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java @@ -38,6 +38,8 @@ import org.assertj.core.api.ListAssert; import org.jspecify.annotations.Nullable; +import org.springframework.lang.CheckReturnValue; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.contentOf; @@ -172,6 +174,7 @@ JarAssert doesNotHaveEntryWithNameStartingWith(String prefix) { return this; } + @CheckReturnValue ListAssert entryNamesInPath(String path) { List matches = new ArrayList<>(); withJarFile((jarFile) -> withEntries(jarFile, diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java index fa46cb9edf25..8eb6e91c6bdc 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java @@ -70,6 +70,7 @@ * @author Phillip Webb * @author Dmytro Nosan * @author Moritz Halbritter + * @author Stefano Cordio */ public abstract class ArchitectureCheck extends DefaultTask { @@ -93,6 +94,8 @@ public ArchitectureCheck() { getRules().addAll(whenMainSources( () -> ArchitectureRules.configurationPropertiesDeprecation(ArchitectureCheckAnnotation.classFor( getAnnotationClasses().get(), ArchitectureCheckAnnotation.DEPRECATED_CONFIGURATION_PROPERTY)))); + getRules().addAll(whenMainSources(() -> Collections.singletonList( + ArchitectureRules.allCustomAssertionMethodsNotReturningSelfShouldBeAnnotatedWithCheckReturnValue()))); getRules().addAll(and(getNullMarkedEnabled(), isMainSourceSet()).map(whenTrue(() -> Collections.singletonList( ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked(getNullMarkedIgnoredPackages().get()))))); getRuleDescriptions().set(getRules().map(this::asDescriptions)); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java index e41cbaf73c58..15e6221e7bd5 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java @@ -63,6 +63,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Role; +import org.springframework.lang.CheckReturnValue; import org.springframework.util.ResourceUtils; /** @@ -75,6 +76,7 @@ * @author Phillip Webb * @author Ngoc Nhan * @author Moritz Halbritter + * @author Stefano Cordio */ final class ArchitectureRules { @@ -158,6 +160,26 @@ private static ArchRule allBeanMethodsShouldNotHaveConditionalOnClassAnnotation( .allowEmptyShould(true); } + static ArchRule allCustomAssertionMethodsNotReturningSelfShouldBeAnnotatedWithCheckReturnValue() { + return ArchRuleDefinition.methods() + .that() + .areDeclaredInClassesThat() + .implement("org.assertj.core.api.Assert") + .and() + .arePublic() + .and() + .doNotHaveModifier(JavaModifier.BRIDGE) + .and(doNotReturnSelfType()) + .should() + .beAnnotatedWith(CheckReturnValue.class) + .allowEmptyShould(true); + } + + private static DescribedPredicate doNotReturnSelfType() { + return DescribedPredicate.describe("do not return self type", + (method) -> !method.getRawReturnType().equals(method.getOwner())); + } + private static ArchRule allPackagesShouldBeFreeOfTangles() { return SlicesRuleDefinition.slices() .matching("(**)") diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java index 7acae3a1693e..b0ab7be6dcbc 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java @@ -62,13 +62,18 @@ * @author Scott Frederick * @author Ivan Malutin * @author Dmytro Nosan + * @author Stefano Cordio */ class ArchitectureCheckTests { - private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.9"; + private static final String ASSERTJ_CORE = "org.assertj:assertj-core:3.27.4"; private static final String JUNIT_JUPITER = "org.junit.jupiter:junit-jupiter:5.12.0"; + private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.9"; + + private static final String SPRING_CORE = "org.springframework:spring-core:6.2.9"; + private static final String SPRING_INTEGRATION_JMX = "org.springframework.integration:spring-integration-jmx:6.5.1"; private GradleBuild gradleBuild; @@ -452,6 +457,23 @@ void whenDeprecatedConfigurationPropertyIsMissingSinceShouldFailAndWriteReport() "DeprecatedConfigurationPropertySince.getProperty"); } + @Test + void whenCustomAssertionMethodNotReturningSelfIsAnnotatedWithCheckReturnValueShouldSucceedAndWriteEmptyReport() + throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "assertj/checkReturnValue"); + build(this.gradleBuild.withDependencies(ASSERTJ_CORE, SPRING_CORE), Task.CHECK_ARCHITECTURE_MAIN); + } + + @Test + void whenCustomAssertionMethodNotReturningSelfIsNotAnnotatedWithCheckReturnValueShouldFailAndWriteReport() + throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "assertj/noCheckReturnValue"); + buildAndFail(this.gradleBuild.withDependencies(ASSERTJ_CORE), Task.CHECK_ARCHITECTURE_MAIN, + "methods that are declared in classes that implement org.assertj.core.api.Assert and " + + "are public and do not have modifier BRIDGE and do not return self type should be annotated " + + "with @CheckReturnValue"); + } + private void prepareTask(Task task, String... sourceDirectories) throws IOException { for (String sourceDirectory : sourceDirectories) { FileSystemUtils.copyRecursively( diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/assertj/checkReturnValue/WithCheckReturnValue.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/assertj/checkReturnValue/WithCheckReturnValue.java new file mode 100644 index 000000000000..74845f9dbd86 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/assertj/checkReturnValue/WithCheckReturnValue.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.assertj.checkReturnValue; + +import org.assertj.core.api.AbstractAssert; + +import org.springframework.lang.CheckReturnValue; + +public class WithCheckReturnValue extends AbstractAssert { + + WithCheckReturnValue() { + super(null, WithCheckReturnValue.class); + } + + @CheckReturnValue + public Object notReturningSelf() { + return new Object(); + } + + @Override + public WithCheckReturnValue isEqualTo(Object expected) { + return super.isEqualTo(expected); + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/assertj/noCheckReturnValue/NoCheckReturnValue.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/assertj/noCheckReturnValue/NoCheckReturnValue.java new file mode 100644 index 000000000000..70e4be97c7d3 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/assertj/noCheckReturnValue/NoCheckReturnValue.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.assertj.noCheckReturnValue; + +import org.assertj.core.api.AbstractAssert; + +public class NoCheckReturnValue extends AbstractAssert { + + NoCheckReturnValue() { + super(null, NoCheckReturnValue.class); + } + + public Object notReturningSelf() { + return new Object(); + } + +} diff --git a/config/checkstyle/import-control.xml b/config/checkstyle/import-control.xml index 2d591aaaa220..575b993240ee 100644 --- a/config/checkstyle/import-control.xml +++ b/config/checkstyle/import-control.xml @@ -6,6 +6,9 @@ + + + diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java index 6f5be5b15ae1..f39319f6d757 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java @@ -26,7 +26,6 @@ import org.assertj.core.api.AbstractObjectArrayAssert; import org.assertj.core.api.AbstractObjectAssert; import org.assertj.core.api.AbstractThrowableAssert; -import org.assertj.core.api.Assertions; import org.assertj.core.api.MapAssert; import org.assertj.core.error.BasicErrorMessageFactory; import org.jspecify.annotations.Nullable; @@ -37,6 +36,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.lang.CheckReturnValue; import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; @@ -224,13 +224,14 @@ public ApplicationContextAssert doesNotHaveBean(String name) { * @return array assertions for the bean names * @throws AssertionError if the application context did not start */ + @CheckReturnValue public AbstractObjectArrayAssert getBeanNames(Class type) { if (this.startupFailure != null) { throwAssertionError(contextFailedToStartWhenExpecting(this.startupFailure, "to get beans names with type:%n <%s>", type)); } - return Assertions.assertThat(getApplicationContext().getBeanNamesForType(type)) - .as("Bean names of type <%s> from <%s>", type, getApplicationContext()); + return assertThat(getApplicationContext().getBeanNamesForType(type)).as("Bean names of type <%s> from <%s>", + type, getApplicationContext()); } /** @@ -249,6 +250,7 @@ public AbstractObjectArrayAssert getBeanNames(Class type) { * @throws AssertionError if the application context contains multiple beans of the * given type */ + @CheckReturnValue public AbstractObjectAssert getBean(Class type) { return getBean(type, Scope.INCLUDE_ANCESTORS); } @@ -270,6 +272,7 @@ public AbstractObjectAssert getBean(Class type) { * @throws AssertionError if the application context contains multiple beans of the * given type */ + @CheckReturnValue public AbstractObjectAssert getBean(Class type, Scope scope) { Assert.notNull(scope, "'scope' must not be null"); if (this.startupFailure != null) { @@ -284,7 +287,7 @@ public AbstractObjectAssert getBean(Class type, Scope scope) { getApplicationContext(), type, names)); } T bean = (name != null) ? getApplicationContext().getBean(name, type) : null; - return Assertions.assertThat(bean).as("Bean of type <%s> from <%s>", type, getApplicationContext()); + return assertThat(bean).as("Bean of type <%s> from <%s>", type, getApplicationContext()); } private @Nullable String getPrimary(String[] names, Scope scope) { @@ -330,13 +333,14 @@ private boolean isPrimary(String name, Scope scope) { * is found * @throws AssertionError if the application context did not start */ + @CheckReturnValue public AbstractObjectAssert getBean(String name) { if (this.startupFailure != null) { throwAssertionError( contextFailedToStartWhenExpecting(this.startupFailure, "to contain a bean of name:%n <%s>", name)); } Object bean = findBean(name); - return Assertions.assertThat(bean).as("Bean of name <%s> from <%s>", name, getApplicationContext()); + return assertThat(bean).as("Bean of name <%s> from <%s>", name, getApplicationContext()); } /** @@ -357,6 +361,7 @@ public AbstractObjectAssert getBean(String name) { * name but a different type */ @SuppressWarnings("unchecked") + @CheckReturnValue public AbstractObjectAssert getBean(String name, Class type) { if (this.startupFailure != null) { throwAssertionError(contextFailedToStartWhenExpecting(this.startupFailure, @@ -368,8 +373,8 @@ public AbstractObjectAssert getBean(String name, Class type) { "%nExpecting:%n <%s>%nto contain a bean of name:%n <%s> (%s)%nbut found:%n <%s> of type <%s>", getApplicationContext(), name, type, bean, bean.getClass())); } - return Assertions.assertThat((T) bean) - .as("Bean of name <%s> and type <%s> from <%s>", name, type, getApplicationContext()); + return assertThat((T) bean).as("Bean of name <%s> and type <%s> from <%s>", name, type, + getApplicationContext()); } private @Nullable Object findBean(String name) { @@ -395,6 +400,7 @@ public AbstractObjectAssert getBean(String name, Class type) { * no beans are found * @throws AssertionError if the application context did not start */ + @CheckReturnValue public MapAssert getBeans(Class type) { return getBeans(type, Scope.INCLUDE_ANCESTORS); } @@ -414,14 +420,15 @@ public MapAssert getBeans(Class type) { * no beans are found * @throws AssertionError if the application context did not start */ + @CheckReturnValue public MapAssert getBeans(Class type, Scope scope) { Assert.notNull(scope, "'scope' must not be null"); if (this.startupFailure != null) { throwAssertionError( contextFailedToStartWhenExpecting(this.startupFailure, "to get beans of type:%n <%s>", type)); } - return Assertions.assertThat(scope.getBeansOfType(getApplicationContext(), type)) - .as("Beans of type <%s> from <%s>", type, getApplicationContext()); + return assertThat(scope.getBeansOfType(getApplicationContext(), type)).as("Beans of type <%s> from <%s>", type, + getApplicationContext()); } /** @@ -434,6 +441,7 @@ public MapAssert getBeans(Class type, Scope scope) { * @return assertions on the cause of the failure * @throws AssertionError if the application context started without a failure */ + @CheckReturnValue public AbstractThrowableAssert getFailure() { hasFailed(); return assertThat(this.startupFailure); diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JsonContentAssert.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JsonContentAssert.java index 93ce0da1ef17..262f33f7aa80 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JsonContentAssert.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JsonContentAssert.java @@ -40,6 +40,7 @@ import org.skyscreamer.jsonassert.comparator.JSONComparator; import org.springframework.core.io.Resource; +import org.springframework.lang.CheckReturnValue; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.util.function.ThrowingFunction; @@ -917,6 +918,7 @@ public JsonContentAssert doesNotHaveEmptyJsonPathValue(CharSequence expression, * @return a new assertion object whose object under test is the extracted item * @throws AssertionError if the path is not valid */ + @CheckReturnValue public AbstractObjectAssert extractingJsonPathValue(CharSequence expression, Object... args) { return Assertions.assertThat(new JsonPathValue(expression, args).getValue(false)); } @@ -929,6 +931,7 @@ public AbstractObjectAssert extractingJsonPathValue(CharSequence expr * @return a new assertion object whose object under test is the extracted item * @throws AssertionError if the path is not valid or does not result in a string */ + @CheckReturnValue public AbstractCharSequenceAssert extractingJsonPathStringValue(CharSequence expression, Object... args) { return Assertions.assertThat(extractingJsonPathValue(expression, args, String.class, "a string")); @@ -942,6 +945,7 @@ public AbstractCharSequenceAssert extractingJsonPathStringValue(CharS * @return a new assertion object whose object under test is the extracted item * @throws AssertionError if the path is not valid or does not result in a number */ + @CheckReturnValue public AbstractObjectAssert extractingJsonPathNumberValue(CharSequence expression, Object... args) { return Assertions.assertThat(extractingJsonPathValue(expression, args, Number.class, "a number")); } @@ -954,6 +958,7 @@ public AbstractObjectAssert extractingJsonPathNumberValue(CharSequenc * @return a new assertion object whose object under test is the extracted item * @throws AssertionError if the path is not valid or does not result in a boolean */ + @CheckReturnValue public AbstractBooleanAssert extractingJsonPathBooleanValue(CharSequence expression, Object... args) { return Assertions.assertThat(extractingJsonPathValue(expression, args, Boolean.class, "a boolean")); } @@ -968,6 +973,7 @@ public AbstractBooleanAssert extractingJsonPathBooleanValue(CharSequence expr * @throws AssertionError if the path is not valid or does not result in an array */ @SuppressWarnings("unchecked") + @CheckReturnValue public ListAssert extractingJsonPathArrayValue(CharSequence expression, Object... args) { return Assertions.assertThat(extractingJsonPathValue(expression, args, List.class, "an array")); } @@ -983,6 +989,7 @@ public ListAssert extractingJsonPathArrayValue(CharSequence expression, O * @throws AssertionError if the path is not valid or does not result in a map */ @SuppressWarnings("unchecked") + @CheckReturnValue public MapAssert extractingJsonPathMapValue(CharSequence expression, Object... args) { return Assertions.assertThat(extractingJsonPathValue(expression, args, Map.class, "a map")); } diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/ObjectContentAssert.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/ObjectContentAssert.java index d58612f85959..d406ee93c21c 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/ObjectContentAssert.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/ObjectContentAssert.java @@ -22,6 +22,8 @@ import org.assertj.core.api.Assert; import org.assertj.core.api.InstanceOfAssertFactories; +import org.springframework.lang.CheckReturnValue; + /** * AssertJ {@link Assert} for {@link ObjectContent}. * @@ -41,6 +43,7 @@ protected ObjectContentAssert(A actual) { * allow chaining of array-specific assertions from this call. * @return an array assertion object */ + @CheckReturnValue public AbstractObjectArrayAssert asArray() { return asInstanceOf(InstanceOfAssertFactories.ARRAY); } @@ -50,6 +53,7 @@ public AbstractObjectArrayAssert asArray() { * chaining of map-specific assertions from this call. * @return a map assertion object */ + @CheckReturnValue public AbstractMapAssert asMap() { return asInstanceOf(InstanceOfAssertFactories.MAP); } diff --git a/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ContainerConfigAssert.java b/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ContainerConfigAssert.java index dbf5dcea437a..1ec1cdc42d9c 100644 --- a/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ContainerConfigAssert.java +++ b/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ContainerConfigAssert.java @@ -32,6 +32,7 @@ import org.assertj.core.api.ObjectAssert; import org.springframework.boot.test.json.JsonContentAssert; +import org.springframework.lang.CheckReturnValue; /** * AssertJ {@link org.assertj.core.api.Assert} for Docker image container configuration. @@ -99,10 +100,12 @@ public static class BuildMetadataAssert extends AbstractAssert buildpacks() { return this.actual.extractingJsonPathArrayValue("$.buildpacks[*].id"); } + @CheckReturnValue public AbstractListAssert, String, ObjectAssert> processOfType(String type) { return this.actual.extractingJsonPathArrayValue("$.processes[?(@.type=='%s')]", type) .singleElement() @@ -132,14 +135,17 @@ public static class LifecycleMetadataAssert extends AbstractAssert buildpackLayers(String buildpackId) { return this.actual.extractingJsonPathArrayValue("$.buildpacks[?(@.key=='%s')].layers", buildpackId); } + @CheckReturnValue public AbstractListAssert, Object, ObjectAssert> appLayerShas() { return this.actual.extractingJsonPathArrayValue("$.app").extracting("sha"); } + @CheckReturnValue public AbstractObjectAssert sbomLayerSha() { return this.actual.extractingJsonPathValue("$.sbom.sha"); } diff --git a/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java b/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java index fbc242a647d6..78afc1764b92 100644 --- a/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java +++ b/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java @@ -34,6 +34,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.Layer; import org.springframework.boot.test.json.JsonContentAssert; +import org.springframework.lang.CheckReturnValue; import org.springframework.util.StreamUtils; /** @@ -73,6 +74,7 @@ public LayerContentAssert(Layer layer) { super(layer, LayerContentAssert.class); } + @CheckReturnValue public ListAssert entries() { List entryNames = new ArrayList<>(); try { diff --git a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/ScheduledExecutorServiceAssert.java b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/ScheduledExecutorServiceAssert.java index b9e5b075ef6d..65155facd61f 100644 --- a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/ScheduledExecutorServiceAssert.java +++ b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/ScheduledExecutorServiceAssert.java @@ -24,6 +24,7 @@ import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.Assert; +import org.springframework.lang.CheckReturnValue; import org.springframework.util.ReflectionUtils; /** @@ -85,6 +86,7 @@ private boolean producesVirtualThreads() { * @param actual the {@link ScheduledExecutorService} * @return the assertion instance */ + @CheckReturnValue public static ScheduledExecutorServiceAssert assertThat(ScheduledExecutorService actual) { return new ScheduledExecutorServiceAssert(actual); } diff --git a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssert.java b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssert.java index 14a5685aec72..5104d4388110 100644 --- a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssert.java +++ b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssert.java @@ -22,6 +22,7 @@ import org.assertj.core.api.Assert; import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.lang.CheckReturnValue; import org.springframework.util.ReflectionUtils; /** @@ -77,6 +78,7 @@ private boolean producesVirtualThreads() { * @param actual the {@link SimpleAsyncTaskExecutor} * @return the assertion instance */ + @CheckReturnValue public static SimpleAsyncTaskExecutorAssert assertThat(SimpleAsyncTaskExecutor actual) { return new SimpleAsyncTaskExecutorAssert(actual); } From dfb05ee4a933f0c405fc197449cdef13c3b4f23b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 19 Dec 2025 10:49:06 +0100 Subject: [PATCH 004/930] Polish "Decorate all Assert implementations with @CheckReturnValue" See gh-46766 --- .../boot/build/architecture/ArchitectureCheckTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java index b0ab7be6dcbc..07f77c99c5b2 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java @@ -70,9 +70,9 @@ class ArchitectureCheckTests { private static final String JUNIT_JUPITER = "org.junit.jupiter:junit-jupiter:5.12.0"; - private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.9"; + private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.15"; - private static final String SPRING_CORE = "org.springframework:spring-core:6.2.9"; + private static final String SPRING_CORE = "org.springframework:spring-core:6.2.15"; private static final String SPRING_INTEGRATION_JMX = "org.springframework.integration:spring-integration-jmx:6.5.1"; From b43cc315abea9214c655abbfdc66790385898222 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 19 Dec 2025 11:09:49 +0100 Subject: [PATCH 005/930] Fix version in forward merges --- git/hooks/prepare-forward-merge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/hooks/prepare-forward-merge b/git/hooks/prepare-forward-merge index fbdb1e19448f..9162bbaa81af 100755 --- a/git/hooks/prepare-forward-merge +++ b/git/hooks/prepare-forward-merge @@ -4,7 +4,7 @@ require 'net/http' require 'yaml' require 'logger' -$main_branch = "4.0.x" +$main_branch = "4.1.x" $log = Logger.new(STDOUT) $log.level = Logger::WARN From 98d843214b4b0e88a531181ae2cfdc5d7732ca85 Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Thu, 27 Feb 2025 15:23:04 +0100 Subject: [PATCH 006/930] Add support for configuring SimpleMessageListenerContainer This commit introduces a `SimpleJmsListenerContainerFactoryConfigurer` that can be used to conveniently apply settings provided by JMS auto-configuration to `SimpleJmsListenerContainerFactory` instances. See gh-47716 Signed-off-by: Vedran Pavic --- .../reference/pages/messaging/jms.adoc | 5 + ...JmsListenerContainerFactoryConfigurer.java | 131 ++++++++++++++++++ ...JmsListenerContainerFactoryConfigurer.java | 93 ++----------- .../JmsAnnotationDrivenConfiguration.java | 16 ++- ...JmsListenerContainerFactoryConfigurer.java | 37 +++++ .../JmsAutoConfigurationTests.java | 6 +- 6 files changed, 200 insertions(+), 88 deletions(-) create mode 100644 module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/AbstractJmsListenerContainerFactoryConfigurer.java create mode 100644 module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/SimpleJmsListenerContainerFactoryConfigurer.java diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/jms.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/jms.adoc index 2e64c1571087..7253d001b4b0 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/jms.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/jms.adoc @@ -191,3 +191,8 @@ NOTE: In the example above, the customization uses javadoc:org.springframework.b Then you can use the factory in any javadoc:org.springframework.jms.annotation.JmsListener[format=annotation]-annotated method as follows: include-code::custom/MyBean[] + +Analogous to `DefaultJmsListenerContainerFactoryConfigurer`, Spring Boot also provides a javadoc:org.springframework.boot.jms.autoconfigure.SimpleJmsListenerContainerFactoryConfigurer[] that you can use to initialize a javadoc:org.springframework.jms.config.SimpleJmsListenerContainerFactory[] and apply the related settings that auto-configuration provides. + +TIP: In contrast to javadoc:org.springframework.jms.listener.DefaultMessageListenerContainer[] that uses a pull-based mechanism (polling) to process messages, javadoc:org.springframework.jms.listener.SimpleMessageListenerContainer[] uses a push-based mechanism that's very close to the spirit of the standalone JMS specification. +To learn more about the differences between the two listener containers, consult their respective javadocs and {url-spring-framework-docs}/integration/jms/using.html#jms-mdp[Spring Framework reference documentation]. diff --git a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/AbstractJmsListenerContainerFactoryConfigurer.java b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/AbstractJmsListenerContainerFactoryConfigurer.java new file mode 100644 index 000000000000..d06fed4c7cb5 --- /dev/null +++ b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/AbstractJmsListenerContainerFactoryConfigurer.java @@ -0,0 +1,131 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jms.autoconfigure; + +import io.micrometer.observation.ObservationRegistry; +import jakarta.jms.ConnectionFactory; +import jakarta.jms.ExceptionListener; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.jms.autoconfigure.JmsProperties.Listener.Session; +import org.springframework.jms.config.AbstractJmsListenerContainerFactory; +import org.springframework.jms.support.converter.MessageConverter; +import org.springframework.jms.support.destination.DestinationResolver; +import org.springframework.util.Assert; + +/** + * Configures {@link AbstractJmsListenerContainerFactory} with sensible defaults. + * + * @param the connection factory type. + * @author Vedran Pavic + * @since 4.0.0 + */ +public abstract class AbstractJmsListenerContainerFactoryConfigurer> { + + private @Nullable DestinationResolver destinationResolver; + + private @Nullable MessageConverter messageConverter; + + private @Nullable ExceptionListener exceptionListener; + + private @Nullable ObservationRegistry observationRegistry; + + private @Nullable JmsProperties jmsProperties; + + /** + * Set the {@link DestinationResolver} to use or {@code null} if no destination + * resolver should be associated with the factory by default. + * @param destinationResolver the {@link DestinationResolver} + */ + void setDestinationResolver(@Nullable DestinationResolver destinationResolver) { + this.destinationResolver = destinationResolver; + } + + /** + * Set the {@link MessageConverter} to use or {@code null} if the out-of-the-box + * converter should be used. + * @param messageConverter the {@link MessageConverter} + */ + void setMessageConverter(@Nullable MessageConverter messageConverter) { + this.messageConverter = messageConverter; + } + + /** + * Set the {@link ExceptionListener} to use or {@code null} if no exception listener + * should be associated by default. + * @param exceptionListener the {@link ExceptionListener} + */ + void setExceptionListener(@Nullable ExceptionListener exceptionListener) { + this.exceptionListener = exceptionListener; + } + + /** + * Set the {@link JmsProperties} to use. + * @param jmsProperties the {@link JmsProperties} + */ + void setJmsProperties(@Nullable JmsProperties jmsProperties) { + this.jmsProperties = jmsProperties; + } + + /** + * Set the {@link ObservationRegistry} to use. + * @param observationRegistry the {@link ObservationRegistry} + */ + void setObservationRegistry(@Nullable ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + + /** + * Configure the specified jms listener container factory. The factory can be further + * tuned and default settings can be overridden. + * @param factory the {@link AbstractJmsListenerContainerFactory} instance to + * configure + * @param connectionFactory the {@link ConnectionFactory} to use + */ + public void configure(T factory, ConnectionFactory connectionFactory) { + Assert.notNull(factory, "'factory' must not be null"); + Assert.notNull(connectionFactory, "'connectionFactory' must not be null"); + Assert.state(this.jmsProperties != null, "'jmsProperties' must not be null"); + JmsProperties.Listener listenerProperties = this.jmsProperties.getListener(); + Session sessionProperties = listenerProperties.getSession(); + factory.setConnectionFactory(connectionFactory); + PropertyMapper map = PropertyMapper.get(); + 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.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(this.observationRegistry).to(factory::setObservationRegistry); + map.from(sessionProperties::getTransacted).to(factory::setSessionTransacted); + map.from(listenerProperties::isAutoStartup).to(factory::setAutoStartup); + configure(factory, connectionFactory, this.jmsProperties); + } + + /** + * Configures the given {@code factory} using the given {@code connectionFactory} and + * {@code jmsProperties}. + * @param factory the {@link AbstractJmsListenerContainerFactory} instance to + * configure + * @param connectionFactory the {@link ConnectionFactory} to use + * @param jmsProperties the {@link JmsProperties} to use + */ + protected abstract void configure(T factory, ConnectionFactory connectionFactory, JmsProperties jmsProperties); + +} diff --git a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/DefaultJmsListenerContainerFactoryConfigurer.java b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/DefaultJmsListenerContainerFactoryConfigurer.java index 27eaf8fddce8..7f9ec6e9e445 100644 --- a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/DefaultJmsListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/DefaultJmsListenerContainerFactoryConfigurer.java @@ -18,22 +18,17 @@ import java.time.Duration; -import io.micrometer.observation.ObservationRegistry; import jakarta.jms.ConnectionFactory; -import jakarta.jms.ExceptionListener; import org.jspecify.annotations.Nullable; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.jms.autoconfigure.JmsProperties.Listener.Session; import org.springframework.jms.config.DefaultJmsListenerContainerFactory; -import org.springframework.jms.support.converter.MessageConverter; -import org.springframework.jms.support.destination.DestinationResolver; import org.springframework.transaction.jta.JtaTransactionManager; -import org.springframework.util.Assert; /** - * Configure {@link DefaultJmsListenerContainerFactory} with sensible defaults tuned using - * configuration properties. + * Configures {@link DefaultJmsListenerContainerFactory} with sensible defaults tuned + * using configuration properties. *

* Can be injected into application code and used to define a custom * {@code DefaultJmsListenerContainerFactory} whose configuration is based upon that @@ -45,47 +40,11 @@ * @author Lasse Wulff * @since 4.0.0 */ -public final class DefaultJmsListenerContainerFactoryConfigurer { - - private @Nullable DestinationResolver destinationResolver; - - private @Nullable MessageConverter messageConverter; - - private @Nullable ExceptionListener exceptionListener; +public final class DefaultJmsListenerContainerFactoryConfigurer + extends AbstractJmsListenerContainerFactoryConfigurer { private @Nullable JtaTransactionManager transactionManager; - private @Nullable JmsProperties jmsProperties; - - private @Nullable ObservationRegistry observationRegistry; - - /** - * Set the {@link DestinationResolver} to use or {@code null} if no destination - * resolver should be associated with the factory by default. - * @param destinationResolver the {@link DestinationResolver} - */ - void setDestinationResolver(@Nullable DestinationResolver destinationResolver) { - this.destinationResolver = destinationResolver; - } - - /** - * Set the {@link MessageConverter} to use or {@code null} if the out-of-the-box - * converter should be used. - * @param messageConverter the {@link MessageConverter} - */ - void setMessageConverter(@Nullable MessageConverter messageConverter) { - this.messageConverter = messageConverter; - } - - /** - * Set the {@link ExceptionListener} to use or {@code null} if no exception listener - * should be associated by default. - * @param exceptionListener the {@link ExceptionListener} - */ - void setExceptionListener(@Nullable ExceptionListener exceptionListener) { - this.exceptionListener = exceptionListener; - } - /** * Set the {@link JtaTransactionManager} to use or {@code null} if the JTA support * should not be used. @@ -95,50 +54,16 @@ void setTransactionManager(@Nullable JtaTransactionManager transactionManager) { this.transactionManager = transactionManager; } - /** - * Set the {@link JmsProperties} to use. - * @param jmsProperties the {@link JmsProperties} - */ - void setJmsProperties(@Nullable JmsProperties jmsProperties) { - this.jmsProperties = jmsProperties; - } - - /** - * Set the {@link ObservationRegistry} to use. - * @param observationRegistry the {@link ObservationRegistry} - */ - void setObservationRegistry(@Nullable ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; - } - - /** - * Configure the specified jms listener container factory. The factory can be further - * tuned and default settings can be overridden. - * @param factory the {@link DefaultJmsListenerContainerFactory} instance to configure - * @param connectionFactory the {@link ConnectionFactory} to use - */ - public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFactory connectionFactory) { - Assert.notNull(factory, "'factory' must not be null"); - Assert.notNull(connectionFactory, "'connectionFactory' must not be null"); - Assert.state(this.jmsProperties != null, "'jmsProperties' must not be null"); - JmsProperties.Listener listenerProperties = this.jmsProperties.getListener(); - Session sessionProperties = listenerProperties.getSession(); - factory.setConnectionFactory(connectionFactory); + @Override + protected void configure(DefaultJmsListenerContainerFactory factory, ConnectionFactory connectionFactory, + JmsProperties jmsProperties) { PropertyMapper map = PropertyMapper.get(); - map.from(this.jmsProperties::isPubSubDomain).to(factory::setPubSubDomain); - map.from(this.jmsProperties::isSubscriptionDurable).to(factory::setSubscriptionDurable); - map.from(this.jmsProperties::getClientId).to(factory::setClientId); + JmsProperties.Listener listenerProperties = jmsProperties.getListener(); + Session sessionProperties = listenerProperties.getSession(); 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); } - 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); map.from(listenerProperties::getReceiveTimeout).as(Duration::toMillis).to(factory::setReceiveTimeout); map.from(listenerProperties::getMaxMessagesPerTask).to(factory::setMaxMessagesPerTask); diff --git a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/JmsAnnotationDrivenConfiguration.java b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/JmsAnnotationDrivenConfiguration.java index d0587784796a..540bb75a78de 100644 --- a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/JmsAnnotationDrivenConfiguration.java +++ b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/JmsAnnotationDrivenConfiguration.java @@ -42,6 +42,7 @@ * @author Phillip Webb * @author Stephane Nicoll * @author Eddú Meléndez + * @author Vedran Pavic */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(EnableJms.class) @@ -73,8 +74,7 @@ class JmsAnnotationDrivenConfiguration { @Bean @ConditionalOnMissingBean - @SuppressWarnings("removal") - DefaultJmsListenerContainerFactoryConfigurer jmsListenerContainerFactoryConfigurer() { + DefaultJmsListenerContainerFactoryConfigurer defaultJmsListenerContainerFactoryConfigurer() { DefaultJmsListenerContainerFactoryConfigurer configurer = new DefaultJmsListenerContainerFactoryConfigurer(); configurer.setDestinationResolver(this.destinationResolver.getIfUnique()); configurer.setTransactionManager(this.transactionManager.getIfUnique()); @@ -85,6 +85,18 @@ DefaultJmsListenerContainerFactoryConfigurer jmsListenerContainerFactoryConfigur return configurer; } + @Bean + @ConditionalOnMissingBean + SimpleJmsListenerContainerFactoryConfigurer simpleJmsListenerContainerFactoryConfigurer() { + SimpleJmsListenerContainerFactoryConfigurer configurer = new SimpleJmsListenerContainerFactoryConfigurer(); + configurer.setDestinationResolver(this.destinationResolver.getIfUnique()); + configurer.setMessageConverter(this.messageConverter.getIfUnique()); + configurer.setExceptionListener(this.exceptionListener.getIfUnique()); + configurer.setObservationRegistry(this.observationRegistry.getIfUnique()); + configurer.setJmsProperties(this.properties); + return configurer; + } + @Bean @ConditionalOnSingleCandidate(ConnectionFactory.class) @ConditionalOnMissingBean(name = "jmsListenerContainerFactory") diff --git a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/SimpleJmsListenerContainerFactoryConfigurer.java b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/SimpleJmsListenerContainerFactoryConfigurer.java new file mode 100644 index 000000000000..c13f6c05e14c --- /dev/null +++ b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/SimpleJmsListenerContainerFactoryConfigurer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jms.autoconfigure; + +import jakarta.jms.ConnectionFactory; + +import org.springframework.jms.config.SimpleJmsListenerContainerFactory; + +/** + * Configures {@link SimpleJmsListenerContainerFactory} with sensible defaults. + * + * @author Vedran Pavic + * @since 4.0.0 + */ +public final class SimpleJmsListenerContainerFactoryConfigurer + extends AbstractJmsListenerContainerFactoryConfigurer { + + @Override + protected void configure(SimpleJmsListenerContainerFactory factory, ConnectionFactory connectionFactory, + JmsProperties jmsProperties) { + } + +} diff --git a/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/JmsAutoConfigurationTests.java b/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/JmsAutoConfigurationTests.java index c089a7000f0c..a140e67252e0 100644 --- a/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/JmsAutoConfigurationTests.java +++ b/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/JmsAutoConfigurationTests.java @@ -75,6 +75,7 @@ void testNoConnectionFactoryJmsConfiguration() { .doesNotHaveBean(JmsMessagingTemplate.class) .doesNotHaveBean(JmsClient.class) .doesNotHaveBean(DefaultJmsListenerContainerFactoryConfigurer.class) + .doesNotHaveBean(SimpleJmsListenerContainerFactoryConfigurer.class) .doesNotHaveBean(DefaultJmsListenerContainerFactory.class)); } @@ -495,9 +496,10 @@ JmsMessagingTemplate jmsMessagingTemplate(JmsTemplate jmsTemplate) { static class TestConfiguration6 { @Bean - JmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory) { + JmsListenerContainerFactory jmsListenerContainerFactory( + SimpleJmsListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) { SimpleJmsListenerContainerFactory factory = new SimpleJmsListenerContainerFactory(); - factory.setConnectionFactory(connectionFactory); + configurer.configure(factory, connectionFactory); return factory; } From 30a56c2de9e4b1c941c731a8bae20fcbcba40094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 17 Nov 2025 13:47:52 +0100 Subject: [PATCH 007/930] Polish "Add support for configuring SimpleMessageListenerContainer" See gh-47716 --- ...JmsListenerContainerFactoryConfigurer.java | 46 +++++++++++-------- ...JmsListenerContainerFactoryConfigurer.java | 11 +++-- ...JmsListenerContainerFactoryConfigurer.java | 24 ++++++---- .../JmsAutoConfigurationTests.java | 43 ++++++++++++----- 4 files changed, 81 insertions(+), 43 deletions(-) diff --git a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/AbstractJmsListenerContainerFactoryConfigurer.java b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/AbstractJmsListenerContainerFactoryConfigurer.java index d06fed4c7cb5..b99fa99326cc 100644 --- a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/AbstractJmsListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/AbstractJmsListenerContainerFactoryConfigurer.java @@ -24,16 +24,28 @@ import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.jms.autoconfigure.JmsProperties.Listener.Session; import org.springframework.jms.config.AbstractJmsListenerContainerFactory; +import org.springframework.jms.config.JmsListenerContainerFactory; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.destination.DestinationResolver; import org.springframework.util.Assert; /** - * Configures {@link AbstractJmsListenerContainerFactory} with sensible defaults. + * Configure common {@link JmsListenerContainerFactory} settings with sensible defaults. + *

+ * This includes: + *

  • A {@link DestinationResolver} is such a component is present.
  • + *
  • A {@link MessageConverter} is such a component is present.
  • + *
  • An {@link ExceptionListener} is such a component is present.
  • + *
  • An {@link ObservationRegistry} is such a component is present.
  • + *
  • Configuration properties of the {@code spring.jms} namespace that are common to all + * implementations.
  • * * @param the connection factory type. + * @author Stephane Nicoll + * @author Eddú Meléndez * @author Vedran Pavic - * @since 4.0.0 + * @author Lasse Wulff + * @since 4.1.0 */ public abstract class AbstractJmsListenerContainerFactoryConfigurer> { @@ -90,6 +102,15 @@ void setObservationRegistry(@Nullable ObservationRegistry observationRegistry) { this.observationRegistry = observationRegistry; } + /** + * Return the {@link JmsProperties}. + * @return the jms properties + */ + protected JmsProperties getJmsProperties() { + Assert.state(this.jmsProperties != null, "'jmsProperties' must not be null"); + return this.jmsProperties; + } + /** * Configure the specified jms listener container factory. The factory can be further * tuned and default settings can be overridden. @@ -100,14 +121,14 @@ void setObservationRegistry(@Nullable ObservationRegistry observationRegistry) { public void configure(T factory, ConnectionFactory connectionFactory) { Assert.notNull(factory, "'factory' must not be null"); Assert.notNull(connectionFactory, "'connectionFactory' must not be null"); - Assert.state(this.jmsProperties != null, "'jmsProperties' must not be null"); - JmsProperties.Listener listenerProperties = this.jmsProperties.getListener(); + JmsProperties properties = getJmsProperties(); + JmsProperties.Listener listenerProperties = properties.getListener(); Session sessionProperties = listenerProperties.getSession(); factory.setConnectionFactory(connectionFactory); PropertyMapper map = PropertyMapper.get(); - 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(properties::isPubSubDomain).to(factory::setPubSubDomain); + map.from(properties::isSubscriptionDurable).to(factory::setSubscriptionDurable); + map.from(properties::getClientId).to(factory::setClientId); map.from(this.destinationResolver).to(factory::setDestinationResolver); map.from(this.messageConverter).to(factory::setMessageConverter); map.from(this.exceptionListener).to(factory::setExceptionListener); @@ -115,17 +136,6 @@ public void configure(T factory, ConnectionFactory connectionFactory) { map.from(this.observationRegistry).to(factory::setObservationRegistry); map.from(sessionProperties::getTransacted).to(factory::setSessionTransacted); map.from(listenerProperties::isAutoStartup).to(factory::setAutoStartup); - configure(factory, connectionFactory, this.jmsProperties); } - /** - * Configures the given {@code factory} using the given {@code connectionFactory} and - * {@code jmsProperties}. - * @param factory the {@link AbstractJmsListenerContainerFactory} instance to - * configure - * @param connectionFactory the {@link ConnectionFactory} to use - * @param jmsProperties the {@link JmsProperties} to use - */ - protected abstract void configure(T factory, ConnectionFactory connectionFactory, JmsProperties jmsProperties); - } diff --git a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/DefaultJmsListenerContainerFactoryConfigurer.java b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/DefaultJmsListenerContainerFactoryConfigurer.java index 7f9ec6e9e445..6eaf3104b623 100644 --- a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/DefaultJmsListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/DefaultJmsListenerContainerFactoryConfigurer.java @@ -27,8 +27,8 @@ import org.springframework.transaction.jta.JtaTransactionManager; /** - * Configures {@link DefaultJmsListenerContainerFactory} with sensible defaults tuned - * using configuration properties. + * Configure {@link DefaultJmsListenerContainerFactory} with sensible defaults tuned using + * configuration properties. *

    * Can be injected into application code and used to define a custom * {@code DefaultJmsListenerContainerFactory} whose configuration is based upon that @@ -39,6 +39,7 @@ * @author Vedran Pavic * @author Lasse Wulff * @since 4.0.0 + * @see SimpleJmsListenerContainerFactoryConfigurer */ public final class DefaultJmsListenerContainerFactoryConfigurer extends AbstractJmsListenerContainerFactoryConfigurer { @@ -55,10 +56,10 @@ void setTransactionManager(@Nullable JtaTransactionManager transactionManager) { } @Override - protected void configure(DefaultJmsListenerContainerFactory factory, ConnectionFactory connectionFactory, - JmsProperties jmsProperties) { + public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFactory connectionFactory) { + super.configure(factory, connectionFactory); PropertyMapper map = PropertyMapper.get(); - JmsProperties.Listener listenerProperties = jmsProperties.getListener(); + JmsProperties.Listener listenerProperties = getJmsProperties().getListener(); Session sessionProperties = listenerProperties.getSession(); map.from(this.transactionManager).to(factory::setTransactionManager); if (this.transactionManager == null && sessionProperties.getTransacted() == null) { diff --git a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/SimpleJmsListenerContainerFactoryConfigurer.java b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/SimpleJmsListenerContainerFactoryConfigurer.java index c13f6c05e14c..3558547d956c 100644 --- a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/SimpleJmsListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/SimpleJmsListenerContainerFactoryConfigurer.java @@ -16,22 +16,30 @@ package org.springframework.boot.jms.autoconfigure; -import jakarta.jms.ConnectionFactory; - import org.springframework.jms.config.SimpleJmsListenerContainerFactory; +import org.springframework.jms.listener.DefaultMessageListenerContainer; +import org.springframework.jms.listener.SimpleMessageListenerContainer; /** - * Configures {@link SimpleJmsListenerContainerFactory} with sensible defaults. + * Configure {@link SimpleJmsListenerContainerFactory} with sensible defaults. In contrast + * to {@link DefaultMessageListenerContainer} that uses a pull-based mechanism (polling) + * to process messages, the {@link SimpleMessageListenerContainer} instances created by + * this factory use a push-based mechanism that's very close to the spirit of the + * standalone JMS specification. + *

    + * As such, concurrency-related configuration properties from the {@code spring.jms} + * namespace are not taken into account by this implementation. + *

    + * Can be injected into application code and used to define a custom + * {@code SimpleJmsListenerContainerFactory} whose configuration is based upon that + * produced by auto-configuration. * * @author Vedran Pavic + * @author Stephane Nicoll * @since 4.0.0 + * @see DefaultJmsListenerContainerFactoryConfigurer */ public final class SimpleJmsListenerContainerFactoryConfigurer extends AbstractJmsListenerContainerFactoryConfigurer { - @Override - protected void configure(SimpleJmsListenerContainerFactory factory, ConnectionFactory connectionFactory, - JmsProperties jmsProperties) { - } - } diff --git a/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/JmsAutoConfigurationTests.java b/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/JmsAutoConfigurationTests.java index a140e67252e0..84b406d10b3f 100644 --- a/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/JmsAutoConfigurationTests.java +++ b/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/JmsAutoConfigurationTests.java @@ -45,6 +45,7 @@ import org.springframework.jms.core.JmsMessagingTemplate; import org.springframework.jms.core.JmsTemplate; import org.springframework.jms.listener.DefaultMessageListenerContainer; +import org.springframework.jms.listener.SimpleMessageListenerContainer; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.destination.DestinationResolver; import org.springframework.transaction.jta.JtaTransactionManager; @@ -168,20 +169,38 @@ void testJmsListenerContainerFactoryWithCustomSettings() { "spring.jms.listener.receiveTimeout=2s", "spring.jms.listener.maxConcurrency=10", "spring.jms.subscription-durable=true", "spring.jms.client-id=exampleId", "spring.jms.listener.max-messages-per-task=5") - .run(this::testJmsListenerContainerFactoryWithCustomSettings); + .run((context) -> { + DefaultMessageListenerContainer container = getContainer(context, "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); + assertThat(container).hasFieldOrPropertyWithValue("maxMessagesPerTask", 5); + assertThat(container.isSubscriptionDurable()).isTrue(); + assertThat(container.getClientId()).isEqualTo("exampleId"); + }); } - private void testJmsListenerContainerFactoryWithCustomSettings(AssertableApplicationContext loaded) { - 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); - assertThat(container).hasFieldOrPropertyWithValue("maxMessagesPerTask", 5); - assertThat(container.isSubscriptionDurable()).isTrue(); - assertThat(container.getClientId()).isEqualTo("exampleId"); + @Test + void testManualJmsListenerContainerFactoryWithCustomSettings() { + this.contextRunner.withUserConfiguration(TestConfiguration6.class, EnableJmsConfiguration.class) + .withPropertyValues("spring.jms.listener.autoStartup=false", + "spring.jms.listener.session.acknowledgeMode=client", + "spring.jms.listener.session.transacted=false", "spring.jms.subscription-durable=true", + "spring.jms.client-id=exampleId") + .run((context) -> { + SimpleJmsListenerContainerFactory containerFactory = context.getBean("jmsListenerContainerFactory", + SimpleJmsListenerContainerFactory.class); + SimpleMessageListenerContainer container = containerFactory + .createListenerContainer(mock(JmsListenerEndpoint.class)); + assertThat(container.isAutoStartup()).isFalse(); + assertThat(container.getSessionAcknowledgeMode()).isEqualTo(Session.CLIENT_ACKNOWLEDGE); + assertThat(container.isSessionTransacted()).isFalse(); + assertThat(container.isSubscriptionDurable()).isTrue(); + assertThat(container.getClientId()).isEqualTo("exampleId"); + }); } @Test From 81f7caad242efd433a2350c72241c5a43d36aae2 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Fri, 24 Oct 2025 15:51:58 +0200 Subject: [PATCH 008/930] Add failure analysis for invalid Docker environments This commit introduces a failure analyzer implementation that provides actionable guidance when Testcontainers fails to detect a running Docker-compatible environment. See gh-47797 Signed-off-by: Dmytro Nosan --- ...kerEnvironmentNotFoundFailureAnalyzer.java | 54 +++++++++++++++ .../diagnostics/package-info.java | 23 +++++++ .../main/resources/META-INF/spring.factories | 3 + ...vironmentNotFoundFailureAnalyzerTests.java | 69 +++++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzer.java create mode 100644 core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/package-info.java create mode 100644 core/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzerTests.java diff --git a/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzer.java b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzer.java new file mode 100644 index 000000000000..428ebfa77d05 --- /dev/null +++ b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzer.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.diagnostics; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.util.StringUtils; + +/** + * Failure analyzer for the Docker environment wasn't found. + * + * @author Dmytro Nosan + */ +class DockerEnvironmentNotFoundFailureAnalyzer extends AbstractFailureAnalyzer { + + private static final String EXPECTED_MESSAGE = "Could not find a valid Docker environment"; + + private static final String DESCRIPTION = "Could not find a valid Docker environment for Testcontainers."; + + private static final String ACTION = """ + - Ensure a Docker-compatible container engine is installed and running. + - If running Testcontainers in CI, ensure the runner has access to the daemon, typically by using a mounted socket or a Docker-in-Docker setup. + - Review the Testcontainers documentation for troubleshooting and advanced configuration options. + """; + + @Override + protected @Nullable FailureAnalysis analyze(Throwable rootFailure, IllegalStateException cause) { + if (isDockerEnvironmentNotFoundError(cause)) { + return new FailureAnalysis(DESCRIPTION, ACTION, cause); + } + return null; + } + + private boolean isDockerEnvironmentNotFoundError(IllegalStateException cause) { + return StringUtils.hasText(cause.getMessage()) && cause.getMessage().contains(EXPECTED_MESSAGE); + } + +} diff --git a/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/package-info.java b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/package-info.java new file mode 100644 index 000000000000..c74bce645a40 --- /dev/null +++ b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 for diagnosing issues with Testcontainers. + */ +@NullMarked +package org.springframework.boot.testcontainers.diagnostics; + +import org.jspecify.annotations.NullMarked; diff --git a/core/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories b/core/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories index 965d32bf8b61..fa136ad89588 100644 --- a/core/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories +++ b/core/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories @@ -6,3 +6,6 @@ org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleApplica org.springframework.test.context.ContextCustomizerFactory=\ org.springframework.boot.testcontainers.service.connection.ServiceConnectionContextCustomizerFactory +# Failure Analyzers +org.springframework.boot.diagnostics.FailureAnalyzer=\ +org.springframework.boot.testcontainers.diagnostics.DockerEnvironmentNotFoundFailureAnalyzer diff --git a/core/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzerTests.java b/core/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzerTests.java new file mode 100644 index 000000000000..f65d49f291e0 --- /dev/null +++ b/core/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzerTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.diagnostics; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.diagnostics.FailureAnalyzer; +import org.springframework.core.io.support.SpringFactoriesLoader; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerEnvironmentNotFoundFailureAnalyzer}. + * + * @author Dmytro Nosan + */ +class DockerEnvironmentNotFoundFailureAnalyzerTests { + + private final DockerEnvironmentNotFoundFailureAnalyzer analyzer = new DockerEnvironmentNotFoundFailureAnalyzer(); + + @Test + void shouldReturnFailureAnalysisWhenMessageMatches() { + IllegalStateException cause = new IllegalStateException( + "Could not find a valid Docker environment. Please see logs and check configuration"); + FailureAnalysis analysis = this.analyzer + .analyze(new RuntimeException("Root", new RuntimeException("Intermediate", cause))); + assertThat(analysis).isNotNull(); + assertThat(analysis.getDescription()) + .isEqualTo("Could not find a valid Docker environment for Testcontainers."); + assertThat(analysis.getAction()) + .contains("Ensure a Docker-compatible container engine is installed and running"); + assertThat(analysis.getCause()).isSameAs(cause); + } + + @Test + void shouldReturnNullWhenMessageDoesNotMatch() { + FailureAnalysis analysis = this.analyzer.analyze(new IllegalStateException("Some message")); + assertThat(analysis).isNull(); + } + + @Test + void shouldReturnNullWhenMessageIsNull() { + FailureAnalysis analysis = this.analyzer.analyze(new IllegalStateException()); + assertThat(analysis).isNull(); + } + + @Test + void shouldBeRegisteredInSpringFactories() { + assertThat(SpringFactoriesLoader.forDefaultResourceLocation(getClass().getClassLoader()) + .load(FailureAnalyzer.class, (factoryType, factoryImplementationName, failure) -> { + })).anyMatch(DockerEnvironmentNotFoundFailureAnalyzer.class::isInstance); + } + +} From 7c519ee56421f12e06e72163d35575ec6345e130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 17 Nov 2025 14:31:52 +0100 Subject: [PATCH 009/930] Polish "Add failure analysis for invalid Docker environments" See gh-47797 --- .../diagnostics/package-info.java | 23 ------------------- ...kerEnvironmentNotFoundFailureAnalyzer.java | 12 ++++------ .../main/resources/META-INF/spring.factories | 2 +- ...vironmentNotFoundFailureAnalyzerTests.java | 11 +-------- 4 files changed, 7 insertions(+), 41 deletions(-) delete mode 100644 core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/package-info.java rename core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/{diagnostics => lifecycle}/DockerEnvironmentNotFoundFailureAnalyzer.java (83%) rename core/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/{diagnostics => lifecycle}/DockerEnvironmentNotFoundFailureAnalyzerTests.java (80%) diff --git a/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/package-info.java b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/package-info.java deleted file mode 100644 index c74bce645a40..000000000000 --- a/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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 for diagnosing issues with Testcontainers. - */ -@NullMarked -package org.springframework.boot.testcontainers.diagnostics; - -import org.jspecify.annotations.NullMarked; diff --git a/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzer.java b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/DockerEnvironmentNotFoundFailureAnalyzer.java similarity index 83% rename from core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzer.java rename to core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/DockerEnvironmentNotFoundFailureAnalyzer.java index 428ebfa77d05..0b096d704841 100644 --- a/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzer.java +++ b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/DockerEnvironmentNotFoundFailureAnalyzer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.testcontainers.diagnostics; +package org.springframework.boot.testcontainers.lifecycle; import org.jspecify.annotations.Nullable; @@ -23,9 +23,11 @@ import org.springframework.util.StringUtils; /** - * Failure analyzer for the Docker environment wasn't found. + * A {@code FailureAnalyzer} that performs analysis of failures caused by Testcontainers + * not finding a valid Docker environment. * * @author Dmytro Nosan + * @author Stephane Nicoll */ class DockerEnvironmentNotFoundFailureAnalyzer extends AbstractFailureAnalyzer { @@ -41,14 +43,10 @@ class DockerEnvironmentNotFoundFailureAnalyzer extends AbstractFailureAnalyzer { - })).anyMatch(DockerEnvironmentNotFoundFailureAnalyzer.class::isInstance); - } - } From b8cefd2b9cf2c7364fa92ed682a4a8025cc879db Mon Sep 17 00:00:00 2001 From: Lansana DIOMANDE Date: Mon, 27 Oct 2025 07:51:53 +0100 Subject: [PATCH 010/930] Add support for configuring additional PersistenceUnitPostProcessor See gh-47802 Signed-off-by: Lansana DIOMANDE --- .../HibernateJpaAutoConfigurationTests.java | 32 +++++++++++++++++++ .../boot/jpa/EntityManagerFactoryBuilder.java | 21 ++++++++++++ 2 files changed, 53 insertions(+) diff --git a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java index bce4c30cde60..d1b259f60575 100644 --- a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java +++ b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java @@ -114,6 +114,7 @@ import org.springframework.orm.jpa.persistenceunit.ManagedClassNameFilter; import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; +import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter; import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; @@ -351,6 +352,19 @@ void customPersistenceUnitPostProcessors() { }); } + @Test + void shouldProcessAllPersistenceUnitPostProcessorsDeclaredAsBeans() { + this.contextRunner.withUserConfiguration(TestConfigurationWithMultipleCustomPersistenceUnitPostProcessors.class) + .run((context) -> { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = context + .getBean(LocalContainerEntityManagerFactoryBean.class); + PersistenceUnitInfo persistenceUnitInfo = entityManagerFactoryBean.getPersistenceUnitInfo(); + assertThat(persistenceUnitInfo).isNotNull(); + assertThat(persistenceUnitInfo.getManagedClassNames()).contains( + "customized.attribute.converter.class.name", "customized.attribute.converter.class.othername"); + }); + } + @Test void customManagedClassNameFilter() { this.contextRunner.withBean(ManagedClassNameFilter.class, () -> (s) -> !s.endsWith("City")) @@ -1168,6 +1182,24 @@ EntityManagerFactoryBuilderCustomizer entityManagerFactoryBuilderCustomizer() { } + @Configuration(proxyBeanMethods = false) + @TestAutoConfigurationPackage(HibernateJpaAutoConfigurationTests.class) + static class TestConfigurationWithMultipleCustomPersistenceUnitPostProcessors { + + @Bean + EntityManagerFactoryBuilderCustomizer entityManagerFactoryBuilderCustomizer() { + return (builder) -> builder.addPersistenceUnitPostProcessors( + (pui) -> pui.addManagedClassName("customized.attribute.converter.class.name")); + } + + @Bean + EntityManagerFactoryBuilderCustomizer otherEntityManagerFactoryBuilderCustomizer() { + return (builder) -> builder.addPersistenceUnitPostProcessors( + (pui) -> pui.addManagedClassName("customized.attribute.converter.class.othername")); + } + + } + static class CustomJpaTransactionManager extends JpaTransactionManager { } diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java index 91ea4f9993cb..405dbadcbfdd 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java @@ -17,9 +17,12 @@ package org.springframework.boot.jpa; import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; @@ -129,6 +132,24 @@ public void setPersistenceUnitPostProcessors(PersistenceUnitPostProcessor... per this.persistenceUnitPostProcessors = persistenceUnitPostProcessors; } + /** + * Add some {@linkplain PersistenceUnitPostProcessor persistence unit post processors} + * to be applied to the PersistenceUnitInfo used for creating the + * {@link LocalContainerEntityManagerFactoryBean}. + * @param persistenceUnitPostProcessors the persistence unit post processors to use + */ + public void addPersistenceUnitPostProcessors(PersistenceUnitPostProcessor... persistenceUnitPostProcessors) { + if (this.persistenceUnitPostProcessors == null) { + this.persistenceUnitPostProcessors = persistenceUnitPostProcessors; + return; + } + + var combined = new ArrayList<>(Arrays.asList(this.persistenceUnitPostProcessors)); + combined.addAll(Arrays.asList(persistenceUnitPostProcessors)); + + this.persistenceUnitPostProcessors = combined.toArray(PersistenceUnitPostProcessor[]::new); + } + /** * A fluent builder for a LocalContainerEntityManagerFactoryBean. */ From 9602be20aaace7395eefc974afd55e8b9aaad361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 17 Nov 2025 15:09:56 +0100 Subject: [PATCH 011/930] Polish contribution See gh-47802 --- .../HibernateJpaAutoConfigurationTests.java | 9 +- module/spring-boot-jpa/build.gradle | 1 - .../boot/jpa/EntityManagerFactoryBuilder.java | 27 +++-- .../jpa/EntityManagerFactoryBuilderTests.java | 104 ++++++++++++++++++ 4 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java diff --git a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java index d1b259f60575..d4970653db9b 100644 --- a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java +++ b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java @@ -114,7 +114,6 @@ import org.springframework.orm.jpa.persistenceunit.ManagedClassNameFilter; import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; -import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter; import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; @@ -353,7 +352,7 @@ void customPersistenceUnitPostProcessors() { } @Test - void shouldProcessAllPersistenceUnitPostProcessorsDeclaredAsBeans() { + void customPersistenceUnitProcessorsAddedByServeralContributors() { this.contextRunner.withUserConfiguration(TestConfigurationWithMultipleCustomPersistenceUnitPostProcessors.class) .run((context) -> { LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = context @@ -361,7 +360,7 @@ void shouldProcessAllPersistenceUnitPostProcessorsDeclaredAsBeans() { PersistenceUnitInfo persistenceUnitInfo = entityManagerFactoryBean.getPersistenceUnitInfo(); assertThat(persistenceUnitInfo).isNotNull(); assertThat(persistenceUnitInfo.getManagedClassNames()).contains( - "customized.attribute.converter.class.name", "customized.attribute.converter.class.othername"); + "customized.attribute.converter.class.name", "customized.attribute.converter.class.anotherName"); }); } @@ -1193,9 +1192,9 @@ EntityManagerFactoryBuilderCustomizer entityManagerFactoryBuilderCustomizer() { } @Bean - EntityManagerFactoryBuilderCustomizer otherEntityManagerFactoryBuilderCustomizer() { + EntityManagerFactoryBuilderCustomizer anotherEntityManagerFactoryBuilderCustomizer() { return (builder) -> builder.addPersistenceUnitPostProcessors( - (pui) -> pui.addManagedClassName("customized.attribute.converter.class.othername")); + (pui) -> pui.addManagedClassName("customized.attribute.converter.class.anotherName")); } } diff --git a/module/spring-boot-jpa/build.gradle b/module/spring-boot-jpa/build.gradle index 80e53b1ab8cb..c0641897c376 100644 --- a/module/spring-boot-jpa/build.gradle +++ b/module/spring-boot-jpa/build.gradle @@ -36,7 +36,6 @@ dependencies { testImplementation(project(":core:spring-boot-test")) testImplementation(project(":test-support:spring-boot-test-support")) testImplementation(testFixtures(project(":core:spring-boot-autoconfigure"))) - testImplementation("org.springframework:spring-context-support") } tasks.named("compileTestJava") { diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java index 405dbadcbfdd..c23760d5e42a 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java @@ -66,7 +66,7 @@ public class EntityManagerFactoryBuilder { private @Nullable AsyncTaskExecutor bootstrapExecutor; - private PersistenceUnitPostProcessor @Nullable [] persistenceUnitPostProcessors; + private @Nullable List persistenceUnitPostProcessors; /** * Create a new instance passing in the common pieces that will be shared if multiple @@ -129,25 +129,23 @@ public void setBootstrapExecutor(AsyncTaskExecutor bootstrapExecutor) { * @param persistenceUnitPostProcessors the persistence unit post processors to use */ public void setPersistenceUnitPostProcessors(PersistenceUnitPostProcessor... persistenceUnitPostProcessors) { - this.persistenceUnitPostProcessors = persistenceUnitPostProcessors; + this.persistenceUnitPostProcessors = new ArrayList<>(Arrays.asList(persistenceUnitPostProcessors)); } /** - * Add some {@linkplain PersistenceUnitPostProcessor persistence unit post processors} - * to be applied to the PersistenceUnitInfo used for creating the + * Add {@linkplain PersistenceUnitPostProcessor persistence unit post processors} to + * be applied to the PersistenceUnitInfo used for creating the * {@link LocalContainerEntityManagerFactoryBean}. - * @param persistenceUnitPostProcessors the persistence unit post processors to use + * @param persistenceUnitPostProcessors the persistence unit post processors to add + * @since 4.1.0 */ public void addPersistenceUnitPostProcessors(PersistenceUnitPostProcessor... persistenceUnitPostProcessors) { - if (this.persistenceUnitPostProcessors == null) { - this.persistenceUnitPostProcessors = persistenceUnitPostProcessors; - return; + if (this.persistenceUnitPostProcessors != null) { + this.persistenceUnitPostProcessors.addAll(Arrays.asList(persistenceUnitPostProcessors)); + } + else { + setPersistenceUnitPostProcessors(persistenceUnitPostProcessors); } - - var combined = new ArrayList<>(Arrays.asList(this.persistenceUnitPostProcessors)); - combined.addAll(Arrays.asList(persistenceUnitPostProcessors)); - - this.persistenceUnitPostProcessors = combined.toArray(PersistenceUnitPostProcessor[]::new); } /** @@ -301,7 +299,8 @@ else if (this.packagesToScan != null) { } if (EntityManagerFactoryBuilder.this.persistenceUnitPostProcessors != null) { entityManagerFactoryBean - .setPersistenceUnitPostProcessors(EntityManagerFactoryBuilder.this.persistenceUnitPostProcessors); + .setPersistenceUnitPostProcessors(EntityManagerFactoryBuilder.this.persistenceUnitPostProcessors + .toArray(PersistenceUnitPostProcessor[]::new)); } return entityManagerFactoryBean; } diff --git a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java new file mode 100644 index 000000000000..8e153dae603e --- /dev/null +++ b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jpa; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; + +import javax.sql.DataSource; + +import jakarta.persistence.spi.PersistenceProvider; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; +import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link EntityManagerFactoryBuilder}. + * + * @author Stephane Nicoll + */ +class EntityManagerFactoryBuilderTests { + + @Test + void setPersistenceUnitPostProcessorsWhenEmpty() { + EntityManagerFactoryBuilder builder = createEmptyBuilder(); + PersistenceUnitPostProcessor postProcessor = mock(); + PersistenceUnitPostProcessor postProcessor2 = mock(); + builder.setPersistenceUnitPostProcessors(postProcessor, postProcessor2); + assertThat(builder).extracting("persistenceUnitPostProcessors") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(postProcessor, postProcessor2); + } + + @Test + void addPersistenceUnitPostProcessorsWhenEmpty() { + EntityManagerFactoryBuilder builder = createEmptyBuilder(); + PersistenceUnitPostProcessor postProcessor = mock(); + PersistenceUnitPostProcessor postProcessor2 = mock(); + builder.addPersistenceUnitPostProcessors(postProcessor, postProcessor2); + assertThat(builder).extracting("persistenceUnitPostProcessors") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(postProcessor, postProcessor2); + } + + @Test + void setPersistenceUnitPostProcessorsWhenNotEmpty() { + EntityManagerFactoryBuilder builder = createEmptyBuilder(); + PersistenceUnitPostProcessor postProcessor = mock(); + builder.addPersistenceUnitPostProcessors(postProcessor); + PersistenceUnitPostProcessor postProcessor2 = mock(); + PersistenceUnitPostProcessor postProcessor3 = mock(); + builder.setPersistenceUnitPostProcessors(postProcessor2, postProcessor3); + assertThat(builder).extracting("persistenceUnitPostProcessors") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(postProcessor2, postProcessor3); + } + + @Test + void addPersistenceUnitPostProcessorsWhenNotEmpty() { + EntityManagerFactoryBuilder builder = createEmptyBuilder(); + PersistenceUnitPostProcessor postProcessor = mock(); + builder.addPersistenceUnitPostProcessors(postProcessor); + PersistenceUnitPostProcessor postProcessor2 = mock(); + builder.addPersistenceUnitPostProcessors(postProcessor2); + assertThat(builder).extracting("persistenceUnitPostProcessors") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(postProcessor, postProcessor2); + } + + private EntityManagerFactoryBuilder createEmptyBuilder() { + Function> jpaPropertiesFactory = (dataSource) -> Collections + .emptyMap(); + return new EntityManagerFactoryBuilder(new TestJpaVendorAdapter(), jpaPropertiesFactory, null); + } + + static class TestJpaVendorAdapter extends AbstractJpaVendorAdapter { + + @Override + public PersistenceProvider getPersistenceProvider() { + return mock(); + } + + } + +} From 8abff710093682efd40fc385e603d596d9445db5 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Tue, 4 Nov 2025 11:29:21 +0100 Subject: [PATCH 012/930] Associate MeterConventions for JVM and system metrics This commit auto-configures the following beans with their corresponding conventions, when available: - ProcessorMetrics with JvmCpuMeterConventions - JvmMemoryMetrics with JvmMemoryMeterConventions - JvmThreadMetrics with JvmThreadMeterConventions - ClassLoaderMetrics with JvmClassLoadingMeterConventions See gh-47935 Signed-off-by: Dmytro Nosan --- .../jvm/JvmMetricsAutoConfiguration.java | 24 ++++++++++---- .../SystemMetricsAutoConfiguration.java | 9 ++++-- .../jvm/JvmMetricsAutoConfigurationTests.java | 32 +++++++++++++++++++ .../SystemMetricsAutoConfigurationTests.java | 11 +++++++ 4 files changed, 68 insertions(+), 8 deletions(-) diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfiguration.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfiguration.java index 2b3f5c3224be..10e4e8b6ce13 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfiguration.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.boot.micrometer.metrics.autoconfigure.jvm; +import java.util.Collections; + import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.MeterBinder; import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics; @@ -25,12 +27,16 @@ import io.micrometer.core.instrument.binder.jvm.JvmInfoMetrics; import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; +import io.micrometer.core.instrument.binder.jvm.convention.JvmClassLoadingMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.JvmThreadMeterConventions; import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.beans.BeanUtils; +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; @@ -71,20 +77,26 @@ JvmHeapPressureMetrics jvmHeapPressureMetrics() { @Bean @ConditionalOnMissingBean - JvmMemoryMetrics jvmMemoryMetrics() { - return new JvmMemoryMetrics(); + JvmMemoryMetrics jvmMemoryMetrics(ObjectProvider jvmMemoryMeterConventions) { + JvmMemoryMeterConventions conventions = jvmMemoryMeterConventions.getIfAvailable(); + return (conventions != null) ? new JvmMemoryMetrics(Collections.emptyList(), conventions) + : new JvmMemoryMetrics(); } @Bean @ConditionalOnMissingBean - JvmThreadMetrics jvmThreadMetrics() { - return new JvmThreadMetrics(); + JvmThreadMetrics jvmThreadMetrics(ObjectProvider jvmThreadMeterConventions) { + JvmThreadMeterConventions conventions = jvmThreadMeterConventions.getIfAvailable(); + return (conventions != null) ? new JvmThreadMetrics(Collections.emptyList(), conventions) + : new JvmThreadMetrics(); } @Bean @ConditionalOnMissingBean - ClassLoaderMetrics classLoaderMetrics() { - return new ClassLoaderMetrics(); + ClassLoaderMetrics classLoaderMetrics( + ObjectProvider jvmClassLoadingMeterConventions) { + JvmClassLoadingMeterConventions conventions = jvmClassLoadingMeterConventions.getIfAvailable(); + return (conventions != null) ? new ClassLoaderMetrics(conventions) : new ClassLoaderMetrics(); } @Bean diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfiguration.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfiguration.java index faf02f23c145..ef3e1dab5ca7 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfiguration.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfiguration.java @@ -17,14 +17,17 @@ package org.springframework.boot.micrometer.metrics.autoconfigure.system; import java.io.File; +import java.util.Collections; import java.util.List; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.binder.jvm.convention.JvmCpuMeterConventions; import io.micrometer.core.instrument.binder.system.FileDescriptorMetrics; import io.micrometer.core.instrument.binder.system.ProcessorMetrics; import io.micrometer.core.instrument.binder.system.UptimeMetrics; +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; @@ -58,8 +61,10 @@ UptimeMetrics uptimeMetrics() { @Bean @ConditionalOnMissingBean - ProcessorMetrics processorMetrics() { - return new ProcessorMetrics(); + ProcessorMetrics processorMetrics(ObjectProvider jvmCpuMeterConventions) { + JvmCpuMeterConventions conventions = jvmCpuMeterConventions.getIfAvailable(); + return (conventions != null) ? new ProcessorMetrics(Collections.emptyList(), conventions) + : new ProcessorMetrics(); } @Bean diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfigurationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfigurationTests.java index a40865473902..a9ca0f518c65 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfigurationTests.java @@ -25,10 +25,14 @@ import io.micrometer.core.instrument.binder.jvm.JvmInfoMetrics; import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; +import io.micrometer.core.instrument.binder.jvm.convention.JvmClassLoadingMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.JvmThreadMeterConventions; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.JRE; +import org.mockito.Mockito; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; @@ -82,18 +86,46 @@ void allowsCustomJvmMemoryMetricsToBeUsed() { .run(assertMetricsBeans().andThen((context) -> assertThat(context).hasBean("customJvmMemoryMetrics"))); } + @Test + void allowCustomJvmMemoryMeterConventionsToBeUsed() { + JvmMemoryMeterConventions jvmMemoryMeterConventions = Mockito.mock(JvmMemoryMeterConventions.class); + this.contextRunner.withBean(JvmMemoryMeterConventions.class, () -> jvmMemoryMeterConventions) + .run((context) -> assertThat(context).hasSingleBean(JvmMemoryMetrics.class) + .getBean(JvmMemoryMetrics.class) + .hasFieldOrPropertyWithValue("conventions", jvmMemoryMeterConventions)); + } + @Test void allowsCustomJvmThreadMetricsToBeUsed() { this.contextRunner.withUserConfiguration(CustomJvmThreadMetricsConfiguration.class) .run(assertMetricsBeans().andThen((context) -> assertThat(context).hasBean("customJvmThreadMetrics"))); } + @Test + void allowCustomJvmThreadMeterConventionsToBeUsed() { + JvmThreadMeterConventions jvmThreadMeterConventions = Mockito.mock(JvmThreadMeterConventions.class); + this.contextRunner.withBean(JvmThreadMeterConventions.class, () -> jvmThreadMeterConventions) + .run((context) -> assertThat(context).hasSingleBean(JvmThreadMetrics.class) + .getBean(JvmThreadMetrics.class) + .hasFieldOrPropertyWithValue("conventions", jvmThreadMeterConventions)); + } + @Test void allowsCustomClassLoaderMetricsToBeUsed() { this.contextRunner.withUserConfiguration(CustomClassLoaderMetricsConfiguration.class) .run(assertMetricsBeans().andThen((context) -> assertThat(context).hasBean("customClassLoaderMetrics"))); } + @Test + void allowCustomJvmClassLoadingMeterConventionsToBeUsed() { + JvmClassLoadingMeterConventions jvmClassLoadingMeterConventions = Mockito + .mock(JvmClassLoadingMeterConventions.class); + this.contextRunner.withBean(JvmClassLoadingMeterConventions.class, () -> jvmClassLoadingMeterConventions) + .run((context) -> assertThat(context).hasSingleBean(ClassLoaderMetrics.class) + .getBean(ClassLoaderMetrics.class) + .hasFieldOrPropertyWithValue("conventions", jvmClassLoadingMeterConventions)); + } + @Test void allowsCustomJvmInfoMetricsToBeUsed() { this.contextRunner.withUserConfiguration(CustomJvmInfoMetricsConfiguration.class) diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfigurationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfigurationTests.java index eb077080f82d..39d1268c4f6c 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfigurationTests.java @@ -21,11 +21,13 @@ import java.util.Collections; import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.binder.jvm.convention.JvmCpuMeterConventions; import io.micrometer.core.instrument.binder.system.FileDescriptorMetrics; import io.micrometer.core.instrument.binder.system.ProcessorMetrics; import io.micrometer.core.instrument.binder.system.UptimeMetrics; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.micrometer.metrics.system.DiskSpaceMetricsBinder; @@ -71,6 +73,15 @@ void allowsCustomProcessorMetricsToBeUsed() { .hasBean("customProcessorMetrics")); } + @Test + void allowsCustomJvmCpuMeterConventionsToBeUsed() { + JvmCpuMeterConventions jvmCpuMeterConventions = Mockito.mock(JvmCpuMeterConventions.class); + this.contextRunner.withBean(JvmCpuMeterConventions.class, () -> jvmCpuMeterConventions) + .run((context) -> assertThat(context).hasSingleBean(ProcessorMetrics.class) + .getBean(ProcessorMetrics.class) + .hasFieldOrPropertyWithValue("conventions", jvmCpuMeterConventions)); + } + @Test void autoConfiguresFileDescriptorMetrics() { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(FileDescriptorMetrics.class)); From e3ae9100c670613c03ccdcc20c463fcd6ac84262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 17 Nov 2025 16:29:11 +0100 Subject: [PATCH 013/930] Polish "Associate MeterConventions for JVM and system metrics" See gh-47935 --- .../jvm/JvmMetricsAutoConfigurationTests.java | 9 ++++----- .../system/SystemMetricsAutoConfigurationTests.java | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfigurationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfigurationTests.java index a9ca0f518c65..ebd88951ef9c 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfigurationTests.java @@ -32,7 +32,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.JRE; -import org.mockito.Mockito; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; @@ -48,6 +47,7 @@ import org.springframework.util.ClassUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link JvmMetricsAutoConfiguration}. @@ -88,7 +88,7 @@ void allowsCustomJvmMemoryMetricsToBeUsed() { @Test void allowCustomJvmMemoryMeterConventionsToBeUsed() { - JvmMemoryMeterConventions jvmMemoryMeterConventions = Mockito.mock(JvmMemoryMeterConventions.class); + JvmMemoryMeterConventions jvmMemoryMeterConventions = mock(JvmMemoryMeterConventions.class); this.contextRunner.withBean(JvmMemoryMeterConventions.class, () -> jvmMemoryMeterConventions) .run((context) -> assertThat(context).hasSingleBean(JvmMemoryMetrics.class) .getBean(JvmMemoryMetrics.class) @@ -103,7 +103,7 @@ void allowsCustomJvmThreadMetricsToBeUsed() { @Test void allowCustomJvmThreadMeterConventionsToBeUsed() { - JvmThreadMeterConventions jvmThreadMeterConventions = Mockito.mock(JvmThreadMeterConventions.class); + JvmThreadMeterConventions jvmThreadMeterConventions = mock(JvmThreadMeterConventions.class); this.contextRunner.withBean(JvmThreadMeterConventions.class, () -> jvmThreadMeterConventions) .run((context) -> assertThat(context).hasSingleBean(JvmThreadMetrics.class) .getBean(JvmThreadMetrics.class) @@ -118,8 +118,7 @@ void allowsCustomClassLoaderMetricsToBeUsed() { @Test void allowCustomJvmClassLoadingMeterConventionsToBeUsed() { - JvmClassLoadingMeterConventions jvmClassLoadingMeterConventions = Mockito - .mock(JvmClassLoadingMeterConventions.class); + JvmClassLoadingMeterConventions jvmClassLoadingMeterConventions = mock(JvmClassLoadingMeterConventions.class); this.contextRunner.withBean(JvmClassLoadingMeterConventions.class, () -> jvmClassLoadingMeterConventions) .run((context) -> assertThat(context).hasSingleBean(ClassLoaderMetrics.class) .getBean(ClassLoaderMetrics.class) diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfigurationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfigurationTests.java index 39d1268c4f6c..58c876518bf5 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfigurationTests.java @@ -27,7 +27,6 @@ import io.micrometer.core.instrument.binder.system.UptimeMetrics; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.micrometer.metrics.system.DiskSpaceMetricsBinder; @@ -36,6 +35,7 @@ import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link SystemMetricsAutoConfiguration}. @@ -75,7 +75,7 @@ void allowsCustomProcessorMetricsToBeUsed() { @Test void allowsCustomJvmCpuMeterConventionsToBeUsed() { - JvmCpuMeterConventions jvmCpuMeterConventions = Mockito.mock(JvmCpuMeterConventions.class); + JvmCpuMeterConventions jvmCpuMeterConventions = mock(JvmCpuMeterConventions.class); this.contextRunner.withBean(JvmCpuMeterConventions.class, () -> jvmCpuMeterConventions) .run((context) -> assertThat(context).hasSingleBean(ProcessorMetrics.class) .getBean(ProcessorMetrics.class) From d0fed0ea6b0805c195bdca4ddac8b75e5401609b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 19 Dec 2025 11:54:24 +0100 Subject: [PATCH 014/930] Fix formatting See gh-47802 --- .../HibernateJpaAutoConfigurationTests.java | 3 ++- .../jpa/EntityManagerFactoryBuilderTests.java | 15 +++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java index d4970653db9b..f02af2785416 100644 --- a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java +++ b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java @@ -360,7 +360,8 @@ void customPersistenceUnitProcessorsAddedByServeralContributors() { PersistenceUnitInfo persistenceUnitInfo = entityManagerFactoryBean.getPersistenceUnitInfo(); assertThat(persistenceUnitInfo).isNotNull(); assertThat(persistenceUnitInfo.getManagedClassNames()).contains( - "customized.attribute.converter.class.name", "customized.attribute.converter.class.anotherName"); + "customized.attribute.converter.class.name", + "customized.attribute.converter.class.anotherName"); }); } diff --git a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java index 8e153dae603e..5925dd6bac43 100644 --- a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java +++ b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java @@ -57,8 +57,8 @@ void addPersistenceUnitPostProcessorsWhenEmpty() { PersistenceUnitPostProcessor postProcessor2 = mock(); builder.addPersistenceUnitPostProcessors(postProcessor, postProcessor2); assertThat(builder).extracting("persistenceUnitPostProcessors") - .asInstanceOf(InstanceOfAssertFactories.LIST) - .containsExactly(postProcessor, postProcessor2); + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(postProcessor, postProcessor2); } @Test @@ -70,8 +70,8 @@ void setPersistenceUnitPostProcessorsWhenNotEmpty() { PersistenceUnitPostProcessor postProcessor3 = mock(); builder.setPersistenceUnitPostProcessors(postProcessor2, postProcessor3); assertThat(builder).extracting("persistenceUnitPostProcessors") - .asInstanceOf(InstanceOfAssertFactories.LIST) - .containsExactly(postProcessor2, postProcessor3); + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(postProcessor2, postProcessor3); } @Test @@ -82,13 +82,12 @@ void addPersistenceUnitPostProcessorsWhenNotEmpty() { PersistenceUnitPostProcessor postProcessor2 = mock(); builder.addPersistenceUnitPostProcessors(postProcessor2); assertThat(builder).extracting("persistenceUnitPostProcessors") - .asInstanceOf(InstanceOfAssertFactories.LIST) - .containsExactly(postProcessor, postProcessor2); + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(postProcessor, postProcessor2); } private EntityManagerFactoryBuilder createEmptyBuilder() { - Function> jpaPropertiesFactory = (dataSource) -> Collections - .emptyMap(); + Function> jpaPropertiesFactory = (dataSource) -> Collections.emptyMap(); return new EntityManagerFactoryBuilder(new TestJpaVendorAdapter(), jpaPropertiesFactory, null); } From 0ac7068f9d1fcc12311198d366529081df4cb737 Mon Sep 17 00:00:00 2001 From: djlee Date: Sun, 30 Nov 2025 00:35:02 +0900 Subject: [PATCH 015/930] Use Charset instead of String for Mustache templates support The MustacheResourceTemplateLoader previously defined the template encoding as a String, defaulting to "UTF-8". This change replaces the field with a Charset and initializes it with StandardCharsets.UTF_8. Using Charset improves type safety and aligns with modern Spring Boot standards, while avoiding implicit charset lookup issues. See gh-48347 Signed-off-by: djlee --- .../autoconfigure/MustacheProperties.java | 6 ++++++ .../MustacheResourceTemplateLoader.java | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheProperties.java b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheProperties.java index 558d90b9e493..9ffe0d4a36de 100644 --- a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheProperties.java +++ b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheProperties.java @@ -124,6 +124,12 @@ public Charset getCharset() { return this.charset; } + /** + * Get the charset name. + * @return the charset name + * @deprecated since 4.1.0 in favor of {@link #getCharset()} + */ + @Deprecated(since = "4.1.0") public String getCharsetName() { return this.charset.name(); } diff --git a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheResourceTemplateLoader.java b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheResourceTemplateLoader.java index b388ff0b33c3..094b69c18f3e 100644 --- a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheResourceTemplateLoader.java +++ b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheResourceTemplateLoader.java @@ -18,6 +18,8 @@ import java.io.InputStreamReader; import java.io.Reader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Mustache.Compiler; @@ -45,7 +47,7 @@ public class MustacheResourceTemplateLoader implements TemplateLoader, ResourceL private String suffix = ""; - private String charSet = "UTF-8"; + private Charset charSet = StandardCharsets.UTF_8; private ResourceLoader resourceLoader = new DefaultResourceLoader(null); @@ -60,8 +62,19 @@ public MustacheResourceTemplateLoader(String prefix, String suffix) { /** * Set the charset. * @param charSet the charset + * @deprecated since 4.1.0 in favor of {@link #setCharset(Charset)} */ + @Deprecated(since = "4.1.0") public void setCharset(String charSet) { + this.charSet = Charset.forName(charSet); + } + + /** + * Set the charset. + * @param charSet the charset + * @since 4.1.0 + */ + public void setCharset(Charset charSet) { this.charSet = charSet; } From 46f653504b5cb01ad7a70b6624074d8f6ca4ad28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 1 Dec 2025 09:50:13 +0100 Subject: [PATCH 016/930] Polish "Use Charset instead of String for Mustache templates support" This replaces the use of charset name for our Mustache support. See gh-48347 --- .../MustacheAutoConfiguration.java | 2 +- .../autoconfigure/MustacheProperties.java | 6 ++--- .../MustacheReactiveWebConfiguration.java | 2 +- .../MustacheResourceTemplateLoader.java | 27 ++++++++++--------- .../MustacheServletWebConfiguration.java | 2 +- .../mustache/reactive/view/MustacheView.java | 20 +++++++++++--- .../reactive/view/MustacheViewResolver.java | 20 +++++++++++--- .../mustache/servlet/view/MustacheView.java | 21 ++++++++++++--- .../servlet/view/MustacheViewResolver.java | 20 +++++++++++--- .../MustacheAutoConfigurationTests.java | 7 ++--- .../reactive/view/MustacheViewTests.java | 2 +- 11 files changed, 92 insertions(+), 37 deletions(-) diff --git a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheAutoConfiguration.java b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheAutoConfiguration.java index 453810dff8e7..b007395cb517 100644 --- a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheAutoConfiguration.java +++ b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheAutoConfiguration.java @@ -78,7 +78,7 @@ Mustache.Compiler mustacheCompiler(TemplateLoader mustacheTemplateLoader) { MustacheResourceTemplateLoader mustacheTemplateLoader() { MustacheResourceTemplateLoader loader = new MustacheResourceTemplateLoader(this.mustache.getPrefix(), this.mustache.getSuffix()); - loader.setCharset(this.mustache.getCharsetName()); + loader.setCharset(this.mustache.getCharset()); return loader; } diff --git a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheProperties.java b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheProperties.java index 9ffe0d4a36de..c65b8f0f141f 100644 --- a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheProperties.java +++ b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheProperties.java @@ -125,11 +125,11 @@ public Charset getCharset() { } /** - * Get the charset name. + * Return name of the charset. * @return the charset name - * @deprecated since 4.1.0 in favor of {@link #getCharset()} + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of {@link #getCharset()} */ - @Deprecated(since = "4.1.0") + @Deprecated(since = "4.1.0", forRemoval = true) public String getCharsetName() { return this.charset.name(); } diff --git a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheReactiveWebConfiguration.java b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheReactiveWebConfiguration.java index d40e59f0f15f..31ea690d5a48 100644 --- a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheReactiveWebConfiguration.java +++ b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheReactiveWebConfiguration.java @@ -40,7 +40,7 @@ MustacheViewResolver mustacheViewResolver(Compiler mustacheCompiler, MustachePro map.from(mustache::getSuffix).to(resolver::setSuffix); map.from(mustache::getViewNames).to(resolver::setViewNames); map.from(mustache::getRequestContextAttribute).to(resolver::setRequestContextAttribute); - map.from(mustache::getCharsetName).to(resolver::setCharset); + map.from(mustache::getCharset).to(resolver::setCharset); map.from(mustache.getReactive()::getMediaTypes).to(resolver::setSupportedMediaTypes); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return resolver; diff --git a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheResourceTemplateLoader.java b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheResourceTemplateLoader.java index 094b69c18f3e..7a1be36ea2dc 100644 --- a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheResourceTemplateLoader.java +++ b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheResourceTemplateLoader.java @@ -47,7 +47,7 @@ public class MustacheResourceTemplateLoader implements TemplateLoader, ResourceL private String suffix = ""; - private Charset charSet = StandardCharsets.UTF_8; + private Charset charset = StandardCharsets.UTF_8; private ResourceLoader resourceLoader = new DefaultResourceLoader(null); @@ -60,22 +60,23 @@ public MustacheResourceTemplateLoader(String prefix, String suffix) { } /** - * Set the charset. - * @param charSet the charset - * @deprecated since 4.1.0 in favor of {@link #setCharset(Charset)} + * Set the {@link Charset} to use. + * @param charset the charset + * @since 4.1.0 */ - @Deprecated(since = "4.1.0") - public void setCharset(String charSet) { - this.charSet = Charset.forName(charSet); + public void setCharset(Charset charset) { + this.charset = charset; } /** - * Set the charset. - * @param charSet the charset - * @since 4.1.0 + * Set the name of the charset to use. + * @param charset the charset + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link #setCharset(Charset)} */ - public void setCharset(Charset charSet) { - this.charSet = charSet; + @Deprecated(since = "4.1.0", forRemoval = true) + public void setCharset(String charset) { + this.charset = Charset.forName(charset); } /** @@ -90,7 +91,7 @@ public void setResourceLoader(ResourceLoader resourceLoader) { @Override public Reader getTemplate(String name) throws Exception { return new InputStreamReader(this.resourceLoader.getResource(this.prefix + name + this.suffix).getInputStream(), - this.charSet); + this.charset); } } diff --git a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheServletWebConfiguration.java b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheServletWebConfiguration.java index a327223c0eb7..1910d0780898 100644 --- a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheServletWebConfiguration.java +++ b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/autoconfigure/MustacheServletWebConfiguration.java @@ -49,7 +49,7 @@ MustacheViewResolver mustacheViewResolver(Compiler mustacheCompiler, MustachePro resolver.setExposeSessionAttributes(mustache.getServlet().isExposeSessionAttributes()); resolver.setExposeSpringMacroHelpers(mustache.getServlet().isExposeSpringMacroHelpers()); resolver.setRequestContextAttribute(mustache.getRequestContextAttribute()); - resolver.setCharset(mustache.getCharsetName()); + resolver.setCharset(mustache.getCharset()); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return resolver; } diff --git a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/reactive/view/MustacheView.java b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/reactive/view/MustacheView.java index d200249319a5..3be5191c4e97 100644 --- a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/reactive/view/MustacheView.java +++ b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/reactive/view/MustacheView.java @@ -53,7 +53,7 @@ public class MustacheView extends AbstractUrlBasedView { private @Nullable Compiler compiler; - private @Nullable String charset; + private @Nullable Charset charset; /** * Set the JMustache compiler to be used by this view. Typically this property is not @@ -66,13 +66,25 @@ public void setCompiler(Compiler compiler) { } /** - * Set the charset used for reading Mustache template files. - * @param charset the charset to use for reading template files + * Set the {@link Charset} used for reading Mustache template files. + * @param charset the charset + * @since 4.1.0 */ - public void setCharset(@Nullable String charset) { + public void setCharset(@Nullable Charset charset) { this.charset = charset; } + /** + * Set the name of the charset used for reading Mustache template files. + * @param charset the charset + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link #setCharset(Charset)} + */ + @Deprecated(since = "4.1.0", forRemoval = true) + public void setCharset(@Nullable String charset) { + setCharset((charset != null) ? Charset.forName(charset) : null); + } + @Override public boolean checkResourceExists(Locale locale) throws Exception { return resolveResource() != null; diff --git a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/reactive/view/MustacheViewResolver.java b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/reactive/view/MustacheViewResolver.java index 40188df7eafa..0c870d977b69 100644 --- a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/reactive/view/MustacheViewResolver.java +++ b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/reactive/view/MustacheViewResolver.java @@ -16,6 +16,8 @@ package org.springframework.boot.mustache.reactive.view; +import java.nio.charset.Charset; + import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Mustache.Compiler; import org.jspecify.annotations.Nullable; @@ -35,7 +37,7 @@ public class MustacheViewResolver extends UrlBasedViewResolver { private final Compiler compiler; - private @Nullable String charset; + private @Nullable Charset charset; /** * Create a {@code MustacheViewResolver} backed by a default instance of a @@ -57,13 +59,25 @@ public MustacheViewResolver(Compiler compiler) { } /** - * Set the charset. + * Set the {@link Charset} to use. * @param charset the charset + * @since 4.1.0 */ - public void setCharset(String charset) { + public void setCharset(@Nullable Charset charset) { this.charset = charset; } + /** + * Set the name of the charset to use. + * @param charset the charset + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link #setCharset(Charset)} + */ + @Deprecated(since = "4.1.0", forRemoval = true) + public void setCharset(@Nullable String charset) { + setCharset((charset != null) ? Charset.forName(charset) : null); + } + @Override protected Class requiredViewClass() { return MustacheView.class; diff --git a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/servlet/view/MustacheView.java b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/servlet/view/MustacheView.java index 57b7e76a5286..2378e404f797 100644 --- a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/servlet/view/MustacheView.java +++ b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/servlet/view/MustacheView.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; +import java.nio.charset.Charset; import java.util.Locale; import java.util.Map; @@ -46,7 +47,7 @@ public class MustacheView extends AbstractTemplateView { private @Nullable Compiler compiler; - private @Nullable String charset; + private @Nullable Charset charset; /** * Set the Mustache compiler to be used by this view. @@ -61,13 +62,25 @@ public void setCompiler(Compiler compiler) { } /** - * Set the charset used for reading Mustache template files. - * @param charset the charset to use for reading template files + * Set the {@link Charset} used for reading Mustache template files. + * @param charset the charset + * @since 4.1.0 */ - public void setCharset(@Nullable String charset) { + public void setCharset(@Nullable Charset charset) { this.charset = charset; } + /** + * Set the name of the charset used for reading Mustache template files. + * @param charset the charset + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link #setCharset(Charset)} + */ + @Deprecated(since = "4.1.0", forRemoval = true) + public void setCharset(@Nullable String charset) { + setCharset((charset != null) ? Charset.forName(charset) : null); + } + @Override public boolean checkResource(Locale locale) throws Exception { Resource resource = getResource(); diff --git a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/servlet/view/MustacheViewResolver.java b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/servlet/view/MustacheViewResolver.java index a856adff0c8d..b0ea99843f74 100644 --- a/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/servlet/view/MustacheViewResolver.java +++ b/module/spring-boot-mustache/src/main/java/org/springframework/boot/mustache/servlet/view/MustacheViewResolver.java @@ -16,6 +16,8 @@ package org.springframework.boot.mustache.servlet.view; +import java.nio.charset.Charset; + import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Mustache.Compiler; import org.jspecify.annotations.Nullable; @@ -34,7 +36,7 @@ public class MustacheViewResolver extends AbstractTemplateViewResolver { private final Mustache.Compiler compiler; - private @Nullable String charset; + private @Nullable Charset charset; /** * Create a {@code MustacheViewResolver} backed by a default instance of a @@ -61,13 +63,25 @@ protected Class requiredViewClass() { } /** - * Set the charset. + * Set the {@link Charset} to use. * @param charset the charset + * @since 4.1.0 */ - public void setCharset(@Nullable String charset) { + public void setCharset(@Nullable Charset charset) { this.charset = charset; } + /** + * Set the name of the charset to use. + * @param charset the charset + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link #setCharset(Charset)} + */ + @Deprecated(since = "4.1.0", forRemoval = true) + public void setCharset(@Nullable String charset) { + setCharset((charset != null) ? Charset.forName(charset) : null); + } + @Override protected AbstractUrlBasedView buildView(String viewName) throws Exception { MustacheView view = (MustacheView) super.buildView(viewName); diff --git a/module/spring-boot-mustache/src/test/java/org/springframework/boot/mustache/autoconfigure/MustacheAutoConfigurationTests.java b/module/spring-boot-mustache/src/test/java/org/springframework/boot/mustache/autoconfigure/MustacheAutoConfigurationTests.java index 4d0a9e7d87f8..2c95a4048c6e 100644 --- a/module/spring-boot-mustache/src/test/java/org/springframework/boot/mustache/autoconfigure/MustacheAutoConfigurationTests.java +++ b/module/spring-boot-mustache/src/test/java/org/springframework/boot/mustache/autoconfigure/MustacheAutoConfigurationTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.mustache.autoconfigure; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.function.Supplier; @@ -96,7 +97,7 @@ void defaultServletViewResolverConfiguration() { assertThat(viewResolver).extracting("allowRequestOverride", InstanceOfAssertFactories.BOOLEAN).isFalse(); assertThat(viewResolver).extracting("allowSessionOverride", InstanceOfAssertFactories.BOOLEAN).isFalse(); assertThat(viewResolver).extracting("cache", InstanceOfAssertFactories.BOOLEAN).isFalse(); - assertThat(viewResolver).extracting("charset").isEqualTo("UTF-8"); + assertThat(viewResolver).extracting("charset").isEqualTo(StandardCharsets.UTF_8); assertThat(viewResolver).extracting("contentType").isEqualTo("text/html;charset=UTF-8"); assertThat(viewResolver).extracting("exposeRequestAttributes", InstanceOfAssertFactories.BOOLEAN).isFalse(); assertThat(viewResolver).extracting("exposeSessionAttributes", InstanceOfAssertFactories.BOOLEAN).isFalse(); @@ -112,7 +113,7 @@ void defaultReactiveViewResolverConfiguration() { configure(new ReactiveWebApplicationContextRunner()).run((context) -> { org.springframework.boot.mustache.reactive.view.MustacheViewResolver viewResolver = context .getBean(org.springframework.boot.mustache.reactive.view.MustacheViewResolver.class); - assertThat(viewResolver).extracting("charset").isEqualTo("UTF-8"); + assertThat(viewResolver).extracting("charset").isEqualTo(StandardCharsets.UTF_8); assertThat(viewResolver).extracting("prefix").isEqualTo("classpath:/templates/"); assertThat(viewResolver).extracting("requestContextAttribute").isNull(); assertThat(viewResolver).extracting("suffix").isEqualTo(".mustache"); @@ -141,7 +142,7 @@ void cacheCanBeCustomizedOnServletViewResolver() { @ParameterizedTest @EnumSource void charsetCanBeCustomizedOnViewResolver(ViewResolverKind kind) { - assertViewResolverProperty(kind, "spring.mustache.charset=UTF-16", "charset", "UTF-16"); + assertViewResolverProperty(kind, "spring.mustache.charset=UTF-16", "charset", StandardCharsets.UTF_16); if (kind == ViewResolverKind.SERVLET) { assertViewResolverProperty(kind, "spring.mustache.charset=UTF-16", "contentType", "text/html;charset=UTF-16"); diff --git a/module/spring-boot-mustache/src/test/java/org/springframework/boot/mustache/reactive/view/MustacheViewTests.java b/module/spring-boot-mustache/src/test/java/org/springframework/boot/mustache/reactive/view/MustacheViewTests.java index f45679f560d7..f51d92584f09 100644 --- a/module/spring-boot-mustache/src/test/java/org/springframework/boot/mustache/reactive/view/MustacheViewTests.java +++ b/module/spring-boot-mustache/src/test/java/org/springframework/boot/mustache/reactive/view/MustacheViewTests.java @@ -47,7 +47,7 @@ void viewResolvesHandlebars() { MustacheView view = new MustacheView(); view.setCompiler(Mustache.compiler()); view.setUrl("classpath:template.html"); - view.setCharset(StandardCharsets.UTF_8.displayName()); + view.setCharset(StandardCharsets.UTF_8); view.setApplicationContext(this.context); view.render(Collections.singletonMap("World", "Spring"), MediaType.TEXT_HTML, exchange) .block(Duration.ofSeconds(30)); From 0934edf5563f0af75debc0234ef64ada7ee5a1d9 Mon Sep 17 00:00:00 2001 From: CatiaCorreia Date: Thu, 27 Nov 2025 16:13:26 +0000 Subject: [PATCH 017/930] Add LDAPS support to embedded LDAP server Signed-off-by: CatiaCorreia See gh-48315 --- .../EmbeddedLdapAutoConfiguration.java | 107 ++++++++++- .../embedded/EmbeddedLdapProperties.java | 180 ++++++++++++++++++ .../EmbeddedLdapAutoConfigurationTests.java | 98 +++++++++- .../boot/ldap/autoconfigure/embedded/test.jks | Bin 0 -> 1294 bytes 4 files changed, 380 insertions(+), 5 deletions(-) create mode 100644 module/spring-boot-ldap/src/test/resources/org/springframework/boot/ldap/autoconfigure/embedded/test.jks diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java index 823ab1dd3e59..982cce9726c3 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java @@ -16,12 +16,28 @@ package org.springframework.boot.ldap.autoconfigure.embedded; +import java.io.IOException; import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; @@ -33,6 +49,7 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionMessage; @@ -47,6 +64,8 @@ import org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration; import org.springframework.boot.ldap.autoconfigure.LdapProperties; import org.springframework.boot.ldap.autoconfigure.embedded.EmbeddedLdapAutoConfiguration.EmbeddedLdapAutoConfigurationRuntimeHints; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @@ -60,9 +79,12 @@ import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -84,6 +106,8 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean { private final EmbeddedLdapProperties embeddedProperties; + private final ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver(); + private @Nullable InMemoryDirectoryServer server; EmbeddedLdapAutoConfiguration(EmbeddedLdapProperties embeddedProperties) { @@ -91,7 +115,9 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean { } @Bean - InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext) throws LDAPException { + InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext, + ObjectProvider sslBundles) throws LDAPException, KeyStoreException, IOException, + NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException { String[] baseDn = StringUtils.toStringArray(this.embeddedProperties.getBaseDn()); InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDn); String username = this.embeddedProperties.getCredential().getUsername(); @@ -100,9 +126,18 @@ InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext) t config.addAdditionalBindCredentials(username, password); } setSchema(config); - InMemoryListenerConfig listenerConfig = InMemoryListenerConfig.createLDAPConfig("LDAP", - this.embeddedProperties.getPort()); - config.setListenerConfigs(listenerConfig); + if (this.embeddedProperties.getSsl().isEnabled()) { + EmbeddedLdapProperties.Ssl ssl = this.embeddedProperties.getSsl(); + SSLContext sslContext = getSslContext(ssl, sslBundles.getIfAvailable()); + SSLServerSocketFactory serverSocketFactory = sslContext.getServerSocketFactory(); + SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory(); + config.setListenerConfigs(InMemoryListenerConfig.createLDAPSConfig("LDAPS", null, + this.embeddedProperties.getPort(), serverSocketFactory, clientSocketFactory)); + } + else { + config + .setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("LDAP", this.embeddedProperties.getPort())); + } this.server = new InMemoryDirectoryServer(config); importLdif(this.server, applicationContext); this.server.startListening(); @@ -181,6 +216,70 @@ public void destroy() throws Exception { } } + private SSLContext getSslContext(EmbeddedLdapProperties.Ssl ssl, @Nullable SslBundles sslBundles) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, + UnrecoverableKeyException, KeyManagementException { + if (sslBundles != null && StringUtils.hasText(ssl.getBundle())) { + SslBundle sslBundle = sslBundles.getBundle(ssl.getBundle()); + Assert.notNull(sslBundle, "SSL bundle name has been set but no SSL bundles found in context"); + return sslBundle.createSslContext(); + + } + else { + SSLContext sslContext = SSLContext.getInstance(ssl.getAlgorithm()); + KeyManager[] keyManagers = configureKeyManagers(ssl); + TrustManager[] trustManagers = configureTrustManagers(ssl); + sslContext.init(keyManagers, trustManagers, new SecureRandom()); + return sslContext; + } + } + + private KeyManager @Nullable [] configureKeyManagers(EmbeddedLdapProperties.Ssl ssl) throws KeyStoreException, + IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException { + String keyStoreName = ssl.getKeyStore(); + String keyStorePassword = ssl.getKeyStorePassword(); + String storeType = ssl.getKeyStoreType(); + char[] keyPassphrase = null; + if (keyStorePassword != null) { + keyPassphrase = keyStorePassword.toCharArray(); + } + KeyManager[] keyManagers = null; + if (StringUtils.hasText(keyStoreName)) { + Resource resource = this.resourceLoader.getResource(keyStoreName); + KeyStore ks = KeyStore.getInstance(storeType); + try (InputStream inputStream = resource.getInputStream()) { + ks.load(inputStream, keyPassphrase); + } + KeyManagerFactory kmf = KeyManagerFactory.getInstance(ssl.getKeyStoreAlgorithm()); + kmf.init(ks, keyPassphrase); + keyManagers = kmf.getKeyManagers(); + } + return keyManagers; + } + + private TrustManager @Nullable [] configureTrustManagers(EmbeddedLdapProperties.Ssl ssl) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { + String trustStoreName = ssl.getTrustStore(); + String trustStorePassword = ssl.getTrustStorePassword(); + String storeType = ssl.getTrustStoreType(); + char[] trustPassphrase = null; + if (trustStorePassword != null) { + trustPassphrase = trustStorePassword.toCharArray(); + } + TrustManager[] trustManagers = null; + if (StringUtils.hasText(trustStoreName)) { + Resource resource = this.resourceLoader.getResource(trustStoreName); + KeyStore tks = KeyStore.getInstance(storeType); + try (InputStream inputStream = resource.getInputStream()) { + tks.load(inputStream, trustPassphrase); + } + TrustManagerFactory tmf = TrustManagerFactory.getInstance(ssl.getTrustStoreAlgorithm()); + tmf.init(tks); + trustManagers = tmf.getTrustManagers(); + } + return trustManagers; + } + /** * {@link SpringBootCondition} to determine when to apply embedded LDAP * auto-configuration. diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java index d348491af48a..3e85ec2ee13e 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java @@ -16,9 +16,12 @@ package org.springframework.boot.ldap.autoconfigure.embedded; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; +import javax.net.ssl.SSLContext; + import org.jspecify.annotations.Nullable; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -62,6 +65,11 @@ public class EmbeddedLdapProperties { */ private final Validation validation = new Validation(); + /** + * SSL configuration. + */ + private final Ssl ssl = new Ssl(); + public int getPort() { return this.port; } @@ -98,6 +106,10 @@ public Validation getValidation() { return this.validation; } + public Ssl getSsl() { + return this.ssl; + } + public static class Credential { /** @@ -132,6 +144,174 @@ boolean isAvailable() { } + public static class Ssl { + + private static final String SUN_X509 = "SunX509"; + + private static final String DEFAULT_PROTOCOL; + + static { + String protocol = "TLSv1.1"; + try { + String[] protocols = SSLContext.getDefault().getSupportedSSLParameters().getProtocols(); + for (String prot : protocols) { + if ("TLSv1.2".equals(prot)) { + protocol = "TLSv1.2"; + break; + } + } + } + catch (NoSuchAlgorithmException ex) { + // nothing + } + DEFAULT_PROTOCOL = protocol; + } + + /** + * Whether to enable SSL support. + */ + private Boolean enabled = false; + + /** + * SSL bundle name. + */ + private @Nullable String bundle; + + /** + * Path to the key store that holds the SSL certificate. + */ + private @Nullable String keyStore; + + /** + * Key store type. + */ + private String keyStoreType = "PKCS12"; + + /** + * Password used to access the key store. + */ + private @Nullable String keyStorePassword; + + /** + * Key store algorithm. + */ + private String keyStoreAlgorithm = SUN_X509; + + /** + * Trust store that holds SSL certificates. + */ + private @Nullable String trustStore; + + /** + * Trust store type. + */ + private String trustStoreType = "JKS"; + + /** + * Password used to access the trust store. + */ + private @Nullable String trustStorePassword; + + /** + * Trust store algorithm. + */ + private String trustStoreAlgorithm = SUN_X509; + + /** + * SSL algorithm to use. + */ + private String algorithm = DEFAULT_PROTOCOL; + + public Boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public @Nullable String getBundle() { + return this.bundle; + } + + public void setBundle(@Nullable String bundle) { + this.bundle = bundle; + } + + public @Nullable String getKeyStore() { + return this.keyStore; + } + + public void setKeyStore(@Nullable String keyStore) { + this.keyStore = keyStore; + } + + public String getKeyStoreType() { + return this.keyStoreType; + } + + public void setKeyStoreType(String keyStoreType) { + this.keyStoreType = keyStoreType; + } + + public @Nullable String getKeyStorePassword() { + return this.keyStorePassword; + } + + public void setKeyStorePassword(@Nullable String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + public String getKeyStoreAlgorithm() { + return this.keyStoreAlgorithm; + } + + public void setKeyStoreAlgorithm(String keyStoreAlgorithm) { + this.keyStoreAlgorithm = keyStoreAlgorithm; + } + + public @Nullable String getTrustStore() { + return this.trustStore; + } + + public void setTrustStore(@Nullable String trustStore) { + this.trustStore = trustStore; + } + + public String getTrustStoreType() { + return this.trustStoreType; + } + + public void setTrustStoreType(String trustStoreType) { + this.trustStoreType = trustStoreType; + } + + public @Nullable String getTrustStorePassword() { + return this.trustStorePassword; + } + + public void setTrustStorePassword(@Nullable String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + public String getTrustStoreAlgorithm() { + return this.trustStoreAlgorithm; + } + + public void setTrustStoreAlgorithm(String trustStoreAlgorithm) { + this.trustStoreAlgorithm = trustStoreAlgorithm; + } + + public String getAlgorithm() { + return this.algorithm; + } + + public void setAlgorithm(String sslAlgorithm) { + this.algorithm = sslAlgorithm; + } + + } + public static class Validation { /** diff --git a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java index 14b4d020638c..c28fd29e4ed2 100644 --- a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java +++ b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java @@ -20,8 +20,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.List; import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.sdk.BindResult; import com.unboundid.ldap.sdk.DN; import com.unboundid.ldap.sdk.LDAPConnection; @@ -32,6 +35,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -54,7 +58,7 @@ class EmbeddedLdapAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(EmbeddedLdapAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(EmbeddedLdapAutoConfiguration.class, SslAutoConfiguration.class)); @Test void testSetDefaultPort() { @@ -66,6 +70,98 @@ void testSetDefaultPort() { }); } + @Test + void testServerDefaultNoSsl() { + this.contextRunner + .withPropertyValues("spring.ldap.embedded.port:1234", "spring.ldap.embedded.base-dn:dc=spring,dc=org") + .run((context) -> { + InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class); + assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1); + InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0); + assertThat(config.getListenerName()).isEqualTo("LDAP"); + }); + } + + @Test + void testServerWithSslBundle() { + List propertyValues = new ArrayList<>(); + String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; + propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret"); + propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks"); + propertyValues.add("spring.ssl.bundle.jks.test.truststore.location=" + location + "test.jks"); + propertyValues.add("spring.ssl.bundle.jks.test.protocol=TLSv1.2"); + propertyValues.add("spring.ldap.embedded.port:1234"); + propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); + propertyValues.add("spring.ldap.embedded.ssl.enabled:true"); + propertyValues.add("spring.ldap.embedded.ssl.bundle:test"); + this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { + InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class); + assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1); + InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0); + assertThat(config.getListenerName()).isEqualTo("LDAPS"); + assertThat(config.getListenPort()).isEqualTo(1234); + assertThat(server.getListenPort()).isEqualTo(1234); + assertThat(server.getConnection("LDAPS").getSSLSession()).isNotNull(); + }); + } + + @Test + void testServerWithInvalidSslBundleShouldFail() { + List propertyValues = new ArrayList<>(); + String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; + propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret"); + propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks"); + propertyValues.add("spring.ldap.embedded.port:1234"); + propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); + propertyValues.add("spring.ldap.embedded.ssl.enabled:true"); + propertyValues.add("spring.ldap.embedded.ssl.bundle:foo"); + this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasMessageContaining("foo"); + assertThat(context).getFailure().hasMessageContaining("cannot be found"); + }); + } + + @Test + void testServerWithSsl() { + List propertyValues = new ArrayList<>(); + String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; + propertyValues.add("spring.ldap.embedded.port:1234"); + propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); + propertyValues.add("spring.ldap.embedded.ssl.enabled:true"); + propertyValues.add("spring.ldap.embedded.ssl.keyStorePassword=secret"); + propertyValues.add("spring.ldap.embedded.ssl.keyStore=" + location + "test.jks"); + propertyValues.add("spring.ldap.embedded.ssl.trustStorePassword=secret"); + propertyValues.add("spring.ldap.embedded.ssl.trustStore=" + location + "test.jks"); + this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { + InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class); + assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1); + InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0); + assertThat(config.getListenerName()).isEqualTo("LDAPS"); + assertThat(config.getListenPort()).isEqualTo(1234); + assertThat(server.getListenPort()).isEqualTo(1234); + assertThat(server.getConnection("LDAPS").getSSLSession()).isNotNull(); + }); + } + + @Test + void testServerWithInvalidSslShouldFail() { + List propertyValues = new ArrayList<>(); + String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; + propertyValues.add("spring.ldap.embedded.port:1234"); + propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); + propertyValues.add("spring.ldap.embedded.ssl.enabled:true"); + propertyValues.add("spring.ldap.embedded.ssl.keyStorePassword=secret"); + propertyValues.add("spring.ldap.embedded.ssl.keyStore=" + location + "foo"); + propertyValues.add("spring.ldap.embedded.ssl.trustStorePassword=secret"); + propertyValues.add("spring.ldap.embedded.ssl.trustStore=" + location + "foo"); + this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasMessageContaining("foo"); + assertThat(context).getFailure().hasMessageContaining("does not exist"); + }); + } + @Test void testRandomPortWithEnvironment() { this.contextRunner.withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org").run((context) -> { diff --git a/module/spring-boot-ldap/src/test/resources/org/springframework/boot/ldap/autoconfigure/embedded/test.jks b/module/spring-boot-ldap/src/test/resources/org/springframework/boot/ldap/autoconfigure/embedded/test.jks new file mode 100644 index 0000000000000000000000000000000000000000..8413be810956262d0da0387e8781fc4d27f6b027 GIT binary patch literal 1294 zcmezO_TO6u1_mY|W&~r_+{*0KN+2&TwQlnkAl+}!#Mo`X$Ht}2#>m2`#U#kc$jZRd z#8^J5!~Wmp7|YE9c_o202CEfvPJAt&F-z+E+Y5Je-P@SM3x!M{?<`L$d7kPy>HgY< z0rl+7ddFPf9_^UC?)~PM8U_ciTzNlZu6ENGso!d;PtM$lt<~ROC~|P;oYPIU`ifCm z3xrl>nF}qFRax}#!Gf-mKc9ryWTqXD5RA>>WDIzayKC#wV=9~F(x>fBQsZQ6{U|+U z!WW$>x*Dgfn4=QHB86`zaB53NZvW1sG()7e{`TIV! z&3V4Ogyo;agqT?@rcVEe)EOO%0lu zJ}+QqVq{`s(UY*NH{fPt*J|@PXTieE%3zRVC;*H-=1>+kVfN6x?7aN)JeUXvh6p=` z2p5J3H--o^T*N?5oY&CYz!C_}4NXl=qCi|jBLfR4m(H$fV%z~ty*jYqU<7jHnHn1z z{?-RK%9aXg2Hp3Xqxa^?^KXLNgKXZrFdUY%Z`c;7mgySnpcME!(zZ9^*Z!klcNu&; zJv%h#dZTrNtV#Z-w1n;1GXs_`nLqypOSOQ~A^+Vgum9VAO!|j)`14~cmWlGS{BHhs z`Ka7sCesk%D6xq~-%3BP(@wmu_5PC*v*g^19*oDCL>ZQ_-Kd;Z$Hmz)N^9lLmRQG?vR>ZjR%X@$!Tw1Ryr|naXt4hMbo5@zmuvM zRjkh}`M7UI;DpW<&G#QhJ)O@ssXHyMp*mDGh0(|Q-+50h=EhbAkUKiu|LR|O{i|~8 zG#9?b`_jG2zRtb(d;J9c=WfRw1SLMc;#t4^)bdH$I%1_8mU!~AelPGZ&F0$q@R`9O zTlJYe_wAPLtUcO4Q%WhRoN3F19Qh9Ow1TBSOuwH0IP>TZsa20}Jy&_$o4sju^m418 zzVifG8;&#`T)}fG(e3OcJ!7?pmWGQJ4de}Ef$3J3k420{WPN8vSnXA Date: Fri, 19 Dec 2025 10:50:39 +0000 Subject: [PATCH 018/930] Polish "Add LDAPS support to embedded LDAP server" See gh-48315 --- .../modules/reference/pages/data/nosql.adoc | 17 ++ .../EmbeddedLdapAutoConfiguration.java | 118 +++----------- .../embedded/EmbeddedLdapProperties.java | 152 +----------------- .../EmbeddedLdapAutoConfigurationTests.java | 147 +++++++---------- 4 files changed, 104 insertions(+), 330 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc index fadce3ad13bf..8bb18267aef0 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc @@ -736,3 +736,20 @@ If you want to load the initialization script from a different resource, you can By default, a standard schema is used to validate `LDIF` files. You can turn off validation altogether by setting the configprop:spring.ldap.embedded.validation.enabled[] property. If you have custom attributes, you can use configprop:spring.ldap.embedded.validation.schema[] to define your custom attribute types or object classes. + + + +[[data.nosql.ldap.embedded.ssl]] +==== SSL + +The in-memory LDAP server supports SSL (LDAPS). +To enable SSL, configure the xref:features/ssl.adoc[SSL bundle] to use by setting the configprop:spring.ldap.embedded.ssl.bundle[] property, as shown in the following example: + +[configprops,yaml] +---- +spring: + ldap: + embedded: + ssl: + bundle: "example" +---- diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java index 982cce9726c3..a2f51bc14195 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java @@ -16,27 +16,15 @@ package org.springframework.boot.ldap.autoconfigure.embedded; -import java.io.IOException; import java.io.InputStream; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; @@ -64,6 +52,7 @@ import org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration; import org.springframework.boot.ldap.autoconfigure.LdapProperties; import org.springframework.boot.ldap.autoconfigure.embedded.EmbeddedLdapAutoConfiguration.EmbeddedLdapAutoConfigurationRuntimeHints; +import org.springframework.boot.ldap.autoconfigure.embedded.EmbeddedLdapProperties.Ssl; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.context.ApplicationContext; @@ -79,8 +68,6 @@ import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.support.LdapContextSource; @@ -106,8 +93,6 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean { private final EmbeddedLdapProperties embeddedProperties; - private final ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver(); - private @Nullable InMemoryDirectoryServer server; EmbeddedLdapAutoConfiguration(EmbeddedLdapProperties embeddedProperties) { @@ -116,8 +101,7 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean { @Bean InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext, - ObjectProvider sslBundles) throws LDAPException, KeyStoreException, IOException, - NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException { + ObjectProvider sslBundles) throws LDAPException { String[] baseDn = StringUtils.toStringArray(this.embeddedProperties.getBaseDn()); InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDn); String username = this.embeddedProperties.getCredential().getUsername(); @@ -125,19 +109,8 @@ InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext, if (StringUtils.hasText(username) && StringUtils.hasText(password)) { config.addAdditionalBindCredentials(username, password); } + config.setListenerConfigs(createListenerConfig(sslBundles)); setSchema(config); - if (this.embeddedProperties.getSsl().isEnabled()) { - EmbeddedLdapProperties.Ssl ssl = this.embeddedProperties.getSsl(); - SSLContext sslContext = getSslContext(ssl, sslBundles.getIfAvailable()); - SSLServerSocketFactory serverSocketFactory = sslContext.getServerSocketFactory(); - SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory(); - config.setListenerConfigs(InMemoryListenerConfig.createLDAPSConfig("LDAPS", null, - this.embeddedProperties.getPort(), serverSocketFactory, clientSocketFactory)); - } - else { - config - .setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("LDAP", this.embeddedProperties.getPort())); - } this.server = new InMemoryDirectoryServer(config); importLdif(this.server, applicationContext); this.server.startListening(); @@ -145,6 +118,27 @@ InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext, return this.server; } + private InMemoryListenerConfig createListenerConfig(ObjectProvider sslBundles) throws LDAPException { + SslBundle sslBundle = getSslBundle(sslBundles.getIfAvailable()); + if (sslBundle != null) { + SSLContext sslContext = sslBundle.createSslContext(); + SSLServerSocketFactory serverSocketFactory = sslContext.getServerSocketFactory(); + SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory(); + return InMemoryListenerConfig.createLDAPSConfig("LDAPS", null, this.embeddedProperties.getPort(), + serverSocketFactory, clientSocketFactory); + } + return InMemoryListenerConfig.createLDAPConfig("LDAP", this.embeddedProperties.getPort()); + } + + private @Nullable SslBundle getSslBundle(@Nullable SslBundles sslBundles) { + Ssl ssl = this.embeddedProperties.getSsl(); + if (ssl.isEnabled() && StringUtils.hasLength(ssl.getBundle())) { + Assert.notNull(sslBundles, "SSL bundle name has been set but no SSL bundles found in context"); + return sslBundles.getBundle(ssl.getBundle()); + } + return null; + } + private void setSchema(InMemoryDirectoryServerConfig config) { if (!this.embeddedProperties.getValidation().isEnabled()) { config.setSchema(null); @@ -216,70 +210,6 @@ public void destroy() throws Exception { } } - private SSLContext getSslContext(EmbeddedLdapProperties.Ssl ssl, @Nullable SslBundles sslBundles) - throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, - UnrecoverableKeyException, KeyManagementException { - if (sslBundles != null && StringUtils.hasText(ssl.getBundle())) { - SslBundle sslBundle = sslBundles.getBundle(ssl.getBundle()); - Assert.notNull(sslBundle, "SSL bundle name has been set but no SSL bundles found in context"); - return sslBundle.createSslContext(); - - } - else { - SSLContext sslContext = SSLContext.getInstance(ssl.getAlgorithm()); - KeyManager[] keyManagers = configureKeyManagers(ssl); - TrustManager[] trustManagers = configureTrustManagers(ssl); - sslContext.init(keyManagers, trustManagers, new SecureRandom()); - return sslContext; - } - } - - private KeyManager @Nullable [] configureKeyManagers(EmbeddedLdapProperties.Ssl ssl) throws KeyStoreException, - IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException { - String keyStoreName = ssl.getKeyStore(); - String keyStorePassword = ssl.getKeyStorePassword(); - String storeType = ssl.getKeyStoreType(); - char[] keyPassphrase = null; - if (keyStorePassword != null) { - keyPassphrase = keyStorePassword.toCharArray(); - } - KeyManager[] keyManagers = null; - if (StringUtils.hasText(keyStoreName)) { - Resource resource = this.resourceLoader.getResource(keyStoreName); - KeyStore ks = KeyStore.getInstance(storeType); - try (InputStream inputStream = resource.getInputStream()) { - ks.load(inputStream, keyPassphrase); - } - KeyManagerFactory kmf = KeyManagerFactory.getInstance(ssl.getKeyStoreAlgorithm()); - kmf.init(ks, keyPassphrase); - keyManagers = kmf.getKeyManagers(); - } - return keyManagers; - } - - private TrustManager @Nullable [] configureTrustManagers(EmbeddedLdapProperties.Ssl ssl) - throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { - String trustStoreName = ssl.getTrustStore(); - String trustStorePassword = ssl.getTrustStorePassword(); - String storeType = ssl.getTrustStoreType(); - char[] trustPassphrase = null; - if (trustStorePassword != null) { - trustPassphrase = trustStorePassword.toCharArray(); - } - TrustManager[] trustManagers = null; - if (StringUtils.hasText(trustStoreName)) { - Resource resource = this.resourceLoader.getResource(trustStoreName); - KeyStore tks = KeyStore.getInstance(storeType); - try (InputStream inputStream = resource.getInputStream()) { - tks.load(inputStream, trustPassphrase); - } - TrustManagerFactory tmf = TrustManagerFactory.getInstance(ssl.getTrustStoreAlgorithm()); - tmf.init(tks); - trustManagers = tmf.getTrustManagers(); - } - return trustManagers; - } - /** * {@link SpringBootCondition} to determine when to apply embedded LDAP * auto-configuration. diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java index 3e85ec2ee13e..c43c72237db9 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java @@ -16,12 +16,9 @@ package org.springframework.boot.ldap.autoconfigure.embedded; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; -import javax.net.ssl.SSLContext; - import org.jspecify.annotations.Nullable; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -146,87 +143,22 @@ boolean isAvailable() { public static class Ssl { - private static final String SUN_X509 = "SunX509"; - - private static final String DEFAULT_PROTOCOL; - - static { - String protocol = "TLSv1.1"; - try { - String[] protocols = SSLContext.getDefault().getSupportedSSLParameters().getProtocols(); - for (String prot : protocols) { - if ("TLSv1.2".equals(prot)) { - protocol = "TLSv1.2"; - break; - } - } - } - catch (NoSuchAlgorithmException ex) { - // nothing - } - DEFAULT_PROTOCOL = protocol; - } - /** - * Whether to enable SSL support. + * Whether to enable SSL support. Enabled automatically if "bundle" is provided + * unless specified otherwise. */ - private Boolean enabled = false; + private @Nullable Boolean enabled; /** * SSL bundle name. */ private @Nullable String bundle; - /** - * Path to the key store that holds the SSL certificate. - */ - private @Nullable String keyStore; - - /** - * Key store type. - */ - private String keyStoreType = "PKCS12"; - - /** - * Password used to access the key store. - */ - private @Nullable String keyStorePassword; - - /** - * Key store algorithm. - */ - private String keyStoreAlgorithm = SUN_X509; - - /** - * Trust store that holds SSL certificates. - */ - private @Nullable String trustStore; - - /** - * Trust store type. - */ - private String trustStoreType = "JKS"; - - /** - * Password used to access the trust store. - */ - private @Nullable String trustStorePassword; - - /** - * Trust store algorithm. - */ - private String trustStoreAlgorithm = SUN_X509; - - /** - * SSL algorithm to use. - */ - private String algorithm = DEFAULT_PROTOCOL; - - public Boolean isEnabled() { - return this.enabled; + public boolean isEnabled() { + return (this.enabled != null) ? this.enabled : this.bundle != null; } - public void setEnabled(Boolean enabled) { + public void setEnabled(boolean enabled) { this.enabled = enabled; } @@ -238,78 +170,6 @@ public void setBundle(@Nullable String bundle) { this.bundle = bundle; } - public @Nullable String getKeyStore() { - return this.keyStore; - } - - public void setKeyStore(@Nullable String keyStore) { - this.keyStore = keyStore; - } - - public String getKeyStoreType() { - return this.keyStoreType; - } - - public void setKeyStoreType(String keyStoreType) { - this.keyStoreType = keyStoreType; - } - - public @Nullable String getKeyStorePassword() { - return this.keyStorePassword; - } - - public void setKeyStorePassword(@Nullable String keyStorePassword) { - this.keyStorePassword = keyStorePassword; - } - - public String getKeyStoreAlgorithm() { - return this.keyStoreAlgorithm; - } - - public void setKeyStoreAlgorithm(String keyStoreAlgorithm) { - this.keyStoreAlgorithm = keyStoreAlgorithm; - } - - public @Nullable String getTrustStore() { - return this.trustStore; - } - - public void setTrustStore(@Nullable String trustStore) { - this.trustStore = trustStore; - } - - public String getTrustStoreType() { - return this.trustStoreType; - } - - public void setTrustStoreType(String trustStoreType) { - this.trustStoreType = trustStoreType; - } - - public @Nullable String getTrustStorePassword() { - return this.trustStorePassword; - } - - public void setTrustStorePassword(@Nullable String trustStorePassword) { - this.trustStorePassword = trustStorePassword; - } - - public String getTrustStoreAlgorithm() { - return this.trustStoreAlgorithm; - } - - public void setTrustStoreAlgorithm(String trustStoreAlgorithm) { - this.trustStoreAlgorithm = trustStoreAlgorithm; - } - - public String getAlgorithm() { - return this.algorithm; - } - - public void setAlgorithm(String sslAlgorithm) { - this.algorithm = sslAlgorithm; - } - } public static class Validation { diff --git a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java index c28fd29e4ed2..9830effb7abc 100644 --- a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java +++ b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java @@ -67,101 +67,11 @@ void testSetDefaultPort() { .run((context) -> { InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class); assertThat(server.getListenPort()).isEqualTo(1234); - }); - } - - @Test - void testServerDefaultNoSsl() { - this.contextRunner - .withPropertyValues("spring.ldap.embedded.port:1234", "spring.ldap.embedded.base-dn:dc=spring,dc=org") - .run((context) -> { - InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class); - assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1); InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0); assertThat(config.getListenerName()).isEqualTo("LDAP"); }); } - @Test - void testServerWithSslBundle() { - List propertyValues = new ArrayList<>(); - String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; - propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret"); - propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks"); - propertyValues.add("spring.ssl.bundle.jks.test.truststore.location=" + location + "test.jks"); - propertyValues.add("spring.ssl.bundle.jks.test.protocol=TLSv1.2"); - propertyValues.add("spring.ldap.embedded.port:1234"); - propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); - propertyValues.add("spring.ldap.embedded.ssl.enabled:true"); - propertyValues.add("spring.ldap.embedded.ssl.bundle:test"); - this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { - InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class); - assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1); - InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0); - assertThat(config.getListenerName()).isEqualTo("LDAPS"); - assertThat(config.getListenPort()).isEqualTo(1234); - assertThat(server.getListenPort()).isEqualTo(1234); - assertThat(server.getConnection("LDAPS").getSSLSession()).isNotNull(); - }); - } - - @Test - void testServerWithInvalidSslBundleShouldFail() { - List propertyValues = new ArrayList<>(); - String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; - propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret"); - propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks"); - propertyValues.add("spring.ldap.embedded.port:1234"); - propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); - propertyValues.add("spring.ldap.embedded.ssl.enabled:true"); - propertyValues.add("spring.ldap.embedded.ssl.bundle:foo"); - this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { - assertThat(context).hasFailed(); - assertThat(context).getFailure().hasMessageContaining("foo"); - assertThat(context).getFailure().hasMessageContaining("cannot be found"); - }); - } - - @Test - void testServerWithSsl() { - List propertyValues = new ArrayList<>(); - String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; - propertyValues.add("spring.ldap.embedded.port:1234"); - propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); - propertyValues.add("spring.ldap.embedded.ssl.enabled:true"); - propertyValues.add("spring.ldap.embedded.ssl.keyStorePassword=secret"); - propertyValues.add("spring.ldap.embedded.ssl.keyStore=" + location + "test.jks"); - propertyValues.add("spring.ldap.embedded.ssl.trustStorePassword=secret"); - propertyValues.add("spring.ldap.embedded.ssl.trustStore=" + location + "test.jks"); - this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { - InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class); - assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1); - InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0); - assertThat(config.getListenerName()).isEqualTo("LDAPS"); - assertThat(config.getListenPort()).isEqualTo(1234); - assertThat(server.getListenPort()).isEqualTo(1234); - assertThat(server.getConnection("LDAPS").getSSLSession()).isNotNull(); - }); - } - - @Test - void testServerWithInvalidSslShouldFail() { - List propertyValues = new ArrayList<>(); - String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; - propertyValues.add("spring.ldap.embedded.port:1234"); - propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); - propertyValues.add("spring.ldap.embedded.ssl.enabled:true"); - propertyValues.add("spring.ldap.embedded.ssl.keyStorePassword=secret"); - propertyValues.add("spring.ldap.embedded.ssl.keyStore=" + location + "foo"); - propertyValues.add("spring.ldap.embedded.ssl.trustStorePassword=secret"); - propertyValues.add("spring.ldap.embedded.ssl.trustStore=" + location + "foo"); - this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { - assertThat(context).hasFailed(); - assertThat(context).getFailure().hasMessageContaining("foo"); - assertThat(context).getFailure().hasMessageContaining("does not exist"); - }); - } - @Test void testRandomPortWithEnvironment() { this.contextRunner.withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org").run((context) -> { @@ -444,6 +354,63 @@ void ldapContextIsCreatedWithBase() { }); } + @Test + void whenSslBundleIsConfiguredLdapsListenerIsConfigured() { + List propertyValues = new ArrayList<>(); + String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; + propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret"); + propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks"); + propertyValues.add("spring.ssl.bundle.jks.test.truststore.location=" + location + "test.jks"); + propertyValues.add("spring.ssl.bundle.jks.test.protocol=TLSv1.2"); + propertyValues.add("spring.ldap.embedded.port:0"); + propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); + propertyValues.add("spring.ldap.embedded.ssl.bundle:test"); + this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { + InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class); + assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1); + InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0); + assertThat(config.getListenerName()).isEqualTo("LDAPS"); + assertThat(server.getConnection("LDAPS").getSSLSession()).isNotNull(); + }); + } + + @Test + void whenSslBundleIsConfiguredButSslIsDisabledLdapListenerIsConfigured() { + List propertyValues = new ArrayList<>(); + String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; + propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret"); + propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks"); + propertyValues.add("spring.ssl.bundle.jks.test.truststore.location=" + location + "test.jks"); + propertyValues.add("spring.ssl.bundle.jks.test.protocol=TLSv1.2"); + propertyValues.add("spring.ldap.embedded.port:0"); + propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); + propertyValues.add("spring.ldap.embedded.ssl.enabled:false"); + propertyValues.add("spring.ldap.embedded.ssl.bundle:test"); + this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { + InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class); + assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1); + InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0); + assertThat(config.getListenerName()).isEqualTo("LDAP"); + }); + } + + @Test + void whenInvalidSslBundleIsConfiguredThenStartFails() { + List propertyValues = new ArrayList<>(); + String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; + propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret"); + propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks"); + propertyValues.add("spring.ldap.embedded.port:0"); + propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); + propertyValues.add("spring.ldap.embedded.ssl.enabled:true"); + propertyValues.add("spring.ldap.embedded.ssl.bundle:foo"); + this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasMessageContaining("foo"); + assertThat(context).getFailure().hasMessageContaining("cannot be found"); + }); + } + @Configuration(proxyBeanMethods = false) static class LdapClientConfiguration { From 301115559329ed80ca81aa1140e5cc09e1ce5bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 10 Dec 2025 14:37:17 +0100 Subject: [PATCH 019/930] Remove support for deprecated logback properties This commit removes support for the following properties that are deprecated since Spring Boot 2.4: - logging.pattern.rolling-file-name - logging.file.clean-history-on-start - logging.file.max-size - logging.file.total-size-cap - logging.file.max-history Closes gh-48489 --- .../LogbackLoggingSystemProperties.java | 1 - .../logback/RollingPolicySystemProperty.java | 19 ++++++------------ ...itional-spring-configuration-metadata.json | 20 +++++-------------- .../LogbackLoggingSystemPropertiesTests.java | 16 --------------- .../logback/LogbackLoggingSystemTests.java | 20 +++++++++---------- 5 files changed, 21 insertions(+), 55 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java index 8ff938aa4d7f..42ed41c7b634 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java @@ -106,7 +106,6 @@ private void applyRollingPolicy(RollingPolicySystemProperty property, PropertyRe 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(property.getEnvironmentVariableName(), stringValue); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/RollingPolicySystemProperty.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/RollingPolicySystemProperty.java index ed823a4ef1d4..9ac424e4faf3 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/RollingPolicySystemProperty.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/RollingPolicySystemProperty.java @@ -29,38 +29,35 @@ 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"), + FILE_NAME_PATTERN("file-name-pattern"), /** * Logging system property for the clean history on start flag. */ - CLEAN_HISTORY_ON_START("clean-history-on-start", "logging.file.clean-history-on-start"), + CLEAN_HISTORY_ON_START("clean-history-on-start"), /** * Logging system property for the file log max size. */ - MAX_FILE_SIZE("max-file-size", "logging.file.max-size"), + MAX_FILE_SIZE("max-file-size"), /** * Logging system property for the file total size cap. */ - TOTAL_SIZE_CAP("total-size-cap", "logging.file.total-size-cap"), + TOTAL_SIZE_CAP("total-size-cap"), /** * Logging system property for the file log max history. */ - MAX_HISTORY("max-history", "logging.file.max-history"); + MAX_HISTORY("max-history"); private final String environmentVariableName; private final String applicationPropertyName; - private final String deprecatedApplicationPropertyName; - - RollingPolicySystemProperty(String applicationPropertyName, String deprecatedApplicationPropertyName) { + RollingPolicySystemProperty(String applicationPropertyName) { this.environmentVariableName = "LOGBACK_ROLLINGPOLICY_" + name(); this.applicationPropertyName = "logging.logback.rollingpolicy." + applicationPropertyName; - this.deprecatedApplicationPropertyName = deprecatedApplicationPropertyName; } /** @@ -75,8 +72,4 @@ String getApplicationPropertyName() { return this.applicationPropertyName; } - String getDeprecatedApplicationPropertyName() { - return this.deprecatedApplicationPropertyName; - } - } diff --git a/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 3afaffa0cddc..651ab994f32d 100644 --- a/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -55,10 +55,8 @@ { "name": "logging.file.clean-history-on-start", "type": "java.lang.Boolean", - "description": "Whether to clean the archive log files on startup. Only supported with the default logback setup.", - "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", - "defaultValue": false, "deprecation": { + "level": "error", "replacement": "logging.logback.rollingpolicy.clean-history-on-start", "since": "2.4.0" } @@ -66,10 +64,8 @@ { "name": "logging.file.max-history", "type": "java.lang.Integer", - "description": "Maximum number of archive log files to keep. Only supported with the default logback setup.", - "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", - "defaultValue": 7, "deprecation": { + "level": "error", "replacement": "logging.logback.rollingpolicy.max-history", "since": "2.4.0" } @@ -77,10 +73,8 @@ { "name": "logging.file.max-size", "type": "org.springframework.util.unit.DataSize", - "description": "Maximum log file size. Only supported with the default logback setup.", - "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", - "defaultValue": "10MB", "deprecation": { + "level": "error", "replacement": "logging.logback.rollingpolicy.max-file-size", "since": "2.4.0" } @@ -100,10 +94,8 @@ { "name": "logging.file.total-size-cap", "type": "org.springframework.util.unit.DataSize", - "description": "Total size of log backups to be kept. Only supported with the default logback setup.", - "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", - "defaultValue": "0B", "deprecation": { + "level": "error", "replacement": "logging.logback.rollingpolicy.total-size-cap", "since": "2.4.0" } @@ -219,10 +211,8 @@ { "name": "logging.pattern.rolling-file-name", "type": "java.lang.String", - "description": "Pattern for rolled-over log file names. Supported only with the default Logback setup.", - "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", - "defaultValue": "${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz", "deprecation": { + "level": "error", "replacement": "logging.logback.rollingpolicy.file-name-pattern", "since": "2.4.0" } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemPropertiesTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemPropertiesTests.java index e399c2d1f3fe..5715dc0a7676 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemPropertiesTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemPropertiesTests.java @@ -89,22 +89,6 @@ void applySetsLogbackSystemProperties() { .containsEntry(RollingPolicySystemProperty.MAX_HISTORY.getEnvironmentVariableName(), "mh"); } - @Test - void applySetsLogbackSystemPropertiesFromDeprecated() { - this.environment.setProperty("logging.pattern.rolling-file-name", "fnp"); - this.environment.setProperty("logging.file.clean-history-on-start", "chos"); - this.environment.setProperty("logging.file.max-size", "1KB"); - this.environment.setProperty("logging.file.total-size-cap", "2KB"); - this.environment.setProperty("logging.file.max-history", "mh"); - new LogbackLoggingSystemProperties(this.environment).apply(); - assertThat(System.getProperties()) - .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() { LogbackLoggingSystemProperties logbackLoggingSystemProperties = spy( diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java index 6adaeb4d8def..d37a96822bac 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java @@ -398,7 +398,7 @@ void testFilePatternProperty(CapturedOutput output) { @Test void testCleanHistoryOnStartProperty() { - this.environment.setProperty("logging.file.clean-history-on-start", "true"); + this.environment.setProperty("logging.logback.rollingpolicy.clean-history-on-start", "true"); LoggingInitializationContext loggingInitializationContext = new LoggingInitializationContext(this.environment); File file = new File(tmpDir(), "logback-test.log"); LogFile logFile = getLogFile(file.getPath(), null); @@ -411,7 +411,7 @@ void testCleanHistoryOnStartProperty() { @Test @WithIncludeBaseXmlResource void testCleanHistoryOnStartPropertyWithXmlConfiguration() { - this.environment.setProperty("logging.file.clean-history-on-start", "true"); + this.environment.setProperty("logging.logback.rollingpolicy.clean-history-on-start", "true"); LoggingInitializationContext loggingInitializationContext = new LoggingInitializationContext(this.environment); File file = new File(tmpDir(), "logback-test.log"); LogFile logFile = getLogFile(file.getPath(), null); @@ -437,7 +437,7 @@ void testMaxFileSizePropertyWithBytesValue() { } private void testMaxFileSizeProperty(String sizeValue, String expectedFileSize) { - this.environment.setProperty("logging.file.max-size", sizeValue); + this.environment.setProperty("logging.logback.rollingpolicy.max-file-size", sizeValue); LoggingInitializationContext loggingInitializationContext = new LoggingInitializationContext(this.environment); File file = new File(tmpDir(), "logback-test.log"); LogFile logFile = getLogFile(file.getPath(), null); @@ -450,7 +450,7 @@ private void testMaxFileSizeProperty(String sizeValue, String expectedFileSize) @Test @WithIncludeBaseXmlResource void testMaxFileSizePropertyWithXmlConfiguration() { - this.environment.setProperty("logging.file.max-size", "100MB"); + this.environment.setProperty("logging.logback.rollingpolicy.max-file-size", "100MB"); LoggingInitializationContext loggingInitializationContext = new LoggingInitializationContext(this.environment); File file = new File(tmpDir(), "logback-test.log"); LogFile logFile = getLogFile(file.getPath(), null); @@ -462,7 +462,7 @@ void testMaxFileSizePropertyWithXmlConfiguration() { @Test void testMaxHistoryProperty() { - this.environment.setProperty("logging.file.max-history", "30"); + this.environment.setProperty("logging.logback.rollingpolicy.max-history", "30"); LoggingInitializationContext loggingInitializationContext = new LoggingInitializationContext(this.environment); File file = new File(tmpDir(), "logback-test.log"); LogFile logFile = getLogFile(file.getPath(), null); @@ -475,7 +475,7 @@ void testMaxHistoryProperty() { @Test @WithIncludeBaseXmlResource void testMaxHistoryPropertyWithXmlConfiguration() { - this.environment.setProperty("logging.file.max-history", "30"); + this.environment.setProperty("logging.logback.rollingpolicy.max-history", "30"); LoggingInitializationContext loggingInitializationContext = new LoggingInitializationContext(this.environment); File file = new File(tmpDir(), "logback-test.log"); LogFile logFile = getLogFile(file.getPath(), null); @@ -501,7 +501,7 @@ void testTotalSizeCapPropertyWithBytesValue() { } private void testTotalSizeCapProperty(String sizeValue, String expectedFileSize) { - this.environment.setProperty("logging.file.total-size-cap", sizeValue); + this.environment.setProperty("logging.logback.rollingpolicy.total-size-cap", sizeValue); LoggingInitializationContext loggingInitializationContext = new LoggingInitializationContext(this.environment); File file = new File(tmpDir(), "logback-test.log"); LogFile logFile = getLogFile(file.getPath(), null); @@ -515,7 +515,7 @@ private void testTotalSizeCapProperty(String sizeValue, String expectedFileSize) @WithIncludeBaseXmlResource void testTotalSizeCapPropertyWithXmlConfiguration() { String expectedSize = "101 MB"; - this.environment.setProperty("logging.file.total-size-cap", expectedSize); + this.environment.setProperty("logging.logback.rollingpolicy.total-size-cap", expectedSize); LoggingInitializationContext loggingInitializationContext = new LoggingInitializationContext(this.environment); File file = new File(tmpDir(), "logback-test.log"); LogFile logFile = getLogFile(file.getPath(), null); @@ -773,9 +773,9 @@ void logbackSystemStatusListenerShouldBeRegisteredWhenUsingCustomLogbackXml(Capt } @Test - void testRollingFileNameProperty() { + void testRollingFileNamePatternProperty() { String rollingFile = "my.log.%d{yyyyMMdd}.%i.gz"; - this.environment.setProperty("logging.pattern.rolling-file-name", rollingFile); + this.environment.setProperty("logging.logback.rollingpolicy.file-name-pattern", rollingFile); LoggingInitializationContext loggingInitializationContext = new LoggingInitializationContext(this.environment); File file = new File(tmpDir(), "my.log"); LogFile logFile = getLogFile(file.getPath(), null); From 5399793fafa68f1b1904318744d5225e0959411a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 16 Dec 2025 11:48:32 +0100 Subject: [PATCH 020/930] Reinstate Spock support Closes gh-48513 --- core/spring-boot-test/build.gradle | 2 +- .../context/ImportsContextCustomizer.java | 1 + .../ImportsContextCustomizerTests.java | 39 +++++++++++++++++++ .../antora/modules/ROOT/pages/redirect.adoc | 2 + .../testing/spring-boot-applications.adoc | 8 ++++ .../boot/health/contributor/StatusTests.java | 2 +- .../build.gradle | 7 ++++ 7 files changed, 59 insertions(+), 2 deletions(-) diff --git a/core/spring-boot-test/build.gradle b/core/spring-boot-test/build.gradle index 8720ea9e6051..17c90e1028a1 100644 --- a/core/spring-boot-test/build.gradle +++ b/core/spring-boot-test/build.gradle @@ -54,10 +54,10 @@ dependencies { testImplementation("org.jetbrains.kotlin:kotlin-reflect") testImplementation("org.jetbrains.kotlin:kotlin-stdlib") testImplementation("org.slf4j:slf4j-api") + testImplementation("org.spockframework:spock-core") testImplementation("org.springframework:spring-webmvc") testImplementation("org.testng:testng") - testRuntimeOnly("org.apache.groovy:groovy") testRuntimeOnly("org.aspectj:aspectjrt") testRuntimeOnly("org.aspectj:aspectjweaver") testRuntimeOnly("org.junit.vintage:junit-vintage-engine") diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java index 79774d916ebc..4237f9f37103 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java @@ -228,6 +228,7 @@ static class ContextCustomizerKey { 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); } diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java index c6fd97346d45..ee9369d4469a 100644 --- a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java @@ -25,6 +25,9 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.spockframework.runtime.model.SpecMetadata; +import spock.lang.Issue; +import spock.lang.Stepwise; import org.springframework.boot.context.annotation.DeterminableImports; import org.springframework.context.annotation.Configuration; @@ -61,6 +64,18 @@ void customizersForTestClassesWithDifferentKotlinMetadataAreEqual() { .isEqualTo(new ImportsContextCustomizer(SecondKotlinAnnotatedTestClass.class)); } + @Test + void customizersForTestClassesWithDifferentSpockFrameworkAnnotationsAreEqual() { + assertThat(new ImportsContextCustomizer(FirstSpockFrameworkAnnotatedTestClass.class)) + .isEqualTo(new ImportsContextCustomizer(SecondSpockFrameworkAnnotatedTestClass.class)); + } + + @Test + void customizersForTestClassesWithDifferentSpockLangAnnotationsAreEqual() { + assertThat(new ImportsContextCustomizer(FirstSpockLangAnnotatedTestClass.class)) + .isEqualTo(new ImportsContextCustomizer(SecondSpockLangAnnotatedTestClass.class)); + } + @Test void customizersForTestClassesWithDifferentJUnitAnnotationsAreEqual() { assertThat(new ImportsContextCustomizer(FirstJUnitAnnotatedTestClass.class)) @@ -127,6 +142,30 @@ static class SecondKotlinAnnotatedTestClass { } + @SpecMetadata(filename = "foo", line = 10) + @Import(TestImportSelector.class) + static class FirstSpockFrameworkAnnotatedTestClass { + + } + + @SpecMetadata(filename = "bar", line = 10) + @Import(TestImportSelector.class) + static class SecondSpockFrameworkAnnotatedTestClass { + + } + + @Stepwise + @Import(TestImportSelector.class) + static class FirstSpockLangAnnotatedTestClass { + + } + + @Issue("1234") + @Import(TestImportSelector.class) + static class SecondSpockLangAnnotatedTestClass { + + } + @Nested @Import(TestImportSelector.class) static class FirstJUnitAnnotatedTestClass { diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc index 07285422bff9..72414df0faae 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc @@ -1901,6 +1901,8 @@ * xref:reference:testing/spring-boot-applications.adoc#testing.spring-boot-applications.mocking-beans[#boot-features-testing-spring-boot-applications-mocking-beans] * xref:reference:testing/spring-boot-applications.adoc#testing.spring-boot-applications.mocking-beans[#features.testing.spring-boot-applications.mocking-beans] * xref:reference:testing/spring-boot-applications.adoc#testing.spring-boot-applications.observations[#features.testing.spring-boot-applications.observations] +* xref:reference:testing/spring-boot-applications.adoc#testing.spring-boot-applications.spock[#boot-features-testing-spring-boot-applications-with-spock] +* xref:reference:testing/spring-boot-applications.adoc#testing.spring-boot-applications.spock[#features.testing.spring-boot-applications.spock] * xref:reference:testing/spring-boot-applications.adoc#testing.spring-boot-applications.spring-graphql-tests[#features.testing.spring-boot-applications.spring-graphql-tests] * xref:reference:testing/spring-boot-applications.adoc#testing.spring-boot-applications.spring-mvc-tests[#boot-features-testing-spring-boot-applications-testing-autoconfigured-mvc-tests] * xref:reference:testing/spring-boot-applications.adoc#testing.spring-boot-applications.spring-mvc-tests[#features.testing.spring-boot-applications.spring-mvc-tests] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc index 2c97d40e529f..34725904bf93 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc @@ -925,3 +925,11 @@ TIP: If this is not an option for you, you can create a javadoc:org.springframew Alternatively, you can specify a source for your test, which disables the behavior of finding a default one. + +[[testing.spring-boot-applications.spock]] +== Using Spock to Test Spring Boot Applications + +Spock 2.4 or later can be used to test a Spring Boot application. +To do so, add a dependency on a `-groovy-5.0` version of Spock's `spock-spring` module to your application's build. +`spock-spring` integrates Spring's test framework into Spock. +See https://spockframework.org/spock/docs/2.4/modules.html#spring-module[the documentation for Spock's Spring module] for further details. diff --git a/module/spring-boot-health/src/test/java/org/springframework/boot/health/contributor/StatusTests.java b/module/spring-boot-health/src/test/java/org/springframework/boot/health/contributor/StatusTests.java index 655c88d3d9b1..f252f567b4ea 100644 --- a/module/spring-boot-health/src/test/java/org/springframework/boot/health/contributor/StatusTests.java +++ b/module/spring-boot-health/src/test/java/org/springframework/boot/health/contributor/StatusTests.java @@ -59,7 +59,7 @@ void getDescriptionReturnsDescription() { void equalsAndHashCode() { Status one = new Status("spring", "boot"); Status two = new Status("spring", "framework"); - Status three = new Status("another", "framework"); + Status three = new Status("spock", "framework"); assertThat(one).isEqualTo(one).isEqualTo(two).isNotEqualTo(three); assertThat(one).hasSameHashCodeAs(two); } diff --git a/platform/spring-boot-internal-dependencies/build.gradle b/platform/spring-boot-internal-dependencies/build.gradle index fd35d8fbb6b6..371ab8827ff6 100644 --- a/platform/spring-boot-internal-dependencies/build.gradle +++ b/platform/spring-boot-internal-dependencies/build.gradle @@ -235,6 +235,13 @@ bom { ] } } + library("Spock Framework", "2.4-groovy-5.0") { + group("org.spockframework") { + modules = [ + "spock-core" + ] + } + } library("TestNG", "6.14.3") { group("org.testng") { modules = [ From 017bd4bc5b85b0eab54cb73e7c8cdb4cdc8bcaa9 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 19 Dec 2025 15:04:48 -0800 Subject: [PATCH 021/930] Bump version in Eclipse project setup to 4.1.x --- eclipse/spring-boot-project.setup | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eclipse/spring-boot-project.setup b/eclipse/spring-boot-project.setup index 56ad7dd1714c..8486022ea6e4 100644 --- a/eclipse/spring-boot-project.setup +++ b/eclipse/spring-boot-project.setup @@ -10,8 +10,8 @@ xmlns:setup.p2="http://www.eclipse.org/oomph/setup/p2/1.0" xmlns:setup.workingsets="http://www.eclipse.org/oomph/setup/workingsets/1.0" xsi:schemaLocation="http://www.eclipse.org/oomph/setup/jdt/1.0 https://raw.githubusercontent.com/eclipse-oomph/oomph/master/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 https://raw.githubusercontent.com/eclipse-oomph/oomph/master/setups/models/Predicates.ecore http://www.eclipse.org/oomph/setup/workingsets/1.0 https://raw.githubusercontent.com/eclipse-oomph/oomph/master/setups/models/SetupWorkingSets.ecore" - name="spring.boot.4.0.x" - label="Spring Boot 4.0.x"> + name="spring.boot.4.1.x" + label="Spring Boot 4.1.x"> Date: Mon, 22 Dec 2025 08:36:52 +0100 Subject: [PATCH 022/930] Remove support for the deprecated layertools jar mode Closes gh-48568 --- .../AbstractBootArchiveIntegrationTests.java | 24 ++-- .../bundling/AbstractBootArchiveTests.java | 16 +-- .../boot/jarmode/tools/LayerToolsJarMode.java | 56 -------- .../boot/jarmode/tools/package-info.java | 2 +- .../main/resources/META-INF/spring.factories | 1 - .../jarmode/tools/LayerToolsJarModeTests.java | 134 ------------------ ...ayertools-error-command-unknown-output.txt | 10 -- ...ools-error-option-missing-value-output.txt | 11 -- ...layertools-error-option-unknown-output.txt | 11 -- .../jarmode/tools/layertools-help-output.txt | 8 -- .../jarmode/tools/layertools-list-output.txt | 5 - 11 files changed, 21 insertions(+), 257 deletions(-) delete mode 100644 loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/LayerToolsJarMode.java delete mode 100644 loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/LayerToolsJarModeTests.java delete mode 100644 loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt delete mode 100644 loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt delete mode 100644 loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt delete mode 100644 loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt delete mode 100644 loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-list-output.txt diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java index 07a878742315..4e398951651d 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java @@ -333,9 +333,9 @@ void implicitLayers() throws IOException { assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Map> indexedLayers; - String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); + String toolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { - assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry(toolsJar)).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "spring-core-5.2.5.RELEASE.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); @@ -360,7 +360,7 @@ void implicitLayers() throws IOException { expectedDependencies.add(this.libPath + "spring-boot-starter-logging-2.2.0.RELEASE.jar"); Set expectedSnapshotDependencies = new TreeSet<>(); expectedSnapshotDependencies.add(this.libPath + "library-1.0-SNAPSHOT.jar"); - (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + (toolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(toolsJar); assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); assertThat(indexedLayers.get("spring-boot-loader")).containsExactly("org/"); assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); @@ -390,9 +390,9 @@ void multiModuleImplicitLayers() throws IOException { assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Map> indexedLayers; - String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); + String toolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { - assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry(toolsJar)).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "alpha-1.2.3.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "bravo-1.2.3.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "charlie-1.2.3.jar")).isNotNull(); @@ -413,7 +413,7 @@ void multiModuleImplicitLayers() throws IOException { expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar"); Set expectedSnapshotDependencies = new TreeSet<>(); expectedSnapshotDependencies.add(this.libPath + "library-1.0-SNAPSHOT.jar"); - (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + (toolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(toolsJar); assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); assertThat(indexedLayers.get("spring-boot-loader")).containsExactly("org/"); assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); @@ -442,9 +442,9 @@ void customLayers() throws IOException { assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Map> indexedLayers; - String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); + String toolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { - assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry(toolsJar)).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "spring-core-5.2.5.RELEASE.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); @@ -462,7 +462,7 @@ void customLayers() throws IOException { expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar"); List expectedSnapshotDependencies = new ArrayList<>(); expectedSnapshotDependencies.add(this.libPath + "library-1.0-SNAPSHOT.jar"); - (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + (toolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(toolsJar); assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); assertThat(indexedLayers.get("commons-dependencies")).containsExactly(this.libPath + "commons-lang3-3.9.jar"); assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); @@ -495,9 +495,9 @@ void multiModuleCustomLayers() throws IOException { assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Map> indexedLayers; - String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); + String toolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { - assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry(toolsJar)).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "alpha-1.2.3.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "bravo-1.2.3.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "charlie-1.2.3.jar")).isNotNull(); @@ -522,7 +522,7 @@ void multiModuleCustomLayers() throws IOException { expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar"); List expectedSnapshotDependencies = new ArrayList<>(); expectedSnapshotDependencies.add(this.libPath + "library-1.0-SNAPSHOT.jar"); - (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + (toolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(toolsJar); assertThat(indexedLayers.get("subproject-dependencies")) .containsExactlyElementsOf(expectedSubprojectDependencies); assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index 5ab4e610a866..10f9eaf5c003 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -488,22 +488,22 @@ void whenJarIsLayeredThenLayersIndexIsPresentAndCorrect() throws IOException { List index = entryLines(jarFile, this.indexPath + "layers.idx"); assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader", "snapshot-dependencies", "application"); - String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); + String toolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); List expected = new ArrayList<>(); expected.add("- \"dependencies\":"); expected.add(" - \"" + this.libPath + "first-library.jar\""); expected.add(" - \"" + this.libPath + "first-project-library.jar\""); expected.add(" - \"" + this.libPath + "fourth-library.jar\""); expected.add(" - \"" + this.libPath + "second-library.jar\""); - if (!layerToolsJar.contains("SNAPSHOT")) { - expected.add(" - \"" + layerToolsJar + "\""); + if (!toolsJar.contains("SNAPSHOT")) { + expected.add(" - \"" + toolsJar + "\""); } expected.add("- \"spring-boot-loader\":"); expected.add(" - \"org/\""); expected.add("- \"snapshot-dependencies\":"); expected.add(" - \"" + this.libPath + "second-project-library-SNAPSHOT.jar\""); - if (layerToolsJar.contains("SNAPSHOT")) { - expected.add(" - \"" + layerToolsJar + "\""); + if (toolsJar.contains("SNAPSHOT")) { + expected.add(" - \"" + toolsJar + "\""); } expected.add(" - \"" + this.libPath + "third-library-SNAPSHOT.jar\""); expected.add("- \"application\":"); @@ -542,10 +542,10 @@ void whenJarIsLayeredWithCustomStrategiesThenLayersIndexIsPresentAndCorrect() th List index = entryLines(jarFile, this.indexPath + "layers.idx"); assertThat(getLayerNames(index)).containsExactly("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application"); - String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); + String toolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); List expected = new ArrayList<>(); expected.add("- \"my-deps\":"); - expected.add(" - \"" + layerToolsJar + "\""); + expected.add(" - \"" + toolsJar + "\""); expected.add("- \"my-internal-deps\":"); expected.add(" - \"" + this.libPath + "first-library.jar\""); expected.add(" - \"" + this.libPath + "first-project-library.jar\""); @@ -570,7 +570,7 @@ void whenJarIsLayeredWithCustomStrategiesThenLayersIndexIsPresentAndCorrect() th } @Test - void whenArchiveIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException { + void whenArchiveIsLayeredThenToolsJarIsIncluded() throws IOException { List entryNames = getEntryNames(createLayeredJar()); assertThat(entryNames).contains(this.libPath + JarModeLibrary.TOOLS.getName()); } diff --git a/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/LayerToolsJarMode.java b/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/LayerToolsJarMode.java deleted file mode 100644 index c1663c648711..000000000000 --- a/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/LayerToolsJarMode.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.jarmode.tools; - -import java.util.List; - -import org.jspecify.annotations.Nullable; - -import org.springframework.boot.loader.jarmode.JarMode; - -/** - * {@link JarMode} providing {@code "layertools"} support. - * - * @author Phillip Webb - * @author Scott Frederick - * @since 2.3.0 - */ -public class LayerToolsJarMode implements JarMode { - - static @Nullable Context contextOverride; - - @Override - public boolean accepts(String mode) { - return "layertools".equalsIgnoreCase(mode); - } - - @Override - public void run(String mode, String[] args) { - try { - Context context = (contextOverride != null) ? contextOverride : new Context(); - new Runner(System.out, context, getCommands(context)).run(args); - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } - } - - static List getCommands(Context context) { - return List.of(new ListCommand(context), new ExtractLayersCommand(context)); - } - -} diff --git a/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/package-info.java b/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/package-info.java index d818f3fd8381..3430de913808 100644 --- a/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/package-info.java +++ b/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/package-info.java @@ -15,7 +15,7 @@ */ /** - * JarMode support for layertools and tools. + * JarMode support for application tools. */ @NullMarked package org.springframework.boot.jarmode.tools; diff --git a/loader/spring-boot-jarmode-tools/src/main/resources/META-INF/spring.factories b/loader/spring-boot-jarmode-tools/src/main/resources/META-INF/spring.factories index bc571075c5f4..80cad5b666a6 100644 --- a/loader/spring-boot-jarmode-tools/src/main/resources/META-INF/spring.factories +++ b/loader/spring-boot-jarmode-tools/src/main/resources/META-INF/spring.factories @@ -1,4 +1,3 @@ # Jar Modes org.springframework.boot.loader.jarmode.JarMode=\ -org.springframework.boot.jarmode.tools.LayerToolsJarMode,\ org.springframework.boot.jarmode.tools.ToolsJarMode diff --git a/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/LayerToolsJarModeTests.java b/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/LayerToolsJarModeTests.java deleted file mode 100644 index 2fa8507ba6c7..000000000000 --- a/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/LayerToolsJarModeTests.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.jarmode.tools; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.PrintStream; -import java.io.Writer; -import java.nio.charset.StandardCharsets; -import java.util.jar.JarEntry; -import java.util.zip.ZipOutputStream; - -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.core.io.ClassPathResource; -import org.springframework.util.FileCopyUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link LayerToolsJarMode}. - * - * @author Phillip Webb - * @author Scott Frederick - */ -class LayerToolsJarModeTests { - - private static final String[] NO_ARGS = {}; - - private TestPrintStream out; - - private PrintStream systemOut; - - @TempDir - @SuppressWarnings("NullAway.Init") - File temp; - - @BeforeEach - void setup() throws Exception { - Context context = mock(Context.class); - given(context.getArchiveFile()).willReturn(createJarFile("test.jar")); - this.out = new TestPrintStream(this); - this.systemOut = System.out; - System.setOut(this.out); - LayerToolsJarMode.contextOverride = context; - System.setProperty("jarmode", "layertools"); - } - - @AfterEach - void restore() { - System.setOut(this.systemOut); - LayerToolsJarMode.contextOverride = null; - System.clearProperty("jarmode"); - } - - @Test - void mainWithNoParametersShowsHelp() { - new LayerToolsJarMode().run("layertools", NO_ARGS); - assertThat(this.out).hasSameContentAsResource("layertools-help-output.txt"); - } - - @Test - void mainWithArgRunsCommand() { - new LayerToolsJarMode().run("layertools", new String[] { "list" }); - assertThat(this.out).hasSameContentAsResource("layertools-list-output.txt"); - } - - @Test - void mainWithUnknownCommandShowsErrorAndHelp() { - new LayerToolsJarMode().run("layertools", new String[] { "invalid" }); - assertThat(this.out).hasSameContentAsResource("layertools-error-command-unknown-output.txt"); - } - - @Test - void mainWithUnknownOptionShowsErrorAndCommandHelp() { - new LayerToolsJarMode().run("layertools", new String[] { "extract", "--invalid" }); - assertThat(this.out).hasSameContentAsResource("layertools-error-option-unknown-output.txt"); - } - - @Test - void mainWithOptionMissingRequiredValueShowsErrorAndCommandHelp() { - new LayerToolsJarMode().run("layertools", new String[] { "extract", "--destination" }); - assertThat(this.out).hasSameContentAsResource("layertools-error-option-missing-value-output.txt"); - } - - private File createJarFile(String name) throws Exception { - File file = new File(this.temp, name); - try (ZipOutputStream jarOutputStream = new ZipOutputStream(new FileOutputStream(file))) { - jarOutputStream.putNextEntry(new JarEntry("META-INF/MANIFEST.MF")); - jarOutputStream.write(getFile("test-manifest.MF").getBytes()); - jarOutputStream.closeEntry(); - JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx"); - jarOutputStream.putNextEntry(indexEntry); - Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8); - writer.write("- \"0001\":\n"); - writer.write(" - \"BOOT-INF/lib/a.jar\"\n"); - writer.write(" - \"BOOT-INF/lib/b.jar\"\n"); - writer.write("- \"0002\":\n"); - writer.write(" - \"0002 BOOT-INF/lib/c.jar\"\n"); - writer.write("- \"0003\":\n"); - writer.write(" - \"BOOT-INF/lib/d.jar\"\n"); - writer.flush(); - } - return file; - } - - private String getFile(String fileName) throws Exception { - ClassPathResource resource = new ClassPathResource(fileName, getClass()); - InputStreamReader reader = new InputStreamReader(resource.getInputStream()); - return FileCopyUtils.copyToString(reader); - } - -} diff --git a/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt b/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt deleted file mode 100644 index 425ac1025e35..000000000000 --- a/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt +++ /dev/null @@ -1,10 +0,0 @@ -Error: Unknown command "invalid" - -Usage: - java -Djarmode=layertools -jar test.jar - -Available commands: - help Help about any command -Deprecated commands: - list List layers from the jar that can be extracted - extract Extracts layers from the jar for image creation diff --git a/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt b/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt deleted file mode 100644 index 18e87069ce09..000000000000 --- a/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt +++ /dev/null @@ -1,11 +0,0 @@ -Warning: This command is deprecated. Use '-Djarmode=tools extract --layers --launcher' instead. - -Error: Option "--destination" for the extract command requires a value - -Extracts layers from the jar for image creation - -Usage: - java -Djarmode=layertools -jar test.jar extract [options] [...] - -Options: - --destination string The destination to extract files to diff --git a/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt b/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt deleted file mode 100644 index 3b832dd642eb..000000000000 --- a/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt +++ /dev/null @@ -1,11 +0,0 @@ -Warning: This command is deprecated. Use '-Djarmode=tools extract --layers --launcher' instead. - -Error: Unknown option "--invalid" for the extract command - -Extracts layers from the jar for image creation - -Usage: - java -Djarmode=layertools -jar test.jar extract [options] [...] - -Options: - --destination string The destination to extract files to diff --git a/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt b/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt deleted file mode 100644 index daea473a9b69..000000000000 --- a/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt +++ /dev/null @@ -1,8 +0,0 @@ -Usage: - java -Djarmode=layertools -jar test.jar - -Available commands: - help Help about any command -Deprecated commands: - list List layers from the jar that can be extracted - extract Extracts layers from the jar for image creation diff --git a/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-list-output.txt b/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-list-output.txt deleted file mode 100644 index 03f7f88556fa..000000000000 --- a/loader/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-list-output.txt +++ /dev/null @@ -1,5 +0,0 @@ -Warning: This command is deprecated. Use '-Djarmode=tools list-layers' instead. - -0001 -0002 -0003 From ec9901c3374c8ae25cf9ce3d771c0ef0f1df3284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 22 Dec 2025 09:37:20 +0100 Subject: [PATCH 023/930] Upgrade to Flyway 11.19.1 Closes gh-48603 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 370c2629ec90..eaeb23c0e07a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -376,7 +376,7 @@ bom { javadoc("elasticsearch-java", version -> "https://javadoc.io/doc/co.elastic.clients/elasticsearch-java/%s/index.html".formatted(version), "co.elastic.clients.elasticsearch", "co.elastic.clients.transport") } } - library("Flyway", "11.14.1") { + library("Flyway", "11.19.1") { group("org.flywaydb") { modules = [ "flyway-commandline", From 644ac088868c4166b3bcad8cc27acfe089fc23b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 27 Nov 2025 22:48:39 +0530 Subject: [PATCH 024/930] Add support for context propagation in task execution Closes gh-48033 --- config/checkstyle/import-control.xml | 2 +- core/spring-boot-autoconfigure/build.gradle | 1 + .../task/TaskExecutionAutoConfiguration.java | 3 +- .../task/TaskExecutionProperties.java | 13 +++ .../task/TaskExecutorConfigurations.java | 15 +++ .../TaskExecutionAutoConfigurationTests.java | 94 +++++++++++++++++++ .../pages/actuator/observability.adoc | 5 +- 7 files changed, 129 insertions(+), 4 deletions(-) diff --git a/config/checkstyle/import-control.xml b/config/checkstyle/import-control.xml index 575b993240ee..87f3caee62b2 100644 --- a/config/checkstyle/import-control.xml +++ b/config/checkstyle/import-control.xml @@ -62,7 +62,7 @@ - + diff --git a/core/spring-boot-autoconfigure/build.gradle b/core/spring-boot-autoconfigure/build.gradle index a8686fe2be75..2070dd412b65 100644 --- a/core/spring-boot-autoconfigure/build.gradle +++ b/core/spring-boot-autoconfigure/build.gradle @@ -30,6 +30,7 @@ dependencies { optional("com.github.ben-manes.caffeine:caffeine") optional("org.aspectj:aspectjweaver") + optional("io.micrometer:context-propagation") optional("jakarta.servlet:jakarta.servlet-api") optional("javax.money:money-api") optional("org.springframework:spring-web") diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java index ab6eb21cf478..7e17fd524d9a 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java @@ -35,7 +35,8 @@ @ConditionalOnClass(ThreadPoolTaskExecutor.class) @AutoConfiguration @EnableConfigurationProperties(TaskExecutionProperties.class) -@Import({ TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class, +@Import({ TaskExecutorConfigurations.TaskExecutorContextPropagationConfiguration.class, + TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class, TaskExecutorConfigurations.SimpleAsyncTaskExecutorBuilderConfiguration.class, TaskExecutorConfigurations.TaskExecutorConfiguration.class, TaskExecutorConfigurations.BootstrapExecutorConfiguration.class }) diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java index 144fbb60e67c..e2dbfcab0612 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java @@ -44,6 +44,11 @@ public class TaskExecutionProperties { */ private Mode mode = Mode.AUTO; + /** + * Whether to propagate the current context to task executions. + */ + private boolean propagateContext; + /** * Prefix to use for the names of newly created threads. */ @@ -69,6 +74,14 @@ public void setMode(Mode mode) { this.mode = mode; } + public boolean getPropagateContext() { + return this.propagateContext; + } + + public void setPropagateContext(boolean propagateContext) { + this.propagateContext = propagateContext; + } + public String getThreadNamePrefix() { return this.threadNamePrefix; } diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java index a519c65374a9..3909065231b1 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.concurrent.Executor; +import io.micrometer.context.ContextSnapshot; import org.jspecify.annotations.Nullable; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; @@ -29,6 +30,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor; 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.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; @@ -47,6 +49,7 @@ import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskExecutor; import org.springframework.core.task.support.CompositeTaskDecorator; +import org.springframework.core.task.support.ContextPropagatingTaskDecorator; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @@ -68,6 +71,18 @@ class TaskExecutorConfigurations { return (!taskDecorators.isEmpty()) ? new CompositeTaskDecorator(taskDecorators) : null; } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ContextSnapshot.class) + static class TaskExecutorContextPropagationConfiguration { + + @Bean + @ConditionalOnProperty(name = "spring.task.execution.propagate-context", havingValue = "true") + ContextPropagatingTaskDecorator contextPropagatingTaskDecorator() { + return new ContextPropagatingTaskDecorator(); + } + + } + @Configuration(proxyBeanMethods = false) @Conditional(OnExecutorCondition.class) @Import({ AsyncConfigurerWrapperConfiguration.class, AsyncConfigurerConfiguration.class }) diff --git a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java index 40d6fb26e7a6..862452952f77 100644 --- a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java +++ b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java @@ -16,6 +16,10 @@ package org.springframework.boot.autoconfigure.task; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; @@ -24,6 +28,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import io.micrometer.context.ThreadLocalAccessor; import org.assertj.core.api.InstanceOfAssertFactories; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; @@ -42,6 +47,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.testsupport.classpath.resources.WithResource; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -50,6 +56,7 @@ import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskExecutor; import org.springframework.core.task.support.CompositeTaskDecorator; +import org.springframework.core.task.support.ContextPropagatingTaskDecorator; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.AsyncConfigurer; @@ -253,6 +260,35 @@ void simpleAsyncTaskExecutorBuilderUsesVirtualThreadsWhenEnabled() { }); } + @Test + @WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor", + content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests$TestThreadLocalAccessor") + void asyncTaskExecutorShouldNotNotRegisterContextPropagatingTaskDecoratorByDefault() { + this.contextRunner.withUserConfiguration(AsyncConfiguration.class, TestBean.class).run((context) -> { + assertThat(context).doesNotHaveBean(ContextPropagatingTaskDecorator.class); + TestBean bean = context.getBean(TestBean.class); + TestThreadLocalHolder.setValue("from-context"); + String text = bean.echoContext().get(); + assertThat(text).contains("task-").endsWith("null"); + }); + + } + + @Test + @WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor", + content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests$TestThreadLocalAccessor") + void asyncTaskExecutorWhenContextPropagationIsEnabledShouldRegisterBean() { + this.contextRunner.withUserConfiguration(AsyncConfiguration.class, TestBean.class) + .withPropertyValues("spring.task.execution.propagate-context=true") + .run((context) -> { + assertThat(context).hasSingleBean(ContextPropagatingTaskDecorator.class); + TestBean bean = context.getBean(TestBean.class); + TestThreadLocalHolder.setValue("from-context"); + String text = bean.echoContext().get(); + assertThat(text).contains("task-").endsWith("from-context"); + }); + } + @Test void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() { this.contextRunner.withBean("customTaskExecutor", Executor.class, SyncTaskExecutor::new).run((context) -> { @@ -591,6 +627,14 @@ private String virtualThreadName(SimpleAsyncTaskExecutor taskExecutor) throws In return thread.getName(); } + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor", + content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests.TestThreadLocalAccessor") + @interface WithThreadLocalAccessor { + + } + @Configuration(proxyBeanMethods = false) static class CustomThreadPoolTaskExecutorBuilderConfig { @@ -622,6 +666,56 @@ Future echo(String text) { return CompletableFuture.completedFuture(Thread.currentThread().getName() + " " + text); } + @Async + Future echoContext() { + return CompletableFuture + .completedFuture(Thread.currentThread().getName() + " " + TestThreadLocalHolder.getValue()); + } + + } + + static class TestThreadLocalHolder { + + private static final ThreadLocal holder = new ThreadLocal<>(); + + static void setValue(String value) { + holder.set(value); + } + + static String getValue() { + return holder.get(); + } + + static void reset() { + holder.remove(); + } + + } + + public static class TestThreadLocalAccessor implements ThreadLocalAccessor { + + static final String KEY = "test.threadlocal"; + + @Override + public Object key() { + return KEY; + } + + @Override + public String getValue() { + return TestThreadLocalHolder.getValue(); + } + + @Override + public void setValue(String value) { + TestThreadLocalHolder.setValue(value); + } + + @Override + public void setValue() { + TestThreadLocalHolder.reset(); + } + } } diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc index e76aeff3c9bd..bff522b6396a 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc @@ -29,8 +29,9 @@ Observability support relies on the https://github.com/micrometer-metrics/contex By default, javadoc:java.lang.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. -If you're working with javadoc:org.springframework.scheduling.annotation.Async[format=annotation] methods or use an javadoc:org.springframework.core.task.AsyncTaskExecutor[], you have to register the javadoc:org.springframework.core.task.support.ContextPropagatingTaskDecorator[] on the executor, otherwise the observability context is lost when switching threads. -This can be done using this configuration: +If you're working with javadoc:org.springframework.scheduling.annotation.Async[format=annotation] methods and the javadoc:org.springframework.core.task.AsyncTaskExecutor[] is auto-configured, you have to opt-in for context propagation using the configprop:spring.task.execution.propagate-context[] property. + +If you are configuring the javadoc:org.springframework.core.task.AsyncTaskExecutor[] yourself, then you need to register a javadoc:org.springframework.core.task.support.ContextPropagatingTaskDecorator[] bean, as shown in the following example: include-code::ContextPropagationConfiguration[] From a4e49c1e0a4c278fe284042517d560bb908836c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 23 Dec 2025 17:22:52 +0100 Subject: [PATCH 025/930] Revisit condition to skip AOT processing of tests This commit stop skipping AOT processing of tests when tests execution is skipped. With this commit, only the presence of maven.test.skip makes the processing to skip. This harmonize the behavior with other standard plugins that do react to the property. For instance, the compiler plugin will not compile the tests if that property is set, but would do so if skipTests was provided. Closes gh-46929 --- .../modules/maven-plugin/pages/aot.adoc | 2 + .../springframework/boot/maven/AotTests.java | 10 +++ .../intTest/projects/aot-test-skip/pom.xml | 67 +++++++++++++++++++ .../main/java/org/test/SampleApplication.java | 29 ++++++++ .../java/org/test/SampleApplicationTests.java | 53 +++++++++++++++ .../boot/maven/ProcessTestAotMojo.java | 2 +- 6 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-skip/pom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-skip/src/main/java/org/test/SampleApplication.java create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-skip/src/test/java/org/test/SampleApplicationTests.java diff --git a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/aot.adoc b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/aot.adoc index fcbec79deefe..5e92a89f7467 100644 --- a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/aot.adoc +++ b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/aot.adoc @@ -92,6 +92,8 @@ include::partial$goals/process-aot.adoc[leveloffset=+1] The AOT engine can be applied to JUnit 5 tests that use Spring's Test Context Framework. Those tests are processed by the AOT engine and are then executed in a native image. +NOTE: Tests are not processed when the regular tests are skipped using the `maven.test.skip` property. + Just like <>, the `spring-boot-starter-parent` defines a `nativeTest` profile that can be used to streamline the steps required to execute your tests in a native image. The `nativeTest` profile configures the following: diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AotTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AotTests.java index 312c77f1917d..ee66d713a5c6 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AotTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AotTests.java @@ -201,6 +201,16 @@ void whenAotTestRunsSourcesAndResourcesAreGenerated(MavenBuild mavenBuild) { }); } + @TestTemplate + void whenTestAotRunsWithTestSkipItIsAlsoSkipped(MavenBuild mavenBuild) { + mavenBuild.project("aot-test-skip").goals("test").execute((project) -> { + Path aotDirectory = project.toPath().resolve("target/spring-aot/test"); + assertThat(aotDirectory).doesNotExist(); + Path testClassesDirectory = project.toPath().resolve("target/test-classes"); + assertThat(testClassesDirectory.resolve("META-INF").resolve("native-image")).doesNotExist(); + }); + } + @TestTemplate void whenTestAotRunsWithDevtoolsInClasspathItIsExcluded(MavenBuild mavenBuild) { mavenBuild.project("aot-test-exclude-devtools").goals("process-test-classes").execute((project) -> { diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-skip/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-skip/pom.xml new file mode 100644 index 000000000000..e10124f2bfd3 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-skip/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aot-test-skip + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + true + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + process-test-aot + + + + + + + + + org.springframework.boot + spring-boot + @project.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + org.springframework + spring-test + @spring-framework.version@ + test + + + org.springframework.boot + spring-boot-test + @project.version@ + test + + + org.assertj + assertj-core + @assertj.version@ + test + + + org.junit.jupiter + junit-jupiter + @junit-jupiter.version@ + test + + + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-skip/src/main/java/org/test/SampleApplication.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-skip/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ee3c16b77dc2 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-skip/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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 org.springframework.boot.SpringApplication; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class SampleApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleApplication.class, args); + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-skip/src/test/java/org/test/SampleApplicationTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-skip/src/test/java/org/test/SampleApplicationTests.java new file mode 100644 index 000000000000..436f6c878060 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/aot-test-skip/src/test/java/org/test/SampleApplicationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +@SpringJUnitConfig +class SampleApplicationTests { + + @Autowired + private MyBean myBean; + + @Test + void contextLoads() { + assertThat(this.myBean).isNotNull(); + } + + @Configuration + static class MyConfig { + + @Bean + MyBean myBean() { + return new MyBean(); + } + + } + + static class MyBean { + + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessTestAotMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessTestAotMojo.java index b88e15ce77ea..985d0c802278 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessTestAotMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessTestAotMojo.java @@ -124,7 +124,7 @@ protected void executeAot() throws Exception { getLog().debug("process-test-aot goal could not be applied to pom project."); return; } - if (Boolean.getBoolean("skipTests") || Boolean.getBoolean("maven.test.skip")) { + if (Boolean.getBoolean("maven.test.skip")) { getLog().info("Skipping AOT test processing since tests are skipped"); return; } From a00f6c0b52a5e7031ef41b7a3e0b5bcb9cab732f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 24 Dec 2025 11:48:34 +0100 Subject: [PATCH 026/930] Polish "Start development of Spring Boot 4.1.x" --- .github/workflows/build-and-deploy-snapshot.yml | 2 +- .github/workflows/release-milestone.yml | 4 ++-- .github/workflows/release.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index 61da3998f773..efd72bf49ff8 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -30,7 +30,7 @@ jobs: - name: Deploy uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4 with: - build-name: ${{ vars.COMMERCIAL && format('spring-boot-commercial-{0}', '4.0.x') || format('spring-boot-{0}', '4.0.x') }} + build-name: ${{ vars.COMMERCIAL && format('spring-boot-commercial-{0}', '4.1.x') || format('spring-boot-{0}', '4.1.x') }} folder: 'deployment-repository' password: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_ARTIFACTORY_PASSWORD || secrets.ARTIFACTORY_PASSWORD }} project: ${{ vars.COMMERCIAL && 'spring' }} diff --git a/.github/workflows/release-milestone.yml b/.github/workflows/release-milestone.yml index 2780b25565b7..6a46387389bf 100644 --- a/.github/workflows/release-milestone.yml +++ b/.github/workflows/release-milestone.yml @@ -2,8 +2,8 @@ name: Release Milestone on: push: tags: - - v4.0.0-M[0-9] - - v4.0.0-RC[0-9] + - v4.1.0-M[0-9] + - v4.1.0-RC[0-9] permissions: contents: read concurrency: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24b9555bd9f8..2529ebe8cca1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Release on: push: tags: - - v4.0.[0-9]+ + - v4.1.[0-9]+ permissions: contents: read concurrency: From 54fc77b51fcdfe29436bb8617d381e4d59fd0c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 24 Dec 2025 14:49:12 +0100 Subject: [PATCH 027/930] Prevent default WebServiceMessageFactory to be created This commit improves the builder to provide the message factory upfront rather than setting it after the WebServiceTemplate has been instantiated. By providing the factory upfront (if it is set), it prevents the default strategy to be created first to be then erased by the custom factory. Closes gh-48615 --- .../boot/webservices/client/WebServiceTemplateBuilder.java | 3 ++- .../webservices/client/WebServiceTemplateBuilderTests.java | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/module/spring-boot-webservices/src/main/java/org/springframework/boot/webservices/client/WebServiceTemplateBuilder.java b/module/spring-boot-webservices/src/main/java/org/springframework/boot/webservices/client/WebServiceTemplateBuilder.java index 916d52eafbe2..8a5513e276bd 100644 --- a/module/spring-boot-webservices/src/main/java/org/springframework/boot/webservices/client/WebServiceTemplateBuilder.java +++ b/module/spring-boot-webservices/src/main/java/org/springframework/boot/webservices/client/WebServiceTemplateBuilder.java @@ -454,7 +454,8 @@ public WebServiceTemplateBuilder setDestinationProvider(DestinationProvider dest * @see #configure(WebServiceTemplate) */ public WebServiceTemplate build() { - return configure(new WebServiceTemplate()); + return configure( + (this.messageFactory != null) ? new WebServiceTemplate(this.messageFactory) : new WebServiceTemplate()); } /** diff --git a/module/spring-boot-webservices/src/test/java/org/springframework/boot/webservices/client/WebServiceTemplateBuilderTests.java b/module/spring-boot-webservices/src/test/java/org/springframework/boot/webservices/client/WebServiceTemplateBuilderTests.java index 049bff9d3d38..8d39b0c6195e 100644 --- a/module/spring-boot-webservices/src/test/java/org/springframework/boot/webservices/client/WebServiceTemplateBuilderTests.java +++ b/module/spring-boot-webservices/src/test/java/org/springframework/boot/webservices/client/WebServiceTemplateBuilderTests.java @@ -38,6 +38,7 @@ import org.springframework.ws.client.core.WebServiceTemplate; import org.springframework.ws.client.support.destination.DestinationProvider; import org.springframework.ws.client.support.interceptor.ClientInterceptor; +import org.springframework.ws.soap.saaj.SaajSoapMessageFactory; import org.springframework.ws.transport.WebServiceMessageSender; import org.springframework.ws.transport.http.ClientHttpRequestMessageSender; import org.springframework.ws.transport.http.HttpUrlConnectionMessageSender; @@ -318,6 +319,12 @@ void setTransformerFactoryClass() { then(template).should().setTransformerFactoryClass(SAXTransformerFactory.class); } + @Test + void buildShouldDetectWebServiceMessageFactory() { + WebServiceTemplate webServiceTemplate = this.builder.build(); + assertThat(webServiceTemplate.getMessageFactory()).isInstanceOf(SaajSoapMessageFactory.class); + } + @Test void setWebServiceMessageFactory() { WebServiceMessageFactory messageFactory = mock(WebServiceMessageFactory.class); From 3948e7c4da078e04503c26ffb61f8fa6635777f6 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 29 Dec 2025 13:41:12 +0100 Subject: [PATCH 028/930] Fix message converter customizers order Prior to this commit, gh-48310 separated client and server message converter configurations by switching from message converter instances as beans in the application context, to server/client customizers that are applied to the `HttpMessageConverters` instances while being built. This change did not order the new ClientHttpMessageConvertersCustomizer or ServerHttpMessageConvertersCustomizer, letting those being at the "lowest precedence" default. As customizers, this means they are applied last and custom instances cannot take over. This commit ensures that such customizers provided by Spring Boot are now ordered at "0" to let applications ones take over. Fixes gh-48609 --- ...sonHttpMessageConvertersConfiguration.java | 2 + ...on2HttpMessageConvertersConfiguration.java | 3 + ...sonHttpMessageConvertersConfiguration.java | 3 + ...onbHttpMessageConvertersConfiguration.java | 2 + ...ionHttpMessageConvertersConfiguration.java | 2 + ...ssageConvertersAutoConfigurationTests.java | 68 +++++++++++++++++-- 6 files changed, 76 insertions(+), 4 deletions(-) diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java index 34f1e551e655..5c9c7cb522ad 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java @@ -27,6 +27,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; import org.springframework.http.converter.json.GsonHttpMessageConverter; @@ -48,6 +49,7 @@ class GsonHttpMessageConvertersConfiguration { static class GsonHttpMessageConverterConfiguration { @Bean + @Order(0) @ConditionalOnMissingBean(GsonHttpMessageConverter.class) GsonHttpConvertersCustomizer gsonHttpMessageConvertersCustomizer(Gson gson) { return new GsonHttpConvertersCustomizer(gson); diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java index 151ef3237e1f..c3ec3b21bcd6 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.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.core.annotation.Order; import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; @@ -51,6 +52,7 @@ class Jackson2HttpMessageConvertersConfiguration { static class MappingJackson2HttpMessageConverterConfiguration { @Bean + @Order(0) @ConditionalOnMissingBean(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class) Jackson2JsonMessageConvertersCustomizer jackson2HttpMessageConvertersCustomizer(ObjectMapper objectMapper) { return new Jackson2JsonMessageConvertersCustomizer(objectMapper); @@ -64,6 +66,7 @@ Jackson2JsonMessageConvertersCustomizer jackson2HttpMessageConvertersCustomizer( protected static class MappingJackson2XmlHttpMessageConverterConfiguration { @Bean + @Order(0) @ConditionalOnMissingBean(org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter.class) Jackson2XmlMessageConvertersCustomizer mappingJackson2XmlHttpMessageConverter( Jackson2ObjectMapperBuilder builder) { diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JacksonHttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JacksonHttpMessageConvertersConfiguration.java index 9207b1986c1d..25107d944a9b 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JacksonHttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JacksonHttpMessageConvertersConfiguration.java @@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; @@ -47,6 +48,7 @@ class JacksonHttpMessageConvertersConfiguration { static class JacksonJsonHttpMessageConverterConfiguration { @Bean + @Order(0) @ConditionalOnMissingBean(value = JacksonJsonHttpMessageConverter.class, ignoredType = { "org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter", "org.springframework.data.rest.webmvc.alps.AlpsJacksonJsonHttpMessageConverter" }) @@ -62,6 +64,7 @@ JacksonJsonHttpMessageConvertersCustomizer jacksonJsonHttpMessageConvertersCusto protected static class JacksonXmlHttpMessageConverterConfiguration { @Bean + @Order(0) @ConditionalOnMissingBean(JacksonXmlHttpMessageConverter.class) JacksonXmlHttpMessageConvertersCustomizer jacksonXmlHttpMessageConvertersCustomizer(XmlMapper xmlMapper) { return new JacksonXmlHttpMessageConvertersCustomizer(xmlMapper); diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java index fa6a182e7f3c..b246446ba3a5 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.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.core.annotation.Order; import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; import org.springframework.http.converter.json.JsonbHttpMessageConverter; @@ -47,6 +48,7 @@ class JsonbHttpMessageConvertersConfiguration { static class JsonbHttpMessageConverterConfiguration { @Bean + @Order(0) @ConditionalOnMissingBean(JsonbHttpMessageConverter.class) JsonbHttpMessageConvertersCustomizer jsonbHttpMessageConvertersCustomizer(Jsonb jsonb) { return new JsonbHttpMessageConvertersCustomizer(jsonb); diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java index 6129b0edbec3..5c4ca8212540 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java @@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.core.io.ResourceLoader; import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; @@ -42,6 +43,7 @@ class KotlinSerializationHttpMessageConvertersConfiguration { @Bean + @Order(0) @ConditionalOnMissingBean(KotlinSerializationJsonHttpMessageConverter.class) KotlinSerializationJsonConvertersCustomizer kotlinSerializationJsonConvertersCustomizer(Json json, ResourceLoader resourceLoader) { diff --git a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java index a6c9e258c50a..a5a5adf5a747 100644 --- a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java +++ b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.http.converter.autoconfigure; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -52,11 +53,16 @@ import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.mediatype.hal.forms.HalFormsHttpMessageConverter; import org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverters; import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; @@ -103,6 +109,14 @@ void jacksonDefaultConverter() { }); } + @Test + void jacksonServerCustomizer() { + this.contextRunner.withUserConfiguration(CustomJsonConverterConfig.class).run((context) -> { + assertConverterIsNotRegistered(context, JacksonJsonHttpMessageConverter.class); + assertConverterIsRegistered(context, CustomConverter.class); + }); + } + @Test void jacksonConverterWithBuilder() { this.contextRunner.withUserConfiguration(JacksonJsonMapperBuilderConfig.class).run((context) -> { @@ -455,16 +469,16 @@ private void assertBeanExists(AssertableApplicationContext context, Class typ private HttpMessageConverters getClientConverters(ApplicationContext context) { ClientBuilder clientBuilder = HttpMessageConverters.forClient().registerDefaults(); - context.getBeansOfType(ClientHttpMessageConvertersCustomizer.class) - .values() + context.getBeanProvider(ClientHttpMessageConvertersCustomizer.class) + .orderedStream() .forEach((customizer) -> customizer.customize(clientBuilder)); return clientBuilder.build(); } private HttpMessageConverters getServerConverters(ApplicationContext context) { ServerBuilder serverBuilder = HttpMessageConverters.forServer().registerDefaults(); - context.getBeansOfType(ServerHttpMessageConvertersCustomizer.class) - .values() + context.getBeanProvider(ServerHttpMessageConvertersCustomizer.class) + .orderedStream() .forEach((customizer) -> customizer.customize(serverBuilder)); return serverBuilder.build(); } @@ -503,6 +517,26 @@ JsonMapper jsonMapper() { } + @Configuration(proxyBeanMethods = false) + static class CustomJsonConverterConfig { + + @Bean + JsonMapper jsonMapper() { + return new JsonMapper(); + } + + @Bean + ServerHttpMessageConvertersCustomizer jsonServerCustomizer() { + return (configurer) -> configurer.withJsonConverter(new CustomConverter(MediaType.APPLICATION_JSON)); + } + + @Bean + ClientHttpMessageConvertersCustomizer jsonClientCustomizer() { + return (configurer) -> configurer.withJsonConverter(new CustomConverter(MediaType.APPLICATION_JSON)); + } + + } + @Configuration(proxyBeanMethods = false) static class JacksonJsonMapperBuilderConfig { @@ -640,4 +674,30 @@ TypeConstrainedJacksonJsonHttpMessageConverter typeConstrainedConverter() { } + @SuppressWarnings("NullAway") + static class CustomConverter extends AbstractHttpMessageConverter { + + CustomConverter(MediaType supportedMediaType) { + super(supportedMediaType); + } + + @Override + protected boolean supports(Class clazz) { + return true; + } + + @Override + protected Object readInternal(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + return null; + } + + @Override + protected void writeInternal(Object o, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + } + + } + } From 12652a313e1aa155d978a6c540c9517c24a3fddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 30 Dec 2025 17:33:03 +0100 Subject: [PATCH 029/930] Deprecate Derby support since Apache Derby is retired Closes gh-48567 --- .../how-to/pages/data-initialization.adoc | 2 +- .../antora/modules/reference/pages/data/sql.adoc | 4 ++-- .../DevToolsDataSourceAutoConfiguration.java | 4 ++++ ...lsPooledDataSourceAutoConfigurationTests.java | 2 ++ .../boot/jdbc/DatabaseDriver.java | 2 ++ .../boot/jdbc/EmbeddedDatabaseConnection.java | 3 +++ .../boot/jdbc/DatabaseDriverTests.java | 16 ++++++++++++++-- .../jdbc/EmbeddedDatabaseConnectionTests.java | 3 +++ 8 files changed, 31 insertions(+), 5 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/data-initialization.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/data-initialization.adoc index 42050f05a7c7..c3fbe3ae7065 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/data-initialization.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/data-initialization.adoc @@ -14,7 +14,7 @@ You can set configprop:spring.jpa.hibernate.ddl-auto[] to control Hibernate's da Supported values are `none`, `validate`, `update`, `create`, and `create-drop`. Spring Boot chooses a default value for you based on whether you are using an embedded database. An embedded database is identified by looking at the javadoc:java.sql.Connection[] type and JDBC url. -`hsqldb`, `h2`, or `derby` are embedded databases and others are not. +`hsqldb`, `h2`, or `derby` (deprecated) are embedded databases and others are not. If an embedded database is identified and no schema manager (Flyway or Liquibase) has been detected, `ddl-auto` defaults to `create-drop`. In all other cases, it defaults to `none`. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc index accaec7f94b3..33b0ee271658 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc @@ -25,7 +25,7 @@ You need to populate your database when your application starts and be prepared TIP: The "`How-to Guides`" section includes a xref:how-to:data-initialization.adoc[section on how to initialize a database]. -Spring Boot can auto-configure embedded https://www.h2database.com[H2], https://hsqldb.org/[HSQL], and https://db.apache.org/derby/[Derby] databases. +Spring Boot can auto-configure embedded https://www.h2database.com[H2], https://hsqldb.org/[HSQL], and https://db.apache.org/derby/[Derby] (deprecated) databases. You need not provide any connection URLs. You need only include a build dependency to the embedded database that you want to use. If there are multiple embedded databases on the classpath, set the configprop:spring.datasource.embedded-database-connection[] configuration property to control which one is used. @@ -283,7 +283,7 @@ NOTE: For more details, check the {url-spring-data-jpa-docs}/envers.html[Spring [[data.sql.jpa-and-spring-data.creating-and-dropping]] === Creating and Dropping JPA Databases -By default, JPA databases are automatically created *only* if you use an embedded database (H2, HSQL, or Derby). +By default, JPA databases are automatically created *only* if you use an embedded database, that is H2, HSQL, or Derby (deprecated). You can explicitly configure JPA settings by using `+spring.jpa.*+` properties. For example, to create and drop tables you can add the following line to your `application.properties`: diff --git a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java index fbf29e608642..3326fc7541ae 100644 --- a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java +++ b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java @@ -119,6 +119,10 @@ public void destroy() throws Exception { private enum InMemoryDatabase { + /* + * @deprecated since 4.1.0 for removal in 4.3.0 as Derby is EOL. + */ + @Deprecated(since = "4.1.0", forRemoval = true) DERBY(null, Set.of("org.apache.derby.jdbc.EmbeddedDriver"), (dataSource) -> { String url; try (Connection connection = dataSource.getConnection()) { diff --git a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsPooledDataSourceAutoConfigurationTests.java b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsPooledDataSourceAutoConfigurationTests.java index f61d0eb68310..cc9826b49aed 100644 --- a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsPooledDataSourceAutoConfigurationTests.java +++ b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsPooledDataSourceAutoConfigurationTests.java @@ -120,6 +120,7 @@ void inMemoryHsqlIsShutdown() throws Exception { } @Test + @Deprecated(since = "4.1.0", forRemoval = true) void derbyClientIsNotShutdown() throws Exception { try (ConfigurableApplicationContext context = getContext( () -> createContext("org.apache.derby.jdbc.ClientDriver", "jdbc:derby://localhost", @@ -131,6 +132,7 @@ void derbyClientIsNotShutdown() throws Exception { } @Test + @Deprecated(since = "4.1.0", forRemoval = true) void inMemoryDerbyIsShutdown() throws Exception { try (ConfigurableApplicationContext context = getContext( () -> createContext("org.apache.derby.jdbc.EmbeddedDriver", "jdbc:derby:memory:test;create=true", diff --git a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DatabaseDriver.java b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DatabaseDriver.java index 0fbaeaa2cfc2..d7c589209b51 100644 --- a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DatabaseDriver.java +++ b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DatabaseDriver.java @@ -44,7 +44,9 @@ public enum DatabaseDriver { /** * Apache Derby. + * @deprecated since 4.1.0 for removal in 4.3.0 as Derby is EOL. */ + @Deprecated(since = "4.1.0", forRemoval = true) DERBY("Apache Derby", "org.apache.derby.jdbc.EmbeddedDriver", "org.apache.derby.jdbc.EmbeddedXADataSource", "SELECT 1 FROM SYSIBM.SYSDUMMY1"), diff --git a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnection.java b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnection.java index b5809c6c023e..b413df5fdacd 100644 --- a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnection.java +++ b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnection.java @@ -57,7 +57,9 @@ public enum EmbeddedDatabaseConnection { /** * Derby Database Connection. + * @deprecated since 4.1.0 for removal in 4.3.0 as Derby is EOL. */ + @Deprecated(since = "4.1.0", forRemoval = true) DERBY("jdbc:derby:memory:%s;create=true"), /** @@ -83,6 +85,7 @@ public enum EmbeddedDatabaseConnection { * Returns the driver class name. * @return the driver class name */ + @SuppressWarnings("removal") public @Nullable String getDriverClassName() { // See https://github.com/spring-projects/spring-boot/issues/32865 return switch (this) { diff --git a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DatabaseDriverTests.java b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DatabaseDriverTests.java index 479098dfe982..7ecb8727a873 100644 --- a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DatabaseDriverTests.java +++ b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DatabaseDriverTests.java @@ -63,7 +63,6 @@ void unknownOnNullProductName() { @Test void databaseProductNameLookups() { assertThat(DatabaseDriver.fromProductName("newone")).isEqualTo(DatabaseDriver.UNKNOWN); - assertThat(DatabaseDriver.fromProductName("Apache Derby")).isEqualTo(DatabaseDriver.DERBY); assertThat(DatabaseDriver.fromProductName("H2")).isEqualTo(DatabaseDriver.H2); assertThat(DatabaseDriver.fromProductName("HDB")).isEqualTo(DatabaseDriver.HANA); assertThat(DatabaseDriver.fromProductName("HSQL Database Engine")).isEqualTo(DatabaseDriver.HSQLDB); @@ -87,10 +86,16 @@ void databaseProductNameLookups() { assertThat(DatabaseDriver.fromProductName("ClickHouse")).isEqualTo(DatabaseDriver.CLICKHOUSE); } + @Test + @Deprecated(since = "4.1.0", forRemoval = true) + @SuppressWarnings("removal") + void deprecatedDatabaseProductNameLookups() { + assertThat(DatabaseDriver.fromProductName("Apache Derby")).isEqualTo(DatabaseDriver.DERBY); + } + @Test void databaseJdbcUrlLookups() { assertThat(DatabaseDriver.fromJdbcUrl("jdbc:newone://localhost")).isEqualTo(DatabaseDriver.UNKNOWN); - assertThat(DatabaseDriver.fromJdbcUrl("jdbc:derby:sample")).isEqualTo(DatabaseDriver.DERBY); assertThat(DatabaseDriver.fromJdbcUrl("jdbc:h2:~/sample")).isEqualTo(DatabaseDriver.H2); assertThat(DatabaseDriver.fromJdbcUrl("jdbc:hsqldb:hsql://localhost")).isEqualTo(DatabaseDriver.HSQLDB); assertThat(DatabaseDriver.fromJdbcUrl("jdbc:sqlite:sample.db")).isEqualTo(DatabaseDriver.SQLITE); @@ -125,4 +130,11 @@ void databaseJdbcUrlLookups() { .isEqualTo(DatabaseDriver.AWS_WRAPPER); } + @Test + @Deprecated(since = "4.1.0", forRemoval = true) + @SuppressWarnings("removal") + void deprecatedDatabaseJdbcUrlLookups() { + assertThat(DatabaseDriver.fromJdbcUrl("jdbc:derby:sample")).isEqualTo(DatabaseDriver.DERBY); + } + } diff --git a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnectionTests.java b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnectionTests.java index 5e33d9d03314..2d51114a4258 100644 --- a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnectionTests.java +++ b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnectionTests.java @@ -51,6 +51,8 @@ void h2CustomDatabaseName() { } @Test + @Deprecated(since = "4.1.0", forRemoval = true) + @SuppressWarnings("removal") void derbyCustomDatabaseName() { assertThat(EmbeddedDatabaseConnection.DERBY.getUrl("myderbydb")) .isEqualTo("jdbc:derby:memory:myderbydb;create=true"); @@ -80,6 +82,7 @@ void isEmbeddedWithDriverAndUrl(String driverClassName, String url, boolean embe assertThat(EmbeddedDatabaseConnection.isEmbedded(driverClassName, url)).isEqualTo(embedded); } + @SuppressWarnings("removal") static Object[] embeddedDriverAndUrlParameters() { return new Object[] { new Object[] { EmbeddedDatabaseConnection.H2.getDriverClassName(), "jdbc:h2:~/test", false }, From 21cf377f7e6ba7b0c85a8a7675ab85219f36db82 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 5 Jan 2026 16:33:32 -0800 Subject: [PATCH 030/930] Add 'WAR_SOURCE_DIRECTORY' environment variable support Add an escape hatch for users that deviate from the standard `src/main/webbapp` directory structure. Fixes gh-23829 --- .../modules/reference/pages/web/servlet.adoc | 2 + .../boot/web/server/servlet/DocumentRoot.java | 24 ++++++++++-- .../web/server/servlet/DocumentRootTests.java | 26 ++++++++++++- .../JspTemplateAvailabilityProvider.java | 22 ++++++++++- .../JspTemplateAvailabilityProviderTests.java | 38 ++++++++++++++++++- 5 files changed, 104 insertions(+), 8 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc index 35f8e9a372ee..5a53f1d36fcc 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc @@ -745,3 +745,5 @@ JSPs are not supported when using an executable jar. * Creating a custom `error.jsp` page does not override the default view for xref:web/servlet.adoc#web.servlet.spring-mvc.error-handling[error handling]. xref:web/servlet.adoc#web.servlet.spring-mvc.error-handling.error-pages[Custom error pages] should be used instead. + +* If you run your application using `mvn spring-boot:run` or `gradle bootRun` and you deviate from the standard `src/main/webbapp` directory structure you may need to set a `WAR_SOURCE_DIRECTORY` environment variable so that Spring Boot can find your JSPs. diff --git a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/servlet/DocumentRoot.java b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/servlet/DocumentRoot.java index 18ba6be49c4d..69ed87eae5b5 100644 --- a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/servlet/DocumentRoot.java +++ b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/servlet/DocumentRoot.java @@ -23,6 +23,7 @@ import java.security.CodeSource; import java.util.Arrays; import java.util.Locale; +import java.util.function.Function; import org.apache.commons.logging.Log; import org.jspecify.annotations.Nullable; @@ -35,14 +36,29 @@ */ public class DocumentRoot { - private static final String[] COMMON_DOC_ROOTS = { "src/main/webapp", "public", "static" }; + private static final String WAR_SOURCE_DIRECTORY_ENVIRONMENT_VARIABLE = "WAR_SOURCE_DIRECTORY"; private final Log logger; + private final File rootDirectory; + + private final String[] commonDocRoots; + private @Nullable File directory; public DocumentRoot(Log logger) { + this(logger, new File("."), System::getenv); + } + + DocumentRoot(Log logger, File rootDirectory, Function systemEnvironment) { this.logger = logger; + this.rootDirectory = rootDirectory; + this.commonDocRoots = new String[] { getWarSourceDirectory(systemEnvironment), "public", "static" }; + } + + private static String getWarSourceDirectory(Function systemEnvironment) { + String name = systemEnvironment.apply(WAR_SOURCE_DIRECTORY_ENVIRONMENT_VARIABLE); + return (name != null) ? name : "src/main/webapp"; } @Nullable File getDirectory() { @@ -137,8 +153,8 @@ else if (this.logger.isDebugEnabled()) { } private @Nullable File getCommonDocumentRoot() { - for (String commonDocRoot : COMMON_DOC_ROOTS) { - File root = new File(commonDocRoot); + for (String commonDocRoot : this.commonDocRoots) { + File root = new File(this.rootDirectory, commonDocRoot); if (root.exists() && root.isDirectory()) { return root.getAbsoluteFile(); } @@ -147,7 +163,7 @@ else if (this.logger.isDebugEnabled()) { } private void logNoDocumentRoots() { - this.logger.debug("None of the document roots " + Arrays.asList(COMMON_DOC_ROOTS) + this.logger.debug("None of the document roots " + Arrays.asList(this.commonDocRoots) + " point to a directory and will be ignored."); } diff --git a/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/servlet/DocumentRootTests.java b/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/servlet/DocumentRootTests.java index 86a61ec515c7..8712fe6ba825 100644 --- a/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/servlet/DocumentRootTests.java +++ b/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/servlet/DocumentRootTests.java @@ -20,7 +20,10 @@ import java.net.URL; import java.security.CodeSource; import java.security.cert.Certificate; +import java.util.HashMap; +import java.util.Map; +import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -34,11 +37,13 @@ */ class DocumentRootTests { + private static final Log logger = LogFactory.getLog(DocumentRootTests.class); + @TempDir @SuppressWarnings("NullAway.Init") File tempDir; - private final DocumentRoot documentRoot = new DocumentRoot(LogFactory.getLog(getClass())); + private final DocumentRoot documentRoot = new DocumentRoot(logger); @Test void explodedWarFileDocumentRootWhenRunningFromExplodedWar() throws Exception { @@ -70,4 +75,23 @@ void codeSourceArchivePathContainingSpaces() throws Exception { assertThat(codeSourceArchive).isEqualTo(new File("/test/path/with space/")); } + @Test + void getValidDirectoryWhenHasSrcMainWebApp() { + Map systemEnvironment = new HashMap<>(); + File directory = new File(this.tempDir, "src/main/webapp"); + directory.mkdirs(); + DocumentRoot documentRoot = new DocumentRoot(logger, this.tempDir, systemEnvironment::get); + assertThat(documentRoot.getValidDirectory()).isEqualTo(directory); + } + + @Test + void getValidDirectoryWhenHasCustomSrcMainWebApp() { + Map systemEnvironment = new HashMap<>(); + systemEnvironment.put("WAR_SOURCE_DIRECTORY", "src/main/unusual"); + File directory = new File(this.tempDir, "src/main/unusual"); + directory.mkdirs(); + DocumentRoot documentRoot = new DocumentRoot(logger, this.tempDir, systemEnvironment::get); + assertThat(documentRoot.getValidDirectory()).isEqualTo(directory); + } + } diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProvider.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProvider.java index 6857277c6054..4e5d8f0bcdef 100755 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProvider.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProvider.java @@ -17,6 +17,9 @@ package org.springframework.boot.webmvc.autoconfigure; import java.io.File; +import java.util.function.Function; + +import org.jspecify.annotations.Nullable; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; import org.springframework.core.env.Environment; @@ -34,6 +37,23 @@ */ public class JspTemplateAvailabilityProvider implements TemplateAvailabilityProvider { + private static final String WAR_SOURCE_DIRECTORY_ENVIRONMENT_VARIABLE = "WAR_SOURCE_DIRECTORY"; + + private final File warSourceDirectory; + + public JspTemplateAvailabilityProvider() { + this(new File("."), System::getenv); + } + + JspTemplateAvailabilityProvider(File rootDirectory, Function systemEnvironment) { + this.warSourceDirectory = new File(rootDirectory, getWarSourceDirectory(systemEnvironment)); + } + + private static String getWarSourceDirectory(Function systemEnvironment) { + String name = systemEnvironment.apply(WAR_SOURCE_DIRECTORY_ENVIRONMENT_VARIABLE); + return (name != null) ? name : "src/main/webapp"; + } + @Override public boolean isTemplateAvailable(String view, Environment environment, ClassLoader classLoader, ResourceLoader resourceLoader) { @@ -42,7 +62,7 @@ public boolean isTemplateAvailable(String view, Environment environment, ClassLo if (resourceLoader.getResource(resourceName).exists()) { return true; } - return new File("src/main/webapp", resourceName).exists(); + return new File(this.warSourceDirectory, resourceName).exists(); } return false; } diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProviderTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProviderTests.java index 767d2c1aa966..595e09d56130 100644 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProviderTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProviderTests.java @@ -16,12 +16,18 @@ package org.springframework.boot.webmvc.autoconfigure; +import java.io.File; +import java.util.HashMap; +import java.util.Map; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.testsupport.classpath.resources.WithResource; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.mock.env.MockEnvironment; +import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -57,9 +63,37 @@ void availabilityOfTemplateWithCustomSuffix() { assertThat(isTemplateAvailable("suffixed")).isTrue(); } + @Test + void availabilityOfTemplateInSrcMainWebapp(@TempDir File rootDirectory) throws Exception { + File jsp = new File(rootDirectory, "src/main/webapp/test.jsp"); + jsp.getParentFile().mkdirs(); + FileCopyUtils.copy(new byte[0], jsp); + Map systemEnvironment = new HashMap<>(); + JspTemplateAvailabilityProvider provider = new JspTemplateAvailabilityProvider(rootDirectory, + systemEnvironment::get); + assertThat(isTemplateAvailable(provider, "test.jsp")).isTrue(); + assertThat(isTemplateAvailable(provider, "missing.jsp")).isFalse(); + } + + @Test + void availabilityOfTemplateInCustomSrcMainWebapp(@TempDir File rootDirectory) throws Exception { + File jsp = new File(rootDirectory, "src/main/unusual/test.jsp"); + jsp.getParentFile().mkdirs(); + FileCopyUtils.copy(new byte[0], jsp); + Map systemEnvironment = new HashMap<>(); + systemEnvironment.put("WAR_SOURCE_DIRECTORY", "src/main/unusual"); + JspTemplateAvailabilityProvider provider = new JspTemplateAvailabilityProvider(rootDirectory, + systemEnvironment::get); + assertThat(isTemplateAvailable(provider, "test.jsp")).isTrue(); + assertThat(isTemplateAvailable(provider, "missing.jsp")).isFalse(); + } + private boolean isTemplateAvailable(String view) { - return this.provider.isTemplateAvailable(view, this.environment, getClass().getClassLoader(), - this.resourceLoader); + return isTemplateAvailable(this.provider, view); + } + + private boolean isTemplateAvailable(JspTemplateAvailabilityProvider provider, String view) { + return provider.isTemplateAvailable(view, this.environment, getClass().getClassLoader(), this.resourceLoader); } } From d9528fdb0b48bea4a29c8bf5af490c0a755f6917 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Tue, 6 Jan 2026 09:29:27 +0800 Subject: [PATCH 031/930] Fix typo See gh-48680 Signed-off-by: Yanming Zhou --- .../src/docs/antora/modules/reference/pages/web/servlet.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc index 5a53f1d36fcc..f01c490099bd 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc @@ -746,4 +746,4 @@ JSPs are not supported when using an executable jar. * Creating a custom `error.jsp` page does not override the default view for xref:web/servlet.adoc#web.servlet.spring-mvc.error-handling[error handling]. xref:web/servlet.adoc#web.servlet.spring-mvc.error-handling.error-pages[Custom error pages] should be used instead. -* If you run your application using `mvn spring-boot:run` or `gradle bootRun` and you deviate from the standard `src/main/webbapp` directory structure you may need to set a `WAR_SOURCE_DIRECTORY` environment variable so that Spring Boot can find your JSPs. +* If you run your application using `mvn spring-boot:run` or `gradle bootRun` and you deviate from the standard `src/main/webapp` directory structure you may need to set a `WAR_SOURCE_DIRECTORY` environment variable so that Spring Boot can find your JSPs. From a749b2fd5818e521b71fd684732f435d74c3f021 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Jan 2026 12:25:53 +0000 Subject: [PATCH 032/930] Allow projects to configure their system requirements Closes gh-48684 --- .../boot/build/ConventionsPlugin.java | 4 +- .../boot/build/JavaConventions.java | 18 ++++-- .../build/SystemRequirementsExtension.java | 58 +++++++++++++++++++ .../build/toolchain/ToolchainExtension.java | 23 ++++---- .../boot/build/toolchain/ToolchainPlugin.java | 10 +++- 5 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java diff --git a/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java index 7c7a47e5051d..d0c9cccef886 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java @@ -43,8 +43,10 @@ public class ConventionsPlugin implements Plugin { @Override public void apply(Project project) { + SystemRequirementsExtension systemRequirements = project.getExtensions() + .create("systemRequirements", SystemRequirementsExtension.class); new NoHttpConventions().apply(project); - new JavaConventions().apply(project); + new JavaConventions(systemRequirements.getJava()).apply(project); new MavenPublishingConventions().apply(project); new AntoraConventions().apply(project); new KotlinConventions().apply(project); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java index 2b1f053a67dc..5c3a784c0b40 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java @@ -32,7 +32,6 @@ import io.spring.javaformat.gradle.SpringJavaFormatPlugin; import io.spring.javaformat.gradle.tasks.CheckFormat; import io.spring.javaformat.gradle.tasks.Format; -import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; @@ -53,7 +52,9 @@ import org.gradle.api.tasks.javadoc.Javadoc; import org.gradle.api.tasks.testing.Test; import org.gradle.external.javadoc.CoreJavadocOptions; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.springframework.boot.build.SystemRequirementsExtension.JavaSpec; import org.springframework.boot.build.architecture.ArchitecturePlugin; import org.springframework.boot.build.classpath.CheckClasspathForProhibitedDependencies; import org.springframework.boot.build.optional.OptionalDependenciesPlugin; @@ -127,7 +128,11 @@ */ class JavaConventions { - private static final String SOURCE_AND_TARGET_COMPATIBILITY = "17"; + private final JavaSpec javaSpec; + + JavaConventions(JavaSpec javaSpec) { + this.javaSpec = javaSpec; + } void apply(Project project) { project.getPlugins().withType(JavaBasePlugin.class, (java) -> { @@ -165,7 +170,7 @@ private void configureJarManifestConventions(Project project) { jar.manifest((manifest) -> { Map attributes = new TreeMap<>(); attributes.put("Automatic-Module-Name", project.getName().replace("-", ".")); - attributes.put("Build-Jdk-Spec", SOURCE_AND_TARGET_COMPATIBILITY); + attributes.put("Build-Jdk-Spec", this.javaSpec.getVersion().get().toString()); attributes.put("Built-By", "Spring"); attributes.put("Implementation-Title", determineImplementationTitle(project, sourceJarTaskNames, javadocJarTaskNames, jar)); @@ -243,14 +248,15 @@ private void addValuelessOption(CoreJavadocOptions options, String option) { } private void configureJavaConventions(Project project) { + JavaLanguageVersion javaVersion = this.javaSpec.getVersion().get(); if (!project.hasProperty("toolchainVersion")) { JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class); - javaPluginExtension.setSourceCompatibility(JavaVersion.toVersion(SOURCE_AND_TARGET_COMPATIBILITY)); - javaPluginExtension.setTargetCompatibility(JavaVersion.toVersion(SOURCE_AND_TARGET_COMPATIBILITY)); + javaPluginExtension.setSourceCompatibility(javaVersion); + javaPluginExtension.setTargetCompatibility(javaVersion); } project.getTasks().withType(JavaCompile.class, (compile) -> { compile.getOptions().setEncoding("UTF-8"); - compile.getOptions().getRelease().set(17); + compile.getOptions().getRelease().set(javaVersion.asInt()); List args = compile.getOptions().getCompilerArgs(); if (!args.contains("-parameters")) { args.add("-parameters"); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java new file mode 100644 index 000000000000..cecf40036244 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java @@ -0,0 +1,58 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build; + +import javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Property; +import org.gradle.jvm.toolchain.JavaLanguageVersion; + +/** + * DSL extension for configuring a project's system requirements. + * + * @author Andy Wilkinson + */ +public class SystemRequirementsExtension { + + private final JavaSpec javaSpec; + + @Inject + public SystemRequirementsExtension(ObjectFactory objects) { + this.javaSpec = objects.newInstance(JavaSpec.class); + } + + public void java(Action action) { + action.execute(this.javaSpec); + } + + public JavaSpec getJava() { + return this.javaSpec; + } + + public abstract static class JavaSpec { + + public JavaSpec() { + getVersion().convention(JavaLanguageVersion.of(17)); + } + + public abstract Property getVersion(); + + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainExtension.java index 6ce52decf6e2..82449aa39cce 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainExtension.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainExtension.java @@ -21,33 +21,30 @@ import org.gradle.api.provider.Property; import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.springframework.boot.build.SystemRequirementsExtension; +import org.springframework.boot.build.SystemRequirementsExtension.JavaSpec; + /** * DSL extension for {@link ToolchainPlugin}. * * @author Christoph Dreis */ -public class ToolchainExtension { - - private final Property maximumCompatibleJavaVersion; - - private final ListProperty testJvmArgs; +public abstract class ToolchainExtension { private final JavaLanguageVersion javaVersion; public ToolchainExtension(Project project) { - this.maximumCompatibleJavaVersion = project.getObjects().property(JavaLanguageVersion.class); - this.testJvmArgs = project.getObjects().listProperty(String.class); String toolchainVersion = (String) project.findProperty("toolchainVersion"); this.javaVersion = (toolchainVersion != null) ? JavaLanguageVersion.of(toolchainVersion) : null; + JavaSpec javaSpec = project.getExtensions().getByType(SystemRequirementsExtension.class).getJava(); + getMinimumCompatibleJavaVersion().convention(javaSpec.getVersion()); } - public Property getMaximumCompatibleJavaVersion() { - return this.maximumCompatibleJavaVersion; - } + public abstract Property getMinimumCompatibleJavaVersion(); - public ListProperty getTestJvmArgs() { - return this.testJvmArgs; - } + public abstract Property getMaximumCompatibleJavaVersion(); + + public abstract ListProperty getTestJvmArgs(); JavaLanguageVersion getJavaVersion() { return this.javaVersion; diff --git a/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainPlugin.java index 6b37f10b6ef4..14c95b57d5b2 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainPlugin.java @@ -53,9 +53,13 @@ private void configure(Project project, ToolchainExtension toolchain) { } private boolean isJavaVersionSupported(ToolchainExtension toolchain, JavaLanguageVersion toolchainVersion) { - return toolchain.getMaximumCompatibleJavaVersion() - .map((version) -> version.canCompileOrRun(toolchainVersion)) - .getOrElse(true); + JavaLanguageVersion minimumVersion = toolchain.getMinimumCompatibleJavaVersion().getOrNull(); + if (minimumVersion == null || toolchainVersion.canCompileOrRun(minimumVersion)) { + return toolchain.getMaximumCompatibleJavaVersion() + .map((version) -> version.canCompileOrRun(toolchainVersion)) + .getOrElse(true); + } + return false; } private void disableToolchainTasks(Project project) { From 56928bbe382b45cbdaee94f0393a49d6e24686e8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Jan 2026 12:30:59 +0000 Subject: [PATCH 033/930] Upgrade to jOOQ 3.20 and require Java 21 to use it Closes gh-48619 --- .../ROOT/pages/system-requirements.adoc | 3 + module/spring-boot-flyway/build.gradle | 1 - .../FlywayAutoConfigurationTests.java | 45 -------- module/spring-boot-jooq-test/build.gradle | 6 + module/spring-boot-jooq/build.gradle | 7 ++ ...JooqFlywayDatabaseInitializationTests.java | 105 ++++++++++++++++++ .../boot/jooq/autoconfigure/settings.xml | 2 +- .../spring-boot-dependencies/build.gradle | 6 +- 8 files changed, 123 insertions(+), 52 deletions(-) create mode 100644 module/spring-boot-jooq/src/test/java/org/springframework/boot/jooq/autoconfigure/JooqFlywayDatabaseInitializationTests.java diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/system-requirements.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/system-requirements.adoc index 04cfa25b4943..1a9fa9e71f51 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/system-requirements.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/system-requirements.adoc @@ -4,6 +4,9 @@ Spring Boot {version-spring-boot} requires at least https://www.java.com[Java 17] and is compatible with versions up to and including Java 25. {url-spring-framework-docs}/[Spring Framework {version-spring-framework}] or above is also required. +Support for third-party projects may have additional or higher requirements. +Refer to the documentation for that support for further details. + Explicit build support is provided for the following build tools: |=== diff --git a/module/spring-boot-flyway/build.gradle b/module/spring-boot-flyway/build.gradle index 6a06299fdadc..d936725a9055 100644 --- a/module/spring-boot-flyway/build.gradle +++ b/module/spring-boot-flyway/build.gradle @@ -49,7 +49,6 @@ dependencies { dockerTestRuntimeOnly("org.postgresql:postgresql") testImplementation(project(":core:spring-boot-test")) - testImplementation(project(":module:spring-boot-jooq")) testImplementation(project(":test-support:spring-boot-test-support")) testImplementation(testFixtures(project(":core:spring-boot-autoconfigure"))) testImplementation("jakarta.persistence:jakarta.persistence-api") diff --git a/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/FlywayAutoConfigurationTests.java b/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/FlywayAutoConfigurationTests.java index b1e47a43bcbf..240d57e6c433 100644 --- a/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/FlywayAutoConfigurationTests.java +++ b/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/FlywayAutoConfigurationTests.java @@ -49,9 +49,6 @@ import org.flywaydb.database.postgresql.PostgreSQLConfigurationExtension; import org.flywaydb.database.sqlserver.SQLServerConfigurationExtension; import org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform; -import org.jooq.DSLContext; -import org.jooq.SQLDialect; -import org.jooq.impl.DefaultDSLContext; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InOrder; @@ -60,7 +57,6 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints; @@ -822,37 +818,6 @@ void skipExecutingMigrationsIsCorrectlyMapped() { }); } - @Test - void whenFlywayIsAutoConfiguredThenJooqDslContextDependsOnFlywayBeans() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, JooqConfiguration.class) - .run((context) -> { - BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); - assertThat(beanDefinition.getDependsOn()).containsExactlyInAnyOrder("flywayInitializer", "flyway"); - }); - } - - @Test - void whenCustomMigrationInitializerIsDefinedThenJooqDslContextDependsOnIt() { - this.contextRunner - .withUserConfiguration(EmbeddedDataSourceConfiguration.class, JooqConfiguration.class, - CustomFlywayMigrationInitializer.class) - .run((context) -> { - BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); - assertThat(beanDefinition.getDependsOn()).containsExactlyInAnyOrder("flywayMigrationInitializer", - "flyway"); - }); - } - - @Test - void whenCustomFlywayIsDefinedThenJooqDslContextDependsOnIt() { - this.contextRunner - .withUserConfiguration(EmbeddedDataSourceConfiguration.class, JooqConfiguration.class, CustomFlyway.class) - .run((context) -> { - BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); - assertThat(beanDefinition.getDependsOn()).containsExactlyInAnyOrder("customFlyway"); - }); - } - @Test void scriptPlaceholderPrefixIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) @@ -1186,16 +1151,6 @@ FlywayConfigurationCustomizer customizerTwo() { } - @Configuration(proxyBeanMethods = false) - static class JooqConfiguration { - - @Bean - DSLContext dslContext() { - return new DefaultDSLContext(SQLDialect.H2); - } - - } - @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(DataSourceProperties.class) abstract static class AbstractUserH2DataSourceConfiguration { diff --git a/module/spring-boot-jooq-test/build.gradle b/module/spring-boot-jooq-test/build.gradle index e49a22569ef5..de8c4b34d972 100644 --- a/module/spring-boot-jooq-test/build.gradle +++ b/module/spring-boot-jooq-test/build.gradle @@ -23,6 +23,12 @@ plugins { description = "Spring Boot jOOQ Test" +systemRequirements { + java { + version = JavaLanguageVersion.of(21) + } +} + dependencies { api(project(":core:spring-boot-test-autoconfigure")) api(project(":module:spring-boot-jdbc-test")) diff --git a/module/spring-boot-jooq/build.gradle b/module/spring-boot-jooq/build.gradle index efc423423757..1b2cb039a804 100644 --- a/module/spring-boot-jooq/build.gradle +++ b/module/spring-boot-jooq/build.gradle @@ -24,6 +24,12 @@ plugins { description = "Spring Boot jOOQ" +systemRequirements { + java { + version = JavaLanguageVersion.of(21) + } +} + dependencies { api(project(":module:spring-boot-jdbc")) api(project(":module:spring-boot-transaction")) @@ -34,6 +40,7 @@ dependencies { optional("jakarta.xml.bind:jakarta.xml.bind-api") testImplementation(project(":core:spring-boot-test")) + testImplementation(project(":module:spring-boot-flyway")) testImplementation(project(":test-support:spring-boot-test-support")) testRuntimeOnly("ch.qos.logback:logback-classic") diff --git a/module/spring-boot-jooq/src/test/java/org/springframework/boot/jooq/autoconfigure/JooqFlywayDatabaseInitializationTests.java b/module/spring-boot-jooq/src/test/java/org/springframework/boot/jooq/autoconfigure/JooqFlywayDatabaseInitializationTests.java new file mode 100644 index 000000000000..da0a53ac3693 --- /dev/null +++ b/module/spring-boot-jooq/src/test/java/org/springframework/boot/jooq/autoconfigure/JooqFlywayDatabaseInitializationTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jooq.autoconfigure; + +import org.flywaydb.core.Flyway; +import org.jooq.DSLContext; +import org.jooq.SQLDialect; +import org.jooq.impl.DefaultDSLContext; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration; +import org.springframework.boot.flyway.autoconfigure.FlywayMigrationInitializer; +import org.springframework.boot.jdbc.autoconfigure.EmbeddedDataSourceConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for jOQQ when Flyway is being used for DB initialization. + * + * @author Andy Wilkinson + */ +class JooqFlywayDatabaseInitializationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) + .withUserConfiguration(EmbeddedDataSourceConfiguration.class, JooqConfiguration.class) + .withPropertyValues("spring.datasource.generate-unique-name=true"); + + @Test + void whenFlywayIsAutoConfiguredThenJooqDslContextDependsOnFlywayBeans() { + this.contextRunner.run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); + assertThat(beanDefinition.getDependsOn()).containsExactlyInAnyOrder("flywayInitializer", "flyway"); + }); + } + + @Test + void whenCustomMigrationInitializerIsDefinedThenJooqDslContextDependsOnIt() { + this.contextRunner.withUserConfiguration(CustomFlywayMigrationInitializer.class).run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); + assertThat(beanDefinition.getDependsOn()).containsExactlyInAnyOrder("flywayMigrationInitializer", "flyway"); + }); + } + + @Test + void whenCustomFlywayIsDefinedThenJooqDslContextDependsOnIt() { + this.contextRunner.withUserConfiguration(CustomFlyway.class).run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); + assertThat(beanDefinition.getDependsOn()).containsExactlyInAnyOrder("customFlyway"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class JooqConfiguration { + + @Bean + DSLContext dslContext() { + return new DefaultDSLContext(SQLDialect.H2); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomFlywayMigrationInitializer { + + @Bean + FlywayMigrationInitializer flywayMigrationInitializer(Flyway flyway) { + FlywayMigrationInitializer initializer = new FlywayMigrationInitializer(flyway); + initializer.setOrder(Ordered.HIGHEST_PRECEDENCE); + return initializer; + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomFlyway { + + @Bean + Flyway customFlyway() { + return Flyway.configure().load(); + } + + } + +} diff --git a/module/spring-boot-jooq/src/test/resources/org/springframework/boot/jooq/autoconfigure/settings.xml b/module/spring-boot-jooq/src/test/resources/org/springframework/boot/jooq/autoconfigure/settings.xml index a27dfdd83cb3..8a8b68db0f67 100644 --- a/module/spring-boot-jooq/src/test/resources/org/springframework/boot/jooq/autoconfigure/settings.xml +++ b/module/spring-boot-jooq/src/test/resources/org/springframework/boot/jooq/autoconfigure/settings.xml @@ -1,4 +1,4 @@ - + 100 diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index eaeb23c0e07a..cbd496374ca9 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1058,11 +1058,7 @@ bom { ] } } - library("jOOQ", "3.19.29") { - prohibit { - versionRange "[3.20.0,)" - because "it requires Java 21" - } + library("jOOQ", "3.20.10") { group("org.jooq") { bom("jooq-bom") plugins = [ From 96865d341d22cacb6c80a48403a68133518214fb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Jan 2026 19:53:30 +0000 Subject: [PATCH 034/930] Improve how system requirements config affects Eclipse See gh-48684 --- .../boot/build/JavaConventions.java | 11 ++------ .../build/SystemRequirementsExtension.java | 28 +++++++++++++++---- .../build/toolchain/ToolchainExtension.java | 3 +- module/spring-boot-jooq-test/build.gradle | 2 +- module/spring-boot-jooq/build.gradle | 2 +- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java index 5c3a784c0b40..06fff0e9be33 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java @@ -52,7 +52,6 @@ import org.gradle.api.tasks.javadoc.Javadoc; import org.gradle.api.tasks.testing.Test; import org.gradle.external.javadoc.CoreJavadocOptions; -import org.gradle.jvm.toolchain.JavaLanguageVersion; import org.springframework.boot.build.SystemRequirementsExtension.JavaSpec; import org.springframework.boot.build.architecture.ArchitecturePlugin; @@ -170,7 +169,7 @@ private void configureJarManifestConventions(Project project) { jar.manifest((manifest) -> { Map attributes = new TreeMap<>(); attributes.put("Automatic-Module-Name", project.getName().replace("-", ".")); - attributes.put("Build-Jdk-Spec", this.javaSpec.getVersion().get().toString()); + attributes.put("Build-Jdk-Spec", this.javaSpec.getVersion()); attributes.put("Built-By", "Spring"); attributes.put("Implementation-Title", determineImplementationTitle(project, sourceJarTaskNames, javadocJarTaskNames, jar)); @@ -248,15 +247,9 @@ private void addValuelessOption(CoreJavadocOptions options, String option) { } private void configureJavaConventions(Project project) { - JavaLanguageVersion javaVersion = this.javaSpec.getVersion().get(); - if (!project.hasProperty("toolchainVersion")) { - JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class); - javaPluginExtension.setSourceCompatibility(javaVersion); - javaPluginExtension.setTargetCompatibility(javaVersion); - } project.getTasks().withType(JavaCompile.class, (compile) -> { compile.getOptions().setEncoding("UTF-8"); - compile.getOptions().getRelease().set(javaVersion.asInt()); + compile.getOptions().getRelease().set(this.javaSpec.getVersion()); List args = compile.getOptions().getCompilerArgs(); if (!args.contains("-parameters")) { args.add("-parameters"); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java index cecf40036244..697ff12dc688 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java @@ -19,8 +19,9 @@ import javax.inject.Inject; import org.gradle.api.Action; +import org.gradle.api.Project; import org.gradle.api.model.ObjectFactory; -import org.gradle.api.provider.Property; +import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.jvm.toolchain.JavaLanguageVersion; /** @@ -33,8 +34,8 @@ public class SystemRequirementsExtension { private final JavaSpec javaSpec; @Inject - public SystemRequirementsExtension(ObjectFactory objects) { - this.javaSpec = objects.newInstance(JavaSpec.class); + public SystemRequirementsExtension(Project project, ObjectFactory objects) { + this.javaSpec = objects.newInstance(JavaSpec.class, project); } public void java(Action action) { @@ -47,11 +48,26 @@ public JavaSpec getJava() { public abstract static class JavaSpec { - public JavaSpec() { - getVersion().convention(JavaLanguageVersion.of(17)); + private final Project project; + + private int version = 17; + + @Inject + public JavaSpec(Project project) { + this.project = project; } - public abstract Property getVersion(); + public int getVersion() { + return this.version; + } + + public void setVersion(int version) { + JavaLanguageVersion javaVersion = JavaLanguageVersion.of(version); + JavaPluginExtension javaPluginExtension = this.project.getExtensions().getByType(JavaPluginExtension.class); + javaPluginExtension.setSourceCompatibility(javaVersion); + javaPluginExtension.setTargetCompatibility(javaVersion); + this.version = version; + } } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainExtension.java index 82449aa39cce..a694dd005dc1 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainExtension.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainExtension.java @@ -37,7 +37,8 @@ public ToolchainExtension(Project project) { String toolchainVersion = (String) project.findProperty("toolchainVersion"); this.javaVersion = (toolchainVersion != null) ? JavaLanguageVersion.of(toolchainVersion) : null; JavaSpec javaSpec = project.getExtensions().getByType(SystemRequirementsExtension.class).getJava(); - getMinimumCompatibleJavaVersion().convention(javaSpec.getVersion()); + getMinimumCompatibleJavaVersion() + .convention(project.provider(() -> JavaLanguageVersion.of(javaSpec.getVersion()))); } public abstract Property getMinimumCompatibleJavaVersion(); diff --git a/module/spring-boot-jooq-test/build.gradle b/module/spring-boot-jooq-test/build.gradle index de8c4b34d972..f681a2bacf86 100644 --- a/module/spring-boot-jooq-test/build.gradle +++ b/module/spring-boot-jooq-test/build.gradle @@ -25,7 +25,7 @@ description = "Spring Boot jOOQ Test" systemRequirements { java { - version = JavaLanguageVersion.of(21) + version = 21 } } diff --git a/module/spring-boot-jooq/build.gradle b/module/spring-boot-jooq/build.gradle index 1b2cb039a804..4a41c6c86433 100644 --- a/module/spring-boot-jooq/build.gradle +++ b/module/spring-boot-jooq/build.gradle @@ -26,7 +26,7 @@ description = "Spring Boot jOOQ" systemRequirements { java { - version = JavaLanguageVersion.of(21) + version = 21 } } From b8b1373f2ee4409314c23229b3c12aec385e1cec Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Jan 2026 19:54:01 +0000 Subject: [PATCH 035/930] Document that jOOQ support requires Java 21 See gh-48619 --- .../src/docs/antora/modules/reference/pages/data/sql.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc index 33b0ee271658..1fb0f2a513e2 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc @@ -390,6 +390,7 @@ TIP: `PathRequest.toH2Console()` returns the correct request matcher also when t jOOQ Object Oriented Querying (https://www.jooq.org/[jOOQ]) is a popular product from https://www.datageekery.com/[Data Geekery] which generates Java code from your database and lets you build type-safe SQL queries through its fluent API. Both the commercial and open source editions can be used with Spring Boot. +jOOQ requires Java 21 or later. From 089b410a0c013b9d450276ee13ad8109ba28e4fd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 15:04:28 +0000 Subject: [PATCH 036/930] Start building against Micrometer 1.17.0-M1 snapshots See gh-48722 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index cbd496374ca9..cc2688aee047 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1528,7 +1528,7 @@ bom { releaseNotes("https://github.com/apache/maven-war-plugin/releases/tag/maven-war-plugin-{version}") } } - library("Micrometer", "1.16.1") { + library("Micrometer", "1.17.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ From 86c17e63438e086fc0904543de7a049236749cb3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 16:45:40 +0000 Subject: [PATCH 037/930] fixup! Start building against Micrometer 1.17.0-M1 snapshots --- ...figurationMeterRegistryPostProcessorIntegrationTests.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfigurationMeterRegistryPostProcessorIntegrationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfigurationMeterRegistryPostProcessorIntegrationTests.java index c611edd94fbb..c48cbf06dd0d 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfigurationMeterRegistryPostProcessorIntegrationTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfigurationMeterRegistryPostProcessorIntegrationTests.java @@ -78,7 +78,7 @@ void counterIsIncrementedOncePerEventWithoutCompositeMeterRegistry() { Map registriesByName = context.getBeansOfType(MeterRegistry.class); assertThat(registriesByName).hasSize(1); MeterRegistry registry = registriesByName.values().iterator().next(); - assertThat(registry.get("logback.events").tag("level", "error").counter().count()).isOne(); + assertThat(registry.get("logback.events").tag("level", "error").functionCounter().count()).isOne(); }); } @@ -93,7 +93,8 @@ void counterIsIncrementedOncePerEventWithCompositeMeterRegistry() { Map registriesByName = context.getBeansOfType(MeterRegistry.class); assertThat(registriesByName).hasSize(3); registriesByName.forEach((name, - registry) -> assertThat(registry.get("logback.events").tag("level", "error").counter().count()) + registry) -> assertThat( + registry.get("logback.events").tag("level", "error").functionCounter().count()) .isOne()); }); } From 2745a7befdaba51fc18be206983d4f23a7073412 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 15:04:32 +0000 Subject: [PATCH 038/930] Start building against Micrometer Tracing 1.7.0-M1 snapshots See gh-48723 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index cc2688aee047..25055b51da3f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1551,7 +1551,7 @@ bom { releaseNotes("https://github.com/micrometer-metrics/micrometer/releases/tag/v{version}") } } - library("Micrometer Tracing", "1.6.1") { + library("Micrometer Tracing", "1.7.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { bom("micrometer-tracing-bom") From 12f126af838418c2811b26b6fb6d9cf1dae48351 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 15:04:37 +0000 Subject: [PATCH 039/930] Start building against Reactor Bom 2025.0.2 snapshots See gh-48724 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 25055b51da3f..aa71c534122c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1946,7 +1946,7 @@ bom { ] } } - library("Reactor Bom", "2025.0.1") { + library("Reactor Bom", "2025.0.2-SNAPSHOT") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From 8bfa92e9ea1c2fbdeb767dbe32a65afab4e8f44e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 15:04:41 +0000 Subject: [PATCH 040/930] Start building against Spring Batch 6.0.2 snapshots See gh-48725 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index aa71c534122c..4e445ae0f22b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2379,7 +2379,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-amqp/releases/tag/v{version}") } } - library("Spring Batch", "6.0.1") { + library("Spring Batch", "6.0.2-SNAPSHOT") { considerSnapshots() group("org.springframework.batch") { bom("spring-batch-bom") From d099454e6176b39136990623c7a01d7d87cd4d98 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 15:04:45 +0000 Subject: [PATCH 041/930] Start building against Spring Data Bom 2025.1.2 snapshots See gh-48726 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4e445ae0f22b..d6d075842597 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2394,7 +2394,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-batch/releases/tag/v{version}") } } - library("Spring Data Bom", "2025.1.1") { + library("Spring Data Bom", "2025.1.2-SNAPSHOT") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From 26258b120b7055173221310334c25e5a1536153a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 15:07:12 +0000 Subject: [PATCH 042/930] Upgrade to Elasticsearch Client 9.2.3 Closes gh-48731 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d6d075842597..1d31dca3c2f0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -352,7 +352,7 @@ bom { releaseNotes("https://github.com/ehcache/ehcache3/releases/tag/v{version}") } } - library("Elasticsearch Client", "9.2.2") { + library("Elasticsearch Client", "9.2.3") { prohibit { contains "-alpha" contains "-beta" From 20cf826916899c8bc5f27f9f0e473b1ec8fa8fc1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 15:04:50 +0000 Subject: [PATCH 043/930] Start building against Spring Framework 7.0.3 snapshots See gh-48727 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8b9300a63425..f6e211a77146 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ mockitoVersion=5.20.0 nativeBuildToolsVersion=0.11.3 nullabilityPluginVersion=0.0.9 snakeYamlVersion=2.5 -springFrameworkVersion=7.0.2 +springFrameworkVersion=7.0.3-SNAPSHOT springFramework60xVersion=6.0.23 tomcatVersion=11.0.15 From 8bf5b249ee2ba5f84803aa94af595f1bb24cdd02 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 15:04:54 +0000 Subject: [PATCH 044/930] Start building against Spring GraphQL 2.0.2 snapshots See gh-48728 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1d31dca3c2f0..f201816361b3 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2424,7 +2424,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-framework/releases/tag/v{version}") } } - library("Spring GraphQL", "2.0.1") { + library("Spring GraphQL", "2.0.2-SNAPSHOT") { considerSnapshots() group("org.springframework.graphql") { modules = [ From d76af5eca102e3e910d2f5a2061b6e05fffae991 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 15:04:58 +0000 Subject: [PATCH 045/930] Start building against Spring Kafka 4.0.2 snapshots See gh-48729 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index f201816361b3..7b4f6a9a85f8 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2474,7 +2474,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-integration/releases/tag/v{version}") } } - library("Spring Kafka", "4.0.1") { + library("Spring Kafka", "4.0.2-SNAPSHOT") { considerSnapshots() group("org.springframework.kafka") { modules = [ From 1a51109031f86bdc2d47179b2fe7dfe1cff7a9e4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 15:05:03 +0000 Subject: [PATCH 046/930] Start building against Spring Pulsar 2.0.2 snapshots See gh-48730 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7b4f6a9a85f8..27ed3bc57f97 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2512,7 +2512,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ldap/releases/tag/{version}") } } - library("Spring Pulsar", "2.0.1") { + library("Spring Pulsar", "2.0.2-SNAPSHOT") { considerSnapshots() group("org.springframework.pulsar") { bom("spring-pulsar-bom") From dd3d9d8f2d52acd15faa3d01e6909e169395577c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:22:37 +0000 Subject: [PATCH 047/930] Upgrade to ActiveMQ 6.2.0 Closes gh-48732 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 27ed3bc57f97..fe01e1a296c6 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -28,7 +28,7 @@ bom { issueLabels = ["type: dependency-upgrade"] } } - library("ActiveMQ", "6.1.8") { + library("ActiveMQ", "6.2.0") { group("org.apache.activemq") { modules = [ "activemq-console", From c2308a5808648be0e418fddd281dd8d461355e7a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:22:41 +0000 Subject: [PATCH 048/930] Upgrade to Artemis 2.44.0 Closes gh-48733 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index fe01e1a296c6..e36b81aeb828 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -62,7 +62,7 @@ bom { releaseNotes("https://github.com/eclipse-ee4j/angus-mail/releases/tag/{version}") } } - library("Artemis", "2.43.0") { + library("Artemis", "2.44.0") { group("org.apache.activemq") { bom("artemis-bom") } From add7b41c8f36e2a7f987378e1fa7682dd1c48a33 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:22:46 +0000 Subject: [PATCH 049/930] Upgrade to Byte Buddy 1.18.3 Closes gh-48734 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e36b81aeb828..850015452b69 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -143,7 +143,7 @@ bom { releaseNotes("https://github.com/mojohaus/build-helper-maven-plugin/releases/tag/{version}") } } - library("Byte Buddy", "1.17.8") { + library("Byte Buddy", "1.18.3") { group("net.bytebuddy") { modules = [ "byte-buddy", From 0152aa40c4523d61604d3c806811a3c83925a57f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:22:50 +0000 Subject: [PATCH 050/930] Upgrade to Commons Codec 1.20.0 Closes gh-48735 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f6e211a77146..051b6c59ac65 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 assertjVersion=3.27.6 checkstyleToolVersion=10.12.4 -commonsCodecVersion=1.19.0 +commonsCodecVersion=1.20.0 graalVersion=25 hamcrestVersion=3.0 jackson2Version=2.20.1 From d938ccfc204ea4530c56469e1381eeca0651d38a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:22:55 +0000 Subject: [PATCH 051/930] Upgrade to Commons DBCP2 2.14.0 Closes gh-48736 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 850015452b69..92f5e598b416 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -219,7 +219,7 @@ bom { releaseNotes("https://commons.apache.org/proper/commons-codec/changes.html#a{version}") } } - library("Commons DBCP2", "2.13.0") { + library("Commons DBCP2", "2.14.0") { group("org.apache.commons") { modules = [ "commons-dbcp2" From 67fe6c0fea52ada0f215c73b1bb099e4e261067f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:22:59 +0000 Subject: [PATCH 052/930] Upgrade to Commons Lang3 3.20.0 Closes gh-48737 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 92f5e598b416..b2914fcde8b2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -230,7 +230,7 @@ bom { releaseNotes("https://commons.apache.org/proper/commons-dbcp/changes-report.html#a{version}") } } - library("Commons Lang3", "3.19.0") { + library("Commons Lang3", "3.20.0") { group("org.apache.commons") { modules = [ "commons-lang3" From 0d1b9d7e18bc6c1af2ad6daa71c230b23c17672d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:23:03 +0000 Subject: [PATCH 053/930] Upgrade to Commons Pool2 2.13.1 Closes gh-48738 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b2914fcde8b2..6350a95b8bbe 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -259,7 +259,7 @@ bom { ] } } - library("Commons Pool2", "2.12.1") { + library("Commons Pool2", "2.13.1") { group("org.apache.commons") { modules = [ "commons-pool2" From 6dfefb512a1e8e3081755ccc8d3d1e24cfc0c453 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:23:08 +0000 Subject: [PATCH 054/930] Upgrade to Couchbase Client 3.10.0 Closes gh-48739 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6350a95b8bbe..a569d4b5a137 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -269,7 +269,7 @@ bom { site("https://commons.apache.org/proper/commons-pool") } } - library("Couchbase Client", "3.9.2") { + library("Couchbase Client", "3.10.0") { group("com.couchbase.client") { modules = [ "java-client" From cd9993eec8b5abaa6f0a3b66b4a8a5573f6a3c2b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:23:13 +0000 Subject: [PATCH 055/930] Upgrade to Flyway 11.20.1 Closes gh-48740 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a569d4b5a137..58dcd0265d03 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -376,7 +376,7 @@ bom { javadoc("elasticsearch-java", version -> "https://javadoc.io/doc/co.elastic.clients/elasticsearch-java/%s/index.html".formatted(version), "co.elastic.clients.elasticsearch", "co.elastic.clients.transport") } } - library("Flyway", "11.19.1") { + library("Flyway", "11.20.1") { group("org.flywaydb") { modules = [ "flyway-commandline", From 753f64a8f997e23083ad634bd6610fce0b721f84 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:23:17 +0000 Subject: [PATCH 056/930] Upgrade to Hibernate Validator 9.1.0.Final Closes gh-48741 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 58dcd0265d03..e4530e166326 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -592,7 +592,7 @@ bom { .formatted(version.major(), version.minor())) } } - library("Hibernate Validator", "9.0.1.Final") { + library("Hibernate Validator", "9.1.0.Final") { group("org.hibernate.validator") { modules = [ "hibernate-validator", From b5252158fdb1bc6c0109a3b3253f2f4a44dc9db5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:23:22 +0000 Subject: [PATCH 057/930] Upgrade to HtmlUnit 4.21.0 Closes gh-48742 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e4530e166326..dafeb856cd24 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -622,7 +622,7 @@ bom { ] } } - library("HtmlUnit", "4.17.0") { + library("HtmlUnit", "4.21.0") { group("org.htmlunit") { modules = [ "htmlunit" From c6cf8ba7320849eb18f6e2ac78dfbe917d71f5b4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:23:26 +0000 Subject: [PATCH 058/930] Upgrade to HttpClient5 5.6 Closes gh-48743 --- .../tomcat/servlet/TomcatServletWebServerFactoryTests.java | 5 +++-- platform/spring-boot-dependencies/build.gradle | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java index f9362d661aa6..6d493800d391 100644 --- a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java @@ -65,7 +65,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.DefaultClientTlsStrategy; -import org.apache.hc.client5.http.ssl.TlsSocketStrategy; +import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.NoHttpResponseException; import org.apache.hc.core5.ssl.SSLContextBuilder; @@ -688,7 +688,8 @@ void shouldUpdateSslWhenReloadingSslBundles() throws Exception { this.webServer.start(); RememberingHostnameVerifier verifier = new RememberingHostnameVerifier(); SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(); - TlsSocketStrategy tlsSocketStrategy = new DefaultClientTlsStrategy(sslContext, verifier); + DefaultClientTlsStrategy tlsSocketStrategy = new DefaultClientTlsStrategy(sslContext, + HostnameVerificationPolicy.CLIENT, verifier); HttpComponentsClientHttpRequestFactory requestFactory = createHttpComponentsRequestFactory(tlsSocketStrategy); assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test"); assertThat(verifier.getLastPrincipal()).isEqualTo("CN=1"); diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index dafeb856cd24..959337e113c3 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -646,7 +646,7 @@ bom { ] } } - library("HttpClient5", "5.5.1") { + library("HttpClient5", "5.6") { prohibit { contains "-alpha" contains "-beta" From 86fe6a316fe4689a48f7c4e810d327f4da560a7b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:23:30 +0000 Subject: [PATCH 059/930] Upgrade to HttpCore5 5.4 Closes gh-48744 --- .../autoconfigure/ElasticsearchProperties.java | 2 +- .../ElasticsearchRestClientAutoConfigurationTests.java | 6 +++--- platform/spring-boot-dependencies/build.gradle | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchProperties.java b/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchProperties.java index e2076e5d30d1..f994afa474fc 100644 --- a/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchProperties.java +++ b/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchProperties.java @@ -67,7 +67,7 @@ public class ElasticsearchProperties { /** * Whether to enable socket keep alive between client and Elasticsearch. */ - private boolean socketKeepAlive; + private boolean socketKeepAlive = true; /** * Prefix added to the path of every request sent to Elasticsearch. diff --git a/module/spring-boot-elasticsearch/src/test/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchRestClientAutoConfigurationTests.java b/module/spring-boot-elasticsearch/src/test/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchRestClientAutoConfigurationTests.java index bbdd0569bce1..b9cf28596436 100644 --- a/module/spring-boot-elasticsearch/src/test/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchRestClientAutoConfigurationTests.java +++ b/module/spring-boot-elasticsearch/src/test/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchRestClientAutoConfigurationTests.java @@ -243,17 +243,17 @@ void configureWithNoSocketKeepAliveApplyDefault() { Rest5Client client = Rest5Client.builder(new HttpHost("http", "localhost", 9201)).build(); assertThat(client.getHttpClient()).extracting("ioReactor.workers") .asInstanceOf(InstanceOfAssertFactories.ARRAY) - .satisfies((workers) -> assertThat(workers[0]).extracting("reactorConfig.soKeepAlive").isEqualTo(false)); + .satisfies((workers) -> assertThat(workers[0]).extracting("reactorConfig.soKeepAlive").isEqualTo(true)); } @Test void configureWithCustomSocketKeepAlive() { - this.contextRunner.withPropertyValues("spring.elasticsearch.socket-keep-alive=true") + this.contextRunner.withPropertyValues("spring.elasticsearch.socket-keep-alive=false") .run((context) -> assertThat(context.getBean(Rest5Client.class).getHttpClient()) .extracting("ioReactor.workers") .asInstanceOf(InstanceOfAssertFactories.ARRAY) .satisfies( - (workers) -> assertThat(workers[0]).extracting("reactorConfig.soKeepAlive").isEqualTo(true))); + (workers) -> assertThat(workers[0]).extracting("reactorConfig.soKeepAlive").isEqualTo(false))); } @Test diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 959337e113c3..09ff48f0d916 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -669,7 +669,7 @@ bom { ] } } - library("HttpCore5", "5.3.6") { + library("HttpCore5", "5.4") { group("org.apache.httpcomponents.core5") { modules = [ "httpcore5", From c12e1dc872fd0b7eddabe0782be5cfd44825e47f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:23:34 +0000 Subject: [PATCH 060/930] Upgrade to Kotlin 2.3.0 Closes gh-48745 --- .../boot/gradle/testkit/PluginClasspathGradleBuild.java | 2 -- gradle.properties | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java index b064ba83f0e1..a9f617a72647 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java @@ -32,7 +32,6 @@ import org.cyclonedx.gradle.CyclonedxPlugin; import org.gradle.testkit.runner.GradleRunner; import org.jetbrains.kotlin.gradle.fus.BuildUidService; -import org.jetbrains.kotlin.gradle.model.KotlinProject; import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin; import org.jetbrains.kotlin.project.model.LanguageSettings; import org.jetbrains.kotlin.tooling.core.KotlinToolingVersion; @@ -86,7 +85,6 @@ private List pluginClasspath() { classpath.add(new File(pathOfJarContaining(DependencyManagementPlugin.class))); if (this.kotlin) { classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.cli.common.PropertiesKt"))); - classpath.add(new File(pathOfJarContaining(KotlinProject.class))); classpath.add(new File(pathOfJarContaining(KotlinToolingVersion.class))); classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.build.report.metrics.BuildTime"))); classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.buildtools.api.CompilationService"))); diff --git a/gradle.properties b/gradle.properties index 051b6c59ac65..3d7626ea2938 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ jackson2Version=2.20.1 jacksonVersion=3.0.3 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.1 -kotlinVersion=2.2.21 +kotlinVersion=2.3.0 mavenVersion=3.9.10 mockitoVersion=5.20.0 nativeBuildToolsVersion=0.11.3 From 9ca9ba547ff476192bc0a4ab32191baa7f9c9378 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:23:39 +0000 Subject: [PATCH 061/930] Upgrade to Maven Assembly Plugin 3.8.0 Closes gh-48746 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 09ff48f0d916..45d023843b4c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1333,7 +1333,7 @@ bom { releaseNotes("https://github.com/apache/maven-antrun-plugin/releases/tag/maven-antrun-plugin-{version}") } } - library("Maven Assembly Plugin", "3.7.1") { + library("Maven Assembly Plugin", "3.8.0") { group("org.apache.maven.plugins") { plugins = [ "maven-assembly-plugin" From 9754ad734f0cf0b7269913667138059ef181d5a0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:23:43 +0000 Subject: [PATCH 062/930] Upgrade to Maven Jar Plugin 3.5.0 Closes gh-48747 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 45d023843b4c..4a67c754b071 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1446,7 +1446,7 @@ bom { releaseNotes("https://github.com/apache/maven-invoker-plugin/releases/tag/maven-invoker-plugin-{version}") } } - library("Maven Jar Plugin", "3.4.2") { + library("Maven Jar Plugin", "3.5.0") { prohibit { contains "-beta-" because "we don't want betas" From f72a25ab9b2c037afc9e9f76a5fea4c6dd6c1434 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:23:47 +0000 Subject: [PATCH 063/930] Upgrade to Maven Resources Plugin 3.4.0 Closes gh-48748 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4a67c754b071..3e6486ca868f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1470,7 +1470,7 @@ bom { releaseNotes("https://github.com/apache/maven-javadoc-plugin/releases/tag/maven-javadoc-plugin-{version}") } } - library("Maven Resources Plugin", "3.3.1") { + library("Maven Resources Plugin", "3.4.0") { prohibit { contains "-beta-" because "we don't want betas" From 3752e957cca665994545ea48081d3eabe5e61d9a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:23:51 +0000 Subject: [PATCH 064/930] Upgrade to Maven Source Plugin 3.4.0 Closes gh-48749 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3e6486ca868f..5715d8c75338 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1494,7 +1494,7 @@ bom { releaseNotes("https://github.com/apache/maven-shade-plugin/releases/tag/maven-shade-plugin-{version}") } } - library("Maven Source Plugin", "3.3.1") { + library("Maven Source Plugin", "3.4.0") { prohibit { contains "-beta-" because "we don't want betas" From 04d53a07dbf42eea40570bd8d3a20286e2a86350 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:23:55 +0000 Subject: [PATCH 065/930] Upgrade to Maven War Plugin 3.5.1 Closes gh-48750 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 5715d8c75338..179f666ff60b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1518,7 +1518,7 @@ bom { releaseNotes("https://github.com/apache/maven-surefire/releases/tag/surefire-{version}") } } - library("Maven War Plugin", "3.4.0") { + library("Maven War Plugin", "3.5.1") { group("org.apache.maven.plugins") { plugins = [ "maven-war-plugin" From 39261fbd3c3a6e47b121a29bdcb03219841806b5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:24:00 +0000 Subject: [PATCH 066/930] Upgrade to Mockito 5.21.0 Closes gh-48751 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3d7626ea2938..d9d28847ba08 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ javaFormatVersion=0.0.47 junitJupiterVersion=6.0.1 kotlinVersion=2.3.0 mavenVersion=3.9.10 -mockitoVersion=5.20.0 +mockitoVersion=5.21.0 nativeBuildToolsVersion=0.11.3 nullabilityPluginVersion=0.0.9 snakeYamlVersion=2.5 From 2957d40f9c8dacdb9f7df6dbe1ea1ccdc2c0cbfb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:24:04 +0000 Subject: [PATCH 067/930] Upgrade to OpenTelemetry 1.57.0 Closes gh-48752 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 179f666ff60b..9bd88a19f18e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1682,7 +1682,7 @@ bom { javadoc(version -> "https://netty.io/%s.%s/api".formatted(version.major(), version.minor()), "io.netty") } } - library("OpenTelemetry", "1.55.0") { + library("OpenTelemetry", "1.57.0") { group("io.opentelemetry") { bom("opentelemetry-bom") } From b39ef3981ce4bd11181cbd4be144daf40290ce9a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:24:08 +0000 Subject: [PATCH 068/930] Upgrade to Oracle Database 23.26.0.0.0 Closes gh-48753 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9bd88a19f18e..59ac342c2b23 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1697,7 +1697,7 @@ bom { releaseNotes("https://github.com/open-telemetry/opentelemetry-java/releases/tag/v{version}") } } - library("Oracle Database", "23.9.0.25.07") { + library("Oracle Database", "23.26.0.0.0") { alignWith { dependencyManagementDeclaredIn("com.oracle.database.jdbc:ojdbc-bom") } From 3da794bacec9d92d5ed5b71de56a53558187ce25 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:24:12 +0000 Subject: [PATCH 069/930] Upgrade to Pooled JMS 3.2.1 Closes gh-48754 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 59ac342c2b23..aaeaa77c35f7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1748,7 +1748,7 @@ bom { releaseNotes("https://github.com/oracle/oracle-r2dbc/releases/tag/{version}") } } - library("Pooled JMS", "3.1.8") { + library("Pooled JMS", "3.2.1") { group("org.messaginghub") { modules = [ "pooled-jms" From 6a4957ef76242f91b8c4c15a3815511125d0ed42 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:24:16 +0000 Subject: [PATCH 070/930] Upgrade to Rabbit AMQP Client 5.28.0 Closes gh-48755 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index aaeaa77c35f7..ad055b590115 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1912,7 +1912,7 @@ bom { releaseNotes("https://github.com/r2dbc/r2dbc-spi/releases/tag/v{version}") } } - library("Rabbit AMQP Client", "5.27.1") { + library("Rabbit AMQP Client", "5.28.0") { group("com.rabbitmq") { modules = [ "amqp-client" From 721ef1db6b3e5ddcecbfd408949e7ce387e2b865 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:24:21 +0000 Subject: [PATCH 071/930] Upgrade to Selenium 4.39.0 Closes gh-48756 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ad055b590115..9b631dc72789 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2300,7 +2300,7 @@ bom { ] } } - library("Selenium", "4.37.0") { + library("Selenium", "4.39.0") { group("org.seleniumhq.selenium") { bom("selenium-bom") } From 8e9becc09813e5482ee23d153ecf416a3f36d817 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:24:25 +0000 Subject: [PATCH 072/930] Upgrade to Selenium HtmlUnit 4.39.0 Closes gh-48757 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9b631dc72789..98cc3969f95f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2310,7 +2310,7 @@ bom { releaseNotes("https://github.com/SeleniumHQ/selenium/releases/tag/selenium-{version}") } } - library("Selenium HtmlUnit", "4.36.1") { + library("Selenium HtmlUnit", "4.39.0") { group("org.seleniumhq.selenium") { modules = [ "htmlunit3-driver" From 6629b635827ddfcc91a4e12a41efd202c3d811ae Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:24:29 +0000 Subject: [PATCH 073/930] Upgrade to SQLite JDBC 3.51.1.0 Closes gh-48758 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 98cc3969f95f..c7d395568541 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2595,7 +2595,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ws/releases/tag/v{version}") } } - library("SQLite JDBC", "3.50.3.0") { + library("SQLite JDBC", "3.51.1.0") { group("org.xerial") { modules = [ "sqlite-jdbc" From 6c73be2628d0b8aef45157d9e3fe7352a10f1099 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:24:33 +0000 Subject: [PATCH 074/930] Upgrade to Versions Maven Plugin 2.20.1 Closes gh-48759 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c7d395568541..8e6b39149357 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2720,7 +2720,7 @@ bom { releaseNotes("https://github.com/pingidentity/ldapsdk/releases/tag/{version}") } } - library("Versions Maven Plugin", "2.19.1") { + library("Versions Maven Plugin", "2.20.1") { group("org.codehaus.mojo") { plugins = [ "versions-maven-plugin" From 94386b3b83cce212a6f575190340f409bf9fc12e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:24:38 +0000 Subject: [PATCH 075/930] Upgrade to XmlUnit2 2.11.0 Closes gh-48760 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8e6b39149357..a85567455bac 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2769,7 +2769,7 @@ bom { releaseNotes("https://github.com/mojohaus/xml-maven-plugin/releases/tag/{version}") } } - library("XmlUnit2", "2.10.4") { + library("XmlUnit2", "2.11.0") { group("org.xmlunit") { modules = [ "xmlunit-assertj", From 03470bfaef7fdeca705fbbb8a63a86dd40707562 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2026 17:33:30 +0000 Subject: [PATCH 076/930] Upgrade to Rabbit Stream Client 1.4.0 Exclusions for org.lz4:lz4-java have been added as the Rabbit Stream Client now depends upon at.yawk.lz4:lz4-java which its replacement. Closes gh-48761 --- documentation/spring-boot-docs/build.gradle | 16 ++++++++++++---- platform/spring-boot-dependencies/build.gradle | 6 +----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index e95c95d2bd2a..c7b3c177f695 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -161,7 +161,9 @@ dependencies { implementation("jakarta.validation:jakarta.validation-api") implementation("org.apache.httpcomponents.client5:httpclient5") implementation("org.apache.commons:commons-dbcp2") - implementation("org.apache.kafka:kafka-streams") + implementation("org.apache.kafka:kafka-streams") { + exclude group: "org.lz4", module: "lz4-java" + } implementation("org.apache.logging.log4j:log4j-to-slf4j") implementation("org.apache.tomcat.embed:tomcat-embed-core") implementation("org.assertj:assertj-core") @@ -208,7 +210,9 @@ dependencies { implementation("org.springframework.data:spring-data-r2dbc") 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") { + exclude group: "org.lz4", module: "lz4-java" + } implementation("org.springframework.kafka:spring-kafka-test") implementation("org.springframework.pulsar:spring-pulsar") implementation("org.springframework.restdocs:spring-restdocs-mockmvc") @@ -409,9 +413,13 @@ project.rootProject.gradle.projectsEvaluated { .findAll { !it.name.startsWith('spring-boot-starter') } .each { javadocProject -> dependencies { - javadoc(project(javadocProject.path)) + javadoc(project(javadocProject.path)) { + exclude group: "org.lz4", module: "lz4-java" + } if (javadocProject.plugins.hasPlugin(OptionalDependenciesPlugin)) { - javadoc(project(path: javadocProject.path, configuration: 'optional')) + javadoc(project(path: javadocProject.path, configuration: 'optional')) { + exclude group: "org.lz4", module: "lz4-java" + } } } } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a85567455bac..bcdf538d60ea 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1924,11 +1924,7 @@ bom { releaseNotes("https://github.com/rabbitmq/rabbitmq-java-client/releases/tag/v{version}") } } - library("Rabbit Stream Client", "0.23.0") { - prohibit { - versionRange "[0.24.0,)" - because "It requires Netty 4.2.0" - } + library("Rabbit Stream Client", "1.4.0") { group("com.rabbitmq") { modules = [ "stream-client" From c9246cec4fc745323a1e18d2d9a06c9c81564315 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Jan 2026 15:28:15 +0000 Subject: [PATCH 077/930] Disable HTTP client's built-in decompression The built-in decompression support in HttpClient 5.6 breaks Elasticsearch's Rest5Client as it results in double decompression. The first decompression is performed by the Apache HTTP client. It succeeds. The Elasticsearch Rest5Client then sees the Content-Encoding: gzip header and makes a second attempt to inflate the data. This fails as it has already been inflated. Closes gh-48743 --- .../ElasticsearchRestClientConfigurations.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchRestClientConfigurations.java b/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchRestClientConfigurations.java index 22ee528bc606..133d5e22496a 100644 --- a/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchRestClientConfigurations.java +++ b/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchRestClientConfigurations.java @@ -107,8 +107,10 @@ Rest5ClientBuilder elasticsearchRestClientBuilder(ElasticsearchConnectionDetails builder.setDefaultHeaders( new Header[] { new BasicHeader("Authorization", "ApiKey " + connectionDetails.getApiKey()) }); } - builder.setHttpClientConfigCallback((httpClientBuilder) -> builderCustomizers.orderedStream() - .forEach((customizer) -> customizer.customize(httpClientBuilder))); + builder.setHttpClientConfigCallback((httpClientBuilder) -> { + httpClientBuilder.disableContentCompression(); + builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder)); + }); builder.setConnectionManagerCallback((connectionManagerBuilder) -> builderCustomizers.orderedStream() .forEach((customizer) -> customizer.customize(connectionManagerBuilder))); builder.setConnectionConfigCallback((connectionConfigBuilder) -> builderCustomizers.orderedStream() From 16c85ef9033f55365c3f770b77d519d56b8d60b6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 12 Jan 2026 11:57:17 +0000 Subject: [PATCH 078/930] Start building against Spring AMQP 4.1.0-M1 snapshots See gh-48800 --- .../boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java | 2 +- platform/spring-boot-dependencies/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java index 27bbc9918845..1277e0afd4f9 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java @@ -26,9 +26,9 @@ import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +import org.springframework.amqp.core.MessageListenerContainer; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.config.ContainerCustomizer; -import org.springframework.amqp.rabbit.listener.MessageListenerContainer; import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; import org.springframework.amqp.support.converter.MessageConverter; diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index cd4b3d3d63a3..8f56853bce41 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2363,7 +2363,7 @@ bom { ] } } - library("Spring AMQP", "4.0.1") { + library("Spring AMQP", "4.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.amqp") { bom("spring-amqp-bom") From 04f0c7dbed4a800284d3bb17a09a6989ce6d0318 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 12 Jan 2026 11:57:21 +0000 Subject: [PATCH 079/930] Start building against Spring Integration 7.1.0-M1 snapshots See gh-48801 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8f56853bce41..59a246f24062 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2458,7 +2458,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-hateoas/releases/tag/{version}") } } - library("Spring Integration", "7.0.1") { + library("Spring Integration", "7.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.integration") { bom("spring-integration-bom") From 12d5543a69db5fe912a815792b7c13c6c7737267 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 12 Jan 2026 11:57:25 +0000 Subject: [PATCH 080/930] Start building against Spring Security 7.1.0-M1 snapshots See gh-48802 --- platform/spring-boot-dependencies/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 59a246f24062..3440861f7fe0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2541,7 +2541,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-restdocs/releases/tag/v{version}") } } - library("Spring Security", "7.0.2") { + library("Spring Security", "7.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { bom("spring-security-bom") @@ -2549,8 +2549,8 @@ bom { links { site("https://spring.io/projects/spring-security") github("https://github.com/spring-projects/spring-security") - javadoc(version -> "https://docs.spring.io/spring-security/site/docs/%s/api" - .formatted(version.forMajorMinorGeneration()), "org.springframework.security") + javadoc(version -> "https://docs.spring.io/spring-security/reference/%s/api/java" + .formatted(version.forAntora()), "org.springframework.security") docs(version -> "https://docs.spring.io/spring-security/reference/%s" .formatted(version.forAntora())) releaseNotes("https://github.com/spring-projects/spring-security/releases/tag/{version}") From a1c83b52b09da96ac9b0f6e74830f37722290461 Mon Sep 17 00:00:00 2001 From: HuitaePark Date: Sat, 10 Jan 2026 22:29:35 +0900 Subject: [PATCH 081/930] Support configuring bootBuildImage's environment on the command line Accumulate command line --environment values in a ListProperty and merge them with the configured environment when building the effective environment map. Add a test that verifies multiple entries are accepted and propagated. See gh-48792 Signed-off-by: HuitaePark --- .../BootBuildImageIntegrationTests.java | 15 ++++++ .../gradle/tasks/bundling/BootBuildImage.java | 47 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index c533fc2ad804..78bcb7f8f4e7 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -469,6 +469,21 @@ void buildsImageOnLinuxArmWithImagePlatformLinuxArm() throws IOException { removeImages(projectName, builderImage, runImage, buildpackImage); } + @TestTemplate + void buildsImageWithMultipleCommandLineEnvironments() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", + "--environment", "BP_LIVE_RELOAD_ENABLED=true", + "--environment", "MY_CUSTOM_VAR=hello_world"); + BuildTask task = result.task(":bootBuildImage"); + assertThat(task).isNotNull(); + assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("BP_LIVE_RELOAD_ENABLED=true"); + assertThat(result.getOutput()).contains("MY_CUSTOM_VAR=hello_world"); + removeImages(this.gradleBuild.getProjectDir().getName()); + } + @TestTemplate @EnabledOnOs(value = { OS.LINUX, OS.MAC }, architectures = "amd64", disabledReason = "The expected failure condition will not fail on ARM architectures") diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 1945c156bd6d..102e6ea40d1e 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -22,6 +22,7 @@ import org.gradle.api.Action; import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.file.RegularFileProperty; @@ -30,6 +31,7 @@ import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.PathSensitive; @@ -160,6 +162,20 @@ public BootBuildImage() { @Input public abstract MapProperty getEnvironment(); + /** + * Returns environment variables contributed from the command line. + * Each entry must be in the form NAME=VALUE. + * @return the environment variables from the command line + */ + @Internal + public abstract ListProperty getEnvironmentFromCommandLine(); + + @Option(option = "environment", + description = "Environment variable to set for the build (NAME=VALUE). Can be specified multiple times.") + public void environment(List environment) { + getEnvironmentFromCommandLine().addAll(environment); + } + /** * Returns whether caches should be cleaned before packaging. * @return whether caches should be cleaned @@ -413,8 +429,8 @@ private BuildRequest customizeRunImage(BuildRequest request) { } private BuildRequest customizeEnvironment(BuildRequest request) { - Map environment = getEnvironment().getOrNull(); - if (!CollectionUtils.isEmpty(environment)) { + Map environment = getEffectiveEnvironment(); + if (!environment.isEmpty()) { request = request.withEnv(environment); } return request; @@ -502,4 +518,31 @@ private BuildRequest customizeSecurityOptions(BuildRequest request) { return request; } + private Map getEffectiveEnvironment() { + Map environment = new java.util.LinkedHashMap<>(); + Map configured = getEnvironment().getOrNull(); + if (!CollectionUtils.isEmpty(configured)) { + environment.putAll(configured); + } + List fromCli = getEnvironmentFromCommandLine().getOrNull(); + if (!CollectionUtils.isEmpty(fromCli)) { + for (String entry : fromCli) { + Map.Entry parsed = parseEnvironmentEntry(entry); + environment.put(parsed.getKey(), parsed.getValue()); + } + } + return environment; + } + + private Map.Entry parseEnvironmentEntry(String entry) { + int index = entry.indexOf('='); + if (index <= 0) { + throw new GradleException( + "Invalid value for option '--environment'. Expected 'NAME=VALUE' but got '" + entry + "'."); + } + String name = entry.substring(0, index); + String value = entry.substring(index + 1); + return Map.entry(name, value); + } + } From 542ea4f18a47eb3d38e1204f96f7fcd6ca72c232 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 12 Jan 2026 11:10:45 +0000 Subject: [PATCH 082/930] Polish "Support configuring bootBuildImage's environment on the command line" See gh-48792 --- .../BootBuildImageIntegrationTests.java | 3 +- .../pages/packaging-oci-image.adoc | 11 ++- .../gradle/tasks/bundling/BootBuildImage.java | 67 ++++++++----------- .../tasks/bundling/BootBuildImageTests.java | 19 ++++++ 4 files changed, 59 insertions(+), 41 deletions(-) diff --git a/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index 78bcb7f8f4e7..b9103ce53ae6 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -473,8 +473,7 @@ void buildsImageOnLinuxArmWithImagePlatformLinuxArm() throws IOException { void buildsImageWithMultipleCommandLineEnvironments() throws IOException { writeMainClass(); writeLongNameResource(); - BuildResult result = this.gradleBuild.build("bootBuildImage", - "--environment", "BP_LIVE_RELOAD_ENABLED=true", + BuildResult result = this.gradleBuild.build("bootBuildImage", "--environment", "BP_LIVE_RELOAD_ENABLED=true", "--environment", "MY_CUSTOM_VAR=hello_world"); BuildTask task = result.task(":bootBuildImage"); assertThat(task).isNotNull(); diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc index 139eeda2adb0..8e3b4bbfaded 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc @@ -162,7 +162,7 @@ Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`. | `ALWAYS` | `environment` -| +| `--environment` | Environment variables that should be passed to the builder. | Empty. @@ -346,6 +346,15 @@ include::example$packaging/boot-build-image-env.gradle.kts[tags=env] ---- ====== +Environment variables can also be specified on the command line, as shown in the following example: + +[source,shell] +---- +$ gradle bootBuildImage --environment BP_JVM_VERSION=17 +---- + +`--environment` can be used multiple times to specify multiple environment variables. + If there is a network proxy between the Docker daemon the builder runs in and network locations that buildpacks download artifacts from, you will need to configure the builder to use the proxy. When using the Paketo builder, this can be accomplished by setting the `HTTPS_PROXY` and/or `HTTP_PROXY` environment variables as show in the following example: diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 102e6ea40d1e..078180fafae4 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -17,12 +17,14 @@ package org.springframework.boot.gradle.tasks.bundling; import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.gradle.api.Action; import org.gradle.api.DefaultTask; -import org.gradle.api.GradleException; +import org.gradle.api.InvalidUserDataException; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.file.RegularFileProperty; @@ -104,6 +106,23 @@ public BootBuildImage() { this.docker = getProject().getObjects().newInstance(DockerSpec.class); this.pullPolicy = getProject().getObjects().property(PullPolicy.class); getSecurityOptions().convention((Iterable) null); + getEffectiveEnvironment().putAll(getEnvironment()); + getEffectiveEnvironment().putAll(getEnvironmentFromCommandLine().map(BootBuildImage::asMap)); + } + + private static Map asMap(List variables) { + Map environment = new LinkedHashMap<>(); + for (String variable : variables) { + int index = variable.indexOf('='); + if (index <= 0) { + throw new InvalidUserDataException( + "Invalid value for option '--environment'. Expected 'NAME=VALUE' but got '" + variable + "'."); + } + String name = variable.substring(0, index); + String value = variable.substring(index + 1); + environment.put(name, value); + } + return environment; } /** @@ -159,22 +178,21 @@ public BootBuildImage() { * Returns the environment that will be used when building the image. * @return the environment */ - @Input + @Internal public abstract MapProperty getEnvironment(); /** - * Returns environment variables contributed from the command line. - * Each entry must be in the form NAME=VALUE. + * Returns environment variables contributed from the command line. Each entry must be + * in the form NAME=VALUE. * @return the environment variables from the command line */ @Internal - public abstract ListProperty getEnvironmentFromCommandLine(); + @Option(option = "environment", description = "Environment variable that will be used when building the image " + + "(NAME=VALUE). Can be specified multiple times.") + abstract ListProperty getEnvironmentFromCommandLine(); - @Option(option = "environment", - description = "Environment variable to set for the build (NAME=VALUE). Can be specified multiple times.") - public void environment(List environment) { - getEnvironmentFromCommandLine().addAll(environment); - } + @Input + abstract MapProperty getEffectiveEnvironment(); /** * Returns whether caches should be cleaned before packaging. @@ -429,7 +447,7 @@ private BuildRequest customizeRunImage(BuildRequest request) { } private BuildRequest customizeEnvironment(BuildRequest request) { - Map environment = getEffectiveEnvironment(); + Map environment = getEffectiveEnvironment().getOrElse(Collections.emptyMap()); if (!environment.isEmpty()) { request = request.withEnv(environment); } @@ -518,31 +536,4 @@ private BuildRequest customizeSecurityOptions(BuildRequest request) { return request; } - private Map getEffectiveEnvironment() { - Map environment = new java.util.LinkedHashMap<>(); - Map configured = getEnvironment().getOrNull(); - if (!CollectionUtils.isEmpty(configured)) { - environment.putAll(configured); - } - List fromCli = getEnvironmentFromCommandLine().getOrNull(); - if (!CollectionUtils.isEmpty(fromCli)) { - for (String entry : fromCli) { - Map.Entry parsed = parseEnvironmentEntry(entry); - environment.put(parsed.getKey(), parsed.getValue()); - } - } - return environment; - } - - private Map.Entry parseEnvironmentEntry(String entry) { - int index = entry.indexOf('='); - if (index <= 0) { - throw new GradleException( - "Invalid value for option '--environment'. Expected 'NAME=VALUE' but got '" + entry + "'."); - } - String name = entry.substring(0, index); - String value = entry.substring(index + 1); - return Map.entry(name, value); - } - } diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java index 0eef7843ef36..9a917d023825 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -144,6 +144,25 @@ void whenTheEnvironmentIsSetItReplacesAnyExistingEntriesAndIsIncludedInTheReques .hasSize(2); } + @Test + void whenEnvironmentVariablesAreSetOnTheCommandLineTheyAreIncludedInTheRequest() { + this.buildImage.getEnvironmentFromCommandLine().add("ALPHA=a"); + this.buildImage.getEnvironmentFromCommandLine().add("BRAVO=b"); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a") + .containsEntry("BRAVO", "b") + .hasSize(2); + } + + @Test + void environmentVariablesFromTheCommandLineOverrideThoseInTheBuildScript() { + this.buildImage.getEnvironment().put("ALPHA", "a"); + this.buildImage.getEnvironmentFromCommandLine().add("ALPHA=apple"); + this.buildImage.getEnvironmentFromCommandLine().add("BRAVO=banana"); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "apple") + .containsEntry("BRAVO", "banana") + .hasSize(2); + } + @Test void whenUsingDefaultConfigurationThenRequestHasVerboseLoggingDisabled() { assertThat(this.buildImage.createRequest().isVerboseLogging()).isFalse(); From 0ed5f2674f10dd371f4ba16bb015887bb6e33189 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 13 Jan 2026 10:44:18 +0000 Subject: [PATCH 083/930] Avoid losing type information in on bean condition messages --- .../condition/OnBeanCondition.java | 69 +++++++++++-------- .../ConditionalOnMissingBeanTests.java | 21 ++++++ 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java index a53c64fcda7d..5bbf83fcfb0f 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java @@ -213,11 +213,11 @@ protected final MatchResult getMatchingBeans(Spec spec) { ConfigurableListableBeanFactory beanFactory = getSearchBeanFactory(spec); ClassLoader classLoader = spec.getContext().getClassLoader(); boolean considerHierarchy = spec.getStrategy() != SearchStrategy.CURRENT; - Set parameterizedContainers = spec.getParameterizedContainers(); + Set parameterizedContainers = spec.getParameterizedContainers(); MatchResult result = new MatchResult(); Set beansIgnoredByType = getNamesOfBeansIgnoredByType(beanFactory, considerHierarchy, spec.getIgnoredTypes(), parameterizedContainers); - for (ResolvableType type : spec.getTypes()) { + for (BeanType type : spec.getTypes()) { Map typeMatchedDefinitions = getBeanDefinitionsForType(beanFactory, considerHierarchy, type, parameterizedContainers); Set typeMatchedNames = matchedNamesFrom(typeMatchedDefinitions, @@ -307,9 +307,9 @@ private boolean isDefaultCandidate(BeanDefinition definition) { } private Set getNamesOfBeansIgnoredByType(ListableBeanFactory beanFactory, boolean considerHierarchy, - Set ignoredTypes, Set parameterizedContainers) { + Set ignoredTypes, Set parameterizedContainers) { Set result = null; - for (ResolvableType ignoredType : ignoredTypes) { + for (BeanType ignoredType : ignoredTypes) { Collection ignoredNames = getBeanDefinitionsForType(beanFactory, considerHierarchy, ignoredType, parameterizedContainers) .keySet(); @@ -319,20 +319,20 @@ private Set getNamesOfBeansIgnoredByType(ListableBeanFactory beanFactory } private Map getBeanDefinitionsForType(ListableBeanFactory beanFactory, - boolean considerHierarchy, ResolvableType type, Set parameterizedContainers) { + boolean considerHierarchy, BeanType type, Set parameterizedContainers) { Map result = collectBeanDefinitionsForType(beanFactory, considerHierarchy, type, parameterizedContainers, null); return (result != null) ? result : Collections.emptyMap(); } private @Nullable Map collectBeanDefinitionsForType( - ListableBeanFactory beanFactory, boolean considerHierarchy, ResolvableType type, - Set parameterizedContainers, @Nullable Map result) { - result = putAll(result, beanFactory.getBeanNamesForType(type, true, false), beanFactory); - for (ResolvableType parameterizedContainer : parameterizedContainers) { - Class resolved = parameterizedContainer.resolve(); + ListableBeanFactory beanFactory, boolean considerHierarchy, BeanType type, + Set parameterizedContainers, @Nullable Map result) { + result = putAll(result, beanFactory.getBeanNamesForType(type.resolvableType(), true, false), beanFactory); + for (BeanType parameterizedContainer : parameterizedContainers) { + Class resolved = parameterizedContainer.resolvableType().resolve(); Assert.state(resolved != null, "'resolved' must not be null"); - ResolvableType generic = ResolvableType.forClassWithGenerics(resolved, type); + ResolvableType generic = ResolvableType.forClassWithGenerics(resolved, type.resolvableType()); result = putAll(result, beanFactory.getBeanNamesForType(generic, true, false), beanFactory); } if (considerHierarchy && beanFactory instanceof HierarchicalBeanFactory hierarchicalBeanFactory) { @@ -563,13 +563,13 @@ private static class Spec { private final Set names; - private final Set types; + private final Set types; private final Set annotations; - private final Set ignoredTypes; + private final Set ignoredTypes; - private final Set parameterizedContainers; + private final Set parameterizedContainers; private final @Nullable SearchStrategy strategy; @@ -586,7 +586,7 @@ private static class Spec { this.ignoredTypes = resolveWhenPossible(extract(attributes, "ignored", "ignoredType")); this.parameterizedContainers = resolveWhenPossible(extract(attributes, "parameterizedContainer")); this.strategy = annotation.getValue("search", SearchStrategy.class).orElse(null); - Set types = resolveWhenPossible(extractTypes(attributes)); + Set types = resolveWhenPossible(extractTypes(attributes)); BeanTypeDeductionException deductionException = null; if (types.isEmpty() && this.names.isEmpty() && this.annotations.isEmpty()) { try { @@ -628,18 +628,18 @@ private void merge(Set result, String... additional) { Collections.addAll(result, additional); } - private Set resolveWhenPossible(Set classNames) { + private Set resolveWhenPossible(Set classNames) { if (classNames.isEmpty()) { return Collections.emptySet(); } - Set resolved = new LinkedHashSet<>(classNames.size()); + Set resolved = new LinkedHashSet<>(classNames.size()); for (String className : classNames) { try { Class type = resolve(className, this.context.getClassLoader()); - resolved.add(ResolvableType.forRawClass(type)); + resolved.add(new BeanType(className, ResolvableType.forRawClass(type))); } catch (ClassNotFoundException | NoClassDefFoundError ex) { - resolved.add(ResolvableType.NONE); + resolved.add(new BeanType(className, ResolvableType.NONE)); } } return resolved; @@ -668,14 +668,14 @@ protected final String getAnnotationName() { return "@" + ClassUtils.getShortName(this.annotationType); } - private Set deducedBeanType(ConditionContext context, AnnotatedTypeMetadata metadata) { + private Set deducedBeanType(ConditionContext context, AnnotatedTypeMetadata metadata) { if (metadata instanceof MethodMetadata && metadata.isAnnotated(Bean.class.getName())) { return deducedBeanTypeForBeanMethod(context, (MethodMetadata) metadata); } return Collections.emptySet(); } - private Set deducedBeanTypeForBeanMethod(ConditionContext context, MethodMetadata metadata) { + private Set deducedBeanTypeForBeanMethod(ConditionContext context, MethodMetadata metadata) { try { return Set.of(getReturnType(context, metadata)); } @@ -684,7 +684,7 @@ private Set deducedBeanTypeForBeanMethod(ConditionContext contex } } - private ResolvableType getReturnType(ConditionContext context, MethodMetadata metadata) + private BeanType getReturnType(ConditionContext context, MethodMetadata metadata) throws ClassNotFoundException, LinkageError { // Safe to load at this point since we are in the REGISTER_BEAN phase ClassLoader classLoader = context.getClassLoader(); @@ -692,12 +692,12 @@ private ResolvableType getReturnType(ConditionContext context, MethodMetadata me if (isParameterizedContainer(returnType.resolve())) { returnType = returnType.getGeneric(); } - return returnType; + return new BeanType(returnType.toString(), returnType); } private boolean isParameterizedContainer(@Nullable Class type) { return (type != null) && this.parameterizedContainers.stream() - .map(ResolvableType::resolve) + .map((beanType) -> beanType.resolvableType().resolve(type)) .anyMatch((container) -> container != null && container.isAssignableFrom(type)); } @@ -732,7 +732,7 @@ private SearchStrategy getStrategy() { return (this.strategy != null) ? this.strategy : SearchStrategy.ALL; } - Set getTypes() { + Set getTypes() { return this.types; } @@ -748,11 +748,11 @@ private Set getAnnotations() { return this.annotations; } - private Set getIgnoredTypes() { + private Set getIgnoredTypes() { return this.ignoredTypes; } - private Set getParameterizedContainers() { + private Set getParameterizedContainers() { return this.parameterizedContainers; } @@ -861,12 +861,12 @@ private void recordUnmatchedAnnotation(String annotation) { this.unmatchedAnnotations.add(annotation); } - private void recordMatchedType(ResolvableType type, Collection matchingNames) { - this.matchedTypes.put(type.toString(), matchingNames); + private void recordMatchedType(BeanType type, Collection matchingNames) { + this.matchedTypes.put(type.name.toString(), matchingNames); this.namesOfAllMatches.addAll(matchingNames); } - private void recordUnmatchedType(ResolvableType type) { + private void recordUnmatchedType(BeanType type) { this.unmatchedTypes.add(type.toString()); } @@ -921,4 +921,13 @@ private BeanTypeDeductionException(String className, String beanMethodName, Thro } + record BeanType(String name, ResolvableType resolvableType) { + + @Override + public String toString() { + return ResolvableType.NONE.equals(this.resolvableType) ? this.name : this.resolvableType.toString(); + } + + } + } diff --git a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java index 521d5807aefb..05d4e9d2e32a 100644 --- a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java +++ b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java @@ -35,6 +35,8 @@ import org.springframework.boot.autoconfigure.condition.scan.ScannedFactoryBeanConfiguration; import org.springframework.boot.autoconfigure.condition.scan.ScannedFactoryBeanWithBeanMethodArgumentsConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; +import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @@ -503,6 +505,14 @@ void parameterizedContainerGenericWhenSubclassTypeArgumentMatches() { .satisfies(beansAndContainersNamed(GenericExampleBean.class, "customGenericExampleBean"))); } + @Test + void yep() { + this.contextRunner.withUserConfiguration(OnMissingBeanForTypeThatDoesNotExist.class) + .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .run((context) -> { + }); + } + private Consumer beansAndContainersNamed(Class type, String... names) { return (context) -> { String[] beans = context.getBeanNamesForType(type); @@ -1136,4 +1146,15 @@ static class TestScope extends SimpleThreadScope { } + @Configuration(proxyBeanMethods = false) + static class OnMissingBeanForTypeThatDoesNotExist { + + @Bean + @ConditionalOnMissingBean(type = "com.example.DoesNotExist") + String bean() { + return "bean"; + } + + } + } From 83f633b04e78a843c51d7dc255cb55eb35f6c4c5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 13 Jan 2026 17:09:57 +0000 Subject: [PATCH 084/930] Upgrade to Micrometer 1.17.0-M1 Closes gh-48722 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3440861f7fe0..cc0fb49d88ff 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1531,7 +1531,7 @@ bom { releaseNotes("https://github.com/apache/maven-war-plugin/releases/tag/maven-war-plugin-{version}") } } - library("Micrometer", "1.17.0-SNAPSHOT") { + library("Micrometer", "1.17.0-M1") { considerSnapshots() group("io.micrometer") { modules = [ From 57de0a59ac72af944336c2aa0119c0eee7475281 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 13 Jan 2026 17:09:58 +0000 Subject: [PATCH 085/930] Upgrade to Micrometer Tracing 1.7.0-M2 Closes gh-48723 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index cc0fb49d88ff..225481e37da8 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1554,7 +1554,7 @@ bom { releaseNotes("https://github.com/micrometer-metrics/micrometer/releases/tag/v{version}") } } - library("Micrometer Tracing", "1.7.0-SNAPSHOT") { + library("Micrometer Tracing", "1.7.0-M2") { considerSnapshots() group("io.micrometer") { bom("micrometer-tracing-bom") From af8235b117e838fedf25c06d72ba0bb406291393 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 13 Jan 2026 17:09:59 +0000 Subject: [PATCH 086/930] Upgrade to Reactor Bom 2025.0.2 Closes gh-48724 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 225481e37da8..0109c387123f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1945,7 +1945,7 @@ bom { ] } } - library("Reactor Bom", "2025.0.2-SNAPSHOT") { + library("Reactor Bom", "2025.0.2") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From 66d635f6a0f593d36f96694471d3dc89cb33f416 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jan 2026 09:55:46 +0000 Subject: [PATCH 087/930] Start building against Spring HATEOAS 3.0.2 snapshots See gh-48837 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0109c387123f..71ee255b6e3d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2441,7 +2441,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-graphql/releases/tag/v{version}") } } - library("Spring HATEOAS", "3.0.1") { + library("Spring HATEOAS", "3.0.2-SNAPSHOT") { considerSnapshots() group("org.springframework.hateoas") { modules = [ From d1e4d72fa58aba53e092f278503ac42e42a37e2c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jan 2026 11:06:56 +0000 Subject: [PATCH 088/930] Polish See 8bd140305aeb7d5211966b65264b2c57771cc663 --- .../autoconfigure/condition/ConditionalOnMissingBeanTests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java index 5ce39f322682..a2e27673e607 100644 --- a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java +++ b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java @@ -38,8 +38,6 @@ import org.springframework.boot.autoconfigure.condition.scan.ScannedFactoryBeanConfiguration; import org.springframework.boot.autoconfigure.condition.scan.ScannedFactoryBeanWithBeanMethodArgumentsConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; -import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; From 890318874fb078918184d72dc49a3d2aece87fb1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jan 2026 14:41:07 +0000 Subject: [PATCH 089/930] Upgrade to Classmate 1.7.3 Closes gh-48849 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 71ee255b6e3d..900bbaefd8a7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -201,7 +201,7 @@ bom { ] } } - library("Classmate", "1.7.1") { + library("Classmate", "1.7.3") { group("com.fasterxml") { modules = [ "classmate" From 191bb3c533a0fc5f3bf9324c7e76aa162508de59 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jan 2026 14:41:11 +0000 Subject: [PATCH 090/930] Upgrade to Flyway 11.20.2 Closes gh-48850 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 900bbaefd8a7..023caf21ef9b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -379,7 +379,7 @@ bom { javadoc("elasticsearch-java", version -> "https://javadoc.io/doc/co.elastic.clients/elasticsearch-java/%s/index.html".formatted(version), "co.elastic.clients.elasticsearch", "co.elastic.clients.transport") } } - library("Flyway", "11.20.1") { + library("Flyway", "11.20.2") { group("org.flywaydb") { modules = [ "flyway-commandline", From d89ba97c4fb17dc1961a1440bb272ed189cc1af1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jan 2026 14:41:12 +0000 Subject: [PATCH 091/930] Upgrade to Hibernate 7.2.1.Final Closes gh-48845 --- platform/spring-boot-dependencies/build.gradle | 2 +- .../src/main/java/smoketest/flyway/Person.java | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 023caf21ef9b..c606536954e7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -563,7 +563,7 @@ bom { releaseNotes("https://github.com/hazelcast/hazelcast/releases/tag/v{version}") } } - library("Hibernate", "7.2.0.Final") { + library("Hibernate", "7.2.1.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", diff --git a/smoke-test/spring-boot-smoke-test-flyway/src/main/java/smoketest/flyway/Person.java b/smoke-test/spring-boot-smoke-test-flyway/src/main/java/smoketest/flyway/Person.java index ec0a7a712eb4..6cea911fc1d4 100644 --- a/smoke-test/spring-boot-smoke-test-flyway/src/main/java/smoketest/flyway/Person.java +++ b/smoke-test/spring-boot-smoke-test-flyway/src/main/java/smoketest/flyway/Person.java @@ -16,6 +16,7 @@ package smoketest.flyway; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; @@ -30,23 +31,27 @@ public class Person { @GeneratedValue(generator = "person_generator") private @Nullable Long id; - private @Nullable String firstName; + @Column(nullable = false) + @SuppressWarnings("NullAway.Init") + private String firstName; - private @Nullable String lastName; + @Column(nullable = false) + @SuppressWarnings("NullAway.Init") + private String lastName; - public @Nullable String getFirstName() { + public String getFirstName() { return this.firstName; } - public void setFirstName(@Nullable String firstName) { + public void setFirstName(String firstName) { this.firstName = firstName; } - public @Nullable String getLastName() { + public String getLastName() { return this.lastName; } - public void setLastName(@Nullable String lastname) { + public void setLastName(String lastname) { this.lastName = lastname; } From 76be5f6f496804b2466fff6ca203ba8996f88cdf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jan 2026 14:41:18 +0000 Subject: [PATCH 092/930] Upgrade to JUnit Jupiter 6.0.2 Closes gh-48851 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d9d28847ba08..4dd6ce8d13c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ hamcrestVersion=3.0 jackson2Version=2.20.1 jacksonVersion=3.0.3 javaFormatVersion=0.0.47 -junitJupiterVersion=6.0.1 +junitJupiterVersion=6.0.2 kotlinVersion=2.3.0 mavenVersion=3.9.10 mockitoVersion=5.21.0 From 94cfc75a23fff61ca9665a1829b7246e76356c77 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jan 2026 14:41:25 +0000 Subject: [PATCH 093/930] Upgrade to Logback 1.5.24 Closes gh-48852 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c606536954e7..281374ff9176 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1291,7 +1291,7 @@ bom { releaseNotes("https://github.com/apache/logging-log4j2/releases/tag/rel%2F{version}") } } - library("Logback", "1.5.22") { + library("Logback", "1.5.24") { group("ch.qos.logback") { modules = [ "logback-classic", From 54010497d98838d14d399f3491c4d8a4b3c9ac88 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jan 2026 14:41:36 +0000 Subject: [PATCH 094/930] Upgrade to Pooled JMS 3.2.2 Closes gh-48854 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 281374ff9176..18d04df83a67 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1751,7 +1751,7 @@ bom { releaseNotes("https://github.com/oracle/oracle-r2dbc/releases/tag/{version}") } } - library("Pooled JMS", "3.2.1") { + library("Pooled JMS", "3.2.2") { group("org.messaginghub") { modules = [ "pooled-jms" From 7b5635c5c0740a74dd26ebc90dc2a57ef0262df3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jan 2026 14:41:36 +0000 Subject: [PATCH 095/930] Upgrade to R2DBC MSSQL 1.0.4.RELEASE Closes gh-48846 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 18d04df83a67..b57d1777a5b0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1848,7 +1848,7 @@ bom { releaseNotes("https://github.com/mariadb-corporation/mariadb-connector-r2dbc/releases/tag/{version}") } } - library("R2DBC MSSQL", "1.0.3.RELEASE") { + library("R2DBC MSSQL", "1.0.4.RELEASE") { group ("io.r2dbc") { modules = [ "r2dbc-mssql" From b0cdf0dd58b597e867e5e53bb8b32460b1bbbfaf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jan 2026 14:41:37 +0000 Subject: [PATCH 096/930] Upgrade to Spring Framework 7.0.3 Closes gh-48727 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4dd6ce8d13c5..7bfc1441970f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ mockitoVersion=5.21.0 nativeBuildToolsVersion=0.11.3 nullabilityPluginVersion=0.0.9 snakeYamlVersion=2.5 -springFrameworkVersion=7.0.3-SNAPSHOT +springFrameworkVersion=7.0.3 springFramework60xVersion=6.0.23 tomcatVersion=11.0.15 From 3e1cd49b9c139d11ed4d272bba822a561b31cd4e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jan 2026 14:41:37 +0000 Subject: [PATCH 097/930] Upgrade to Spring HATEOAS 3.0.2 Closes gh-48837 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b57d1777a5b0..141cd4a0eaf8 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2441,7 +2441,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-graphql/releases/tag/v{version}") } } - library("Spring HATEOAS", "3.0.2-SNAPSHOT") { + library("Spring HATEOAS", "3.0.2") { considerSnapshots() group("org.springframework.hateoas") { modules = [ From 531fa436a11cc467afc3ef201364c4a3b09f55db Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jan 2026 14:41:44 +0000 Subject: [PATCH 098/930] Upgrade to WebJars Locator Lite 1.1.3 Closes gh-48855 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 141cd4a0eaf8..b0314b1b93f7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2744,7 +2744,7 @@ bom { ] } } - library("WebJars Locator Lite", "1.1.2") { + library("WebJars Locator Lite", "1.1.3") { group("org.webjars") { modules = [ "webjars-locator-lite" From 44e6f41e53b7dcf3f396902ce04cd6c6f11c68d5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jan 2026 18:48:16 +0000 Subject: [PATCH 099/930] Upgrade to Lettuce 6.8.2.RELEASE Closes gh-48860 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b0314b1b93f7..5e84d8c5a988 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1234,7 +1234,7 @@ bom { releaseNotes("https://github.com/Kotlin/kotlinx.serialization/releases/tag/v{version}") } } - library("Lettuce", "6.8.1.RELEASE") { + library("Lettuce", "6.8.2.RELEASE") { prohibit { contains ".BETA" because "we don't want betas" From f21bab293be772d88b37c029f2a6622ace3f6c00 Mon Sep 17 00:00:00 2001 From: HuitaePark Date: Wed, 14 Jan 2026 20:20:57 +0900 Subject: [PATCH 100/930] Allow user-provided alternatives to MockRestServiceServer beans Previously, MockRestServiceServerAutoConfiguration unconditionally defined its beans, preventing the user from providing an alternative. This commit adds @ConditionalOnMissingBean to ensure the auto-configuration backs off when a user-provided alternatives are present. See gh-48825 Signed-off-by: HuitaePark --- ...ockRestServiceServerAutoConfiguration.java | 4 ++ ...stServiceServerAutoConfigurationTests.java | 57 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfigurationTests.java diff --git a/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfiguration.java b/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfiguration.java index 16096ec0feeb..b40ad6546bb8 100644 --- a/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfiguration.java +++ b/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfiguration.java @@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.restclient.test.MockServerRestClientCustomizer; import org.springframework.boot.restclient.test.MockServerRestTemplateCustomizer; import org.springframework.context.annotation.Bean; @@ -53,16 +54,19 @@ public final class MockRestServiceServerAutoConfiguration { @Bean + @ConditionalOnMissingBean MockServerRestTemplateCustomizer mockServerRestTemplateCustomizer() { return new MockServerRestTemplateCustomizer(); } @Bean + @ConditionalOnMissingBean MockServerRestClientCustomizer mockServerRestClientCustomizer() { return new MockServerRestClientCustomizer(); } @Bean + @ConditionalOnMissingBean MockRestServiceServer mockRestServiceServer(MockServerRestTemplateCustomizer restTemplateCustomizer, MockServerRestClientCustomizer restClientCustomizer) { try { diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfigurationTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfigurationTests.java new file mode 100644 index 000000000000..d86f2a9d9504 --- /dev/null +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfigurationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.restclient.test.autoconfigure; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.restclient.test.MockServerRestClientCustomizer; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MockRestServiceServerAutoConfiguration}. + * + * @author HuitaePark + */ +class MockRestServiceServerAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(MockRestServiceServerAutoConfiguration.class)); + + @Test + void registersRestClientCustomizerWhenMissing() { + this.contextRunner.withPropertyValues("spring.test.restclient.mockrestserviceserver.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(MockServerRestClientCustomizer.class)); + } + + @Test + void backsOffWhenUserProvidesRestClientCustomizer() { + MockServerRestClientCustomizer customCustomizer = new MockServerRestClientCustomizer(); + + this.contextRunner.withPropertyValues("spring.test.restclient.mockrestserviceserver.enabled=true") + .withBean("userMockServerRestClientCustomizer", MockServerRestClientCustomizer.class, + () -> customCustomizer) + .run((context) -> { + assertThat(context).hasSingleBean(MockServerRestClientCustomizer.class); + assertThat(context.getBean(MockServerRestClientCustomizer.class)) + .isSameAs(context.getBean("userMockServerRestClientCustomizer")); + }); + } + +} From bbe82d61c585e5a7b5bfa358a9c40ca07bfd8776 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 16 Jan 2026 09:19:25 +0000 Subject: [PATCH 101/930] Polish "Allow user-provided alternatives to MockRestServiceServer beans" See gh-48825 --- ...stServiceServerAutoConfigurationTests.java | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfigurationTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfigurationTests.java index d86f2a9d9504..87f4b52356f2 100644 --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfigurationTests.java +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfigurationTests.java @@ -20,38 +20,61 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.restclient.test.MockServerRestClientCustomizer; +import org.springframework.boot.restclient.test.MockServerRestTemplateCustomizer; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.client.RequestExpectationManager; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link MockRestServiceServerAutoConfiguration}. * * @author HuitaePark + * @author Andy Wilkinson */ class MockRestServiceServerAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.test.restclient.mockrestserviceserver.enabled=true") .withConfiguration(AutoConfigurations.of(MockRestServiceServerAutoConfiguration.class)); @Test - void registersRestClientCustomizerWhenMissing() { - this.contextRunner.withPropertyValues("spring.test.restclient.mockrestserviceserver.enabled=true") - .run((context) -> assertThat(context).hasSingleBean(MockServerRestClientCustomizer.class)); + void registersMockServerRestClientCustomizer() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(MockServerRestClientCustomizer.class)); } @Test - void backsOffWhenUserProvidesRestClientCustomizer() { - MockServerRestClientCustomizer customCustomizer = new MockServerRestClientCustomizer(); - - this.contextRunner.withPropertyValues("spring.test.restclient.mockrestserviceserver.enabled=true") - .withBean("userMockServerRestClientCustomizer", MockServerRestClientCustomizer.class, - () -> customCustomizer) - .run((context) -> { - assertThat(context).hasSingleBean(MockServerRestClientCustomizer.class); - assertThat(context.getBean(MockServerRestClientCustomizer.class)) - .isSameAs(context.getBean("userMockServerRestClientCustomizer")); - }); + void registersMockServerRestTemplateCustomizer() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(MockServerRestTemplateCustomizer.class)); + } + + @Test + void registersMockRestServiceServer() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(MockRestServiceServer.class)); + } + + @Test + void backsOffWhenUserProvidesMockServerRestClientCustomizer() { + this.contextRunner.withBean("userMockServerRestClientCustomizer", MockServerRestClientCustomizer.class) + .run((context) -> assertThat(context).hasSingleBean(MockServerRestClientCustomizer.class) + .hasBean("userMockServerRestClientCustomizer")); + } + + @Test + void backsOffWhenUserProvidesMockServerRestTemplateCustomizer() { + this.contextRunner.withBean("userMockServerRestTemplateCustomizer", MockServerRestTemplateCustomizer.class) + .run((context) -> assertThat(context).hasSingleBean(MockServerRestTemplateCustomizer.class) + .hasBean("userMockServerRestTemplateCustomizer")); + } + + @Test + void backsOffWhenUserProvidesMockRestServiceServer() { + this.contextRunner + .withBean("userMockRestServiceServer", MockRestServiceServer.class, mock(RequestExpectationManager.class)) + .run((context) -> assertThat(context).hasSingleBean(MockRestServiceServer.class) + .hasBean("userMockRestServiceServer")); } } From d93e8268c8671754de6a9215370630cac7b10569 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jan 2026 09:23:52 +0000 Subject: [PATCH 102/930] Upgrade to Byte Buddy 1.18.4 Closes gh-48891 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 5e84d8c5a988..d4502f751f9d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -146,7 +146,7 @@ bom { releaseNotes("https://github.com/mojohaus/build-helper-maven-plugin/releases/tag/{version}") } } - library("Byte Buddy", "1.18.3") { + library("Byte Buddy", "1.18.4") { group("net.bytebuddy") { modules = [ "byte-buddy", From 9a7a420a088ccbd3e44f97f8c0e27b478f6ff4ad Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jan 2026 09:23:56 +0000 Subject: [PATCH 103/930] Upgrade to Jackson 2 Bom 2.21.0 Closes gh-48892 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7bfc1441970f..aa96e328fa7e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ checkstyleToolVersion=10.12.4 commonsCodecVersion=1.20.0 graalVersion=25 hamcrestVersion=3.0 -jackson2Version=2.20.1 +jackson2Version=2.21.0 jacksonVersion=3.0.3 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.2 From 71583c53b3a778c82e9dc13f74297c12ccfccf8b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jan 2026 09:24:02 +0000 Subject: [PATCH 104/930] Upgrade to Logback 1.5.25 Closes gh-48893 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d4502f751f9d..9a2ae9eb45a5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1291,7 +1291,7 @@ bom { releaseNotes("https://github.com/apache/logging-log4j2/releases/tag/rel%2F{version}") } } - library("Logback", "1.5.24") { + library("Logback", "1.5.25") { group("ch.qos.logback") { modules = [ "logback-classic", From 9e18f96d29c24dfc16a37d4aed6e50e9a5da8119 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jan 2026 09:24:06 +0000 Subject: [PATCH 105/930] Upgrade to Postgresql 42.7.9 Closes gh-48894 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9a2ae9eb45a5..c6098f9c3963 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1761,7 +1761,7 @@ bom { javadoc("https://javadoc.io/doc/org.messaginghub/pooled-jms/{version}", "org.messaginghub.pooled.jms") } } - library("Postgresql", "42.7.8") { + library("Postgresql", "42.7.9") { group("org.postgresql") { modules = [ "postgresql" From bc7902e38cee47c09c1387709442b9cca36b5cf1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jan 2026 09:24:11 +0000 Subject: [PATCH 106/930] Upgrade to Selenium 4.40.0 Closes gh-48895 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c6098f9c3963..dbe9ddeedd7b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2299,7 +2299,7 @@ bom { ] } } - library("Selenium", "4.39.0") { + library("Selenium", "4.40.0") { group("org.seleniumhq.selenium") { bom("selenium-bom") } From c3ff9390027d2d5d4c323049da452686d4f67e14 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jan 2026 09:24:12 +0000 Subject: [PATCH 107/930] Upgrade to Spring Data Bom 2025.1.2 Closes gh-48726 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index dbe9ddeedd7b..0b2909c1011b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2393,7 +2393,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-batch/releases/tag/v{version}") } } - library("Spring Data Bom", "2025.1.2-SNAPSHOT") { + library("Spring Data Bom", "2025.1.2") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From c7a4fd1fbc50e828cd68283931b6380e25893b58 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jan 2026 09:24:16 +0000 Subject: [PATCH 108/930] Upgrade to Versions Maven Plugin 2.21.0 Closes gh-48896 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0b2909c1011b..880a1394297a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2719,7 +2719,7 @@ bom { releaseNotes("https://github.com/pingidentity/ldapsdk/releases/tag/{version}") } } - library("Versions Maven Plugin", "2.20.1") { + library("Versions Maven Plugin", "2.21.0") { group("org.codehaus.mojo") { plugins = [ "versions-maven-plugin" From 298fe258d94c51a40f23bd0365385aabda65a102 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jan 2026 09:24:20 +0000 Subject: [PATCH 109/930] Upgrade to XML Maven Plugin 1.2.1 Closes gh-48897 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 880a1394297a..a990e7c5854d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2758,7 +2758,7 @@ bom { ] } } - library("XML Maven Plugin", "1.2.0") { + library("XML Maven Plugin", "1.2.1") { group("org.codehaus.mojo") { plugins = [ "xml-maven-plugin" From 82a34a5595a8cd51364ec3c8f8009a6bb4d80e12 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jan 2026 09:44:51 +0000 Subject: [PATCH 110/930] Revert "Upgrade to Couchbase Client 3.10.0" This reverts commit 6dfefb512a1e8e3081755ccc8d3d1e24cfc0c453. See gh-48739 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a990e7c5854d..245ea806c52a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -272,7 +272,7 @@ bom { site("https://commons.apache.org/proper/commons-pool") } } - library("Couchbase Client", "3.10.0") { + library("Couchbase Client", "3.9.2") { group("com.couchbase.client") { modules = [ "java-client" From fca8487c80b74c198872712c10f309d98a0eb856 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jan 2026 09:53:55 +0000 Subject: [PATCH 111/930] Add version alignment configuration for OpenTelemetry Closes gh-48889 --- platform/spring-boot-dependencies/build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 245ea806c52a..c03b96e02da8 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1689,6 +1689,13 @@ bom { group("io.opentelemetry") { bom("opentelemetry-bom") } + alignWith { + version { + of "io.opentelemetry:opentelemetry-api" + from "io.micrometer:micrometer-tracing-bridge-otel" + managedBy "Micrometer Tracing" + } + } links { site("https://github.com/open-telemetry/opentelemetry-java") javadoc("opentelemetry-api", version -> "https://javadoc.io/doc/io.opentelemetry/opentelemetry-api/%s".formatted(version), "io.opentelemetry.api") From 25a54e0ba67c60fda4178c8c7f4a0b4f3ec796b2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jan 2026 09:54:26 +0000 Subject: [PATCH 112/930] Add version alignment configuration for Couchbase Client Closes gh-48890 --- platform/spring-boot-dependencies/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c03b96e02da8..dee18fa71711 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -278,6 +278,12 @@ bom { "java-client" ] } + alignWith { + version { + from "org.springframework.data:spring-data-couchbase" + managedBy "Spring Data Bom" + } + } links { site("https://docs.couchbase.com/java-sdk/current/hello-world/overview.html") javadoc("https://javadoc.io/doc/com.couchbase.client/java-client/{version}", "com.couchbase.client") From 1e458eff63b0e47034b9673f6b6513c2f8ee127a Mon Sep 17 00:00:00 2001 From: paullee714 Date: Sat, 17 Jan 2026 09:39:18 +0900 Subject: [PATCH 113/930] Add spring.data.rest.return-body-on-delete configuration property This commit adds the missing returnBodyOnDelete property to DataRestProperties, completing the symmetric configuration for controlling response body behavior across all CRUD operations. The property maps to RepositoryRestConfiguration.setReturnBodyOnDelete() which was added in Spring Data REST 4.1. Signed-off-by: paullee714 See gh-48872 --- .../rest/autoconfigure/DataRestProperties.java | 14 ++++++++++++++ .../DataRestAutoConfigurationTests.java | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/module/spring-boot-data-rest/src/main/java/org/springframework/boot/data/rest/autoconfigure/DataRestProperties.java b/module/spring-boot-data-rest/src/main/java/org/springframework/boot/data/rest/autoconfigure/DataRestProperties.java index 78bdbb96a6dc..09df95712700 100644 --- a/module/spring-boot-data-rest/src/main/java/org/springframework/boot/data/rest/autoconfigure/DataRestProperties.java +++ b/module/spring-boot-data-rest/src/main/java/org/springframework/boot/data/rest/autoconfigure/DataRestProperties.java @@ -85,6 +85,11 @@ public class DataRestProperties { */ private @Nullable Boolean returnBodyOnUpdate; + /** + * Whether to return a response body after deleting an entity. + */ + private @Nullable Boolean returnBodyOnDelete; + /** * Whether to enable enum value translation through the Spring Data REST default * resource bundle. @@ -171,6 +176,14 @@ public void setReturnBodyOnUpdate(@Nullable Boolean returnBodyOnUpdate) { this.returnBodyOnUpdate = returnBodyOnUpdate; } + public @Nullable Boolean getReturnBodyOnDelete() { + return this.returnBodyOnDelete; + } + + public void setReturnBodyOnDelete(@Nullable Boolean returnBodyOnDelete) { + this.returnBodyOnDelete = returnBodyOnDelete; + } + public @Nullable Boolean getEnableEnumTranslation() { return this.enableEnumTranslation; } @@ -191,6 +204,7 @@ public void applyTo(RepositoryRestConfiguration rest) { map.from(this::getDefaultMediaType).to(rest::setDefaultMediaType); map.from(this::getReturnBodyOnCreate).to(rest::setReturnBodyOnCreate); map.from(this::getReturnBodyOnUpdate).to(rest::setReturnBodyOnUpdate); + map.from(this::getReturnBodyOnDelete).to(rest::setReturnBodyOnDelete); map.from(this::getEnableEnumTranslation).to(rest::setEnableEnumTranslation); } diff --git a/module/spring-boot-data-rest/src/test/java/org/springframework/boot/data/rest/autoconfigure/DataRestAutoConfigurationTests.java b/module/spring-boot-data-rest/src/test/java/org/springframework/boot/data/rest/autoconfigure/DataRestAutoConfigurationTests.java index de7d3d70717d..73e5d3ede33a 100644 --- a/module/spring-boot-data-rest/src/test/java/org/springframework/boot/data/rest/autoconfigure/DataRestAutoConfigurationTests.java +++ b/module/spring-boot-data-rest/src/test/java/org/springframework/boot/data/rest/autoconfigure/DataRestAutoConfigurationTests.java @@ -91,7 +91,7 @@ void testWithCustomSettings() { "spring.data.rest.sort-param-name:_sort", "spring.data.rest.detection-strategy=visibility", "spring.data.rest.default-media-type:application/my-json", "spring.data.rest.return-body-on-create:false", "spring.data.rest.return-body-on-update:false", - "spring.data.rest.enable-enum-translation:true"); + "spring.data.rest.return-body-on-delete:false", "spring.data.rest.enable-enum-translation:true"); assertThat(getContext().getBean(RepositoryRestMvcConfiguration.class)).isNotNull(); RepositoryRestConfiguration bean = getContext().getBean(RepositoryRestConfiguration.class); assertThat(bean.getDefaultPageSize()).isEqualTo(42); @@ -108,6 +108,7 @@ void testWithCustomSettings() { private void assertReturnBody(RepositoryRestConfiguration bean) { assertThat(bean.returnBodyOnCreate(null)).isFalse(); assertThat(bean.returnBodyOnUpdate(null)).isFalse(); + assertThat(bean.returnBodyOnDelete(null)).isFalse(); } @Test From c9be84af6eef0f53cd712eaac67311ccd6271f4d Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 14 Jan 2026 15:48:34 +0100 Subject: [PATCH 114/930] Delete web server's temporary directories when the context is closed Closes gh-9983 --- .../MyCloudFoundryConfiguration.java | 4 +- .../MyCloudFoundryConfiguration.kt | 4 +- .../servlet/JettyServletWebServerFactory.java | 41 ++++++++++++-- .../boot/tomcat/TomcatWebServerFactory.java | 39 ++++++++++++- .../TomcatReactiveWebServerFactory.java | 30 ++++++++-- .../TomcatServletWebServerFactory.java | 27 ++++++++- .../TomcatReactiveWebServerFactoryTests.java | 8 ++- .../TomcatServletWebServerFactoryTests.java | 8 ++- .../AbstractConfigurableWebServerFactory.java | 55 +++++++++++++++++++ ...ractConfigurableWebServerFactoryTests.java | 50 +++++++++++++++++ 10 files changed, 243 insertions(+), 23 deletions(-) create mode 100644 module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactoryTests.java diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.java index 3474d0b8bcfc..7c5171599a3d 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.java @@ -43,8 +43,8 @@ public TomcatServletWebServerFactory servletWebServerFactory() { return new TomcatServletWebServerFactory() { @Override - protected void prepareContext(Host host, ServletContextInitializer[] initializers) { - super.prepareContext(host, initializers); + protected void prepareContext(Host host, ServletContextInitializer[] initializers, TempDirs tempDirs) { + super.prepareContext(host, initializers, tempDirs); StandardContext child = new StandardContext(); child.addLifecycleListener(new Tomcat.FixContextListener()); child.setPath("/cloudfoundryapplication"); diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.kt index 82346ce9b580..3800ae8d1ff3 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.kt @@ -41,8 +41,8 @@ class MyCloudFoundryConfiguration { fun servletWebServerFactory(): TomcatServletWebServerFactory { return object : TomcatServletWebServerFactory() { - override fun prepareContext(host: Host, initializers: Array) { - super.prepareContext(host, initializers) + override fun prepareContext(host: Host, initializers: Array, tempDirs: TempDirs) { + super.prepareContext(host, initializers, tempDirs) val child = StandardContext() child.addLifecycleListener(FixContextListener()) child.path = "/cloudfoundryapplication" diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java index b673c8d2a80c..aeadd1de85d5 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java @@ -63,6 +63,7 @@ import org.eclipse.jetty.session.FileSessionDataStore; import org.eclipse.jetty.session.SessionConfig; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; import org.eclipse.jetty.util.resource.URLResourceFactory; @@ -161,7 +162,9 @@ public WebServer getWebServer(ServletContextInitializer... initializers) { InetSocketAddress address = new InetSocketAddress(getAddress(), port); Server server = createServer(address); context.setServer(server); - configureWebAppContext(context, initializers); + TempDirs tempDirs = new TempDirs(port); + server.addEventListener(new CleanTempDirsListener(tempDirs)); + configureWebAppContext(context, tempDirs, initializers); server.setHandler(addHandlerWrappers(context)); logger.info("Server initialized with port: " + port); if (this.getMaxConnections() > -1) { @@ -214,8 +217,23 @@ private String getSessionCookieName() { * Configure the given Jetty {@link WebAppContext} for use. * @param context the context to configure * @param initializers the set of initializers to apply + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link #configureWebAppContext(WebAppContext, TempDirs, ServletContextInitializer...)} */ + @Deprecated(forRemoval = true, since = "4.1.0") protected final void configureWebAppContext(WebAppContext context, ServletContextInitializer... initializers) { + configureWebAppContext(context, new TempDirs(getPort()), initializers); + } + + /** + * Configure the given Jetty {@link WebAppContext} for use. + * @param context the context to configure + * @param tempDirs to manage temporary directories + * @param initializers the set of initializers to apply + * @since 4.1.0 + */ + protected final void configureWebAppContext(WebAppContext context, TempDirs tempDirs, + ServletContextInitializer... initializers) { Assert.notNull(context, "'context' must not be null"); context.clearAliasChecks(); if (this.resourceLoader != null) { @@ -224,7 +242,7 @@ protected final void configureWebAppContext(WebAppContext context, ServletContex String contextPath = getSettings().getContextPath().toString(); context.setContextPath(StringUtils.hasLength(contextPath) ? contextPath : "/"); context.setDisplayName(getSettings().getDisplayName()); - configureDocumentRoot(context); + configureDocumentRoot(context, tempDirs); if (getSettings().isRegisterDefaultServlet()) { addDefaultServlet(context); } @@ -289,11 +307,11 @@ private String getTempDirectoryPrefix(WebAppContext context) { } } - private void configureDocumentRoot(WebAppContext handler) { + private void configureDocumentRoot(WebAppContext handler, TempDirs tempDirs) { DocumentRoot documentRoot = new DocumentRoot(logger); documentRoot.setDirectory(this.settings.getDocumentRoot()); File root = documentRoot.getValidDirectory(); - File docBase = (root != null) ? root : createTempDir("jetty-docbase"); + File docBase = (root != null) ? root : tempDirs.createTempDir("jetty-docbase").toFile(); try { ResourceFactory resourceFactory = handler.getResourceFactory(); List resources = new ArrayList<>(); @@ -616,4 +634,19 @@ private Cookie asServletCookie(HttpCookie cookie) { } + private static class CleanTempDirsListener implements LifeCycle.Listener { + + private final TempDirs tempDirs; + + CleanTempDirsListener(TempDirs tempDirs) { + this.tempDirs = tempDirs; + } + + @Override + public void lifeCycleStopped(LifeCycle event) { + this.tempDirs.cleanup(); + } + + } + } diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/TomcatWebServerFactory.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/TomcatWebServerFactory.java index ffa6d90f1b4d..87af56d4fee5 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/TomcatWebServerFactory.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/TomcatWebServerFactory.java @@ -29,6 +29,8 @@ import org.apache.catalina.Context; import org.apache.catalina.Engine; import org.apache.catalina.Executor; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleListener; import org.apache.catalina.Valve; import org.apache.catalina.connector.Connector; @@ -367,16 +369,34 @@ public void setUseApr(boolean useApr) { this.useApr = useApr; } + /** + * Creates the {@link Tomcat} web server. + * @return the web server. + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link #createTomcat(TempDirs)} + */ + @Deprecated(forRemoval = true, since = "4.1.0") protected Tomcat createTomcat() { + return createTomcat(new TempDirs(getPort())); + } + + /** + * Creates the {@link Tomcat} web server. + * @param tempDirs to manage temporary directories + * @return the web server + * @since 4.1.0 + */ + protected Tomcat createTomcat(TempDirs tempDirs) { if (this.isDisableMBeanRegistry()) { Registry.disableRegistry(); } Tomcat tomcat = new Tomcat(); - File baseDir = (getBaseDirectory() != null) ? getBaseDirectory() : createTempDir("tomcat"); + File baseDir = (getBaseDirectory() != null) ? getBaseDirectory() : tempDirs.createTempDir("tomcat").toFile(); tomcat.setBaseDir(baseDir.getAbsolutePath()); for (LifecycleListener listener : getDefaultServerLifecycleListeners()) { tomcat.getServer().addLifecycleListener(listener); } + tomcat.getServer().addLifecycleListener(new CleanTempDirsListener(tempDirs)); Connector connector = new Connector(getProtocol()); connector.setThrowOnFailure(true); tomcat.getService().addConnector(connector); @@ -463,4 +483,21 @@ private void configureEngine(Engine engine) { } } + private static class CleanTempDirsListener implements LifecycleListener { + + private final TempDirs tempDirs; + + CleanTempDirsListener(TempDirs tempDirs) { + this.tempDirs = tempDirs; + } + + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) { + this.tempDirs.cleanup(); + } + } + + } + } diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactory.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactory.java index 0d52aa95c498..dcc82cb91c37 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactory.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactory.java @@ -16,7 +16,7 @@ package org.springframework.boot.tomcat.reactive; -import java.io.File; +import java.nio.file.Path; import org.apache.catalina.Context; import org.apache.catalina.Host; @@ -68,20 +68,40 @@ public TomcatReactiveWebServerFactory(int port) { @Override public WebServer getWebServer(HttpHandler httpHandler) { - Tomcat tomcat = createTomcat(); + TempDirs tempDirs = new TempDirs(getPort()); + Tomcat tomcat = createTomcat(tempDirs); TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(httpHandler); - prepareContext(tomcat.getHost(), servlet); + prepareContext(tomcat.getHost(), servlet, tempDirs); return getTomcatWebServer(tomcat); } + /** + * Prepares the context. + * @param host the host + * @param servlet the servlet adapter + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link #prepareContext(Host, TomcatHttpHandlerAdapter, TempDirs)} + */ + @Deprecated(forRemoval = true, since = "4.1.0") protected void prepareContext(Host host, TomcatHttpHandlerAdapter servlet) { - File docBase = createTempDir("tomcat-docbase"); + prepareContext(host, servlet, new TempDirs(getPort())); + } + + /** + * Prepares the context. + * @param host the host + * @param servlet the servlet adapter + * @param tempDirs to manage temporary directories + * @since 4.1.0 + */ + protected void prepareContext(Host host, TomcatHttpHandlerAdapter servlet, TempDirs tempDirs) { + Path docBase = tempDirs.createTempDir("tomcat-docbase"); TomcatEmbeddedContext context = new TomcatEmbeddedContext(); WebResourceRoot resourceRoot = new StandardRoot(context); ignoringNoSuchMethodError(() -> resourceRoot.setReadOnly(true)); context.setResources(resourceRoot); context.setPath(""); - context.setDocBase(docBase.getAbsolutePath()); + context.setDocBase(docBase.toAbsolutePath().toString()); context.addLifecycleListener(new Tomcat.FixContextListener()); ClassLoader parentClassLoader = ClassUtils.getDefaultClassLoader(); context.setParentClassLoader(parentClassLoader); diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactory.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactory.java index 5297b1fb096d..6f0fa10b30b0 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactory.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactory.java @@ -161,12 +161,32 @@ public TomcatServletWebServerFactory(String contextPath, int port) { @Override public WebServer getWebServer(ServletContextInitializer... initializers) { - Tomcat tomcat = createTomcat(); - prepareContext(tomcat.getHost(), initializers); + TempDirs tempDirs = new TempDirs(getPort()); + Tomcat tomcat = createTomcat(tempDirs); + prepareContext(tomcat.getHost(), initializers, tempDirs); return getTomcatWebServer(tomcat); } + /** + * Prepares the context. + * @param host the host + * @param initializers the servlet context initializers + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link #prepareContext(Host, ServletContextInitializer[], TempDirs)} + */ + @Deprecated(forRemoval = true, since = "4.1.0") protected void prepareContext(Host host, ServletContextInitializer[] initializers) { + prepareContext(host, initializers, new TempDirs(getPort())); + } + + /** + * Prepares the context. + * @param host the host + * @param initializers the servlet context initializers + * @param tempDirs to manage temporary directories + * @since 4.1.0 + */ + protected void prepareContext(Host host, ServletContextInitializer[] initializers, TempDirs tempDirs) { DocumentRoot documentRoot = new DocumentRoot(logger); documentRoot.setDirectory(this.settings.getDocumentRoot()); File documentRootFile = documentRoot.getValidDirectory(); @@ -179,7 +199,8 @@ protected void prepareContext(Host host, ServletContextInitializer[] initializer context.setName(contextPath); context.setDisplayName(this.settings.getDisplayName()); context.setPath(contextPath); - File docBase = (documentRootFile != null) ? documentRootFile : createTempDir("tomcat-docbase"); + File docBase = (documentRootFile != null) ? documentRootFile + : tempDirs.createTempDir("tomcat-docbase").toFile(); context.setDocBase(docBase.getAbsolutePath()); context.addLifecycleListener(new FixContextListener()); ClassLoader parentClassLoader = (this.resourceLoader != null) ? this.resourceLoader.getClassLoader() diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactoryTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactoryTests.java index 60d0bb69d0c9..c9fff9eccecd 100644 --- a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactoryTests.java +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactoryTests.java @@ -104,7 +104,9 @@ void defaultTomcatListeners() { assertThat(factory.getContextLifecycleListeners()).isEmpty(); TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer(mock(HttpHandler.class)); this.webServer = tomcatWebServer; - assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).isEmpty(); + assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()) + .extracting((l) -> l.getClass().getSimpleName()) + .containsExactly("CleanTempDirsListener"); } @Test @@ -113,8 +115,8 @@ void aprShouldBeOptIn() { factory.setUseApr(true); TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer(mock(HttpHandler.class)); this.webServer = tomcatWebServer; - assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).singleElement() - .isInstanceOf(AprLifecycleListener.class); + assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).extracting(Object::getClass) + .contains(AprLifecycleListener.class); } @Test diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java index 6d493800d391..32b1c453798e 100644 --- a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java @@ -160,7 +160,9 @@ void defaultTomcatListeners() { assertThat(factory.getContextLifecycleListeners()).isEmpty(); TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer(); this.webServer = tomcatWebServer; - assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).isEmpty(); + assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()) + .extracting((listener) -> listener.getClass().getSimpleName()) + .containsExactly("CleanTempDirsListener"); } @Test @@ -169,8 +171,8 @@ void aprShouldBeOptIn() { factory.setUseApr(true); TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer(); this.webServer = tomcatWebServer; - assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).singleElement() - .isInstanceOf(AprLifecycleListener.class); + assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).extracting(Object::getClass) + .contains(AprLifecycleListener.class); } @Test diff --git a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java index 2b1ca3ce3d6b..579232bda425 100644 --- a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java +++ b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java @@ -20,7 +20,9 @@ import java.io.IOException; import java.net.InetAddress; import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -33,6 +35,7 @@ import org.springframework.boot.web.error.ErrorPage; import org.springframework.boot.web.server.Ssl.ServerNameSslBundle; import org.springframework.util.Assert; +import org.springframework.util.FileSystemUtils; /** * Abstract base class for {@link ConfigurableWebServerFactory} implementations. @@ -215,7 +218,9 @@ protected final Map getServerNameSslBundles() { * Return the absolute temp dir for given web server. * @param prefix server name * @return the temp dir for given server. + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of {@link TempDirs}. */ + @Deprecated(since = "4.1.0", forRemoval = true) protected final File createTempDir(String prefix) { try { File tempDir = Files.createTempDirectory(prefix + "." + getPort() + ".").toFile(); @@ -228,4 +233,54 @@ protected final File createTempDir(String prefix) { } } + /** + * Manages temporary directories. + * + * @since 4.1.0 + */ + public static class TempDirs { + + private final Set dirs = new HashSet<>(); + + private final int port; + + public TempDirs(int port) { + this.port = port; + } + + /** + * Creates a temporary directory and registers it for {@link #cleanup cleanup}. + * @param prefix the directory prefix + * @return the path to the temporary directory + */ + public Path createTempDir(String prefix) { + try { + Path directory = Files.createTempDirectory(prefix + "." + this.port + ".").toAbsolutePath(); + this.dirs.add(directory); + return directory; + } + catch (IOException ex) { + throw new WebServerException( + "Unable to create tempDir. java.io.tmpdir is set to " + System.getProperty("java.io.tmpdir"), + ex); + } + } + + /** + * Deletes all created temporary directories. + */ + public void cleanup() { + for (Path dir : this.dirs) { + try { + FileSystemUtils.deleteRecursively(dir); + } + catch (IOException ex) { + // Ignore + } + } + this.dirs.clear(); + } + + } + } diff --git a/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactoryTests.java b/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactoryTests.java new file mode 100644 index 000000000000..450f776d4c78 --- /dev/null +++ b/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactoryTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory.TempDirs; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AbstractConfigurableWebServerFactory}. + * + * @author Moritz Halbritter + */ +class AbstractConfigurableWebServerFactoryTests { + + @Test + void shouldCleanUpTempDirs() throws IOException { + TempDirs tempDirs = new TempDirs(1234); + Path tempDir1 = tempDirs.createTempDir(getClass().getSimpleName()); + Path tempDir2 = tempDirs.createTempDir(getClass().getSimpleName()); + assertThat(tempDir1).isNotEqualTo(tempDir2); + assertThat(tempDir1).exists(); + assertThat(tempDir2).exists(); + Files.writeString(tempDir1.resolve("test.txt"), "test"); + tempDirs.cleanup(); + assertThat(tempDir1).doesNotExist(); + assertThat(tempDir2).doesNotExist(); + } + +} From 9c15debfb2e319064b65f02602cbb85f8fe5ac19 Mon Sep 17 00:00:00 2001 From: michaldo Date: Wed, 7 Jan 2026 01:07:15 +0100 Subject: [PATCH 115/930] Add properties for format-agnostic read and write features Signed-off-by: michaldo See gh-48870 --- .../JacksonAutoConfiguration.java | 2 ++ .../autoconfigure/JacksonProperties.java | 20 +++++++++++++++++++ .../JacksonAutoConfigurationTests.java | 18 +++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java index 9ccd6cb8d73a..704d3da39710 100644 --- a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java @@ -158,6 +158,8 @@ static final class StandardJsonMapperBuilderCustomizer @Override public void customize(JsonMapper.Builder builder) { super.customize(builder); + configureFeatures(builder, properties().getRead(), builder::configure); + configureFeatures(builder, properties().getWrite(), builder::configure); configureFeatures(builder, properties().getJson().getRead(), builder::configure); configureFeatures(builder, properties().getJson().getWrite(), builder::configure); } diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java index 1b2a77adef36..f62083ad76b8 100644 --- a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java @@ -25,6 +25,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.PropertyAccessor; import org.jspecify.annotations.Nullable; +import tools.jackson.core.StreamReadFeature; +import tools.jackson.core.StreamWriteFeature; import tools.jackson.core.json.JsonReadFeature; import tools.jackson.core.json.JsonWriteFeature; import tools.jackson.databind.DeserializationFeature; @@ -123,6 +125,16 @@ public class JacksonProperties { private final Datatype datatype = new Datatype(); + /** + * Jackson on/off token reader features common to multiple formats. + */ + private final Map read = new EnumMap<>(StreamReadFeature.class); + + /** + * Jackson on/off token writer features common to multiple formats. + */ + private final Map write = new EnumMap<>(StreamWriteFeature.class); + private final Json json = new Json(); public @Nullable String getDateFormat() { @@ -217,6 +229,14 @@ public Datatype getDatatype() { return this.datatype; } + public Map getRead() { + return this.read; + } + + public Map getWrite() { + return this.write; + } + public Json getJson() { return this.json; } diff --git a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java index f4228667cccf..915dc4c4212e 100644 --- a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java +++ b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java @@ -347,6 +347,24 @@ void disableJsonWriteFeature() { }); } + @Test + void enableWriteBigDecimalAsPlain() { + this.contextRunner.withPropertyValues("spring.jackson.write.write-bigdecimal-as-plain:true").run((context) -> { + JsonMapper mapper = context.getBean(JsonMapper.class); + assertThat(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN.enabledByDefault()).isFalse(); + assertThat(mapper.isEnabled(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN)).isTrue(); + }); + } + + @Test + void enableStringDuplicateDetection() { + this.contextRunner.withPropertyValues("spring.jackson.read.strict-duplicate-detection:true").run((context) -> { + JsonMapper mapper = context.getBean(JsonMapper.class); + assertThat(StreamReadFeature.STRICT_DUPLICATE_DETECTION.enabledByDefault()).isFalse(); + assertThat(mapper.isEnabled(StreamReadFeature.STRICT_DUPLICATE_DETECTION)).isTrue(); + }); + } + @EnumSource @ParameterizedTest void enableDatetimeFeature(MapperType mapperType) { From e1cf93c5457a1ede40af165f677766d867e6f3c7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 20 Jan 2026 09:21:17 +0000 Subject: [PATCH 116/930] Polish "Add properties for format-agnostic read and write features" Apply the format-agnostic read and write feature properties to all supported mappers (CBOR, JSON, and XML), not just JSON. See gh-48870 --- .../JacksonAutoConfiguration.java | 4 +- .../JacksonAutoConfigurationTests.java | 38 ++++++++++--------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java index 704d3da39710..565731174f03 100644 --- a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java @@ -158,8 +158,6 @@ static final class StandardJsonMapperBuilderCustomizer @Override public void customize(JsonMapper.Builder builder) { super.customize(builder); - configureFeatures(builder, properties().getRead(), builder::configure); - configureFeatures(builder, properties().getWrite(), builder::configure); configureFeatures(builder, properties().getJson().getRead(), builder::configure); configureFeatures(builder, properties().getJson().getWrite(), builder::configure); } @@ -391,6 +389,8 @@ protected void customize(B builder) { configureFeatures(builder, this.jacksonProperties.getDatatype().getDatetime(), builder::configure); configureFeatures(builder, this.jacksonProperties.getDatatype().getEnum(), builder::configure); configureFeatures(builder, this.jacksonProperties.getDatatype().getJsonNode(), builder::configure); + configureFeatures(builder, this.jacksonProperties.getRead(), builder::configure); + configureFeatures(builder, this.jacksonProperties.getWrite(), builder::configure); configureDateFormat(builder); configurePropertyNamingStrategy(builder); configureModules(builder); diff --git a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java index 915dc4c4212e..c9eb123290c0 100644 --- a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java +++ b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java @@ -347,24 +347,6 @@ void disableJsonWriteFeature() { }); } - @Test - void enableWriteBigDecimalAsPlain() { - this.contextRunner.withPropertyValues("spring.jackson.write.write-bigdecimal-as-plain:true").run((context) -> { - JsonMapper mapper = context.getBean(JsonMapper.class); - assertThat(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN.enabledByDefault()).isFalse(); - assertThat(mapper.isEnabled(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN)).isTrue(); - }); - } - - @Test - void enableStringDuplicateDetection() { - this.contextRunner.withPropertyValues("spring.jackson.read.strict-duplicate-detection:true").run((context) -> { - JsonMapper mapper = context.getBean(JsonMapper.class); - assertThat(StreamReadFeature.STRICT_DUPLICATE_DETECTION.enabledByDefault()).isFalse(); - assertThat(mapper.isEnabled(StreamReadFeature.STRICT_DUPLICATE_DETECTION)).isTrue(); - }); - } - @EnumSource @ParameterizedTest void enableDatetimeFeature(MapperType mapperType) { @@ -411,6 +393,26 @@ void disableJsonNodeFeature(MapperType mapperType) { }); } + @EnumSource + @ParameterizedTest + void enableWriteFeature(MapperType mapperType) { + this.contextRunner.withPropertyValues("spring.jackson.write.write-bigdecimal-as-plain:true").run((context) -> { + ObjectMapper mapper = mapperType.getMapper(context); + assertThat(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN.enabledByDefault()).isFalse(); + assertThat(mapper.isEnabled(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN)).isTrue(); + }); + } + + @EnumSource + @ParameterizedTest + void enableReadFeature(MapperType mapperType) { + this.contextRunner.withPropertyValues("spring.jackson.read.strict-duplicate-detection:true").run((context) -> { + ObjectMapper mapper = mapperType.getMapper(context); + assertThat(StreamReadFeature.STRICT_DUPLICATE_DETECTION.enabledByDefault()).isFalse(); + assertThat(mapper.isEnabled(StreamReadFeature.STRICT_DUPLICATE_DETECTION)).isTrue(); + }); + } + @Test void defaultJsonFactoryIsRegisteredWithTheMapperBuilderWhenNoCustomFactoryExists() { this.contextRunner.run((context) -> { From c51788b4eeae445250d5516782a0e0b252b62a06 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 20 Jan 2026 12:58:52 +0000 Subject: [PATCH 117/930] Remove stale metadata that was hiding new properties See gh-48870 --- ...itional-spring-configuration-metadata.json | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/module/spring-boot-jackson/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-jackson/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 8a69f6c8815f..e6936e1cc93a 100644 --- a/module/spring-boot-jackson/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-jackson/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -32,28 +32,6 @@ "reason": "Partially replaced by 'spring.jackson.json.read'.", "since": "4.0.0" } - }, - { - "name": "spring.jackson.read", - "type": "java.util.Map", - "description": "Jackson on/off token reader features that are specific to JSON.", - "sourceType": "org.springframework.boot.jackson.autoconfigure.JacksonProperties", - "deprecation": { - "level": "error", - "replacement": "spring.jackson.json.read", - "since": "4.0.0" - } - }, - { - "name": "spring.jackson.write", - "type": "java.util.Map", - "description": "Jackson on/off token writer features that are specific to JSON.", - "sourceType": "org.springframework.boot.jackson.autoconfigure.JacksonProperties", - "deprecation": { - "level": "error", - "replacement": "spring.jackson.json.write", - "since": "4.0.0" - } } ] } From 607eee99c6d16ace20515877e7f3147087546ade Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 20 Jan 2026 15:45:47 +0000 Subject: [PATCH 118/930] Upgrade to Native Build Tools Plugin 0.11.4 Closes gh-48912 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index aa96e328fa7e..6e566ad3e086 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ junitJupiterVersion=6.0.2 kotlinVersion=2.3.0 mavenVersion=3.9.10 mockitoVersion=5.21.0 -nativeBuildToolsVersion=0.11.3 +nativeBuildToolsVersion=0.11.4 nullabilityPluginVersion=0.0.9 snakeYamlVersion=2.5 springFrameworkVersion=7.0.3 From 64d757579ed97e6b01cf28bb2e5780a40ff72f9d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 20 Jan 2026 15:45:51 +0000 Subject: [PATCH 119/930] Upgrade to Selenium HtmlUnit 4.40.0 Closes gh-48913 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index dee18fa71711..bae4724ee02b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2322,7 +2322,7 @@ bom { releaseNotes("https://github.com/SeleniumHQ/selenium/releases/tag/selenium-{version}") } } - library("Selenium HtmlUnit", "4.39.0") { + library("Selenium HtmlUnit", "4.40.0") { group("org.seleniumhq.selenium") { modules = [ "htmlunit3-driver" From 1f4e53b41d502e1863c60b95367fbc93578ef351 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 20 Jan 2026 15:45:52 +0000 Subject: [PATCH 120/930] Upgrade to Spring AMQP 4.1.0-M1 Closes gh-48800 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index bae4724ee02b..35c1365a38da 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2376,7 +2376,7 @@ bom { ] } } - library("Spring AMQP", "4.1.0-SNAPSHOT") { + library("Spring AMQP", "4.1.0-M1") { considerSnapshots() group("org.springframework.amqp") { bom("spring-amqp-bom") From f886ebe446d81a00850610aca953dfa58fddee54 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 20 Jan 2026 15:45:52 +0000 Subject: [PATCH 121/930] Upgrade to Spring Pulsar 2.0.2 Closes gh-48730 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 35c1365a38da..4ce847309714 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2524,7 +2524,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ldap/releases/tag/{version}") } } - library("Spring Pulsar", "2.0.2-SNAPSHOT") { + library("Spring Pulsar", "2.0.2") { considerSnapshots() group("org.springframework.pulsar") { bom("spring-pulsar-bom") From 697c8fe3854855bc5fdc39d0fd9186191a33142d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 20 Jan 2026 15:45:53 +0000 Subject: [PATCH 122/930] Upgrade to Spring Security 7.1.0-M1 Closes gh-48802 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4ce847309714..aedab33bfb47 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2554,7 +2554,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-restdocs/releases/tag/v{version}") } } - library("Spring Security", "7.1.0-SNAPSHOT") { + library("Spring Security", "7.1.0-M1") { considerSnapshots() group("org.springframework.security") { bom("spring-security-bom") From d65eb696476f35c3967cc7ff03c2b3d10b44b943 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 20 Jan 2026 15:01:50 +0100 Subject: [PATCH 123/930] Automatically configure Spring Kafka's observation convention beans This automatically registers KafkaListenerObservationConvention on the container factory, and KafkaTemplateObservationConvention on the Kafka template. Closes gh-48914 --- .../modules/reference/pages/messaging/kafka.adoc | 4 ++++ ...rrentKafkaListenerContainerFactoryConfigurer.java | 12 ++++++++++++ .../KafkaAnnotationDrivenConfiguration.java | 8 +++++++- .../kafka/autoconfigure/KafkaAutoConfiguration.java | 5 ++++- ...KafkaListenerContainerFactoryConfigurerTests.java | 9 +++++++++ .../autoconfigure/KafkaAutoConfigurationTests.java | 11 +++++++++++ 6 files changed, 47 insertions(+), 2 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/kafka.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/kafka.adoc index 5fc72a22660e..c4e7e325a528 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/kafka.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/kafka.adoc @@ -32,6 +32,8 @@ include-code::MyBean[] NOTE: If the property configprop:spring.kafka.producer.transaction-id-prefix[] is defined, a javadoc:org.springframework.kafka.transaction.KafkaTransactionManager[] is automatically configured. Also, if a javadoc:org.springframework.kafka.support.converter.RecordMessageConverter[] bean is defined, it is automatically associated to the auto-configured javadoc:org.springframework.kafka.core.KafkaTemplate[]. +If there's a bean of type `KafkaTemplateObservationConvention` in the context, it is automatically registered on the `KafkaTemplate`. + [[messaging.kafka.receiving]] @@ -52,6 +54,8 @@ If only a javadoc:org.springframework.kafka.support.converter.RecordMessageConve TIP: A custom javadoc:org.springframework.kafka.transaction.ChainedKafkaTransactionManager[] must be marked javadoc:org.springframework.context.annotation.Primary[format=annotation] as it usually references the auto-configured javadoc:org.springframework.kafka.transaction.KafkaTransactionManager[] bean. +If there's a bean of type `KafkaListenerObservationConvention` in the context, it is automatically registered on the container factory. + [[messaging.kafka.streams]] diff --git a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/ConcurrentKafkaListenerContainerFactoryConfigurer.java b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/ConcurrentKafkaListenerContainerFactoryConfigurer.java index 12b8c1ccbd1e..df90651c947a 100644 --- a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/ConcurrentKafkaListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/ConcurrentKafkaListenerContainerFactoryConfigurer.java @@ -37,6 +37,7 @@ import org.springframework.kafka.listener.adapter.RecordFilterStrategy; import org.springframework.kafka.support.converter.BatchMessageConverter; import org.springframework.kafka.support.converter.RecordMessageConverter; +import org.springframework.kafka.support.micrometer.KafkaListenerObservationConvention; import org.springframework.kafka.transaction.KafkaAwareTransactionManager; import org.springframework.util.Assert; @@ -82,6 +83,8 @@ public class ConcurrentKafkaListenerContainerFactoryConfigurer { private @Nullable SimpleAsyncTaskExecutor listenerTaskExecutor; + private @Nullable KafkaListenerObservationConvention observationConvention; + /** * Set the {@link KafkaProperties} to use. * @param properties the properties @@ -186,6 +189,14 @@ void setListenerTaskExecutor(@Nullable SimpleAsyncTaskExecutor listenerTaskExecu this.listenerTaskExecutor = listenerTaskExecutor; } + /** + * Sets the observation convention. + * @param observationConvention the observation convention + */ + void setObservationConvention(@Nullable KafkaListenerObservationConvention observationConvention) { + this.observationConvention = observationConvention; + } + /** * Configure the specified Kafka listener container factory. The factory can be * further tuned and default settings can be overridden. @@ -249,6 +260,7 @@ private void configureContainer(ContainerProperties container) { map.from(this.transactionManager).to(container::setKafkaAwareTransactionManager); map.from(this.rebalanceListener).to(container::setConsumerRebalanceListener); map.from(this.listenerTaskExecutor).to(container::setListenerTaskExecutor); + map.from(this.observationConvention).to(container::setObservationConvention); } } diff --git a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaAnnotationDrivenConfiguration.java b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaAnnotationDrivenConfiguration.java index 4182e4fca223..19d52507e225 100644 --- a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaAnnotationDrivenConfiguration.java +++ b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaAnnotationDrivenConfiguration.java @@ -46,6 +46,7 @@ import org.springframework.kafka.support.converter.BatchMessageConverter; import org.springframework.kafka.support.converter.BatchMessagingMessageConverter; import org.springframework.kafka.support.converter.RecordMessageConverter; +import org.springframework.kafka.support.micrometer.KafkaListenerObservationConvention; import org.springframework.kafka.transaction.KafkaAwareTransactionManager; /** @@ -86,6 +87,8 @@ class KafkaAnnotationDrivenConfiguration { private final @Nullable Function threadNameSupplier; + private final @Nullable KafkaListenerObservationConvention observationConvention; + KafkaAnnotationDrivenConfiguration(KafkaProperties properties, ObjectProvider recordMessageConverter, ObjectProvider> recordFilterStrategy, @@ -97,7 +100,8 @@ class KafkaAnnotationDrivenConfiguration { ObjectProvider> afterRollbackProcessor, ObjectProvider> recordInterceptor, ObjectProvider> batchInterceptor, - ObjectProvider> threadNameSupplier) { + ObjectProvider> threadNameSupplier, + ObjectProvider observationConvention) { this.properties = properties; this.recordMessageConverter = recordMessageConverter.getIfUnique(); this.recordFilterStrategy = recordFilterStrategy.getIfUnique(); @@ -111,6 +115,7 @@ class KafkaAnnotationDrivenConfiguration { this.recordInterceptor = recordInterceptor.getIfUnique(); this.batchInterceptor = batchInterceptor.getIfUnique(); this.threadNameSupplier = threadNameSupplier.getIfUnique(); + this.observationConvention = observationConvention.getIfUnique(); } @Bean @@ -145,6 +150,7 @@ private ConcurrentKafkaListenerContainerFactoryConfigurer configurer() { configurer.setRecordInterceptor(this.recordInterceptor); configurer.setBatchInterceptor(this.batchInterceptor); configurer.setThreadNameSupplier(this.threadNameSupplier); + configurer.setObservationConvention(this.observationConvention); return configurer; } diff --git a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfiguration.java b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfiguration.java index 4e370eeb3416..8a79e2803c76 100644 --- a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfiguration.java +++ b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfiguration.java @@ -61,6 +61,7 @@ import org.springframework.kafka.support.LoggingProducerListener; import org.springframework.kafka.support.ProducerListener; import org.springframework.kafka.support.converter.RecordMessageConverter; +import org.springframework.kafka.support.micrometer.KafkaTemplateObservationConvention; import org.springframework.kafka.transaction.KafkaTransactionManager; import org.springframework.util.StringUtils; import org.springframework.util.backoff.BackOff; @@ -103,10 +104,12 @@ PropertiesKafkaConnectionDetails kafkaConnectionDetails(ObjectProvider kafkaTemplate(ProducerFactory kafkaProducerFactory, ProducerListener kafkaProducerListener, - ObjectProvider messageConverter) { + ObjectProvider messageConverter, + ObjectProvider observationConvention) { PropertyMapper map = PropertyMapper.get(); KafkaTemplate kafkaTemplate = new KafkaTemplate<>(kafkaProducerFactory); messageConverter.ifUnique(kafkaTemplate::setMessageConverter); + observationConvention.ifUnique(kafkaTemplate::setObservationConvention); map.from(kafkaProducerListener).to(kafkaTemplate::setProducerListener); map.from(this.properties.getTemplate().getDefaultTopic()).to(kafkaTemplate::setDefaultTopic); map.from(this.properties.getTemplate().getTransactionIdPrefix()).to(kafkaTemplate::setTransactionIdPrefix); diff --git a/module/spring-boot-kafka/src/test/java/org/springframework/boot/kafka/autoconfigure/ConcurrentKafkaListenerContainerFactoryConfigurerTests.java b/module/spring-boot-kafka/src/test/java/org/springframework/boot/kafka/autoconfigure/ConcurrentKafkaListenerContainerFactoryConfigurerTests.java index 33becc8bf22a..932ee15a73d5 100644 --- a/module/spring-boot-kafka/src/test/java/org/springframework/boot/kafka/autoconfigure/ConcurrentKafkaListenerContainerFactoryConfigurerTests.java +++ b/module/spring-boot-kafka/src/test/java/org/springframework/boot/kafka/autoconfigure/ConcurrentKafkaListenerContainerFactoryConfigurerTests.java @@ -26,6 +26,7 @@ import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; import org.springframework.kafka.core.ConsumerFactory; import org.springframework.kafka.listener.MessageListenerContainer; +import org.springframework.kafka.support.micrometer.KafkaListenerObservation.DefaultKafkaListenerObservationConvention; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.then; @@ -89,4 +90,12 @@ void shouldApplyAuthExceptionRetryInterval() { .isEqualTo(Duration.ofSeconds(10)); } + @Test + void shouldApplyObservationConvention() { + DefaultKafkaListenerObservationConvention convention = new DefaultKafkaListenerObservationConvention(); + this.configurer.setObservationConvention(convention); + this.configurer.configure(this.factory, this.consumerFactory); + assertThat(this.factory.getContainerProperties().getObservationConvention()).isSameAs(convention); + } + } diff --git a/module/spring-boot-kafka/src/test/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfigurationTests.java b/module/spring-boot-kafka/src/test/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfigurationTests.java index d6b87983c360..e5726228637b 100644 --- a/module/spring-boot-kafka/src/test/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfigurationTests.java +++ b/module/spring-boot-kafka/src/test/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfigurationTests.java @@ -94,6 +94,8 @@ import org.springframework.kafka.support.converter.BatchMessagingMessageConverter; import org.springframework.kafka.support.converter.MessagingMessageConverter; import org.springframework.kafka.support.converter.RecordMessageConverter; +import org.springframework.kafka.support.micrometer.KafkaTemplateObservation.DefaultKafkaTemplateObservationConvention; +import org.springframework.kafka.support.micrometer.KafkaTemplateObservationConvention; import org.springframework.kafka.transaction.KafkaAwareTransactionManager; import org.springframework.kafka.transaction.KafkaTransactionManager; import org.springframework.test.util.ReflectionTestUtils; @@ -1015,6 +1017,15 @@ void shouldRegisterRuntimeHints() { .withMemberCategories(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)).accepts(runtimeHints); } + @Test + void shouldConfigureObservationConvention() { + KafkaTemplateObservationConvention convention = new DefaultKafkaTemplateObservationConvention(); + this.contextRunner.withBean(KafkaTemplateObservationConvention.class, () -> convention).run((context) -> { + KafkaTemplate template = context.getBean(KafkaTemplate.class); + assertThat(template).hasFieldOrPropertyWithValue("observationConvention", convention); + }); + } + private KafkaConnectionDetails kafkaConnectionDetails() { return new KafkaConnectionDetails() { From 34768c2d527cbf2993690f60cf29540f555c0f2c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 20 Jan 2026 19:00:54 +0000 Subject: [PATCH 124/930] Upgrade to Spring Kafka 4.0.2 Closes gh-48729 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index aedab33bfb47..3cdd8506b3e9 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2486,7 +2486,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-integration/releases/tag/v{version}") } } - library("Spring Kafka", "4.0.2-SNAPSHOT") { + library("Spring Kafka", "4.0.2") { considerSnapshots() group("org.springframework.kafka") { modules = [ From 9f3720a171debf44b00dbdc1b65e3894148e8c9b Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 21 Jan 2026 09:26:04 +0100 Subject: [PATCH 125/930] Automatically configure Spring AMQP's observation convention beans This automatically registers RabbitListenerObservationConvention, RabbitTemplateObservationConvention, RabbitStreamListenerObservationConvention and RabbitStreamTemplateObservationConvention beans. Closes gh-48922 --- .../reference/pages/messaging/amqp.adoc | 6 ++-- ...bitListenerContainerFactoryConfigurer.java | 15 ++++++++++ .../RabbitAnnotationDrivenConfiguration.java | 8 +++++- .../RabbitAutoConfiguration.java | 5 +++- .../RabbitStreamConfiguration.java | 10 +++++-- .../RabbitStreamTemplateConfigurer.java | 15 ++++++++++ .../RabbitTemplateConfigurer.java | 15 ++++++++++ .../RabbitAutoConfigurationTests.java | 23 +++++++++++++++ .../RabbitStreamConfigurationTests.java | 28 +++++++++++++++++++ 9 files changed, 119 insertions(+), 6 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index 56b05eeb7b8e..2f928840c73e 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc @@ -78,6 +78,8 @@ You can also customize the javadoc:org.springframework.core.retry.RetryTemplate[ If you need to create more javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] with the same settings as the factories used by the auto-configuration. +If there's a bean of type javadoc:org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservationConvention[] in the context, it will automatically be configured on the javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[]. + [[messaging.amqp.sending-stream]] @@ -93,7 +95,7 @@ spring: name: "my-stream" ---- -If a javadoc:org.springframework.amqp.support.converter.MessageConverter[], javadoc:org.springframework.rabbit.stream.support.converter.StreamMessageConverter[], or javadoc:org.springframework.rabbit.stream.producer.ProducerCustomizer[] bean is defined, it is associated automatically to the auto-configured javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[]. +If a javadoc:org.springframework.amqp.support.converter.MessageConverter[], javadoc:org.springframework.rabbit.stream.support.converter.StreamMessageConverter[], javadoc:org.springframework.rabbit.stream.producer.ProducerCustomizer[] or javadoc:org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservationConvention[] bean is defined, it is associated automatically to the auto-configured javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[]. If you need to create more javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[] with the same settings as the factories used by the auto-configuration. @@ -104,7 +106,7 @@ If you need to create more javadoc:org.springframework.rabbit.stream.producer.Ra When the Rabbit infrastructure is present, any bean can be annotated with javadoc:org.springframework.amqp.rabbit.annotation.RabbitListener[format=annotation] to create a listener endpoint. If no javadoc:org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory[] has been defined, a default javadoc:org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory[] is automatically configured and you can switch to a direct container using the configprop:spring.rabbitmq.listener.type[] property. -If a javadoc:org.springframework.amqp.support.converter.MessageConverter[] or a javadoc:org.springframework.amqp.rabbit.retry.MessageRecoverer[] bean is defined, it is automatically associated with the default factory. +If a javadoc:org.springframework.amqp.support.converter.MessageConverter[], a javadoc:org.springframework.amqp.rabbit.retry.MessageRecoverer[] or a javadoc:org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention[] bean is defined, it is automatically associated with the default factory. The following sample component creates a listener endpoint on the `someQueue` queue: diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java index 21ebe011dda3..ffed69eda93e 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java @@ -26,6 +26,7 @@ import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.retry.MessageRecoverer; import org.springframework.amqp.rabbit.retry.RejectAndDontRequeueRecoverer; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.boot.amqp.autoconfigure.RabbitProperties.ListenerRetry; import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Retry; @@ -54,6 +55,8 @@ public abstract class AbstractRabbitListenerContainerFactoryConfigurer observationConvention; + RabbitAnnotationDrivenConfiguration(ObjectProvider messageConverter, ObjectProvider messageRecoverer, ObjectProvider retrySettingsCustomizers, - RabbitProperties properties) { + ObjectProvider observationConvention, RabbitProperties properties) { this.messageConverter = messageConverter; this.messageRecoverer = messageRecoverer; this.retrySettingsCustomizers = retrySettingsCustomizers; + this.observationConvention = observationConvention; this.properties = properties; } @@ -126,6 +130,7 @@ private SimpleRabbitListenerContainerFactoryConfigurer simpleListenerConfigurer( this.properties); configurer.setMessageConverter(this.messageConverter.getIfUnique()); configurer.setMessageRecoverer(this.messageRecoverer.getIfUnique()); + configurer.setObservationConvention(this.observationConvention.getIfUnique()); configurer.setRetrySettingsCustomizers(this.retrySettingsCustomizers.orderedStream().toList()); return configurer; } @@ -135,6 +140,7 @@ private DirectRabbitListenerContainerFactoryConfigurer directListenerConfigurer( this.properties); configurer.setMessageConverter(this.messageConverter.getIfUnique()); configurer.setMessageRecoverer(this.messageRecoverer.getIfUnique()); + configurer.setObservationConvention(this.observationConvention.getIfUnique()); configurer.setRetrySettingsCustomizers(this.retrySettingsCustomizers.orderedStream().toList()); return configurer; } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java index 38812bbacae5..e6f98bcbd2a8 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java @@ -29,6 +29,7 @@ import org.springframework.amqp.rabbit.core.RabbitMessagingTemplate; import org.springframework.amqp.rabbit.core.RabbitOperations; import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservationConvention; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -143,10 +144,12 @@ protected static class RabbitTemplateConfiguration { @ConditionalOnMissingBean RabbitTemplateConfigurer rabbitTemplateConfigurer(RabbitProperties properties, ObjectProvider messageConverter, - ObjectProvider retrySettingsCustomizers) { + ObjectProvider retrySettingsCustomizers, + ObjectProvider observationConvention) { RabbitTemplateConfigurer configurer = new RabbitTemplateConfigurer(properties); configurer.setMessageConverter(messageConverter.getIfUnique()); configurer.setRetrySettingsCustomizers(retrySettingsCustomizers.orderedStream().toList()); + configurer.setObservationConvention(observationConvention.getIfUnique()); return configurer; } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java index 38826821b68f..316b981b6df8 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java @@ -32,6 +32,8 @@ import org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory; import org.springframework.rabbit.stream.listener.ConsumerCustomizer; import org.springframework.rabbit.stream.listener.StreamListenerContainer; +import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservationConvention; +import org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservationConvention; import org.springframework.rabbit.stream.producer.ProducerCustomizer; import org.springframework.rabbit.stream.producer.RabbitStreamOperations; import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; @@ -53,12 +55,14 @@ class RabbitStreamConfiguration { @ConditionalOnProperty(name = "spring.rabbitmq.listener.type", havingValue = "stream") StreamRabbitListenerContainerFactory streamRabbitListenerContainerFactory(Environment rabbitStreamEnvironment, RabbitProperties properties, ObjectProvider consumerCustomizer, - ObjectProvider> containerCustomizer) { + ObjectProvider> containerCustomizer, + ObjectProvider observationConvention) { StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory( rabbitStreamEnvironment); StreamContainer stream = properties.getListener().getStream(); factory.setObservationEnabled(stream.isObservationEnabled()); factory.setNativeListener(stream.isNativeListener()); + observationConvention.ifUnique(factory::setStreamListenerObservationConvention); consumerCustomizer.ifUnique(factory::setConsumerCustomizer); containerCustomizer.ifUnique(factory::setContainerCustomizer); return factory; @@ -78,11 +82,13 @@ Environment rabbitStreamEnvironment(RabbitProperties properties, RabbitConnectio RabbitStreamTemplateConfigurer rabbitStreamTemplateConfigurer(RabbitProperties properties, ObjectProvider messageConverter, ObjectProvider streamMessageConverter, - ObjectProvider producerCustomizer) { + ObjectProvider producerCustomizer, + ObjectProvider observationConvention) { RabbitStreamTemplateConfigurer configurer = new RabbitStreamTemplateConfigurer(); configurer.setMessageConverter(messageConverter.getIfUnique()); configurer.setStreamMessageConverter(streamMessageConverter.getIfUnique()); configurer.setProducerCustomizer(producerCustomizer.getIfUnique()); + configurer.setObservationConvention(observationConvention.getIfUnique()); return configurer; } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java index 6fb5519ab6a6..fe7306d3e473 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java @@ -19,6 +19,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservationConvention; import org.springframework.rabbit.stream.producer.ProducerCustomizer; import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; @@ -41,6 +42,8 @@ public class RabbitStreamTemplateConfigurer { private @Nullable ProducerCustomizer producerCustomizer; + private @Nullable RabbitStreamTemplateObservationConvention observationConvention; + /** * Set the {@link MessageConverter} to use or {@code null} if the out-of-the-box * converter should be used. @@ -67,6 +70,15 @@ public void setProducerCustomizer(@Nullable ProducerCustomizer producerCustomize this.producerCustomizer = producerCustomizer; } + /** + * Set the observation convention to use. + * @param observationConvention the observation convention to use + * @since 4.1.0 + */ + public void setObservationConvention(@Nullable RabbitStreamTemplateObservationConvention observationConvention) { + this.observationConvention = observationConvention; + } + /** * Configure the specified {@link RabbitStreamTemplate}. The template can be further * tuned and default settings can be overridden. @@ -82,6 +94,9 @@ public void configure(RabbitStreamTemplate template) { if (this.producerCustomizer != null) { template.setProducerCustomizer(this.producerCustomizer); } + if (this.observationConvention != null) { + template.setObservationConvention(this.observationConvention); + } } } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java index 50cfc248c68c..52823cb48d9a 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java @@ -23,6 +23,7 @@ import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservationConvention; import org.springframework.amqp.support.converter.AllowedListDeserializingMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.boot.context.properties.PropertyMapper; @@ -52,6 +53,8 @@ public class RabbitTemplateConfigurer { private @Nullable List retrySettingsCustomizers; + private @Nullable RabbitTemplateObservationConvention observationConvention; + private final RabbitProperties rabbitProperties; /** @@ -81,6 +84,15 @@ public void setRetrySettingsCustomizers( this.retrySettingsCustomizers = retrySettingsCustomizers; } + /** + * Sets the observation convention to use. + * @param observationConvention the observation convention to use + * @since 4.1.0 + */ + public void setObservationConvention(@Nullable RabbitTemplateObservationConvention observationConvention) { + this.observationConvention = observationConvention; + } + protected final RabbitProperties getRabbitProperties() { return this.rabbitProperties; } @@ -111,6 +123,9 @@ public void configure(RabbitTemplate template, ConnectionFactory connectionFacto map.from(templateProperties::getAllowedListPatterns) .whenNot(CollectionUtils::isEmpty) .to((allowedListPatterns) -> setAllowedListPatterns(template.getMessageConverter(), allowedListPatterns)); + if (this.observationConvention != null) { + template.setObservationConvention(this.observationConvention); + } } protected RetryTemplate createRetryTemplate(RabbitProperties.Retry properties) { diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java index b170f51e9648..7db86941280d 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java @@ -64,6 +64,10 @@ import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.rabbit.retry.MessageRecoverer; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservation.DefaultRabbitListenerObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservation.DefaultRabbitTemplateObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservationConvention; import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.SerializerMessageConverter; @@ -322,6 +326,15 @@ void testRabbitTemplateMessageConverters() { }); } + @Test + void shouldConfigureRabbitTemplateObservationConvention() { + RabbitTemplateObservationConvention convention = new DefaultRabbitTemplateObservationConvention(); + this.contextRunner.withBean(RabbitTemplateObservationConvention.class, () -> convention).run((context) -> { + RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class); + assertThat(rabbitTemplate).hasFieldOrPropertyWithValue("observationConvention", convention); + }); + } + @Test void testRabbitTemplateRetry() { this.contextRunner.withUserConfiguration(TestConfiguration.class) @@ -598,6 +611,16 @@ void shouldConfigureVirtualThreadsForDirectListener() { }); } + @Test + void shouldConfigureRabbitListenerObservationConvention() { + RabbitListenerObservationConvention convention = new DefaultRabbitListenerObservationConvention(); + this.contextRunner.withBean(RabbitListenerObservationConvention.class, () -> convention).run((context) -> { + SimpleRabbitListenerContainerFactory listenerFactory = context.getBean("rabbitListenerContainerFactory", + SimpleRabbitListenerContainerFactory.class); + assertThat(listenerFactory).hasFieldOrPropertyWithValue("observationConvention", convention); + }); + } + @Test void testSimpleRabbitListenerContainerFactoryWithDefaultForceStop() { this.contextRunner diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java index 1277e0afd4f9..b9aa3b32afde 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java @@ -41,6 +41,10 @@ import org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory; import org.springframework.rabbit.stream.listener.ConsumerCustomizer; import org.springframework.rabbit.stream.listener.StreamListenerContainer; +import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservation.DefaultRabbitStreamListenerObservationConvention; +import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservationConvention; +import org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservation.DefaultRabbitStreamTemplateObservationConvention; +import org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservationConvention; import org.springframework.rabbit.stream.producer.ProducerCustomizer; import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; @@ -100,6 +104,19 @@ void shouldConfigureObservations() { .isTrue()); } + @Test + void shouldConfigureRabbitStreamListenerObservationConvention() { + RabbitStreamListenerObservationConvention convention = new DefaultRabbitStreamListenerObservationConvention(); + this.contextRunner.withBean(RabbitStreamListenerObservationConvention.class, () -> convention) + .withPropertyValues("spring.rabbitmq.listener.type:stream") + .run((context) -> { + StreamRabbitListenerContainerFactory containerFactory = context + .getBean(StreamRabbitListenerContainerFactory.class); + assertThat(containerFactory).hasFieldOrPropertyWithValue("streamListenerObservationConvention", + convention); + }); + } + @Test void environmentIsAutoConfiguredByDefault() { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(Environment.class)); @@ -258,6 +275,17 @@ void testRabbitStreamTemplateConfigurationWithCustomProducerCustomizer() { }); } + @Test + void shouldConfigureRabbitStreamTemplateObservationConvention() { + RabbitStreamTemplateObservationConvention convention = new DefaultRabbitStreamTemplateObservationConvention(); + this.contextRunner.withBean(RabbitStreamTemplateObservationConvention.class, () -> convention) + .withPropertyValues("spring.rabbitmq.stream.name:stream-test") + .run((context) -> { + RabbitStreamTemplate rabbitStreamTemplate = context.getBean(RabbitStreamTemplate.class); + assertThat(rabbitStreamTemplate).hasFieldOrPropertyWithValue("observationConvention", convention); + }); + } + @Test void environmentCreatedByBuilderCanBeCustomized() { this.contextRunner.withUserConfiguration(EnvironmentBuilderCustomizers.class).run((context) -> { From bd3d59c74d38699f78a7f1bde8eb95decd394ee5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 21 Jan 2026 17:29:17 +0000 Subject: [PATCH 126/930] Upgrade to Spring GraphQL 2.0.2 Closes gh-48728 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3cdd8506b3e9..937ab63fd3c2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2436,7 +2436,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-framework/releases/tag/v{version}") } } - library("Spring GraphQL", "2.0.2-SNAPSHOT") { + library("Spring GraphQL", "2.0.2") { considerSnapshots() group("org.springframework.graphql") { modules = [ From 93c20dbde284173457e4ec1ee893e991b610fe89 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 21 Jan 2026 17:29:17 +0000 Subject: [PATCH 127/930] Upgrade to Spring Integration 7.1.0-M1 Closes gh-48801 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 937ab63fd3c2..04b22e512a54 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2471,7 +2471,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-hateoas/releases/tag/{version}") } } - library("Spring Integration", "7.1.0-SNAPSHOT") { + library("Spring Integration", "7.1.0-M1") { considerSnapshots() group("org.springframework.integration") { bom("spring-integration-bom") From f73a809c8c871ed0cf346f4b1a06c0ede4470cc9 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 21 Jan 2026 11:07:03 -0800 Subject: [PATCH 128/930] Polish --- .../tomcat/reactive/TomcatReactiveWebServerFactoryTests.java | 4 ++-- .../tomcat/servlet/TomcatServletWebServerFactoryTests.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactoryTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactoryTests.java index c9fff9eccecd..fa4f13e06ed8 100644 --- a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactoryTests.java +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactoryTests.java @@ -115,8 +115,8 @@ void aprShouldBeOptIn() { factory.setUseApr(true); TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer(mock(HttpHandler.class)); this.webServer = tomcatWebServer; - assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).extracting(Object::getClass) - .contains(AprLifecycleListener.class); + assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()) + .anyMatch(AprLifecycleListener.class::isInstance); } @Test diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java index 32b1c453798e..ce992e78d273 100644 --- a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java @@ -171,8 +171,8 @@ void aprShouldBeOptIn() { factory.setUseApr(true); TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer(); this.webServer = tomcatWebServer; - assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).extracting(Object::getClass) - .contains(AprLifecycleListener.class); + assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()) + .anyMatch(AprLifecycleListener.class::isInstance); } @Test From 0463b4d0bc357e6aca6653d4991c8ab99a2b8714 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 20 Jan 2026 17:16:08 -0800 Subject: [PATCH 129/930] Add `@AutoConfigureWebServer` test annotation Introduce `@AutoConfigureWebServer` test annotation to support `@SpringBootTest` with specific classes and a random port. Closes gh-48815 --- .../build.gradle | 1 + documentation/spring-boot-docs/build.gradle | 1 + .../testing/spring-boot-applications.adoc | 2 + .../reference/pages/testing/test-modules.adoc | 3 + .../build.gradle | 1 + ...figureWebServerWebMvcIntegrationTests.java | 63 +++++++++++++++ module/spring-boot-jetty/build.gradle | 1 + ...server.test.AutoConfigureWebServer.imports | 2 + ...oConfigureWebServerJettyReactiveTests.java | 73 ++++++++++++++++++ ...toConfigureWebServerJettyServletTests.java | 76 +++++++++++++++++++ module/spring-boot-reactor-netty/build.gradle | 1 + .../NettyWebServerTestAutoConfiguration.java | 30 ++++++++ ...server.test.AutoConfigureWebServer.imports | 1 + ...toConfigureWebServerReactorNettyTests.java | 73 ++++++++++++++++++ .../build.gradle | 3 + module/spring-boot-tomcat/build.gradle | 1 + ...server.test.AutoConfigureWebServer.imports | 2 + ...ConfigureWebServerTomcatReactiveTests.java | 73 ++++++++++++++++++ ...oConfigureWebServerTomcatServletTests.java | 76 +++++++++++++++++++ .../spring-boot-web-server-test/build.gradle | 40 ++++++++++ .../server/test/AutoConfigureWebServer.java | 47 ++++++++++++ .../boot/web/server/test/package-info.java | 23 ++++++ ...server.test.AutoConfigureWebServer.imports | 1 + .../spring-boot-dependencies/build.gradle | 1 + settings.gradle | 1 + .../build.gradle | 27 +++++++ 26 files changed, 623 insertions(+) create mode 100644 integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AutoConfigureWebServerWebMvcIntegrationTests.java create mode 100644 module/spring-boot-jetty/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports create mode 100644 module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/AutoConfigureWebServerJettyReactiveTests.java create mode 100644 module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/AutoConfigureWebServerJettyServletTests.java create mode 100644 module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyWebServerTestAutoConfiguration.java create mode 100644 module/spring-boot-reactor-netty/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports create mode 100644 module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/AutoConfigureWebServerReactorNettyTests.java create mode 100644 module/spring-boot-tomcat/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports create mode 100644 module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/AutoConfigureWebServerTomcatReactiveTests.java create mode 100644 module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/AutoConfigureWebServerTomcatServletTests.java create mode 100644 module/spring-boot-web-server-test/build.gradle create mode 100644 module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/AutoConfigureWebServer.java create mode 100644 module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/package-info.java create mode 100644 module/spring-boot-webmvc/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports create mode 100644 starter/spring-boot-starter-web-server-test/build.gradle diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle index d01732ff37dc..5644d3f9251b 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle @@ -139,6 +139,7 @@ def dependenciesOf(String version) { "spring-boot-transaction", "spring-boot-validation", "spring-boot-web-server", + "spring-boot-web-server-test", "spring-boot-webclient", "spring-boot-webflux", "spring-boot-webflux-test", diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index c7b3c177f695..b23c74f394ff 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -133,6 +133,7 @@ dependencies { implementation(project(path: ":module:spring-boot-resttestclient")) implementation(project(path: ":module:spring-boot-security")) implementation(project(path: ":module:spring-boot-tomcat")) + implementation(project(path: ":module:spring-boot-web-server-test")) implementation(project(path: ":module:spring-boot-webclient")) implementation(project(path: ":module:spring-boot-webclient-test")) implementation(project(path: ":module:spring-boot-webflux")) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc index 62b61596545d..7ba261b01e7a 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc @@ -197,6 +197,8 @@ include-code::MyRandomPortWebTestClientTests[] TIP: javadoc:org.springframework.test.web.reactive.server.WebTestClient[] can also be used with a xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-mock-environment[mock environment], removing the need for a running server, by annotating your test class with javadoc:org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient[format=annotation] from `spring-boot-webflux-test`. +NOTE: For certain xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.autoconfigured-tests[sliced tests] you may need to use the javadoc:org.springframework.boot.web.server.test.AutoConfigureWebServer[format=annotation] annotation to auto-configure an embedded web server. + The `spring-boot-resttestclient` module also provides a javadoc:org.springframework.boot.resttestclient.TestRestTemplate[] facility: include-code::MyRandomPortTestRestTemplateTests[] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-modules.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-modules.adoc index 3c80cee41bb6..8a16fbe5f7c6 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-modules.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-modules.adoc @@ -64,6 +64,9 @@ Spring Boot offers several focused, feature-specific `-test` modules: |`spring-boot-security-test` |Testing applications that use Spring Security. +|`spring-boot-webserver-test` +|Testing applications that use `WebServer` implementations. Provides the `@AutoconfigureWebServer` annotation. + |`spring-boot-webclient-test` |Testing applications that use `WebClient`. Provides the `@WebClientTest` test slice. diff --git a/integration-test/spring-boot-test-integration-tests/build.gradle b/integration-test/spring-boot-test-integration-tests/build.gradle index ebee6fad6e75..0cea24380618 100644 --- a/integration-test/spring-boot-test-integration-tests/build.gradle +++ b/integration-test/spring-boot-test-integration-tests/build.gradle @@ -33,6 +33,7 @@ dependencies { testImplementation(project(":module:spring-boot-webmvc-test")) testImplementation(project(":module:spring-boot-webtestclient")) testImplementation(project(":module:spring-boot-web-server")) + testImplementation(project(":module:spring-boot-web-server-test")) testImplementation("io.projectreactor.netty:reactor-netty-http") testImplementation("org.springframework:spring-webmvc") testImplementation("org.springframework:spring-webflux") diff --git a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AutoConfigureWebServerWebMvcIntegrationTests.java b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AutoConfigureWebServerWebMvcIntegrationTests.java new file mode 100644 index 000000000000..bf1ca961973b --- /dev/null +++ b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AutoConfigureWebServerWebMvcIntegrationTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.test; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.server.test.AutoConfigureWebServerWebMvcIntegrationTests.TestController; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureWebMvc; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.test.web.servlet.client.assertj.RestTestClientResponse; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link AutoConfigureWebServer @AutoConfigureWebServer} and Spring + * MVC. + * + * @author Phillip Webb + */ +@SpringBootTest(classes = TestController.class, webEnvironment = WebEnvironment.RANDOM_PORT) +@AutoConfigureWebServer +@AutoConfigureWebMvc +@AutoConfigureRestTestClient +class AutoConfigureWebServerWebMvcIntegrationTests { + + @Test + void embeddedWebServerWithSpringMvcIsAvailable(@Autowired RestTestClient restClient) { + assertThat(RestTestClientResponse.from(restClient.get().uri("/hello").exchange())).hasStatusOk() + .bodyText() + .isEqualTo("hello"); + } + + @RestController + static class TestController { + + @RequestMapping("/hello") + String hello() { + return "hello"; + } + + } + +} diff --git a/module/spring-boot-jetty/build.gradle b/module/spring-boot-jetty/build.gradle index 2944bcfec797..1193c2299a1c 100644 --- a/module/spring-boot-jetty/build.gradle +++ b/module/spring-boot-jetty/build.gradle @@ -34,6 +34,7 @@ dependencies { optional(project(":core:spring-boot-autoconfigure")) optional(project(":module:spring-boot-actuator-autoconfigure")) optional(project(":module:spring-boot-micrometer-metrics")) + optional(project(":module:spring-boot-web-server-test")) optional("org.apache.tomcat.embed:tomcat-embed-jasper") optional("org.eclipse.jetty:jetty-alpn-conscrypt-server") optional("org.eclipse.jetty.ee11.websocket:jetty-ee11-websocket-jakarta-server") diff --git a/module/spring-boot-jetty/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports b/module/spring-boot-jetty/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports new file mode 100644 index 000000000000..de5cccb60e87 --- /dev/null +++ b/module/spring-boot-jetty/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports @@ -0,0 +1,2 @@ +org.springframework.boot.jetty.autoconfigure.reactive.JettyReactiveWebServerAutoConfiguration +org.springframework.boot.jetty.autoconfigure.servlet.JettyServletWebServerAutoConfiguration diff --git a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/AutoConfigureWebServerJettyReactiveTests.java b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/AutoConfigureWebServerJettyReactiveTests.java new file mode 100644 index 000000000000..12f531b15c36 --- /dev/null +++ b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/AutoConfigureWebServerJettyReactiveTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jetty.autoconfigure; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.jetty.autoconfigure.AutoConfigureWebServerJettyReactiveTests.ReactiveConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.boot.web.server.test.AutoConfigureWebServer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.test.web.servlet.client.assertj.RestTestClientResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutoConfigureWebServer @AutoConfigureWebServer} with Jetty. + * + * @author Phillip Webb + */ +@SpringBootTest(classes = ReactiveConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT, + properties = "spring.main.web-application-type=reactive") +@AutoConfigureWebServer +class AutoConfigureWebServerJettyReactiveTests { + + @LocalServerPort + private int port; + + @Test + void startsTestServer() { + RestTestClient restClient = RestTestClient.bindToServer().baseUrl("http://localhost:" + this.port).build(); + assertThat(RestTestClientResponse.from(restClient.get().exchange())).bodyText().isEqualTo("hello"); + } + + @Configuration + static class ReactiveConfiguration { + + @Bean + HttpHandler httpHandler() { + return (request, response) -> { + response.setStatusCode(HttpStatus.OK); + DataBuffer dataBuffer = new DefaultDataBufferFactory().wrap("hello".getBytes(StandardCharsets.UTF_8)); + return response.writeWith(Mono.just(dataBuffer)); + }; + } + + } + +} diff --git a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/AutoConfigureWebServerJettyServletTests.java b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/AutoConfigureWebServerJettyServletTests.java new file mode 100644 index 000000000000..f76e986cd553 --- /dev/null +++ b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/AutoConfigureWebServerJettyServletTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jetty.autoconfigure; + +import java.io.IOException; + +import jakarta.servlet.GenericServlet; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.jetty.autoconfigure.AutoConfigureWebServerJettyServletTests.ServletConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.boot.web.server.test.AutoConfigureWebServer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.test.web.servlet.client.assertj.RestTestClientResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutoConfigureWebServer @AutoConfigureWebServer} with Jetty. + * + * @author Phillip Webb + */ +@SpringBootTest(classes = ServletConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT) +@AutoConfigureWebServer +class AutoConfigureWebServerJettyServletTests { + + @LocalServerPort + private int port; + + @Test + void startsTestServer() { + RestTestClient restClient = RestTestClient.bindToServer().baseUrl("http://localhost:" + this.port).build(); + assertThat(RestTestClientResponse.from(restClient.get().exchange())).bodyText().isEqualTo("hello"); + } + + @Configuration + static class ServletConfiguration { + + @Bean + Servlet servlet() { + return new GenericServlet() { + + @Override + public void service(ServletRequest request, ServletResponse response) + throws ServletException, IOException { + response.getWriter().append("hello").flush(); + } + + }; + } + + } + +} diff --git a/module/spring-boot-reactor-netty/build.gradle b/module/spring-boot-reactor-netty/build.gradle index 9ed4c281f4bc..7156e656e69d 100644 --- a/module/spring-boot-reactor-netty/build.gradle +++ b/module/spring-boot-reactor-netty/build.gradle @@ -35,6 +35,7 @@ dependencies { optional(project(":core:spring-boot-autoconfigure")) optional(project(":module:spring-boot-actuator-autoconfigure")) + optional(project(":module:spring-boot-web-server-test")) testImplementation(project(":core:spring-boot-test")) testImplementation(project(":test-support:spring-boot-test-support")) diff --git a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyWebServerTestAutoConfiguration.java b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyWebServerTestAutoConfiguration.java new file mode 100644 index 000000000000..99b6de0396ff --- /dev/null +++ b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyWebServerTestAutoConfiguration.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.reactor.netty.autoconfigure; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.web.server.test.AutoConfigureWebServer; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Jetty + * {@link AutoConfigureWebServer @AutoConfigureWebServer} support. + * + * @author Phillip Webb + */ +class NettyWebServerTestAutoConfiguration { + +} diff --git a/module/spring-boot-reactor-netty/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports b/module/spring-boot-reactor-netty/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports new file mode 100644 index 000000000000..833645211d74 --- /dev/null +++ b/module/spring-boot-reactor-netty/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports @@ -0,0 +1 @@ +org.springframework.boot.reactor.netty.autoconfigure.NettyReactiveWebServerAutoConfiguration diff --git a/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/AutoConfigureWebServerReactorNettyTests.java b/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/AutoConfigureWebServerReactorNettyTests.java new file mode 100644 index 000000000000..1b9df9b61b7e --- /dev/null +++ b/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/AutoConfigureWebServerReactorNettyTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.reactor.netty.autoconfigure; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.reactor.netty.autoconfigure.AutoConfigureWebServerReactorNettyTests.ReactiveConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.boot.web.server.test.AutoConfigureWebServer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.test.web.servlet.client.assertj.RestTestClientResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutoConfigureWebServer @AutoConfigureWebServer} with Reactor Netty. + * + * @author Phillip Webb + */ +@SpringBootTest(classes = ReactiveConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT, + properties = "spring.main.web-application-type=reactive") +@AutoConfigureWebServer +class AutoConfigureWebServerReactorNettyTests { + + @LocalServerPort + private int port; + + @Test + void startsTestServer() { + RestTestClient restClient = RestTestClient.bindToServer().baseUrl("http://localhost:" + this.port).build(); + assertThat(RestTestClientResponse.from(restClient.get().exchange())).bodyText().isEqualTo("hello"); + } + + @Configuration + static class ReactiveConfiguration { + + @Bean + HttpHandler httpHandler() { + return (request, response) -> { + response.setStatusCode(HttpStatus.OK); + DataBuffer dataBuffer = new DefaultDataBufferFactory().wrap("hello".getBytes(StandardCharsets.UTF_8)); + return response.writeWith(Mono.just(dataBuffer)); + }; + } + + } + +} diff --git a/module/spring-boot-test-classic-modules/build.gradle b/module/spring-boot-test-classic-modules/build.gradle index 8bb5433d7aa9..0b3932661eaf 100644 --- a/module/spring-boot-test-classic-modules/build.gradle +++ b/module/spring-boot-test-classic-modules/build.gradle @@ -91,6 +91,9 @@ dependencies { api(project(":module:spring-boot-security-test")) { transitive = false } + api(project(":module:spring-boot-web-server-test")) { + transitive = false + } api(project(":module:spring-boot-webflux-test")) { transitive = false } diff --git a/module/spring-boot-tomcat/build.gradle b/module/spring-boot-tomcat/build.gradle index 252f953b2648..a06e4564bc69 100644 --- a/module/spring-boot-tomcat/build.gradle +++ b/module/spring-boot-tomcat/build.gradle @@ -39,6 +39,7 @@ dependencies { optional(project(":core:spring-boot-autoconfigure")) optional(project(":module:spring-boot-actuator-autoconfigure")) optional(project(":module:spring-boot-micrometer-metrics")) + optional(project(":module:spring-boot-web-server-test")) optional("io.micrometer:micrometer-core") optional("org.apache.tomcat.embed:tomcat-embed-jasper") optional("org.apache.tomcat.embed:tomcat-embed-websocket") { diff --git a/module/spring-boot-tomcat/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports b/module/spring-boot-tomcat/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports new file mode 100644 index 000000000000..27a87c24fbe4 --- /dev/null +++ b/module/spring-boot-tomcat/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports @@ -0,0 +1,2 @@ +org.springframework.boot.tomcat.autoconfigure.reactive.TomcatReactiveWebServerAutoConfiguration +org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/AutoConfigureWebServerTomcatReactiveTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/AutoConfigureWebServerTomcatReactiveTests.java new file mode 100644 index 000000000000..c1f400d798f2 --- /dev/null +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/AutoConfigureWebServerTomcatReactiveTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.tomcat.autoconfigure; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.boot.tomcat.autoconfigure.AutoConfigureWebServerTomcatReactiveTests.ReactiveConfiguration; +import org.springframework.boot.web.server.test.AutoConfigureWebServer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.test.web.servlet.client.assertj.RestTestClientResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutoConfigureWebServer @AutoConfigureWebServer} with Tomcat. + * + * @author Phillip Webb + */ +@SpringBootTest(classes = ReactiveConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT, + properties = "spring.main.web-application-type=reactive") +@AutoConfigureWebServer +class AutoConfigureWebServerTomcatReactiveTests { + + @LocalServerPort + private int port; + + @Test + void startsTestServer() { + RestTestClient restClient = RestTestClient.bindToServer().baseUrl("http://localhost:" + this.port).build(); + assertThat(RestTestClientResponse.from(restClient.get().exchange())).bodyText().isEqualTo("hello"); + } + + @Configuration + static class ReactiveConfiguration { + + @Bean + HttpHandler httpHandler() { + return (request, response) -> { + response.setStatusCode(HttpStatus.OK); + DataBuffer dataBuffer = new DefaultDataBufferFactory().wrap("hello".getBytes(StandardCharsets.UTF_8)); + return response.writeWith(Mono.just(dataBuffer)); + }; + } + + } + +} diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/AutoConfigureWebServerTomcatServletTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/AutoConfigureWebServerTomcatServletTests.java new file mode 100644 index 000000000000..0ce46f511f72 --- /dev/null +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/AutoConfigureWebServerTomcatServletTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.tomcat.autoconfigure; + +import java.io.IOException; + +import jakarta.servlet.GenericServlet; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.boot.tomcat.autoconfigure.AutoConfigureWebServerTomcatServletTests.ServletConfiguration; +import org.springframework.boot.web.server.test.AutoConfigureWebServer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.test.web.servlet.client.assertj.RestTestClientResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutoConfigureWebServer @AutoConfigureWebServer} with Tomcat. + * + * @author Phillip Webb + */ +@SpringBootTest(classes = ServletConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT) +@AutoConfigureWebServer +class AutoConfigureWebServerTomcatServletTests { + + @LocalServerPort + private int port; + + @Test + void startsTestServer() { + RestTestClient restClient = RestTestClient.bindToServer().baseUrl("http://localhost:" + this.port).build(); + assertThat(RestTestClientResponse.from(restClient.get().exchange())).bodyText().isEqualTo("hello"); + } + + @Configuration + static class ServletConfiguration { + + @Bean + Servlet servlet() { + return new GenericServlet() { + + @Override + public void service(ServletRequest request, ServletResponse response) + throws ServletException, IOException { + response.getWriter().append("hello").flush(); + } + + }; + } + + } + +} diff --git a/module/spring-boot-web-server-test/build.gradle b/module/spring-boot-web-server-test/build.gradle new file mode 100644 index 000000000000..e4ee21f226af --- /dev/null +++ b/module/spring-boot-web-server-test/build.gradle @@ -0,0 +1,40 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java-library" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" + id "org.springframework.boot.test-slice" +} + +description = "Spring Boot Web Server Test" + +dependencies { + api(project(":core:spring-boot-test-autoconfigure")) + api(project(":module:spring-boot-web-server")) + + optional(project(":core:spring-boot-autoconfigure")) + optional("org.junit.jupiter:junit-jupiter-api") + + testImplementation(project(":test-support:spring-boot-test-support")) + + testRuntimeOnly("ch.qos.logback:logback-classic") +} + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} diff --git a/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/AutoConfigureWebServer.java b/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/AutoConfigureWebServer.java new file mode 100644 index 000000000000..dbff9e864e46 --- /dev/null +++ b/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/AutoConfigureWebServer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.test; + +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.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.autoconfigure.json.AutoConfigureJson; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * {@link ImportAutoConfiguration Auto-configuration imports} for starting and embedded + * web server for tests. Commonly used with {@link SpringBootTest @SpringBootTest} when + * {@code classes} are specified and the {@code webEnvironment} attribute is + * {@code RANDOM_PORT} or {@code DEFINED_PORT}. + * + * @author Phillip Webb + * @since 4.1.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@AutoConfigureJson +@ImportAutoConfiguration +public @interface AutoConfigureWebServer { + +} diff --git a/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/package-info.java b/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/package-info.java new file mode 100644 index 000000000000..581c99555d91 --- /dev/null +++ b/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 embedded web servers tests. + */ +@NullMarked +package org.springframework.boot.web.server.test; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-webmvc/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports b/module/spring-boot-webmvc/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports new file mode 100644 index 000000000000..5d4607f9143c --- /dev/null +++ b/module/spring-boot-webmvc/src/main/resources/META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports @@ -0,0 +1 @@ +org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration \ No newline at end of file diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 04b22e512a54..022094e96533 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2278,6 +2278,7 @@ bom { "spring-boot-transaction", "spring-boot-validation", "spring-boot-web-server", + "spring-boot-web-server-test", "spring-boot-webclient", "spring-boot-webclient-test", "spring-boot-webflux", diff --git a/settings.gradle b/settings.gradle index b9b9ae94a66c..e82d310b0628 100644 --- a/settings.gradle +++ b/settings.gradle @@ -189,6 +189,7 @@ include "module:spring-boot-tomcat" include "module:spring-boot-transaction" include "module:spring-boot-validation" include "module:spring-boot-web-server" +include "module:spring-boot-web-server-test" include "module:spring-boot-webclient" include "module:spring-boot-webclient-test" include "module:spring-boot-webflux" diff --git a/starter/spring-boot-starter-web-server-test/build.gradle b/starter/spring-boot-starter-web-server-test/build.gradle new file mode 100644 index 000000000000..90cd98f2b886 --- /dev/null +++ b/starter/spring-boot-starter-web-server-test/build.gradle @@ -0,0 +1,27 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for testing Spring Web Server" + +dependencies { + api(project(":starter:spring-boot-starter-test")) + + api(project(":module:spring-boot-web-server-test")) +} From e680142dd4c0b961e8c1f9f0cc57268f2533e308 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 21 Jan 2026 21:03:26 -0800 Subject: [PATCH 130/930] Update Gradle conventions to ensure Eclipse IDE defaults to JRE 17 Closes gh-48929 --- .../org/springframework/boot/build/JavaConventions.java | 9 ++++++++- .../boot/build/SystemRequirementsExtension.java | 6 ++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java index 06fff0e9be33..ec019da08bfb 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java @@ -32,6 +32,7 @@ import io.spring.javaformat.gradle.SpringJavaFormatPlugin; import io.spring.javaformat.gradle.tasks.CheckFormat; import io.spring.javaformat.gradle.tasks.Format; +import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; @@ -247,9 +248,15 @@ private void addValuelessOption(CoreJavadocOptions options, String option) { } private void configureJavaConventions(Project project) { + if (!project.hasProperty("toolchainVersion")) { + JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class); + JavaVersion javaVersion = JavaVersion.toVersion(SystemRequirementsExtension.DEFAULT_JAVA_VERSION); + javaPluginExtension.setSourceCompatibility(javaVersion); + javaPluginExtension.setTargetCompatibility(javaVersion); + } project.getTasks().withType(JavaCompile.class, (compile) -> { compile.getOptions().setEncoding("UTF-8"); - compile.getOptions().getRelease().set(this.javaSpec.getVersion()); + compile.getOptions().getRelease().set(project.provider(() -> this.javaSpec.getVersion())); List args = compile.getOptions().getCompilerArgs(); if (!args.contains("-parameters")) { args.add("-parameters"); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java index 697ff12dc688..aaf7eb81b320 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java @@ -31,6 +31,8 @@ */ public class SystemRequirementsExtension { + static final int DEFAULT_JAVA_VERSION = 17; + private final JavaSpec javaSpec; @Inject @@ -50,7 +52,7 @@ public abstract static class JavaSpec { private final Project project; - private int version = 17; + private int version = DEFAULT_JAVA_VERSION; @Inject public JavaSpec(Project project) { @@ -62,11 +64,11 @@ public int getVersion() { } public void setVersion(int version) { + this.version = version; JavaLanguageVersion javaVersion = JavaLanguageVersion.of(version); JavaPluginExtension javaPluginExtension = this.project.getExtensions().getByType(JavaPluginExtension.class); javaPluginExtension.setSourceCompatibility(javaVersion); javaPluginExtension.setTargetCompatibility(javaVersion); - this.version = version; } } From 156c0e1fbbf19b1dc9a62ca159049a37326b16d9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 22 Jan 2026 07:35:04 +0000 Subject: [PATCH 131/930] Revert "Update Gradle conventions to ensure Eclipse IDE defaults to JRE 17" This reverts commit e680142dd4c0b961e8c1f9f0cc57268f2533e308. See gh-48929 --- .../org/springframework/boot/build/JavaConventions.java | 9 +-------- .../boot/build/SystemRequirementsExtension.java | 6 ++---- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java index ec019da08bfb..06fff0e9be33 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java @@ -32,7 +32,6 @@ import io.spring.javaformat.gradle.SpringJavaFormatPlugin; import io.spring.javaformat.gradle.tasks.CheckFormat; import io.spring.javaformat.gradle.tasks.Format; -import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; @@ -248,15 +247,9 @@ private void addValuelessOption(CoreJavadocOptions options, String option) { } private void configureJavaConventions(Project project) { - if (!project.hasProperty("toolchainVersion")) { - JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class); - JavaVersion javaVersion = JavaVersion.toVersion(SystemRequirementsExtension.DEFAULT_JAVA_VERSION); - javaPluginExtension.setSourceCompatibility(javaVersion); - javaPluginExtension.setTargetCompatibility(javaVersion); - } project.getTasks().withType(JavaCompile.class, (compile) -> { compile.getOptions().setEncoding("UTF-8"); - compile.getOptions().getRelease().set(project.provider(() -> this.javaSpec.getVersion())); + compile.getOptions().getRelease().set(this.javaSpec.getVersion()); List args = compile.getOptions().getCompilerArgs(); if (!args.contains("-parameters")) { args.add("-parameters"); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java index aaf7eb81b320..697ff12dc688 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java @@ -31,8 +31,6 @@ */ public class SystemRequirementsExtension { - static final int DEFAULT_JAVA_VERSION = 17; - private final JavaSpec javaSpec; @Inject @@ -52,7 +50,7 @@ public abstract static class JavaSpec { private final Project project; - private int version = DEFAULT_JAVA_VERSION; + private int version = 17; @Inject public JavaSpec(Project project) { @@ -64,11 +62,11 @@ public int getVersion() { } public void setVersion(int version) { - this.version = version; JavaLanguageVersion javaVersion = JavaLanguageVersion.of(version); JavaPluginExtension javaPluginExtension = this.project.getExtensions().getByType(JavaPluginExtension.class); javaPluginExtension.setSourceCompatibility(javaVersion); javaPluginExtension.setTargetCompatibility(javaVersion); + this.version = version; } } From 0a3a9069ff504af374cffe44215cfbedc5e8d24f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 22 Jan 2026 07:38:16 +0000 Subject: [PATCH 132/930] Upgrade to Jackson Bom 3.0.4 Closes gh-48932 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ce0d25ff2e41..7ab1d7a385ae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ commonsCodecVersion=1.20.0 graalVersion=25 hamcrestVersion=3.0 jackson2Version=2.21.0 -jacksonVersion=3.0.3 +jacksonVersion=3.0.4 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.2 kotlinVersion=2.3.0 From eab69232f8ebf29662cd15a89a0e4b68d0d59f67 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 22 Jan 2026 07:38:17 +0000 Subject: [PATCH 133/930] Upgrade to Spring Batch 6.0.2 Closes gh-48725 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 022094e96533..f7e7ffbc8a32 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2392,7 +2392,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-amqp/releases/tag/v{version}") } } - library("Spring Batch", "6.0.2-SNAPSHOT") { + library("Spring Batch", "6.0.2") { considerSnapshots() group("org.springframework.batch") { bom("spring-batch-bom") From 944b191bc3e2c2d5ff963101e221917b18b3c500 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 22 Jan 2026 11:22:35 +0000 Subject: [PATCH 134/930] Upgrade to Kotlin Serialization 1.10.0 Closes gh-48938 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index f7e7ffbc8a32..36d993ad8900 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1231,7 +1231,7 @@ bom { releaseNotes("https://github.com/Kotlin/kotlinx.coroutines/releases/tag/{version}") } } - library("Kotlin Serialization", "1.9.0") { + library("Kotlin Serialization", "1.10.0") { group("org.jetbrains.kotlinx") { bom("kotlinx-serialization-bom") } From b1e9fdc3ecf49be576a2727eb2147677ee194902 Mon Sep 17 00:00:00 2001 From: jdsalasca Date: Wed, 21 Jan 2026 16:31:01 -0500 Subject: [PATCH 135/930] Provide support for adding to the mime types that are compressed Signed-off-by: jdsalasca See gh-48930 --- .../boot/jetty/JettyHandlerWrappers.java | 2 +- .../reactor/netty/CompressionCustomizer.java | 2 +- .../CompressionConnectorCustomizer.java | 2 +- .../boot/web/server/Compression.java | 23 +++++++++++++++++++ .../autoconfigure/ServerPropertiesTests.java | 8 +++++++ 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyHandlerWrappers.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyHandlerWrappers.java index 70cbb19a3496..4b86404cd9be 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyHandlerWrappers.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyHandlerWrappers.java @@ -46,7 +46,7 @@ static Handler.Wrapper createGzipHandlerWrapper(Compression compression) { gzip.setMinCompressSize((int) compression.getMinResponseSize().toBytes()); compressionHandler.putCompression(gzip); Builder configBuilder = CompressionConfig.builder(); - for (String mimeType : compression.getMimeTypes()) { + for (String mimeType : compression.getAllMimeTypes()) { configBuilder.compressIncludeMimeType(mimeType); } for (HttpMethod httpMethod : HttpMethod.values()) { diff --git a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/CompressionCustomizer.java b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/CompressionCustomizer.java index 61c340482e90..b7136524ac90 100644 --- a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/CompressionCustomizer.java +++ b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/CompressionCustomizer.java @@ -56,7 +56,7 @@ public HttpServer apply(HttpServer server) { if (!this.compression.getMinResponseSize().isNegative()) { server = server.compress((int) this.compression.getMinResponseSize().toBytes()); } - CompressionPredicate mimeTypes = getMimeTypesPredicate(this.compression.getMimeTypes()); + CompressionPredicate mimeTypes = getMimeTypesPredicate(this.compression.getAllMimeTypes()); CompressionPredicate excludedUserAgents = getExcludedUserAgentsPredicate( this.compression.getExcludedUserAgents()); server = server.compress(mimeTypes.and(excludedUserAgents)); diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/CompressionConnectorCustomizer.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/CompressionConnectorCustomizer.java index 2f85cf9ceb2e..31eaa4b8d6ea 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/CompressionConnectorCustomizer.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/CompressionConnectorCustomizer.java @@ -63,7 +63,7 @@ private int getMinResponseSize(Compression compression) { } private String getMimeTypes(Compression compression) { - return StringUtils.arrayToCommaDelimitedString(compression.getMimeTypes()); + return StringUtils.arrayToCommaDelimitedString(compression.getAllMimeTypes()); } private String getExcludedUserAgents(Compression compression) { diff --git a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/Compression.java b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/Compression.java index 76a530ce6d17..cc43ff3b6db5 100644 --- a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/Compression.java +++ b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/Compression.java @@ -19,6 +19,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.context.properties.ConfigurationPropertiesSource; +import org.springframework.util.StringUtils; import org.springframework.util.unit.DataSize; /** @@ -43,6 +44,11 @@ public class Compression { private String[] mimeTypes = new String[] { "text/html", "text/xml", "text/plain", "text/css", "text/javascript", "application/javascript", "application/json", "application/xml" }; + /** + * Comma-separated list of additional MIME types that should be compressed. + */ + private String[] additionalMimeTypes = new String[0]; + /** * Comma-separated list of user agents for which responses should not be compressed. */ @@ -77,6 +83,23 @@ public void setMimeTypes(String[] mimeTypes) { this.mimeTypes = mimeTypes; } + /** + * Return the MIME types that should be compressed, including any additional types. + * @return the MIME types that should be compressed + */ + public String[] getAllMimeTypes() { + String[] combined = StringUtils.concatenateStringArrays(this.mimeTypes, this.additionalMimeTypes); + return (combined != null) ? combined : this.mimeTypes; + } + + public String[] getAdditionalMimeTypes() { + return this.additionalMimeTypes; + } + + public void setAdditionalMimeTypes(String[] additionalMimeTypes) { + this.additionalMimeTypes = additionalMimeTypes; + } + public String @Nullable [] getExcludedUserAgents() { return this.excludedUserAgents; } diff --git a/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/autoconfigure/ServerPropertiesTests.java b/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/autoconfigure/ServerPropertiesTests.java index 2e4f4eae5068..97aa2b566f1a 100644 --- a/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/autoconfigure/ServerPropertiesTests.java +++ b/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/autoconfigure/ServerPropertiesTests.java @@ -27,6 +27,7 @@ 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.Compression; import org.springframework.boot.web.server.MimeMappings; import org.springframework.boot.web.server.MimeMappings.Mapping; import org.springframework.util.unit.DataSize; @@ -148,6 +149,13 @@ void defaultMaxHttpRequestHeaderSizeMatchesTomcatsDefault() { .isEqualTo(new Http11Nio2Protocol().getMaxHttpRequestHeaderSize()); } + @Test + void additionalCompressionMimeTypesAreAddedToDefaults() { + bind("server.compression.additional-mime-types", "application/zip"); + assertThat(this.properties.getCompression().getAllMimeTypes()).contains(new Compression().getMimeTypes()); + assertThat(this.properties.getCompression().getAllMimeTypes()).contains("application/zip"); + } + private void bind(String name, String value) { bind(Collections.singletonMap(name, value)); } From a8fc38f5654880641d23ea0533a43d69a3d5f8ca Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 23 Jan 2026 09:26:09 +0000 Subject: [PATCH 136/930] Polish "Provide support for adding to the mime types that are compressed" See gh-48930 --- .../src/docs/antora/modules/how-to/pages/webserver.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc index 09f4b0f79fc4..e02fe7c0272f 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc @@ -205,7 +205,8 @@ By default, responses are compressed only if their content type is one of the fo * `application/json` * `application/xml` -You can configure this behavior by setting the configprop:server.compression.mime-types[] property. +To configure the mime types that will be compressed, replacing the defaults, set the configprop:server.compression.mime-types[] property. +To keep the defaults and configure additional mime types, set the configprop:server.compression.additional-mime-types[] property instead. From 36315afa0ff530b2dc443575471bef3dd79beb9e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 26 Jan 2026 12:12:14 -0800 Subject: [PATCH 137/930] Fix method name --- .../org/springframework/boot/build/EclipseConventions.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/EclipseConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/EclipseConventions.java index 56636d929345..31f98eeb1e4d 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/EclipseConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/EclipseConventions.java @@ -46,10 +46,10 @@ class EclipseConventions { void apply(Project project) { project.getPlugins().withType(EclipsePlugin.class, (eclipse) -> configure(project, eclipse)); - project.afterEvaluate(this::dunno); + project.afterEvaluate(this::setJavaRuntimeName); } - private void dunno(Project project) { + private void setJavaRuntimeName(Project project) { EclipseModel model = project.getExtensions().findByType(EclipseModel.class); EclipseJdt jdt = (model != null) ? model.getJdt() : null; if (jdt != null) { From 6a87d5b81200569f01c3f54c9bbcbd72f398b459 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 26 Jan 2026 12:19:36 -0800 Subject: [PATCH 138/930] Polish --- .../boot/build/EclipseSynchronizeJdtSettings.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/EclipseSynchronizeJdtSettings.java b/buildSrc/src/main/java/org/springframework/boot/build/EclipseSynchronizeJdtSettings.java index 66211334b265..9e6836a9f98a 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/EclipseSynchronizeJdtSettings.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/EclipseSynchronizeJdtSettings.java @@ -21,8 +21,6 @@ import org.gradle.api.Task; import org.gradle.api.internal.PropertiesTransformer; import org.gradle.plugins.ide.api.PropertiesGeneratorTask; -import org.gradle.plugins.ide.eclipse.model.EclipseJdt; -import org.gradle.plugins.ide.eclipse.model.EclipseModel; import org.gradle.plugins.ide.internal.generator.PropertiesPersistableConfigurationObject; import org.springframework.boot.build.EclipseSynchronizeJdtSettings.Configuration; @@ -41,10 +39,6 @@ protected Configuration create() { @Override protected void configure(Configuration configuration) { - getProject().getExtensions().getByType(EclipseModel.class).jdt(this::setJavaRuntimeName); - } - - private void setJavaRuntimeName(EclipseJdt jdt) { } static class Configuration extends PropertiesPersistableConfigurationObject { From 70a945e8a0100a82ca8875e66e50e66e851a4b5a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 26 Jan 2026 16:53:55 -0800 Subject: [PATCH 139/930] Remove debug sysout --- .../springframework/boot/build/SystemRequirementsExtension.java | 1 - 1 file changed, 1 deletion(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java index dc9a2c7e4878..2fb63d7af1bd 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java @@ -52,7 +52,6 @@ public int getVersion() { } public void setVersion(int version) { - System.err.println(">>>> SET " + version); this.version = version; } From 743d54e403f83734ced3c7338eb2dcb9873cd87d Mon Sep 17 00:00:00 2001 From: Joaquin Santana Date: Wed, 28 Jan 2026 10:38:35 +0000 Subject: [PATCH 140/930] Add customizers for OtlpHttpLogRecordExporterBuilder and OtlpGrpcLogRecordExporterBuilder See gh-49002 Signed-off-by: Joaquin Santana --- ...rpcLogRecordExporterBuilderCustomizer.java | 37 ++++++++++++++++ ...ttpLogRecordExporterBuilderCustomizer.java | 37 ++++++++++++++++ .../otlp/OtlpLoggingConfigurations.java | 8 +++- .../OtlpLoggingAutoConfigurationTests.java | 42 +++++++++++++++++++ 4 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpGrpcLogRecordExporterBuilderCustomizer.java create mode 100644 module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpHttpLogRecordExporterBuilderCustomizer.java diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpGrpcLogRecordExporterBuilderCustomizer.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpGrpcLogRecordExporterBuilderCustomizer.java new file mode 100644 index 000000000000..6d02983dc215 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpGrpcLogRecordExporterBuilderCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.opentelemetry.autoconfigure.logging.otlp; + +import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporterBuilder; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link OtlpGrpcLogRecordExporterBuilder} whilst retaining default auto-configuration. + * + * @author Joaquin Santana + * @since 4.1.0 + */ +@FunctionalInterface +public interface OtlpGrpcLogRecordExporterBuilderCustomizer { + + /** + * Customize the {@link OtlpGrpcLogRecordExporterBuilder}. + * @param builder the builder to customize + */ + void customize(OtlpGrpcLogRecordExporterBuilder builder); + +} diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpHttpLogRecordExporterBuilderCustomizer.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpHttpLogRecordExporterBuilderCustomizer.java new file mode 100644 index 000000000000..e49a250cc2b3 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpHttpLogRecordExporterBuilderCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.opentelemetry.autoconfigure.logging.otlp; + +import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporterBuilder; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link OtlpHttpLogRecordExporterBuilder} whilst retaining default auto-configuration. + * + * @author Joaquin Santana + * @since 4.1.0 + */ +@FunctionalInterface +public interface OtlpHttpLogRecordExporterBuilderCustomizer { + + /** + * Customize the {@link OtlpHttpLogRecordExporterBuilder}. + * @param builder the builder to customize + */ + void customize(OtlpHttpLogRecordExporterBuilder builder); + +} diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java index 33e3bb7742b3..4763b7affc66 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java @@ -88,7 +88,8 @@ static class Exporters { @ConditionalOnProperty(name = "management.opentelemetry.logging.export.otlp.transport", havingValue = "http", matchIfMissing = true) OtlpHttpLogRecordExporter otlpHttpLogRecordExporter(OtlpLoggingProperties properties, - OtlpLoggingConnectionDetails connectionDetails, ObjectProvider meterProvider) { + OtlpLoggingConnectionDetails connectionDetails, ObjectProvider meterProvider, + ObjectProvider customizers) { OtlpHttpLogRecordExporterBuilder builder = OtlpHttpLogRecordExporter.builder() .setEndpoint(connectionDetails.getUrl(Transport.HTTP)) .setTimeout(properties.getTimeout()) @@ -96,13 +97,15 @@ OtlpHttpLogRecordExporter otlpHttpLogRecordExporter(OtlpLoggingProperties proper .setCompression(properties.getCompression().name().toLowerCase(Locale.US)); properties.getHeaders().forEach(builder::addHeader); meterProvider.ifAvailable(builder::setMeterProvider); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } @Bean @ConditionalOnProperty(name = "management.opentelemetry.logging.export.otlp.transport", havingValue = "grpc") OtlpGrpcLogRecordExporter otlpGrpcLogRecordExporter(OtlpLoggingProperties properties, - OtlpLoggingConnectionDetails connectionDetails, ObjectProvider meterProvider) { + OtlpLoggingConnectionDetails connectionDetails, ObjectProvider meterProvider, + ObjectProvider customizers) { OtlpGrpcLogRecordExporterBuilder builder = OtlpGrpcLogRecordExporter.builder() .setEndpoint(connectionDetails.getUrl(Transport.GRPC)) .setTimeout(properties.getTimeout()) @@ -110,6 +113,7 @@ OtlpGrpcLogRecordExporter otlpGrpcLogRecordExporter(OtlpLoggingProperties proper .setCompression(properties.getCompression().name().toLowerCase(Locale.US)); properties.getHeaders().forEach(builder::addHeader); meterProvider.ifAvailable(builder::setMeterProvider); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java index 20709e64371d..41313a95de1e 100644 --- a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.opentelemetry.autoconfigure.logging.otlp; +import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; @@ -224,6 +225,47 @@ void whenHasMeterProviderBeanAddsItToGrpcExporter() { }); } + @Test + void shouldCustomizeHttpTransportWithOtlpHttpLogRecordExporterBuilderCustomizer() { + Duration connectTimeout = Duration.ofMinutes(20); + Duration timeout = Duration.ofMinutes(10); + this.contextRunner + .withBean("httpCustomizer1", OtlpHttpLogRecordExporterBuilderCustomizer.class, + () -> (builder) -> builder.setConnectTimeout(connectTimeout)) + .withBean("httpCustomizer2", OtlpHttpLogRecordExporterBuilderCustomizer.class, + () -> (builder) -> builder.setTimeout(timeout)) + .withPropertyValues("management.opentelemetry.logging.export.otlp.endpoint=http://localhost:4318/v1/logs") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class) + .hasSingleBean(LogRecordExporter.class); + OtlpHttpLogRecordExporter exporter = context.getBean(OtlpHttpLogRecordExporter.class); + assertThat(exporter).extracting("delegate.httpSender.client") + .hasFieldOrPropertyWithValue("connectTimeoutMillis", (int) connectTimeout.toMillis()) + .hasFieldOrPropertyWithValue("callTimeoutMillis", (int) timeout.toMillis()); + }); + } + + @Test + void shouldCustomizeGrpcTransportWhenEnabledWithOtlpGrpcLogRecordExporterBuilderCustomizer() { + Duration timeout = Duration.ofMinutes(10); + Duration connectTimeout = Duration.ofMinutes(20); + this.contextRunner + .withBean("grpcCustomizer1", OtlpGrpcLogRecordExporterBuilderCustomizer.class, + () -> (builder) -> builder.setConnectTimeout(connectTimeout)) + .withBean("grpcCustomizer2", OtlpGrpcLogRecordExporterBuilderCustomizer.class, + () -> (builder) -> builder.setTimeout(timeout)) + .withPropertyValues("management.opentelemetry.logging.export.otlp.endpoint=http://localhost:4318/v1/logs", + "management.opentelemetry.logging.export.otlp.transport=grpc") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpGrpcLogRecordExporter.class) + .hasSingleBean(LogRecordExporter.class); + OtlpGrpcLogRecordExporter exporter = context.getBean(OtlpGrpcLogRecordExporter.class); + assertThat(exporter).extracting("delegate.grpcSender.client") + .hasFieldOrPropertyWithValue("connectTimeoutMillis", (int) connectTimeout.toMillis()) + .hasFieldOrPropertyWithValue("callTimeoutMillis", (int) timeout.toMillis()); + }); + } + @Configuration(proxyBeanMethods = false) public static class MultipleSdkLoggerProviderBuilderCustomizersConfig { From a3c5f61567a675cf44efdd423b2c731da0db0336 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 28 Jan 2026 13:24:52 +0100 Subject: [PATCH 141/930] Polish "Add customizers for OtlpHttpLogRecordExporterBuilder and OtlpGrpcLogRecordExporterBuilder" See gh-49002 --- .../modules/reference/pages/actuator/observability.adoc | 4 ++++ .../otlp/OtlpGrpcLogRecordExporterBuilderCustomizer.java | 2 +- .../otlp/OtlpHttpLogRecordExporterBuilderCustomizer.java | 2 +- platform/spring-boot-dependencies/build.gradle | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc index bff522b6396a..0b04549a57f0 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc @@ -162,6 +162,10 @@ include-code::AutoConfiguredOpenTelemetrySdkConfiguration[] The javadoc:org.springframework.boot.opentelemetry.autoconfigure.logging.OpenTelemetryLoggingAutoConfiguration[] configures OpenTelemetry's javadoc:io.opentelemetry.sdk.logs.SdkLoggerProvider[]. Exporting logs via OTLP is supported through the javadoc:org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingAutoConfiguration[], which enables OTLP log exporting over HTTP or gRPC. +NOTE: If you need to apply advanced customizations to OTLP log record exporters, consider registering javadoc:org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpHttpLogRecordExporterBuilderCustomizer[] or javadoc:org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpGrpcLogRecordExporterBuilderCustomizer[] beans. +These will be invoked before the creation of the javadoc:io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter[] or javadoc:io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter[]. +The customizers take precedence over anything applied by the auto-configuration. + However, while there is a `SdkLoggerProvider` bean, Spring Boot doesn't support bridging logs to this bean out of the box. This can be done with 3rd-party log bridges, as described in the xref:reference:actuator/loggers.adoc#actuator.loggers.opentelemetry[Logging with OpenTelemetry] section. diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpGrpcLogRecordExporterBuilderCustomizer.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpGrpcLogRecordExporterBuilderCustomizer.java index 6d02983dc215..e6de0869cae4 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpGrpcLogRecordExporterBuilderCustomizer.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpGrpcLogRecordExporterBuilderCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2026 the original author or authors. + * Copyright 2012-present the original author 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/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpHttpLogRecordExporterBuilderCustomizer.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpHttpLogRecordExporterBuilderCustomizer.java index e49a250cc2b3..bf6a0a43e27f 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpHttpLogRecordExporterBuilderCustomizer.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpHttpLogRecordExporterBuilderCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2026 the original author or authors. + * Copyright 2012-present the original author 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/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 36d993ad8900..040858d4bd92 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1710,6 +1710,7 @@ bom { javadoc("opentelemetry-sdk-logs", version -> "https://javadoc.io/doc/io.opentelemetry/opentelemetry-sdk-logs/%s".formatted(version), "io.opentelemetry.sdk.logs") javadoc("opentelemetry-sdk-metrics", version -> "https://javadoc.io/doc/io.opentelemetry/opentelemetry-sdk-metrics/%s".formatted(version), "io.opentelemetry.sdk.metrics") javadoc("opentelemetry-sdk-trace", version -> "https://javadoc.io/doc/io.opentelemetry/opentelemetry-sdk-trace/%s".formatted(version), "io.opentelemetry.sdk.trace") + javadoc("opentelemetry-exporter-otlp", version -> "https://javadoc.io/doc/io.opentelemetry/opentelemetry-exporter-otlp/%s".formatted(version), "io.opentelemetry.exporter.otlp") releaseNotes("https://github.com/open-telemetry/opentelemetry-java/releases/tag/v{version}") } } From d760b171c3538ebce79402ca3eb1e010b8c2e952 Mon Sep 17 00:00:00 2001 From: junpak Date: Sat, 24 Jan 2026 23:48:41 +0900 Subject: [PATCH 142/930] Document properties whose default values have changed This commit adds a new section in the Configuration Changelog to indicate properties whose default values have changed. See gh-48962 Signed-off-by: junpak --- .../changelog/ChangelogWriter.java | 13 +++++++++++++ .../changelog/Difference.java | 5 +++++ .../changelog/DifferenceType.java | 7 ++++++- .../changelog/ChangelogTests.java | 9 ++++++++- .../src/test/resources/sample-1.0.json | 14 +++++++++++++- .../src/test/resources/sample-2.0.json | 12 ++++++++++++ .../src/test/resources/sample.adoc | 16 ++++++++++++++++ 7 files changed, 73 insertions(+), 3 deletions(-) diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java index 32db7488bdb0..e0af2670236b 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java @@ -68,6 +68,8 @@ void write(Changelog changelog) { String newVersionNumber = changelog.newVersionNumber(); Map> differencesByType = collateByType(changelog); write("Configuration property changes between `%s` and `%s`%n", oldVersionNumber, newVersionNumber); + write("%n%n%n== Default Value Changed in %s%n%n", newVersionNumber); + writeDefaultValueChanged(differencesByType.get(DifferenceType.DEFAULT_VALUE_CHANGED)); write("%n%n%n== Deprecated in %s%n%n", newVersionNumber); writeDeprecated(differencesByType.get(DifferenceType.DEPRECATED)); write("%n%n%n== Added in %s%n%n", newVersionNumber); @@ -98,6 +100,17 @@ private void writeDeprecated(Difference difference) { writeDeprecatedPropertyRow(difference.newProperty()); } + private void writeDefaultValueChanged(List differences) { + List rows = sortProperties(differences, Difference::newProperty); + writeTable("| Key | Old Default | New Default", rows, this::writeDefaultValueChanged); + } + + private void writeDefaultValueChanged(Difference difference) { + writeCell(monospace(difference.newProperty().getId())); + writeCell(monospace(asString(difference.oldProperty().getDefaultValue()))); + writeCell(monospace(asString(difference.newProperty().getDefaultValue()))); + } + private void writeAdded(List differences) { List rows = sortProperties(differences, Difference::newProperty); writeTable("| Key | Default value | Description", rows, this::writeAdded); diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java index 2f8f5e3e45dd..a3af8e1f1ff9 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java @@ -16,6 +16,8 @@ package org.springframework.boot.configurationmetadata.changelog; +import java.util.Objects; + import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; import org.springframework.boot.configurationmetadata.Deprecation.Level; @@ -49,6 +51,9 @@ static Difference compute(ConfigurationMetadataProperty oldProperty, Configurati && newProperty.isDeprecated() && newProperty.getDeprecation().getLevel() == Level.ERROR) { return new Difference(DifferenceType.DELETED, oldProperty, newProperty); } + if (!Objects.equals(oldProperty.getDefaultValue(), newProperty.getDefaultValue())) { + return new Difference(DifferenceType.DEFAULT_VALUE_CHANGED, oldProperty, newProperty); + } return null; } diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java index d8bbe93560b9..aa81fe45b522 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java @@ -37,6 +37,11 @@ enum DifferenceType { /** * The entry has been deleted. */ - DELETED + DELETED, + + /** + * The entry's default value has been changed. + */ + DEFAULT_VALUE_CHANGED } diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java index 100f5a99f27d..7854ab076f83 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java @@ -39,7 +39,7 @@ void diffContainsDifferencesBetweenLeftAndRightInputs() { assertThat(differences).isNotNull(); assertThat(differences.oldVersionNumber()).isEqualTo("1.0"); assertThat(differences.newVersionNumber()).isEqualTo("2.0"); - assertThat(differences.differences()).hasSize(7); + assertThat(differences.differences()).hasSize(9); List added = differences.differences() .stream() .filter((difference) -> difference.type() == DifferenceType.ADDED) @@ -65,6 +65,13 @@ void diffContainsDifferencesBetweenLeftAndRightInputs() { assertThat(deprecated).hasSize(1); assertProperty(deprecated.get(0).oldProperty(), "test.deprecate", String.class, "wrong"); assertProperty(deprecated.get(0).newProperty(), "test.deprecate", String.class, "wrong"); + List defaultValueChanged = differences.differences() + .stream() + .filter((difference) -> difference.type() == DifferenceType.DEFAULT_VALUE_CHANGED) + .toList(); + assertThat(defaultValueChanged).hasSize(2) + .anySatisfy((entry) -> assertProperty(entry.newProperty(), "test.default.change", String.class, "new")) + .anySatisfy((entry) -> assertThat(entry.newProperty().getId()).isEqualTo("test.array.change")); } private void assertProperty(ConfigurationMetadataProperty property, String id, Class type, Object defaultValue) { diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json index 6f6ab8468453..37ace809760e 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json @@ -6,6 +6,12 @@ "description": "Test equality.", "defaultValue": "test" }, + { + "name": "test.default.change", + "type": "java.lang.String", + "description": "Test default value change.", + "defaultValue": "old" + }, { "name": "test.replace", "type": "java.lang.String", @@ -41,6 +47,12 @@ "deprecation": { "level": "error" } + }, + { + "name": "test.array.change", + "type": "java.lang.String[]", + "description": "Test array default value change.", + "defaultValue": ["a", "b"] } ] -} \ No newline at end of file +} diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json index e650460140de..73bd835b0264 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json @@ -6,6 +6,12 @@ "description": "Test add.", "defaultValue": "new" }, + { + "name": "test.default.change", + "type": "java.lang.String", + "description": "Test default value change.", + "defaultValue": "new" + }, { "name": "test.replace", "type": "java.lang.String", @@ -60,6 +66,12 @@ "replacement": "test.new.property", "reason": "removed in third-party library without deprecation" } + }, + { + "name": "test.array.change", + "type": "java.lang.String[]", + "description": "Test array default value change.", + "defaultValue": ["a", "b", "c"] } ] } diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc index 908f48e73c53..147f7d976e1b 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc @@ -2,6 +2,22 @@ Configuration property changes between `1.0` and `2.0` +== Default Value Changed in 2.0 + +|====================== +| Key | Old Default | New Default + +| `test.array.change` +| `a, b` +| `a, b, c` + +| `test.default.change` +| `old` +| `new` +|====================== + + + == Deprecated in 2.0 |====================== From a0a1ebfc1846fd03b9f7b981b94eaaef719fbe41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 30 Jan 2026 16:58:39 +0100 Subject: [PATCH 143/930] Polish "Document properties whose default values have changed" This commit adds a new section in the Configuration Changelog to indicate properties whose default values have changed. See gh-48962 --- .../changelog/ChangelogWriter.java | 10 +++++----- .../changelog/Difference.java | 2 +- .../changelog/DifferenceType.java | 5 +++-- .../changelog/ChangelogTests.java | 13 +++++++------ .../src/test/resources/sample-1.0.json | 14 +++++++------- .../src/test/resources/sample-2.0.json | 12 ++++++------ .../src/test/resources/sample.adoc | 6 +++--- 7 files changed, 32 insertions(+), 30 deletions(-) diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java index e0af2670236b..802f59769b03 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java @@ -68,8 +68,8 @@ void write(Changelog changelog) { String newVersionNumber = changelog.newVersionNumber(); Map> differencesByType = collateByType(changelog); write("Configuration property changes between `%s` and `%s`%n", oldVersionNumber, newVersionNumber); - write("%n%n%n== Default Value Changed in %s%n%n", newVersionNumber); - writeDefaultValueChanged(differencesByType.get(DifferenceType.DEFAULT_VALUE_CHANGED)); + write("%n%n%n== Default Changed in %s%n%n", newVersionNumber); + writeDefaultChanged(differencesByType.get(DifferenceType.DEFAULT_CHANGED)); write("%n%n%n== Deprecated in %s%n%n", newVersionNumber); writeDeprecated(differencesByType.get(DifferenceType.DEPRECATED)); write("%n%n%n== Added in %s%n%n", newVersionNumber); @@ -100,12 +100,12 @@ private void writeDeprecated(Difference difference) { writeDeprecatedPropertyRow(difference.newProperty()); } - private void writeDefaultValueChanged(List differences) { + private void writeDefaultChanged(List differences) { List rows = sortProperties(differences, Difference::newProperty); - writeTable("| Key | Old Default | New Default", rows, this::writeDefaultValueChanged); + writeTable("| Key | Old Default | New Default", rows, this::writeDefaultChanged); } - private void writeDefaultValueChanged(Difference difference) { + private void writeDefaultChanged(Difference difference) { writeCell(monospace(difference.newProperty().getId())); writeCell(monospace(asString(difference.oldProperty().getDefaultValue()))); writeCell(monospace(asString(difference.newProperty().getDefaultValue()))); diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java index a3af8e1f1ff9..35a1867c89b2 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java @@ -52,7 +52,7 @@ static Difference compute(ConfigurationMetadataProperty oldProperty, Configurati return new Difference(DifferenceType.DELETED, oldProperty, newProperty); } if (!Objects.equals(oldProperty.getDefaultValue(), newProperty.getDefaultValue())) { - return new Difference(DifferenceType.DEFAULT_VALUE_CHANGED, oldProperty, newProperty); + return new Difference(DifferenceType.DEFAULT_CHANGED, oldProperty, newProperty); } return null; } diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java index aa81fe45b522..46246c99296f 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java @@ -40,8 +40,9 @@ enum DifferenceType { DELETED, /** - * The entry's default value has been changed. + * The entry's default value has changed. The entry exists in both the previous and + * current versions, but its default value is different. */ - DEFAULT_VALUE_CHANGED + DEFAULT_CHANGED } diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java index 7854ab076f83..487d78ccdfd5 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java @@ -65,19 +65,20 @@ void diffContainsDifferencesBetweenLeftAndRightInputs() { assertThat(deprecated).hasSize(1); assertProperty(deprecated.get(0).oldProperty(), "test.deprecate", String.class, "wrong"); assertProperty(deprecated.get(0).newProperty(), "test.deprecate", String.class, "wrong"); - List defaultValueChanged = differences.differences() + List defaultChanged = differences.differences() .stream() - .filter((difference) -> difference.type() == DifferenceType.DEFAULT_VALUE_CHANGED) + .filter((difference) -> difference.type() == DifferenceType.DEFAULT_CHANGED) .toList(); - assertThat(defaultValueChanged).hasSize(2) - .anySatisfy((entry) -> assertProperty(entry.newProperty(), "test.default.change", String.class, "new")) - .anySatisfy((entry) -> assertThat(entry.newProperty().getId()).isEqualTo("test.array.change")); + assertThat(defaultChanged).hasSize(2) + .anySatisfy((entry) -> assertProperty(entry.newProperty(), "test.default.array.change", String[].class, + new String[] { "b", "a" })) + .anySatisfy((entry) -> assertProperty(entry.newProperty(), "test.default.change", String.class, "new")); } 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.getType()).isEqualTo(type.getCanonicalName()); assertThat(property.getDefaultValue()).isEqualTo(defaultValue); } diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json index 37ace809760e..e28ce4affd6c 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json @@ -12,6 +12,12 @@ "description": "Test default value change.", "defaultValue": "old" }, + { + "name": "test.default.array.change", + "type": "java.lang.String[]", + "description": "Test default array value change.", + "defaultValue": ["a", "b"] + }, { "name": "test.replace", "type": "java.lang.String", @@ -47,12 +53,6 @@ "deprecation": { "level": "error" } - }, - { - "name": "test.array.change", - "type": "java.lang.String[]", - "description": "Test array default value change.", - "defaultValue": ["a", "b"] } ] -} +} \ No newline at end of file diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json index 73bd835b0264..48923d76ec42 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json @@ -12,6 +12,12 @@ "description": "Test default value change.", "defaultValue": "new" }, + { + "name": "test.default.array.change", + "type": "java.lang.String[]", + "description": "Test default array value change.", + "defaultValue": ["b", "a"] + }, { "name": "test.replace", "type": "java.lang.String", @@ -66,12 +72,6 @@ "replacement": "test.new.property", "reason": "removed in third-party library without deprecation" } - }, - { - "name": "test.array.change", - "type": "java.lang.String[]", - "description": "Test array default value change.", - "defaultValue": ["a", "b", "c"] } ] } diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc index 147f7d976e1b..7a0f17b8ada5 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc @@ -2,14 +2,14 @@ Configuration property changes between `1.0` and `2.0` -== Default Value Changed in 2.0 +== Default Changed in 2.0 |====================== | Key | Old Default | New Default -| `test.array.change` +| `test.default.array.change` | `a, b` -| `a, b, c` +| `b, a` | `test.default.change` | `old` From 3d960bc6d025070244777b61ff05cc47cb42f111 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 23 Jan 2026 11:10:44 +0100 Subject: [PATCH 144/930] Add support for specifying encoding when loading files with spring.config.import In the brackets we can now specify the encoding of the file instead of only the file extension: spring.config.import=classpath:import.properties[encoding=utf-8] The old format spring.config.import=classpath:import[.properties] is still supported and is a shorthand for: spring.config.import=classpath:import[extension=.properties] Attributes can be combined, too: spring.config.import=classpath:import[extension=.properties][encoding=utf-8] Closes gh-28663 --- .../context/config/FileExtensionHint.java | 84 -------- .../boot/context/config/FileHint.java | 188 ++++++++++++++++++ .../config/StandardConfigDataLoader.java | 3 +- .../StandardConfigDataLocationResolver.java | 13 +- .../config/StandardConfigDataReference.java | 14 +- ...EnvironmentConfigDataLocationResolver.java | 9 +- .../SystemEnvironmentConfigDataResource.java | 9 +- .../env/OriginTrackedPropertiesLoader.java | 20 +- .../env/PropertiesPropertySourceLoader.java | 22 +- .../boot/env/PropertySourceLoader.java | 20 ++ .../config/FileExtensionHintTests.java | 72 ------- .../boot/context/config/FileHintTests.java | 99 +++++++++ .../config/StandardConfigDataLoaderTests.java | 4 +- ...andardConfigDataLocationResolverTests.java | 37 +++- ...ystemEnvironmentConfigDataLoaderTests.java | 2 +- ...temEnvironmentConfigDataResourceTests.java | 2 +- .../OriginTrackedPropertiesLoaderTests.java | 39 ++-- .../pages/features/external-config.adoc | 38 +++- 18 files changed, 473 insertions(+), 202 deletions(-) delete mode 100644 core/spring-boot/src/main/java/org/springframework/boot/context/config/FileExtensionHint.java create mode 100644 core/spring-boot/src/main/java/org/springframework/boot/context/config/FileHint.java delete mode 100644 core/spring-boot/src/test/java/org/springframework/boot/context/config/FileExtensionHintTests.java create mode 100644 core/spring-boot/src/test/java/org/springframework/boot/context/config/FileHintTests.java diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/config/FileExtensionHint.java b/core/spring-boot/src/main/java/org/springframework/boot/context/config/FileExtensionHint.java deleted file mode 100644 index 891975bed645..000000000000 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/config/FileExtensionHint.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.config; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.jspecify.annotations.Nullable; - -/** - * User-provided hint for an otherwise missing file extension. - * - * @author Phillip Webb - */ -final class FileExtensionHint { - - private static final Pattern PATTERN = Pattern.compile("^(.*)\\[(\\.\\w+)](?!\\[)$"); - - private static final FileExtensionHint NONE = new FileExtensionHint(null); - - private final @Nullable Matcher matcher; - - private FileExtensionHint(@Nullable Matcher matcher) { - this.matcher = matcher; - } - - /** - * Return {@code true} if the hint is present. - * @return if the hint is present - */ - boolean isPresent() { - return this.matcher != null; - } - - /** - * Return the extension from the hint or return the parameter if the hint is not - * {@link #isPresent() present}. - * @param extension the fallback extension - * @return the extension either from the hint or fallback - */ - String orElse(String extension) { - return (this.matcher != null) ? toString() : extension; - } - - @Override - public String toString() { - return (this.matcher != null) ? this.matcher.group(2) : ""; - } - - /** - * Return the {@link FileExtensionHint} from the given value. - * @param value the source value - * @return the {@link FileExtensionHint} (never {@code null}) - */ - static FileExtensionHint from(String value) { - Matcher matcher = PATTERN.matcher(value); - return (matcher.matches()) ? new FileExtensionHint(matcher) : NONE; - } - - /** - * Remove any hint from the given value. - * @param value the source value - * @return the value without any hint - */ - static String removeFrom(String value) { - Matcher matcher = PATTERN.matcher(value); - return (matcher.matches()) ? matcher.group(1) : value; - } - -} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/config/FileHint.java b/core/spring-boot/src/main/java/org/springframework/boot/context/config/FileHint.java new file mode 100644 index 000000000000..9e385012caea --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/config/FileHint.java @@ -0,0 +1,188 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.config; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +/** + * User-provided hint for file attributes, like extension or encoding. + * + * @author Phillip Webb + * @author Moritz Halbritter + */ +final class FileHint { + + private static final FileHint NONE = new FileHint(Collections.emptyMap()); + + private final Map attributes; + + private FileHint(Map attributes) { + this.attributes = attributes; + } + + /** + * Return {@code true} if the hint is present. + * @return if the hint is present + */ + boolean isPresent() { + return getExtension() != null || getEncoding() != null; + } + + /** + * Returns the extension. + * @return the extension + */ + @Nullable String getExtension() { + String extension = this.attributes.get("extension"); + if (extension == null) { + return null; + } + if (!extension.startsWith(".")) { + return null; + } + return extension; + } + + /** + * Return the extension from the hint or return the {@code fallback} if the extension + * is not present. + * @param fallback the fallback extension + * @return the extension either from the hint or fallback + */ + String getExtensionOrElse(String fallback) { + String extension = getExtension(); + return (extension != null) ? extension : fallback; + } + + /** + * Returns the encoding. + * @return the encoding + */ + @Nullable String getEncoding() { + return this.attributes.get("encoding"); + } + + /** + * Returns the encoding as a {@link Charset}. + * @return the encoding as a {@link Charset} + */ + @Nullable Charset getEncodingAsCharset() { + String encoding = getEncoding(); + return (encoding != null) ? Charset.forName(encoding) : null; + } + + @Override + public String toString() { + return "FileHint{attributes=" + this.attributes + '}'; + } + + /** + * Return the {@link FileHint} from the given value. + * @param value the source value + * @return the {@link FileHint} + */ + static FileHint from(String value) { + if (!hasBrackets(value)) { + return NONE; + } + List betweenBrackets = findBetweenBrackets(value); + Map attributes = new HashMap<>(); + for (String entry : betweenBrackets) { + String cleaned = entry.trim(); + if (cleaned.isEmpty()) { + continue; + } + int equals = cleaned.indexOf('='); + if (equals == -1) { + attributes.put("extension", cleaned); + } + else { + attributes.put(cleaned.substring(0, equals).trim(), cleaned.substring(equals + 1).trim()); + } + } + return new FileHint(Collections.unmodifiableMap(attributes)); + } + + private static List findBetweenBrackets(String value) { + List collected = new ArrayList<>(); + StringBuilder buffer = new StringBuilder(); + int brackets = 0; + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '[') { + if (brackets > 0) { + buffer.append(c); + } + brackets++; + } + else if (c == ']') { + brackets--; + if (brackets > 0) { + buffer.append(c); + } + else if (brackets == 0 && !buffer.isEmpty()) { + collected.add(buffer.toString()); + buffer.setLength(0); + } + } + else if (brackets > 0) { + buffer.append(c); + } + } + return collected; + } + + /** + * Remove any hint from the given value. + * @param value the source value + * @return the value without any hint + */ + static String removeFrom(String value) { + if (!hasBrackets(value)) { + return value; + } + StringBuilder result = new StringBuilder(); + int brackets = 0; + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '[') { + brackets++; + continue; + } + if (c == ']') { + brackets--; + continue; + } + if (brackets <= 0) { + result.append(c); + } + } + return result.toString(); + } + + private static boolean hasBrackets(String value) { + return value.indexOf('[') != -1 && value.indexOf(']') != -1; + } + +} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLoader.java b/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLoader.java index 4245f0d37214..468881933934 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLoader.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLoader.java @@ -51,7 +51,8 @@ public ConfigData load(ConfigDataLoaderContext context, StandardConfigDataResour Origin.from(reference.getConfigDataLocation())); String name = String.format("Config resource '%s' via location '%s'", resource, reference.getConfigDataLocation()); - List> propertySources = reference.getPropertySourceLoader().load(name, originTrackedResource); + List> propertySources = reference.getPropertySourceLoader() + .load(name, originTrackedResource, reference.getEncoding()); PropertySourceOptions options = (resource.getProfile() != null) ? PROFILE_SPECIFIC : NON_PROFILE_SPECIFIC; return new ConfigData(propertySources, options); } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java b/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java index fe49fc84c7bf..ae3a5a09190c 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java @@ -203,7 +203,7 @@ private Deque getReferencesForConfigName(String nam for (PropertySourceLoader propertySourceLoader : this.propertySourceLoaders) { for (String extension : propertySourceLoader.getFileExtensions()) { StandardConfigDataReference reference = new StandardConfigDataReference(configDataLocation, directory, - directory + name, profile, extension, propertySourceLoader); + directory + name, profile, extension, propertySourceLoader, null); if (!references.contains(reference)) { references.addFirst(reference); } @@ -214,16 +214,19 @@ private Deque getReferencesForConfigName(String nam private Set getReferencesForFile(ConfigDataLocation configDataLocation, String file, @Nullable String profile) { - FileExtensionHint fileExtensionHint = FileExtensionHint.from(file); - if (fileExtensionHint.isPresent()) { - file = FileExtensionHint.removeFrom(file) + fileExtensionHint; + FileHint fileHint = FileHint.from(file); + file = FileHint.removeFrom(file); + boolean hasFileHintExtension = fileHint.getExtension() != null; + if (hasFileHintExtension) { + file = file + fileHint.getExtension(); } for (PropertySourceLoader propertySourceLoader : this.propertySourceLoaders) { String fileExtension = getLoadableFileExtension(propertySourceLoader, file); if (fileExtension != null) { String root = file.substring(0, file.length() - fileExtension.length() - 1); StandardConfigDataReference reference = new StandardConfigDataReference(configDataLocation, null, root, - profile, (!fileExtensionHint.isPresent()) ? fileExtension : null, propertySourceLoader); + profile, (!hasFileHintExtension) ? fileExtension : null, propertySourceLoader, + fileHint.getEncodingAsCharset()); return Collections.singleton(reference); } } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataReference.java b/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataReference.java index f2b1000871c9..9df98ecfc915 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataReference.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataReference.java @@ -16,6 +16,8 @@ package org.springframework.boot.context.config; +import java.nio.charset.Charset; + import org.jspecify.annotations.Nullable; import org.springframework.boot.env.PropertySourceLoader; @@ -26,6 +28,7 @@ * be resolved to one or more {@link StandardConfigDataResource resources}. * * @author Phillip Webb + * @author Moritz Halbritter */ class StandardConfigDataReference { @@ -39,6 +42,8 @@ class StandardConfigDataReference { private final PropertySourceLoader propertySourceLoader; + private final @Nullable Charset encoding; + /** * Create a new {@link StandardConfigDataReference} instance. * @param configDataLocation the original location passed to the resolver @@ -49,15 +54,18 @@ class StandardConfigDataReference { * @param extension the file extension for the resource * @param propertySourceLoader the property source loader that should be used for this * reference + * @param encoding the encoding of the resource */ StandardConfigDataReference(ConfigDataLocation configDataLocation, @Nullable String directory, String root, - @Nullable String profile, @Nullable String extension, PropertySourceLoader propertySourceLoader) { + @Nullable String profile, @Nullable String extension, PropertySourceLoader propertySourceLoader, + @Nullable Charset encoding) { this.configDataLocation = configDataLocation; String profileSuffix = (StringUtils.hasText(profile)) ? "-" + profile : ""; this.resourceLocation = root + profileSuffix + ((extension != null) ? "." + extension : ""); this.directory = directory; this.profile = profile; this.propertySourceLoader = propertySourceLoader; + this.encoding = encoding; } ConfigDataLocation getConfigDataLocation() { @@ -80,6 +88,10 @@ boolean isMandatoryDirectory() { return this.profile; } + @Nullable Charset getEncoding() { + return this.encoding; + } + boolean isSkippable() { return this.configDataLocation.isOptional() || this.directory != null || this.profile != null; } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataLocationResolver.java b/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataLocationResolver.java index 18849fb7312a..6f95be76b72e 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataLocationResolver.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataLocationResolver.java @@ -63,11 +63,12 @@ public List resolve(ConfigDataLocationResol ConfigDataLocation location) throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException { String value = location.getNonPrefixedValue(PREFIX); - FileExtensionHint fileExtensionHint = FileExtensionHint.from(value); - String variableName = FileExtensionHint.removeFrom(value); - PropertySourceLoader loader = getLoader(fileExtensionHint.orElse(DEFAULT_EXTENSION)); + FileHint fileHint = FileHint.from(value); + String variableName = FileHint.removeFrom(value); + PropertySourceLoader loader = getLoader(fileHint.getExtensionOrElse(DEFAULT_EXTENSION)); if (hasEnvVariable(variableName)) { - return List.of(new SystemEnvironmentConfigDataResource(variableName, loader, this.environment)); + return List.of(new SystemEnvironmentConfigDataResource(variableName, loader, this.environment, + fileHint.getEncodingAsCharset())); } if (location.isOptional()) { return Collections.emptyList(); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataResource.java b/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataResource.java index 53a62c2e9798..a681b0d73dae 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataResource.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataResource.java @@ -17,6 +17,7 @@ package org.springframework.boot.context.config; import java.io.IOException; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Objects; @@ -43,11 +44,14 @@ class SystemEnvironmentConfigDataResource extends ConfigDataResource { private final Function environment; + private final @Nullable Charset encoding; + SystemEnvironmentConfigDataResource(String variableName, PropertySourceLoader loader, - Function environment) { + Function environment, @Nullable Charset encoding) { this.variableName = variableName; this.loader = loader; this.environment = environment; + this.encoding = encoding; } String getVariableName() { @@ -60,7 +64,8 @@ PropertySourceLoader getLoader() { @Nullable List> load() throws IOException { String content = this.environment.apply(this.variableName); - return (content != null) ? this.loader.load(StringUtils.capitalize(toString()), asResource(content)) : null; + return (content != null) + ? this.loader.load(StringUtils.capitalize(toString()), asResource(content), this.encoding) : null; } private ByteArrayResource asResource(String content) { diff --git a/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java b/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java index 8e0ff1b0892e..eff6e306a312 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.LineNumberReader; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -27,6 +28,8 @@ import java.util.Map; import java.util.function.BooleanSupplier; +import org.jspecify.annotations.Nullable; + import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.boot.origin.TextResourceOrigin; @@ -43,6 +46,7 @@ * @author Phillip Webb * @author Thiago Hirata * @author Guirong Hu + * @author Moritz Halbritter */ class OriginTrackedPropertiesLoader { @@ -59,25 +63,27 @@ class OriginTrackedPropertiesLoader { /** * Load {@code .properties} data and return a list of documents. + * @param encoding the resource encoding. Uses ISO-8859-1 if {@code null} * @return the loaded properties * @throws IOException on read error */ - List load() throws IOException { - return load(true); + List load(@Nullable Charset encoding) throws IOException { + return load(encoding, true); } /** * Load {@code .properties} data and return a map of {@code String} -> * {@link OriginTrackedValue}. + * @param encoding the resource encoding. Uses ISO-8859-1 if {@code null} * @param expandLists if list {@code name[]=a,b,c} shortcuts should be expanded * @return the loaded properties * @throws IOException on read error */ - List load(boolean expandLists) throws IOException { + List load(@Nullable Charset encoding, boolean expandLists) throws IOException { List documents = new ArrayList<>(); Document document = new Document(); StringBuilder buffer = new StringBuilder(); - try (CharacterReader reader = new CharacterReader(this.resource)) { + try (CharacterReader reader = new CharacterReader(this.resource, encoding)) { while (reader.read()) { if (reader.isCommentPrefixCharacter()) { char commentPrefixCharacter = reader.getCharacter(); @@ -200,9 +206,9 @@ private static class CharacterReader implements Closeable { private int lastLineCommentPrefixCharacter; - CharacterReader(Resource resource) throws IOException { - this.reader = new LineNumberReader( - new InputStreamReader(resource.getInputStream(), StandardCharsets.ISO_8859_1)); + CharacterReader(Resource resource, @Nullable Charset encoding) throws IOException { + this.reader = new LineNumberReader(new InputStreamReader(resource.getInputStream(), + (encoding != null) ? encoding : StandardCharsets.ISO_8859_1)); } @Override diff --git a/core/spring-boot/src/main/java/org/springframework/boot/env/PropertiesPropertySourceLoader.java b/core/spring-boot/src/main/java/org/springframework/boot/env/PropertiesPropertySourceLoader.java index 91c807e68cb4..3f11dc8334be 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/env/PropertiesPropertySourceLoader.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/env/PropertiesPropertySourceLoader.java @@ -17,14 +17,18 @@ package org.springframework.boot.env; import java.io.IOException; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.boot.env.OriginTrackedPropertiesLoader.Document; import org.springframework.core.env.PropertySource; import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; import org.springframework.core.io.support.PropertiesLoaderUtils; /** @@ -46,7 +50,12 @@ public String[] getFileExtensions() { @Override public List> load(String name, Resource resource) throws IOException { - List> properties = loadProperties(resource); + return load(name, resource, null); + } + + @Override + public List> load(String name, Resource resource, @Nullable Charset encoding) throws IOException { + List> properties = loadProperties(resource, encoding); if (properties.isEmpty()) { return Collections.emptyList(); } @@ -60,14 +69,19 @@ public List> load(String name, Resource resource) throws IOExc } @SuppressWarnings({ "unchecked", "rawtypes" }) - private List> loadProperties(Resource resource) throws IOException { + private List> loadProperties(Resource resource, @Nullable Charset encoding) throws IOException { String filename = resource.getFilename(); List> result = new ArrayList<>(); if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) { - result.add((Map) PropertiesLoaderUtils.loadProperties(resource)); + if (encoding == null) { + result.add((Map) PropertiesLoaderUtils.loadProperties(resource)); + } + else { + result.add((Map) PropertiesLoaderUtils.loadProperties(new EncodedResource(resource, encoding))); + } } else { - List documents = new OriginTrackedPropertiesLoader(resource).load(); + List documents = new OriginTrackedPropertiesLoader(resource).load(encoding); documents.forEach((document) -> result.add(document.asMap())); } return result; diff --git a/core/spring-boot/src/main/java/org/springframework/boot/env/PropertySourceLoader.java b/core/spring-boot/src/main/java/org/springframework/boot/env/PropertySourceLoader.java index ce8276e3ae48..76570cabc13b 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/env/PropertySourceLoader.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/env/PropertySourceLoader.java @@ -17,8 +17,11 @@ package org.springframework.boot.env; import java.io.IOException; +import java.nio.charset.Charset; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.env.PropertySource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.SpringFactoriesLoader; @@ -51,4 +54,21 @@ public interface PropertySourceLoader { */ List> load(String name, Resource resource) throws IOException; + /** + * Load the resource into one or more property sources. Implementations may either + * return a list containing a single source, or in the case of a multi-document format + * such as yaml a source for each document in the resource. + * @param name the root name of the property source. If multiple documents are loaded + * an additional suffix should be added to the name for each source loaded. + * @param resource the resource to load + * @param encoding encoding of the resource + * @return a list property sources + * @throws IOException if the source cannot be loaded + * @since 4.1.0 + */ + default List> load(String name, Resource resource, @Nullable Charset encoding) + throws IOException { + return load(name, resource); + } + } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/config/FileExtensionHintTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/config/FileExtensionHintTests.java deleted file mode 100644 index 6888d4f9b3fb..000000000000 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/config/FileExtensionHintTests.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.config; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link FileExtensionHint}. - * - * @author Phillip Webb - */ -class FileExtensionHintTests { - - @Test - void isPresentWhenHasHint() { - assertThat(FileExtensionHint.from("foo[.bar]").isPresent()).isTrue(); - } - - @Test - void isPresentWhenHasNoHint() { - assertThat(FileExtensionHint.from("foo").isPresent()).isFalse(); - assertThat(FileExtensionHint.from("foo[bar]").isPresent()).isFalse(); - assertThat(FileExtensionHint.from("foo[.b[ar]").isPresent()).isFalse(); - } - - @Test - void orElseWhenHasHint() { - assertThat(FileExtensionHint.from("foo[.bar]").orElse(".txt")).isEqualTo(".bar"); - } - - @Test - void orElseWhenHasNoHint() { - assertThat(FileExtensionHint.from("foo").orElse(".txt")).isEqualTo(".txt"); - } - - @Test - void toStringWhenHasHintReturnsDotExtension() { - assertThat(FileExtensionHint.from("foo[.bar]")).hasToString(".bar"); - } - - @Test - void toStringWhenHasNoHintReturnsEmpty() { - assertThat(FileExtensionHint.from("foo")).hasToString(""); - } - - @Test - void removeFromWhenHasHint() { - assertThat(FileExtensionHint.removeFrom("foo[.bar]")).isEqualTo("foo"); - } - - @Test - void removeFromWhenHasNoHint() { - assertThat(FileExtensionHint.removeFrom("foo[bar]")).isEqualTo("foo[bar]"); - } - -} diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/config/FileHintTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/config/FileHintTests.java new file mode 100644 index 000000000000..eadde9341b4c --- /dev/null +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/config/FileHintTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.config; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link FileHint}. + * + * @author Phillip Webb + * @author Moritz Halbritter + */ +class FileHintTests { + + @Test + void shouldParseImplicitExtension() { + String value = "foo[.bar]"; + FileHint hint = FileHint.from(value); + assertThat(hint.isPresent()).isTrue(); + assertThat(hint.getExtension()).isEqualTo(".bar"); + assertThat(FileHint.removeFrom(value)).isEqualTo("foo"); + } + + @Test + void shouldParseExplicitExtension() { + String value = "foo[extension=.bar]"; + FileHint hint = FileHint.from(value); + assertThat(hint.isPresent()).isTrue(); + assertThat(hint.getExtension()).isEqualTo(".bar"); + assertThat(FileHint.removeFrom(value)).isEqualTo("foo"); + } + + @Test + void shouldParseNestedBrackets() { + String value = "foo[encoding=[utf-8]]"; + FileHint hint = FileHint.from(value); + assertThat(hint.isPresent()).isTrue(); + assertThat(hint.getEncoding()).isEqualTo("[utf-8]"); + assertThat(FileHint.removeFrom(value)).isEqualTo("foo"); + } + + @Test + void shouldParseEncoding() { + String value = "foo[encoding=utf-8]"; + FileHint hint = FileHint.from(value); + assertThat(hint.isPresent()).isTrue(); + assertThat(hint.getEncoding()).isEqualTo("utf-8"); + assertThat(FileHint.removeFrom(value)).isEqualTo("foo"); + } + + @Test + void shouldParseAll() { + String value = "foo[extension=.bar][encoding=utf-8]"; + FileHint hint = FileHint.from(value); + assertThat(hint.isPresent()).isTrue(); + assertThat(hint.getExtension()).isEqualTo(".bar"); + assertThat(hint.getEncoding()).isEqualTo("utf-8"); + assertThat(FileHint.removeFrom(value)).isEqualTo("foo"); + } + + @Test + void isPresentWhenHasNoHint() { + assertThat(FileHint.from("foo").isPresent()).isFalse(); + assertThat(FileHint.from("foo[bar]").isPresent()).isFalse(); + assertThat(FileHint.from("foo[.b[ar]").isPresent()).isFalse(); + } + + @Test + void getExtensionOrElseWhenHasHint() { + assertThat(FileHint.from("foo[.bar]").getExtensionOrElse(".txt")).isEqualTo(".bar"); + } + + @Test + void getExtensionOrElseWhenHasNoHint() { + assertThat(FileHint.from("foo").getExtensionOrElse(".txt")).isEqualTo(".txt"); + } + + @Test + void removeFromWhenHasNoHint() { + assertThat(FileHint.removeFrom("foo")).isEqualTo("foo"); + } + +} diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLoaderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLoaderTests.java index 7561973c8cd9..a668f66eacf0 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLoaderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLoaderTests.java @@ -51,7 +51,7 @@ void loadWhenLocationResultsInMultiplePropertySourcesAddsAllToConfigData() throw ClassPathResource resource = new ClassPathResource("application.yml"); StandardConfigDataReference reference = new StandardConfigDataReference( ConfigDataLocation.of("classpath:application.yml"), null, "classpath:application", null, "yml", - new YamlPropertySourceLoader()); + new YamlPropertySourceLoader(), null); StandardConfigDataResource location = new StandardConfigDataResource(reference, resource); ConfigData configData = this.loader.load(this.loaderContext, location); assertThat(configData.getPropertySources()).hasSize(2); @@ -71,7 +71,7 @@ void loadWhenPropertySourceIsEmptyAddsNothingToConfigData() throws IOException { ClassPathResource resource = new ClassPathResource("empty.properties"); StandardConfigDataReference reference = new StandardConfigDataReference( ConfigDataLocation.of("empty.properties"), null, "empty", null, "properties", - new PropertiesPropertySourceLoader()); + new PropertiesPropertySourceLoader(), null); StandardConfigDataResource location = new StandardConfigDataResource(reference, resource); ConfigData configData = this.loader.load(this.loaderContext, location); assertThat(configData.getPropertySources()).isEmpty(); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLocationResolverTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLocationResolverTests.java index 022539d33940..5bee5b5a269f 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLocationResolverTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLocationResolverTests.java @@ -17,6 +17,8 @@ package org.springframework.boot.context.config; import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Collections; import java.util.List; @@ -205,8 +207,8 @@ void resolveWhenLocationIsRelativeAndFileResolves() { this.resourceLoader); StandardConfigDataReference parentReference = new StandardConfigDataReference( ConfigDataLocation.of("classpath:configdata/properties/application.properties"), null, - "classpath:configdata/properties/application", null, "properties", - new PropertiesPropertySourceLoader()); + "classpath:configdata/properties/application", null, "properties", new PropertiesPropertySourceLoader(), + null); ClassPathResource parentResource = new ClassPathResource("configdata/properties/application.properties"); StandardConfigDataResource parent = new StandardConfigDataResource(parentReference, parentResource); given(this.context.getParent()).willReturn(parent); @@ -226,7 +228,7 @@ void resolveWhenLocationIsRelativeAndDirectoryResolves() { this.resourceLoader); StandardConfigDataReference parentReference = new StandardConfigDataReference( ConfigDataLocation.of("optional:classpath:configdata/"), null, "classpath:config/specific", null, - "properties", new PropertiesPropertySourceLoader()); + "properties", new PropertiesPropertySourceLoader(), null); ClassPathResource parentResource = new ClassPathResource("config/specific.properties"); StandardConfigDataResource parent = new StandardConfigDataResource(parentReference, parentResource); given(this.context.getParent()).willReturn(parent); @@ -241,7 +243,7 @@ void resolveWhenLocationIsRelativeAndNoMatchingLoaderThrowsException() { ConfigDataLocation location = ConfigDataLocation.of("application.other"); StandardConfigDataReference parentReference = new StandardConfigDataReference( ConfigDataLocation.of("classpath:configdata/properties/application.properties"), null, - "configdata/properties/application", null, "properties", new PropertiesPropertySourceLoader()); + "configdata/properties/application", null, "properties", new PropertiesPropertySourceLoader(), null); ClassPathResource parentResource = new ClassPathResource("configdata/properties/application.properties"); StandardConfigDataResource parent = new StandardConfigDataResource(parentReference, parentResource); given(this.context.getParent()).willReturn(parent); @@ -263,6 +265,33 @@ void resolveWhenLocationUsesOptionalExtensionSyntaxResolves() throws Exception { assertThat(propertySource.getProperty("withnoextension")).isEqualTo("test"); } + @Test + @WithResource(name = "application-props-no-extension", content = "withnoextension=test") + void resolveWhenLocationUsesOptionalExplicitExtensionSyntaxResolves() throws Exception { + ConfigDataLocation location = ConfigDataLocation + .of("classpath:/application-props-no-extension[extension=.properties]"); + List locations = this.resolver.resolve(this.context, location); + assertThat(locations).hasSize(1); + StandardConfigDataResource resolved = locations.get(0); + assertThat(resolved.getResource().getFilename()).endsWith("application-props-no-extension"); + ConfigData loaded = new StandardConfigDataLoader().load(mock(ConfigDataLoaderContext.class), resolved); + PropertySource propertySource = loaded.getPropertySources().get(0); + assertThat(propertySource.getProperty("withnoextension")).isEqualTo("test"); + } + + @Test + @WithResource(name = "utf-8.properties", content = "smiley=😃") + void resolveWhenLocationUsesEncodingSyntaxResolves() throws IOException { + ConfigDataLocation location = ConfigDataLocation.of("classpath:utf-8.properties[encoding=utf-8]"); + List locations = this.resolver.resolve(this.context, location); + assertThat(locations).hasSize(1); + StandardConfigDataResource resolved = locations.get(0); + assertThat(resolved.getReference().getEncoding()).isEqualTo(StandardCharsets.UTF_8); + ConfigData loaded = new StandardConfigDataLoader().load(mock(ConfigDataLoaderContext.class), resolved); + PropertySource propertySource = loaded.getPropertySources().get(0); + assertThat(propertySource.getProperty("smiley")).isEqualTo("😃"); + } + @Test @WithResource(name = "application-dev.properties") void resolveProfileSpecificReturnsProfileSpecificFiles() { diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataLoaderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataLoaderTests.java index 2c7328d0b513..1229886e8434 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataLoaderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataLoaderTests.java @@ -69,7 +69,7 @@ void loadWhenNoContentThrowsException() { private SystemEnvironmentConfigDataResource createResource(String variableName) { return new SystemEnvironmentConfigDataResource(variableName, new PropertiesPropertySourceLoader(), - this.environment::get); + this.environment::get, null); } } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataResourceTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataResourceTests.java index c944c0235cd2..f9bfcacdb67c 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataResourceTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataResourceTests.java @@ -82,7 +82,7 @@ private SystemEnvironmentConfigDataResource createResource(String variableName) private SystemEnvironmentConfigDataResource createResource(String variableName, PropertySourceLoader propertySourceLoader) { - return new SystemEnvironmentConfigDataResource(variableName, propertySourceLoader, this.environment::get); + return new SystemEnvironmentConfigDataResource(variableName, propertySourceLoader, this.environment::get, null); } } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedPropertiesLoaderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedPropertiesLoaderTests.java index 3c24164eed32..bc12242ed580 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedPropertiesLoaderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedPropertiesLoaderTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.env; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Properties; @@ -42,6 +43,7 @@ * * @author Madhura Bhave * @author Phillip Webb + * @author Moritz Halbritter */ class OriginTrackedPropertiesLoaderTests { @@ -53,14 +55,14 @@ class OriginTrackedPropertiesLoaderTests { void setUp() throws Exception { String path = "test-properties.properties"; this.resource = new ClassPathResource(path, getClass()); - this.documents = new OriginTrackedPropertiesLoader(this.resource).load(); + this.documents = new OriginTrackedPropertiesLoader(this.resource).load(null); } @Test void compareToJavaProperties() throws Exception { Properties java = PropertiesLoaderUtils.loadProperties(this.resource); Properties ours = new Properties(); - new OriginTrackedPropertiesLoader(this.resource).load(false) + new OriginTrackedPropertiesLoader(this.resource).load(null, false) .get(0) .asMap() .forEach((k, v) -> ours.put(k, v.getValue())); @@ -100,10 +102,20 @@ void getUnicodeProperty() { void getMalformedUnicodeProperty() { // gh-12716 ClassPathResource resource = new ClassPathResource("malformed-unicode.properties"); - assertThatIllegalStateException().isThrownBy(() -> new OriginTrackedPropertiesLoader(resource).load()) + assertThatIllegalStateException().isThrownBy(() -> new OriginTrackedPropertiesLoader(resource).load(null)) .withMessageContaining("Malformed \\uxxxx encoding"); } + @Test + @WithResource(name = "utf-8.properties", content = "smiley=😃") + void shouldRespectEncoding() throws IOException { + ClassPathResource resource = new ClassPathResource("utf-8.properties"); + List documents = new OriginTrackedPropertiesLoader(resource).load(StandardCharsets.UTF_8); + OriginTrackedValue value = documents.get(0).asMap().get("smiley"); + assertThat(value).isNotNull(); + assertThat(value.getValue()).isEqualTo("😃"); + } + @Test void getEscapedProperty() { OriginTrackedValue value = getFromFirst("test=property"); @@ -192,56 +204,56 @@ void getImmediateMultiline() { @Test void loadWhenMultiDocumentWithPoundPrefixAndWithoutWhitespaceLoadsMultiDoc() throws IOException { String content = "a=a\n#---\nb=b"; - List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); + List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(null); assertThat(loaded).hasSize(2); } @Test void loadWhenMultiDocumentWithExclamationPrefixAndWithoutWhitespaceLoadsMultiDoc() throws IOException { String content = "a=a\n!---\nb=b"; - List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); + List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(null); assertThat(loaded).hasSize(2); } @Test void loadWhenMultiDocumentWithPoundPrefixAndLeadingWhitespaceLoadsSingleDoc() throws IOException { String content = "a=a\n \t#---\nb=b"; - List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); + List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(null); assertThat(loaded).hasSize(1); } @Test void loadWhenMultiDocumentWithExclamationPrefixAndLeadingWhitespaceLoadsSingleDoc() throws IOException { String content = "a=a\n \t!---\nb=b"; - List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); + List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(null); assertThat(loaded).hasSize(1); } @Test void loadWhenMultiDocumentWithPoundPrefixAndTrailingWhitespaceLoadsMultiDoc() throws IOException { String content = "a=a\n#--- \t \nb=b"; - List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); + List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(null); assertThat(loaded).hasSize(2); } @Test void loadWhenMultiDocumentWithExclamationPrefixAndTrailingWhitespaceLoadsMultiDoc() throws IOException { String content = "a=a\n!--- \t \nb=b"; - List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); + List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(null); assertThat(loaded).hasSize(2); } @Test void loadWhenMultiDocumentWithPoundPrefixAndTrailingCharsLoadsSingleDoc() throws IOException { String content = "a=a\n#--- \tcomment\nb=b"; - List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); + List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(null); assertThat(loaded).hasSize(1); } @Test void loadWhenMultiDocumentWithExclamationPrefixAndTrailingCharsLoadsSingleDoc() throws IOException { String content = "a=a\n!--- \tcomment\nb=b"; - List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); + List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(null); assertThat(loaded).hasSize(1); } @@ -249,7 +261,8 @@ void loadWhenMultiDocumentWithExclamationPrefixAndTrailingCharsLoadsSingleDoc() void loadWhenMultiDocumentSeparatorPrefixDifferentFromCommentPrefixLoadsMultiDoc() throws IOException { String[] contents = new String[] { "a=a\n# comment\n!---\nb=b", "a=a\n! comment\n#---\nb=b" }; for (String content : contents) { - List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); + List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())) + .load(null); assertThat(loaded).hasSize(2); } } @@ -368,7 +381,7 @@ void getPropertyWithEscapedTrailingSpace() { """) void existingCommentsAreNotTreatedAsMultiDoc() throws Exception { this.resource = new ClassPathResource("existing-non-multi-document.properties"); - this.documents = new OriginTrackedPropertiesLoader(this.resource).load(); + this.documents = new OriginTrackedPropertiesLoader(this.resource).load(null); assertThat(this.documents).hasSize(1); } diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc index 03d331e6f14b..f301c84d8d0d 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc @@ -294,7 +294,18 @@ This will trigger the import of a `dev.properties` file in current directory (if Values from the imported `dev.properties` will take precedence over the file that triggered the import. In the above example, the `dev.properties` could redefine `spring.application.name` to a different value. -An import will only be imported once no matter how many times it is declared. +NOTE: An import will only be imported once no matter how many times it is declared. + +By default, properties files are imported using the ISO-8859-1 charset. To change that, you can use the encoding attribute: + +[configprops,yaml] +---- +spring: + config: + import: "classpath:import.properties[encoding=utf-8]" +---- + +The `import.properties` file will now be read in UTF-8 encoding. @@ -394,6 +405,31 @@ spring: import: "file:/etc/config/myconfig[.yaml]" ---- +This is the shorthand for: + +[configprops,yaml] +---- +spring: + config: + import: "file:/etc/config/myconfig[extension=.yaml]" +---- + + + +[[features.external-config.files.attributes]] +=== File attributes + +The configprop:spring.config.import[] configuration property supports file attributes, as shown when specifying the xref:features/external-config.adoc#features.external-config.files.importing[encoding] or the xref:features/external-config.adoc#features.external-config.files.importing-extensionless[extension]. + +If you need to specify multiple attributes, you can use this syntax: + +[configprops,yaml] +---- +spring: + config: + import: "file:/etc/config/myconfig[extension=.yaml][encoding=utf-8]" +---- + [[features.external-config.files.env-variables]] From 177b33eccf7cd29187372ef6d4bdc03b28555f32 Mon Sep 17 00:00:00 2001 From: Beom Su Date: Thu, 5 Feb 2026 20:20:47 +0900 Subject: [PATCH 145/930] Add Kotlin DSL extension functions for TestEntityManager See gh-49063 Signed-off-by: Beom Su --- module/spring-boot-jpa-test/build.gradle | 2 + .../TestEntityManagerExtensions.kt | 50 ++++++++++ .../TestEntityManagerExtensionsTests.kt | 94 +++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 module/spring-boot-jpa-test/src/main/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensions.kt create mode 100644 module/spring-boot-jpa-test/src/test/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensionsTests.kt diff --git a/module/spring-boot-jpa-test/build.gradle b/module/spring-boot-jpa-test/build.gradle index 254e6210b91f..bd7a18eed172 100644 --- a/module/spring-boot-jpa-test/build.gradle +++ b/module/spring-boot-jpa-test/build.gradle @@ -15,6 +15,7 @@ */ plugins { + id "org.jetbrains.kotlin.jvm" id "java-library" id "org.springframework.boot.deployed" id "org.springframework.boot.optional-dependencies" @@ -29,6 +30,7 @@ dependencies { api(project(":module:spring-boot-jpa")) optional(project(":core:spring-boot-autoconfigure")) + optional("org.jetbrains.kotlin:kotlin-stdlib") testImplementation(project(":test-support:spring-boot-test-support")) diff --git a/module/spring-boot-jpa-test/src/main/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensions.kt b/module/spring-boot-jpa-test/src/main/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensions.kt new file mode 100644 index 000000000000..8eeea0d07293 --- /dev/null +++ b/module/spring-boot-jpa-test/src/main/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensions.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jpa.test.autoconfigure + +/** + * Extension for [TestEntityManager.find] providing a `find(...)` + * variant leveraging Kotlin reified type parameters. + * + * @param primaryKey the entity primary key + * @author Beom Su + * @since 4.1.0 + */ +inline fun TestEntityManager.find(primaryKey: Any): E? = + find(E::class.java, primaryKey) + +/** + * Extension for [TestEntityManager.persistAndGetId] providing a + * `persistAndGetId(...)` variant leveraging Kotlin reified type parameters. + * + * @param entity the source entity + * @author Beom Su + * @since 4.1.0 + */ +inline fun TestEntityManager.persistAndGetId(entity: Any): T? = + persistAndGetId(entity, T::class.java) + +/** + * Extension for [TestEntityManager.getId] providing a `getId(...)` + * variant leveraging Kotlin reified type parameters. + * + * @param entity the source entity + * @author Beom Su + * @since 4.1.0 + */ +inline fun TestEntityManager.getId(entity: Any): T? = + getId(entity, T::class.java) \ No newline at end of file diff --git a/module/spring-boot-jpa-test/src/test/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensionsTests.kt b/module/spring-boot-jpa-test/src/test/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensionsTests.kt new file mode 100644 index 000000000000..c592d30ace5f --- /dev/null +++ b/module/spring-boot-jpa-test/src/test/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensionsTests.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jpa.test.autoconfigure + +import jakarta.persistence.EntityManager +import jakarta.persistence.EntityManagerFactory +import jakarta.persistence.PersistenceUnitUtil +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.BDDMockito.given +import org.mockito.BDDMockito.then +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.springframework.orm.jpa.EntityManagerHolder +import org.springframework.transaction.support.TransactionSynchronizationManager + +/** + * Tests for [TestEntityManager] Kotlin extensions. + * + * @author Beom Su + * @since 4.1.0 + */ +@ExtendWith(MockitoExtension::class) +class TestEntityManagerExtensionsTests { + + @Mock + private lateinit var entityManagerFactory: EntityManagerFactory + + @Mock + private lateinit var entityManager: EntityManager + + @Mock + private lateinit var persistenceUnitUtil: PersistenceUnitUtil + + private lateinit var testEntityManager: TestEntityManager + + @BeforeEach + fun setup() { + this.testEntityManager = TestEntityManager(this.entityManagerFactory) + } + + @Test + fun `find with reified type parameter`() { + bindEntityManager() + val entity = TestEntity() + given(this.entityManager.find(TestEntity::class.java, 123)).willReturn(entity) + val result = this.testEntityManager.find(123) + assertThat(result).isSameAs(entity) + } + + @Test + fun `persistAndGetId with reified type parameter`() { + bindEntityManager() + val entity = TestEntity() + given(this.entityManagerFactory.persistenceUnitUtil).willReturn(this.persistenceUnitUtil) + given(this.persistenceUnitUtil.getIdentifier(entity)).willReturn(123) + val result = this.testEntityManager.persistAndGetId(entity) + then(this.entityManager).should().persist(entity) + assertThat(result).isEqualTo(123) + } + + @Test + fun `getId with reified type parameter`() { + val entity = TestEntity() + given(this.entityManagerFactory.persistenceUnitUtil).willReturn(this.persistenceUnitUtil) + given(this.persistenceUnitUtil.getIdentifier(entity)).willReturn(123) + val result = this.testEntityManager.getId(entity) + assertThat(result).isEqualTo(123) + } + + private fun bindEntityManager() { + val holder = EntityManagerHolder(this.entityManager) + TransactionSynchronizationManager.bindResource(this.entityManagerFactory, holder) + } + + class TestEntity + +} \ No newline at end of file From c607594a1ede4110f08ad385593675336a8a5976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 5 Feb 2026 14:50:34 +0100 Subject: [PATCH 146/930] Polish "Add Kotlin DSL extension functions for TestEntityManager" See gh-49063 --- .../jpa/test/autoconfigure/TestEntityManagerExtensions.kt | 8 ++++---- .../autoconfigure/TestEntityManagerExtensionsTests.kt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/module/spring-boot-jpa-test/src/main/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensions.kt b/module/spring-boot-jpa-test/src/main/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensions.kt index 8eeea0d07293..c061623a8e07 100644 --- a/module/spring-boot-jpa-test/src/main/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensions.kt +++ b/module/spring-boot-jpa-test/src/main/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensions.kt @@ -17,10 +17,10 @@ package org.springframework.boot.jpa.test.autoconfigure /** - * Extension for [TestEntityManager.find] providing a `find(...)` + * Extension for [TestEntityManager.find] providing a `find(...)` * variant leveraging Kotlin reified type parameters. * - * @param primaryKey the entity primary key + * @param primaryKey the primary key of the entity * @author Beom Su * @since 4.1.0 */ @@ -39,7 +39,7 @@ inline fun TestEntityManager.persistAndGetId(entity: Any): T? persistAndGetId(entity, T::class.java) /** - * Extension for [TestEntityManager.getId] providing a `getId(...)` + * Extension for [TestEntityManager.getId] providing a `getId(...)` * variant leveraging Kotlin reified type parameters. * * @param entity the source entity @@ -47,4 +47,4 @@ inline fun TestEntityManager.persistAndGetId(entity: Any): T? * @since 4.1.0 */ inline fun TestEntityManager.getId(entity: Any): T? = - getId(entity, T::class.java) \ No newline at end of file + getId(entity, T::class.java) diff --git a/module/spring-boot-jpa-test/src/test/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensionsTests.kt b/module/spring-boot-jpa-test/src/test/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensionsTests.kt index c592d30ace5f..c433b091a124 100644 --- a/module/spring-boot-jpa-test/src/test/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensionsTests.kt +++ b/module/spring-boot-jpa-test/src/test/kotlin/org/springframework/boot/jpa/test/autoconfigure/TestEntityManagerExtensionsTests.kt @@ -91,4 +91,4 @@ class TestEntityManagerExtensionsTests { class TestEntity -} \ No newline at end of file +} From 9c18f0b0136b77a2c772a69d953fe639c7321c0c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:16:43 +0000 Subject: [PATCH 147/930] Start building against Micrometer 1.17.0-M2 snapshots See gh-49111 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 040858d4bd92..66e96061095c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1537,7 +1537,7 @@ bom { releaseNotes("https://github.com/apache/maven-war-plugin/releases/tag/maven-war-plugin-{version}") } } - library("Micrometer", "1.17.0-M1") { + library("Micrometer", "1.17.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ From ec61760cbb11580e7715db13a43748aec05a2a5c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:16:47 +0000 Subject: [PATCH 148/930] Start building against Micrometer Tracing 1.7.0-M3 snapshots See gh-49112 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 66e96061095c..7b06538f9157 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1560,7 +1560,7 @@ bom { releaseNotes("https://github.com/micrometer-metrics/micrometer/releases/tag/v{version}") } } - library("Micrometer Tracing", "1.7.0-M2") { + library("Micrometer Tracing", "1.7.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { bom("micrometer-tracing-bom") From f008f9e1997ac42c8c05bdf8b94c65912495fd5d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:16:52 +0000 Subject: [PATCH 149/930] Start building against Reactor Bom 2025.0.3 snapshots See gh-49113 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7b06538f9157..bb750dc89498 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1959,7 +1959,7 @@ bom { ] } } - library("Reactor Bom", "2025.0.2") { + library("Reactor Bom", "2025.0.3-SNAPSHOT") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From eb4a57db8b3c81f02ac68b8ccf08de9ce541ee71 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:16:56 +0000 Subject: [PATCH 150/930] Start building against Spring AMQP 4.1.0-M2 snapshots See gh-49114 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index bb750dc89498..e91b6f958c9a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2378,7 +2378,7 @@ bom { ] } } - library("Spring AMQP", "4.1.0-M1") { + library("Spring AMQP", "4.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.amqp") { bom("spring-amqp-bom") From a956bc365828d9b14da4bfbd6952fdbe20ab8960 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:17:00 +0000 Subject: [PATCH 151/930] Start building against Spring Data Bom 2025.1.3 snapshots See gh-49115 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e91b6f958c9a..8b9ec510d13e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2408,7 +2408,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-batch/releases/tag/v{version}") } } - library("Spring Data Bom", "2025.1.2") { + library("Spring Data Bom", "2025.1.3-SNAPSHOT") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From 6eaca7c2a432e63cf580e6f4f3114831ab050142 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:17:05 +0000 Subject: [PATCH 152/930] Start building against Spring Framework 7.0.4 snapshots See gh-49116 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 72071ffda024..31ba4fb3f870 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ mockitoVersion=5.21.0 nativeBuildToolsVersion=0.11.4 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.5 -springFrameworkVersion=7.0.3 +springFrameworkVersion=7.0.4-SNAPSHOT springFramework60xVersion=6.0.23 tomcatVersion=11.0.15 From e1300d29c87100e83d6289f730acfb638329d36b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:17:09 +0000 Subject: [PATCH 153/930] Start building against Spring Integration 7.1.0-M2 snapshots See gh-49117 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8b9ec510d13e..49279bfb99d2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2473,7 +2473,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-hateoas/releases/tag/{version}") } } - library("Spring Integration", "7.1.0-M1") { + library("Spring Integration", "7.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.integration") { bom("spring-integration-bom") From b5a252f57c3f11f639f7875d96a28324df104179 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:17:13 +0000 Subject: [PATCH 154/930] Start building against Spring Kafka 4.1.0-M1 snapshots See gh-49118 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 49279bfb99d2..6ad19d85bffe 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2488,7 +2488,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-integration/releases/tag/v{version}") } } - library("Spring Kafka", "4.0.2") { + library("Spring Kafka", "4.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.kafka") { modules = [ From 4f25e51422298bb5fea42bfa8799acf1c8bb367d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:17:22 +0000 Subject: [PATCH 155/930] Start building against Spring Pulsar 2.0.3 snapshots See gh-49120 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6ad19d85bffe..9b1b99787d16 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2526,7 +2526,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ldap/releases/tag/{version}") } } - library("Spring Pulsar", "2.0.2") { + library("Spring Pulsar", "2.0.3-SNAPSHOT") { considerSnapshots() group("org.springframework.pulsar") { bom("spring-pulsar-bom") From 4b3d6e5bcfd4c9640504bdcca5d3f2475b31b3a3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:17:26 +0000 Subject: [PATCH 156/930] Start building against Spring Security 7.1.0-M2 snapshots See gh-49121 --- ...2RelyingPartyRegistrationConfiguration.java | 18 ++++++++++++------ ...aml2RelyingPartyAutoConfigurationTests.java | 7 ++++++- platform/spring-boot-dependencies/build.gradle | 2 +- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/module/spring-boot-security-saml2/src/main/java/org/springframework/boot/security/saml2/autoconfigure/Saml2RelyingPartyRegistrationConfiguration.java b/module/spring-boot-security-saml2/src/main/java/org/springframework/boot/security/saml2/autoconfigure/Saml2RelyingPartyRegistrationConfiguration.java index e804bc0315cb..1f29d9276eb2 100644 --- a/module/spring-boot-security-saml2/src/main/java/org/springframework/boot/security/saml2/autoconfigure/Saml2RelyingPartyRegistrationConfiguration.java +++ b/module/spring-boot-security-saml2/src/main/java/org/springframework/boot/security/saml2/autoconfigure/Saml2RelyingPartyRegistrationConfiguration.java @@ -47,6 +47,7 @@ import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.Builder; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -106,7 +107,10 @@ private RelyingPartyRegistration asRegistration(String id, Registration properti .forEach(credentials::add))); builder.singleLogoutServiceLocation(properties.getSinglelogout().getUrl()); builder.singleLogoutServiceResponseLocation(properties.getSinglelogout().getResponseUrl()); - builder.singleLogoutServiceBinding(properties.getSinglelogout().getBinding()); + @Nullable Saml2MessageBinding binding = properties.getSinglelogout().getBinding(); + if (binding != null) { + builder.singleLogoutServiceBinding(binding); + } builder.entityId(properties.getEntityId()); builder.nameIdFormat(properties.getNameIdFormat()); RelyingPartyRegistration registration = builder.build(); @@ -117,11 +121,13 @@ private RelyingPartyRegistration asRegistration(String id, Registration properti private RelyingPartyRegistration.Builder createBuilderUsingMetadata(AssertingParty properties) { String requiredEntityId = properties.getEntityId(); - Collection candidates = RelyingPartyRegistrations - .collectionFromMetadataLocation(properties.getMetadataUri()); - for (RelyingPartyRegistration.Builder candidate : candidates) { - if (requiredEntityId == null || requiredEntityId.equals(getEntityId(candidate))) { - return candidate; + String metadataUri = properties.getMetadataUri(); + if (metadataUri != null) { + Collection candidates = RelyingPartyRegistrations.collectionFromMetadataLocation(metadataUri); + for (RelyingPartyRegistration.Builder candidate : candidates) { + if (requiredEntityId == null || requiredEntityId.equals(getEntityId(candidate))) { + return candidate; + } } } throw new IllegalStateException("No relying party with Entity ID '" + requiredEntityId + "' found"); diff --git a/module/spring-boot-security-saml2/src/test/java/org/springframework/boot/security/saml2/autoconfigure/Saml2RelyingPartyAutoConfigurationTests.java b/module/spring-boot-security-saml2/src/test/java/org/springframework/boot/security/saml2/autoconfigure/Saml2RelyingPartyAutoConfigurationTests.java index 2194678c6583..8061f26a175c 100644 --- a/module/spring-boot-security-saml2/src/test/java/org/springframework/boot/security/saml2/autoconfigure/Saml2RelyingPartyAutoConfigurationTests.java +++ b/module/spring-boot-security-saml2/src/test/java/org/springframework/boot/security/saml2/autoconfigure/Saml2RelyingPartyAutoConfigurationTests.java @@ -102,7 +102,7 @@ void relyingPartyRegistrationRepositoryBeanShouldBeCreatedWhenPropertiesPresent( this.contextRunner.withPropertyValues(getPropertyValues()).run((context) -> { RelyingPartyRegistrationRepository repository = context.getBean(RelyingPartyRegistrationRepository.class); RelyingPartyRegistration registration = repository.findByRegistrationId("foo"); - + assertThat(registration).isNotNull(); assertThat(registration.getAssertingPartyMetadata().getSingleSignOnServiceLocation()) .isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php"); assertThat(registration.getAssertingPartyMetadata().getEntityId()) @@ -175,6 +175,7 @@ void autoconfigurationShouldUseBindingFromMetadataUrlIfPresent() throws Exceptio RelyingPartyRegistrationRepository repository = context .getBean(RelyingPartyRegistrationRepository.class); RelyingPartyRegistration registration = repository.findByRegistrationId("foo"); + assertThat(registration).isNotNull(); assertThat(registration.getAssertingPartyMetadata().getSingleSignOnServiceBinding()) .isEqualTo(Saml2MessageBinding.POST); }); @@ -195,6 +196,7 @@ void autoconfigurationWhenMetadataUrlAndPropertyPresentShouldUseBindingFromPrope RelyingPartyRegistrationRepository repository = context .getBean(RelyingPartyRegistrationRepository.class); RelyingPartyRegistration registration = repository.findByRegistrationId("foo"); + assertThat(registration).isNotNull(); assertThat(registration.getAssertingPartyMetadata().getSingleSignOnServiceBinding()) .isEqualTo(Saml2MessageBinding.REDIRECT); }); @@ -207,6 +209,7 @@ void autoconfigurationWhenNoMetadataUrlOrPropertyPresentShouldUseRedirectBinding this.contextRunner.withPropertyValues(getPropertyValuesWithoutSsoBinding()).run((context) -> { RelyingPartyRegistrationRepository repository = context.getBean(RelyingPartyRegistrationRepository.class); RelyingPartyRegistration registration = repository.findByRegistrationId("foo"); + assertThat(registration).isNotNull(); assertThat(registration.getAssertingPartyMetadata().getSingleSignOnServiceBinding()) .isEqualTo(Saml2MessageBinding.REDIRECT); }); @@ -292,6 +295,7 @@ void signRequestShouldApplyIfMetadataUriIsSet() throws Exception { RelyingPartyRegistrationRepository repository = context .getBean(RelyingPartyRegistrationRepository.class); RelyingPartyRegistration registration = repository.findByRegistrationId("foo"); + assertThat(registration).isNotNull(); assertThat(registration.getAssertingPartyMetadata().getWantAuthnRequestsSigned()).isTrue(); }); } @@ -364,6 +368,7 @@ private void testMultipleProviders(@Nullable String specifiedEntityId, String ex RelyingPartyRegistrationRepository repository = context .getBean(RelyingPartyRegistrationRepository.class); RelyingPartyRegistration registration = repository.findByRegistrationId("foo"); + assertThat(registration).isNotNull(); assertThat(registration.getAssertingPartyMetadata().getEntityId()).isEqualTo(expected); }); } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9b1b99787d16..9a5a09e864e7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2556,7 +2556,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-restdocs/releases/tag/v{version}") } } - library("Spring Security", "7.1.0-M1") { + library("Spring Security", "7.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { bom("spring-security-bom") From e2f53ee9ee48f2441bc21b869e47842a29eda13c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:26:46 +0000 Subject: [PATCH 157/930] Upgrade to Artemis 2.50.0 Closes gh-49123 --- documentation/spring-boot-docs/build.gradle | 2 +- module/spring-boot-artemis/build.gradle | 4 +- .../spring-boot-dependencies/build.gradle | 46 +++++++++++++++++-- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index b23c74f394ff..b4a3d44067d9 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -285,7 +285,7 @@ dependencies { javadocMacros("jakarta.json.bind:jakarta.json.bind-api") javadocMacros("jakarta.mail:jakarta.mail-api") javadocMacros("jakarta.websocket:jakarta.websocket-api") - javadocMacros("org.apache.activemq:artemis-jakarta-server") + javadocMacros("org.apache.artemis:artemis-jakarta-server") javadocMacros("org.apache.logging.log4j:log4j-core") javadocMacros("org.eclipse.jetty:jetty-client") javadocMacros("org.infinispan:infinispan-core") diff --git a/module/spring-boot-artemis/build.gradle b/module/spring-boot-artemis/build.gradle index ee5a4979a2b9..1615fa42e004 100644 --- a/module/spring-boot-artemis/build.gradle +++ b/module/spring-boot-artemis/build.gradle @@ -27,13 +27,13 @@ description = "Spring Boot Artemis" dependencies { api(project(":module:spring-boot-jms")) - api("org.apache.activemq:artemis-jakarta-client") + api("org.apache.artemis:artemis-jakarta-client") optional(project(":core:spring-boot-autoconfigure")) optional(project(":core:spring-boot-docker-compose")) optional(project(":core:spring-boot-testcontainers")) optional(project(":module:spring-boot-transaction")) - optional("org.apache.activemq:artemis-jakarta-server") + optional("org.apache.artemis:artemis-jakarta-server") optional("org.messaginghub:pooled-jms") { exclude group: "org.apache.geronimo.specs", module: "geronimo-jms_2.0_spec" } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9a5a09e864e7..088d4963b41e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -65,9 +65,49 @@ bom { releaseNotes("https://github.com/eclipse-ee4j/angus-mail/releases/tag/{version}") } } - library("Artemis", "2.44.0") { - group("org.apache.activemq") { - bom("artemis-bom") + library("Artemis", "2.50.0") { + group("org.apache.artemis") { + bom("artemis-bom") { + permit("org.apache.activemq:artemis-amqp-protocol") + permit("org.apache.activemq:artemis-boot") + permit("org.apache.activemq:artemis-cdi-client") + permit("org.apache.activemq:artemis-cli") + permit("org.apache.activemq:artemis-commons") + permit("org.apache.activemq:artemis-console") + permit("org.apache.activemq:artemis-core-client-all") + permit("org.apache.activemq:artemis-core-client-osgi") + permit("org.apache.activemq:artemis-core-client") + permit("org.apache.activemq:artemis-dto") + permit("org.apache.activemq:artemis-features:2.50.0:f") + permit("org.apache.activemq:artemis-hornetq-protocol") + permit("org.apache.activemq:artemis-hqclient-protocol") + permit("org.apache.activemq:artemis-jakarta-cdi-client") + permit("org.apache.activemq:artemis-jakarta-client-all") + permit("org.apache.activemq:artemis-jakarta-client-osgi") + permit("org.apache.activemq:artemis-jakarta-client") + permit("org.apache.activemq:artemis-jakarta-openwire-protocol") + permit("org.apache.activemq:artemis-jakarta-ra") + permit("org.apache.activemq:artemis-jakarta-server") + permit("org.apache.activemq:artemis-jakarta-service-extensions") + permit("org.apache.activemq:artemis-jdbc-store") + permit("org.apache.activemq:artemis-jms-client-all") + permit("org.apache.activemq:artemis-jms-client-osgi") + permit("org.apache.activemq:artemis-jms-client") + permit("org.apache.activemq:artemis-jms-server") + permit("org.apache.activemq:artemis-journal") + permit("org.apache.activemq:artemis-lockmanager-api") + permit("org.apache.activemq:artemis-lockmanager-ri") + permit("org.apache.activemq:artemis-mqtt-protocol") + permit("org.apache.activemq:artemis-openwire-protocol") + permit("org.apache.activemq:artemis-ra") + permit("org.apache.activemq:artemis-selector") + permit("org.apache.activemq:artemis-server-osgi") + permit("org.apache.activemq:artemis-server") + permit("org.apache.activemq:artemis-service-extensions") + permit("org.apache.activemq:artemis-stomp-protocol") + permit("org.apache.activemq:artemis-web") + permit("org.apache.activemq:artemis-website") + } } links { site("https://activemq.apache.org/components/artemis") From c1e93e10f4558db58eb10e091775807366e1024f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:26:50 +0000 Subject: [PATCH 158/930] Upgrade to AssertJ 3.27.7 Closes gh-49124 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 31ba4fb3f870..c55f5023c7eb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.caching=true org.gradle.parallel=true org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8 -assertjVersion=3.27.6 +assertjVersion=3.27.7 checkstyleToolVersion=10.12.4 commonsCodecVersion=1.20.0 graalVersion=25 From 19c39b8cbd0166fef161dabe69340496a592156f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:26:55 +0000 Subject: [PATCH 159/930] Upgrade to Commons Codec 1.21.0 Closes gh-49125 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c55f5023c7eb..6b4f24384410 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8 assertjVersion=3.27.7 checkstyleToolVersion=10.12.4 -commonsCodecVersion=1.20.0 +commonsCodecVersion=1.21.0 graalVersion=25 hamcrestVersion=3.0 jackson2Version=2.21.0 From a7794196bab0afdce51a21b76bbc143849050717 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:26:59 +0000 Subject: [PATCH 160/930] Upgrade to Elasticsearch Client 9.2.4 Closes gh-49126 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 088d4963b41e..3c646b386fe7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -401,7 +401,7 @@ bom { releaseNotes("https://github.com/ehcache/ehcache3/releases/tag/v{version}") } } - library("Elasticsearch Client", "9.2.3") { + library("Elasticsearch Client", "9.2.4") { prohibit { contains "-alpha" contains "-beta" From 643af91afa544ba2fdc8228e0a806a0374a6272d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:27:04 +0000 Subject: [PATCH 161/930] Upgrade to Flyway 11.20.3 Closes gh-49127 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3c646b386fe7..d36cc6b0fe5e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -425,7 +425,7 @@ bom { javadoc("elasticsearch-java", version -> "https://javadoc.io/doc/co.elastic.clients/elasticsearch-java/%s/index.html".formatted(version), "co.elastic.clients.elasticsearch", "co.elastic.clients.transport") } } - library("Flyway", "11.20.2") { + library("Flyway", "11.20.3") { group("org.flywaydb") { modules = [ "flyway-commandline", From f2aeabbc5d5ecffaa1696bc665dbf1ec19c2a9b5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:27:08 +0000 Subject: [PATCH 162/930] Upgrade to Groovy 5.0.4 Closes gh-49128 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d36cc6b0fe5e..604e77abbd25 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -544,7 +544,7 @@ bom { releaseNotes("https://github.com/graphql-java/graphql-java/releases/tag/v{version}") } } - library("Groovy", "5.0.3") { + library("Groovy", "5.0.4") { prohibit { contains "-alpha-" because "we don't want alpha dependencies" From b33dffaa0b271334ccb4b4863cc2e0b1513d13f4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:27:12 +0000 Subject: [PATCH 163/930] Upgrade to Hibernate 7.2.3.Final Closes gh-49129 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 604e77abbd25..2978a12282c8 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -609,7 +609,7 @@ bom { releaseNotes("https://github.com/hazelcast/hazelcast/releases/tag/v{version}") } } - library("Hibernate", "7.2.1.Final") { + library("Hibernate", "7.2.3.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From 2e7cc8bcc3a5cbb8543e63ecedb328b69d3c25a1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:27:17 +0000 Subject: [PATCH 164/930] Upgrade to Jaybird 6.0.4 Closes gh-49130 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 2978a12282c8..d8209a53cbb7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1013,7 +1013,7 @@ bom { releaseNotes("https://github.com/jaxen-xpath/jaxen/releases/tag/v{version}") } } - library("Jaybird", "6.0.3") { + library("Jaybird", "6.0.4") { prohibit { endsWith ".java8" because "we use the .java11 version" From 7faa94ef2cade00e696ab50b30fdd9df4eb3cfcd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:27:21 +0000 Subject: [PATCH 165/930] Upgrade to JBoss Logging 3.6.2.Final Closes gh-49131 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d8209a53cbb7..9ffe1fb6d7b1 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1028,7 +1028,7 @@ bom { .formatted(version.toString().replace(".java11", ""))) } } - library("JBoss Logging", "3.6.1.Final") { + library("JBoss Logging", "3.6.2.Final") { group("org.jboss.logging") { modules = [ "jboss-logging" From 4a2d2438d6a3f246b396100b03b4eec477d29d08 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:27:25 +0000 Subject: [PATCH 166/930] Upgrade to Jersey 4.0.2 Closes gh-49132 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9ffe1fb6d7b1..e3324f850d54 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1066,7 +1066,7 @@ bom { releaseNotes("https://github.com/redis/jedis/releases/tag/v{version}") } } - library("Jersey", "4.0.0") { + library("Jersey", "4.0.2") { group("org.glassfish.jersey") { bom("jersey-bom") } From 809578cd008d55fbc6cefefa05efadd96f3230f3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:27:30 +0000 Subject: [PATCH 167/930] Upgrade to Jetty 12.1.6 Closes gh-49133 --- ...rtualThreadsWebServerFactoryCustomizer.java | 18 +++++++++++------- ...ThreadsWebServerFactoryCustomizerTests.java | 2 +- platform/spring-boot-dependencies/build.gradle | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/autoconfigure/JettyVirtualThreadsWebServerFactoryCustomizer.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/autoconfigure/JettyVirtualThreadsWebServerFactoryCustomizer.java index 2bf589296a52..820f1d96dc06 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/autoconfigure/JettyVirtualThreadsWebServerFactoryCustomizer.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/autoconfigure/JettyVirtualThreadsWebServerFactoryCustomizer.java @@ -21,7 +21,6 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.jetty.ConfigurableJettyWebServerFactory; -import org.springframework.boot.jetty.autoconfigure.JettyServerProperties.Threads; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.core.Ordered; import org.springframework.util.Assert; @@ -62,16 +61,21 @@ public JettyVirtualThreadsWebServerFactoryCustomizer(JettyServerProperties serve @Override public void customize(ConfigurableJettyWebServerFactory factory) { Assert.state(VirtualThreads.areSupported(), "Virtual threads are not supported"); - VirtualThreadPool virtualThreadPool = new VirtualThreadPool(); + Integer maxTasks = getMaxTasks(); + VirtualThreadPool virtualThreadPool = (maxTasks != null) ? new VirtualThreadPool(maxTasks) + : new VirtualThreadPool(); virtualThreadPool.setName("jetty-"); - if (this.serverProperties != null) { - Threads properties = this.serverProperties.getThreads(); - int maxThreadCount = (properties.getMax() > 0) ? properties.getMax() : 200; - virtualThreadPool.setMaxThreads(maxThreadCount); - } factory.setThreadPool(virtualThreadPool); } + private @Nullable Integer getMaxTasks() { + if (this.serverProperties == null) { + return null; + } + Integer maxThreads = this.serverProperties.getThreads().getMax(); + return (maxThreads > 0) ? maxThreads : null; + } + @Override public int getOrder() { return JettyWebServerFactoryCustomizer.ORDER + 1; diff --git a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/JettyVirtualThreadsWebServerFactoryCustomizerTests.java b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/JettyVirtualThreadsWebServerFactoryCustomizerTests.java index c5d31db9b4ee..06e01be448bf 100644 --- a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/JettyVirtualThreadsWebServerFactoryCustomizerTests.java +++ b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/JettyVirtualThreadsWebServerFactoryCustomizerTests.java @@ -48,7 +48,7 @@ void shouldConfigureVirtualThreads() { assertThat(threadPool).isInstanceOf(VirtualThreadPool.class); VirtualThreadPool virtualThreadPool = (VirtualThreadPool) threadPool; assertThat(virtualThreadPool.getName()).isEqualTo("jetty-"); - assertThat(virtualThreadPool.getMaxThreads()).isEqualTo(100); + assertThat(virtualThreadPool.getMaxConcurrentTasks()).isEqualTo(100); })); } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e3324f850d54..b8e882525385 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1083,7 +1083,7 @@ bom { ] } } - library("Jetty", "12.1.5") { + library("Jetty", "12.1.6") { prohibit { contains ".alpha" because "we don't want alpha dependencies" From edd1440fea84ad704647583d4b80c42511d09f75 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:27:34 +0000 Subject: [PATCH 168/930] Upgrade to jOOQ 3.20.11 Closes gh-49134 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b8e882525385..ffcf374c4d06 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1107,7 +1107,7 @@ bom { ] } } - library("jOOQ", "3.20.10") { + library("jOOQ", "3.20.11") { group("org.jooq") { bom("jooq-bom") plugins = [ From 4d624691a108f7d684fa9418e2fc6517d6c0275d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:27:39 +0000 Subject: [PATCH 169/930] Upgrade to Kotlin 2.3.10 Closes gh-49135 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6b4f24384410..46d588c056f2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ jackson2Version=2.21.0 jacksonVersion=3.0.4 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.2 -kotlinVersion=2.3.0 +kotlinVersion=2.3.10 mavenVersion=3.9.10 mockitoVersion=5.21.0 nativeBuildToolsVersion=0.11.4 From 0d5693bbe23497b85249241951479c44cb5dd856 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:27:43 +0000 Subject: [PATCH 170/930] Upgrade to Logback 1.5.27 Closes gh-49136 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ffcf374c4d06..061f1db9a5f3 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1337,7 +1337,7 @@ bom { releaseNotes("https://github.com/apache/logging-log4j2/releases/tag/rel%2F{version}") } } - library("Logback", "1.5.25") { + library("Logback", "1.5.27") { group("ch.qos.logback") { modules = [ "logback-classic", From f8a4730d08417a5e9f37cc26118f482636840ecf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:27:47 +0000 Subject: [PATCH 171/930] Upgrade to Maven Compiler Plugin 3.15.0 Closes gh-49137 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 061f1db9a5f3..4ff786f6d0dc 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1406,7 +1406,7 @@ bom { releaseNotes("https://github.com/apache/maven-clean-plugin/releases/tag/maven-clean-plugin-{version}") } } - library("Maven Compiler Plugin", "3.14.1") { + library("Maven Compiler Plugin", "3.15.0") { prohibit { contains "-beta-" because "we don't want betas" From 47cfa5de13f633a6225f38c9260b2dfe1ef09264 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:27:52 +0000 Subject: [PATCH 172/930] Upgrade to MongoDB 5.6.3 Closes gh-49138 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4ff786f6d0dc..afc88cb73d36 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1622,7 +1622,7 @@ bom { releaseNotes("https://github.com/mockito/mockito/releases/tag/v{version}") } } - library("MongoDB", "5.6.2") { + library("MongoDB", "5.6.3") { alignWith { version { of "org.mongodb:mongodb-driver-core" From 9ddfc334c451b8f0b27914e66035c7a3e8aa6c18 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:27:56 +0000 Subject: [PATCH 173/930] Upgrade to MySQL 9.6.0 Closes gh-49139 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index afc88cb73d36..11223d574b67 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1661,7 +1661,7 @@ bom { .formatted(version.toString().replace(".jre11", ""))) } } - library("MySQL", "9.5.0") { + library("MySQL", "9.6.0") { upgradePolicy = "same-major-version" group("com.mysql") { modules = [ From 3685c0170700248869612b5ded5bd9a55a833a94 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:28:00 +0000 Subject: [PATCH 174/930] Upgrade to Netty 4.2.10.Final Closes gh-49140 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 11223d574b67..13b7aa888445 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1716,7 +1716,7 @@ bom { releaseNotes("https://github.com/neo4j/neo4j-java-driver/releases/tag/{version}") } } - library("Netty", "4.2.9.Final") { + library("Netty", "4.2.10.Final") { prohibit { contains ".Alpha" contains ".Beta" From 00748fca8c4f5ef949bd051e32755a40d71f8c41 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:28:05 +0000 Subject: [PATCH 175/930] Upgrade to OpenTelemetry 1.58.0 Closes gh-49141 --- .../OpenTelemetryTracingAutoConfigurationTests.java | 11 +++++++---- platform/spring-boot-dependencies/build.gradle | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java index 642d9711a7b8..ad22957b9e13 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java @@ -24,6 +24,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; import io.micrometer.tracing.SpanCustomizer; import io.micrometer.tracing.Tracer.SpanInScope; @@ -58,7 +59,6 @@ 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.micrometer.observation.autoconfigure.ObservationAutoConfiguration; @@ -76,7 +76,6 @@ 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; /** @@ -308,11 +307,15 @@ void shouldCustomizeSdkTracerProvider() { } @Test + @SuppressWarnings("rawtypes") 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()); + SpanProcessor spanProcessor = context.getBean(SpanProcessor.class); + assertThat(spanProcessor).extracting("worker.spanProcessorInstrumentation.meterProvider") + .asInstanceOf(InstanceOfAssertFactories.type(Supplier.class)) + .extracting(Supplier::get) + .isEqualTo(meterProvider); }); } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 13b7aa888445..ccb9e1ef2fa0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1731,7 +1731,7 @@ bom { javadoc(version -> "https://netty.io/%s.%s/api".formatted(version.major(), version.minor()), "io.netty") } } - library("OpenTelemetry", "1.57.0") { + library("OpenTelemetry", "1.58.0") { group("io.opentelemetry") { bom("opentelemetry-bom") } From d169fcd19ccc3facdd6304b541dd2e4018274c15 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:28:09 +0000 Subject: [PATCH 176/930] Upgrade to Oracle Database 23.26.1.0.0 Closes gh-49142 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ccb9e1ef2fa0..38aca9e0f6e2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1754,7 +1754,7 @@ bom { releaseNotes("https://github.com/open-telemetry/opentelemetry-java/releases/tag/v{version}") } } - library("Oracle Database", "23.26.0.0.0") { + library("Oracle Database", "23.26.1.0.0") { alignWith { dependencyManagementDeclaredIn("com.oracle.database.jdbc:ojdbc-bom") } From c9facf1b97be2684faed899abbdd22796681d982 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:28:14 +0000 Subject: [PATCH 177/930] Upgrade to Tomcat 11.0.18 Tomcat 9.0.115, 10.1.52, and 11.0.18 include a breaking change [1] to how ciphers are configured when using HTTPS. Previously, a single setting was used but this has now been split in two; the existing ciphers setting for TLSv1.2 ciphers and a new ciperSuites setting for TLSv1.3. As part of this split, the behavior of the ciphers setting has been changed such that any TLSv1.3 ciphers are ignored and a warning is logged. This change in Tomcat is problematic without also making some changes in Boot. If we had done nothing, a user that had configured only TLSv1.3 cipers would have them all ignored, leaving their SSL connection unexpectedly using all of the default ciphers which may be less secure. This commit adapts to the breaking change in Tomcat by taking the user's list of ciphers and splitting into into TLSv1.2 and TLSv1.3 ciphers before passing them into Tomcat's two settings (ciphers and cipherSuites respectively). This is done defensively for backwards compatibility. If the methods to identify and configure the TLSv1.3 ciphers are not present, we assume that we're running with an earlier version of Tomcat and fall back to passing them all into the ciphers setting as we did previously. Closes gh-49143 [1] https://github.com/apache/tomcat/commit/9abf6bddb2e84ecf1668780bb3150b799f832ccf --- gradle.properties | 2 +- .../boot/tomcat/SslConnectorCustomizer.java | 64 +++++++++++++++++-- .../tomcat/SslConnectorCustomizerTests.java | 39 ++++++++++- 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/gradle.properties b/gradle.properties index 46d588c056f2..795a568cc621 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,6 +23,6 @@ nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.5 springFrameworkVersion=7.0.4-SNAPSHOT springFramework60xVersion=6.0.23 -tomcatVersion=11.0.15 +tomcatVersion=11.0.18 kotlin.stdlib.default.dependency=false diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/SslConnectorCustomizer.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/SslConnectorCustomizer.java index 9b5afd1e6ee3..590d3924c602 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/SslConnectorCustomizer.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/SslConnectorCustomizer.java @@ -16,6 +16,8 @@ package org.springframework.boot.tomcat; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import org.apache.catalina.connector.Connector; @@ -25,6 +27,7 @@ import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.net.SSLHostConfigCertificate; import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; +import org.apache.tomcat.util.net.openssl.ciphers.OpenSSLCipherConfigurationParser; import org.jspecify.annotations.Nullable; import org.springframework.boot.ssl.SslBundle; @@ -113,14 +116,24 @@ private void applySslBundle(AbstractHttp11Protocol protocol, SSLHostConfig ss certificate.setCertificateKeyAlias(key.getAlias()); } sslHostConfig.addCertificate(certificate); - if (options.getCiphers() != null) { - String ciphers = StringUtils.arrayToCommaDelimitedString(options.getCiphers()); - sslHostConfig.setCiphers(ciphers); - } + configureCiphers(options, sslHostConfig); configureSslStores(sslHostConfig, certificate, stores); configureEnabledProtocols(sslHostConfig, options); } + private void configureCiphers(SslOptions options, SSLHostConfig sslHostConfig) { + CipherConfiguration cipherConfiguration = CipherConfiguration.from(options); + if (cipherConfiguration != null) { + sslHostConfig.setCiphers(cipherConfiguration.tls12Ciphers); + try { + sslHostConfig.setCipherSuites(cipherConfiguration.tls13Ciphers); + } + catch (Exception ex) { + // Tomcat version without setCipherSuites method. Continue. + } + } + } + private void configureEnabledProtocols(SSLHostConfig sslHostConfig, SslOptions options) { if (options.getEnabledProtocols() != null) { String enabledProtocols = StringUtils.arrayToDelimitedString(options.getEnabledProtocols(), "+"); @@ -147,4 +160,47 @@ private void configureSslStores(SSLHostConfig sslHostConfig, SSLHostConfigCertif } } + private static class CipherConfiguration { + + private final String tls12Ciphers; + + private final String tls13Ciphers; + + CipherConfiguration(String tls12Ciphers, String tls13Ciphers) { + this.tls12Ciphers = tls12Ciphers; + this.tls13Ciphers = tls13Ciphers; + } + + static @Nullable CipherConfiguration from(SslOptions options) { + List tls12Ciphers = new ArrayList<>(); + List tls13Ciphers = new ArrayList<>(); + String[] ciphers = options.getCiphers(); + if (ciphers == null || ciphers.length == 0) { + return null; + } + for (String cipher : ciphers) { + if (isTls13(cipher)) { + tls13Ciphers.add(cipher); + } + else { + tls12Ciphers.add(cipher); + } + } + return new CipherConfiguration(StringUtils.collectionToCommaDelimitedString(tls12Ciphers), + StringUtils.collectionToCommaDelimitedString(tls13Ciphers)); + } + + private static boolean isTls13(String cipher) { + try { + return OpenSSLCipherConfigurationParser.isTls13Cipher(cipher); + } + catch (Exception ex) { + // Tomcat version without isTls13Cipher method. Continue, treating all + // ciphers as TLSv1.2 + return false; + } + } + + } + } diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/SslConnectorCustomizerTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/SslConnectorCustomizerTests.java index 69b49538ce0f..af81da187288 100644 --- a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/SslConnectorCustomizerTests.java +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/SslConnectorCustomizerTests.java @@ -23,6 +23,7 @@ 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.openssl.ciphers.Cipher; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -73,7 +74,7 @@ void stop() throws Exception { @Test @WithPackageResources("test.jks") - void sslCiphersConfiguration() throws Exception { + void tls12CiphersConfiguration() throws Exception { Ssl ssl = new Ssl(); ssl.setKeyStore("classpath:test.jks"); ssl.setKeyStorePassword("secret"); @@ -84,6 +85,42 @@ void sslCiphersConfiguration() throws Exception { this.tomcat.start(); SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs(); assertThat(sslHostConfigs[0].getCiphers()).isEqualTo("ALPHA:BRAVO:CHARLIE"); + assertThat(sslHostConfigs[0].getCipherSuites()).isEmpty(); + } + + @Test + @WithPackageResources("test.jks") + void tls13CiphersConfiguration() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStore("classpath:test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setCiphers(new String[] { Cipher.TLS_AES_128_CCM_SHA256.getOpenSSLAlias(), + Cipher.TLS_AES_256_GCM_SHA384.getOpenSSLAlias() }); + Connector connector = this.tomcat.getConnector(); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); + customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap()); + this.tomcat.start(); + SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs(); + assertThat(sslHostConfigs[0].getCiphers()).isEmpty(); + assertThat(sslHostConfigs[0].getCipherSuites()).isEqualTo("TLS_AES_128_CCM_SHA256:TLS_AES_256_GCM_SHA384"); + } + + @Test + @WithPackageResources("test.jks") + void mixedTls12AndTls13CiphersConfiguration() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStore("classpath:test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setCiphers(new String[] { Cipher.TLS_AES_128_CCM_SHA256.getOpenSSLAlias(), + Cipher.TLS_DH_DSS_WITH_AES_128_CBC_SHA256.getOpenSSLAlias(), + Cipher.TLS_AES_256_GCM_SHA384.getOpenSSLAlias() }); + Connector connector = this.tomcat.getConnector(); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); + customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap()); + this.tomcat.start(); + SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs(); + assertThat(sslHostConfigs[0].getCiphers()).isEqualTo("DH-DSS-AES128-SHA256"); + assertThat(sslHostConfigs[0].getCipherSuites()).isEqualTo("TLS_AES_128_CCM_SHA256:TLS_AES_256_GCM_SHA384"); } @Test From 630643777c8819e5e929c46bb3518636a49f54db Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 6 Feb 2026 08:19:20 +0000 Subject: [PATCH 178/930] Start building against Spring Session 4.1.0-M1 snapshots See gh-49145 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 38aca9e0f6e2..0d48330627ff 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2611,7 +2611,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-security/releases/tag/{version}") } } - library("Spring Session", "4.0.1") { + library("Spring Session", "4.1.0-SNAPSHOT") { considerSnapshots() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) From e1a1d0e233926c8612d073d43bbcf96fdd77b17e Mon Sep 17 00:00:00 2001 From: Garvit Joshi Date: Mon, 26 Jan 2026 15:22:20 +0530 Subject: [PATCH 179/930] Show certificates from truststore in SSL info endpoint See gh-48967 Signed-off-by: Garvit Joshi --- .../springframework/boot/info/SslInfo.java | 19 ++++- .../boot/info/SslInfoTests.java | 69 +++++++++++++++++- .../springframework/boot/info/keystore.jks | Bin 0 -> 2604 bytes .../springframework/boot/info/truststore.jks | Bin 0 -> 1837 bytes .../info/InfoEndpointDocumentationTests.java | 38 +++++++++- 5 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 core/spring-boot/src/test/resources/org/springframework/boot/info/keystore.jks create mode 100644 core/spring-boot/src/test/resources/org/springframework/boot/info/truststore.jks diff --git a/core/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java b/core/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java index 46da37d4e64b..4c848b0d82e1 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java @@ -104,9 +104,12 @@ public final class BundleInfo { private final List certificateChains; + private final List trustStoreCertificates; + private BundleInfo(String name, SslBundle sslBundle) { this.name = name; this.certificateChains = extractCertificateChains(sslBundle.getStores().getKeyStore()); + this.trustStoreCertificates = extractCertificateChains(sslBundle.getStores().getTrustStore()); } private List extractCertificateChains(@Nullable KeyStore keyStore) { @@ -132,6 +135,10 @@ public List getCertificateChains() { return this.certificateChains; } + public List getTrustStoreCertificates() { + return this.trustStoreCertificates; + } + } /** @@ -150,9 +157,17 @@ public final class CertificateChainInfo { private List extractCertificates(KeyStore keyStore, String alias) { try { + // First try to get the certificate chain (works for PrivateKeyEntry) Certificate[] certificates = keyStore.getCertificateChain(alias); - return (!ObjectUtils.isEmpty(certificates)) - ? Arrays.stream(certificates).map(CertificateInfo::new).toList() : Collections.emptyList(); + if (!ObjectUtils.isEmpty(certificates)) { + return Arrays.stream(certificates).map(CertificateInfo::new).toList(); + } + // Fall back to single certificate (works for trustedCertEntry) + Certificate certificate = keyStore.getCertificate(alias); + if (certificate != null) { + return List.of(new CertificateInfo(certificate)); + } + return Collections.emptyList(); } catch (KeyStoreException ex) { return Collections.emptyList(); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/info/SslInfoTests.java b/core/spring-boot/src/test/java/org/springframework/boot/info/SslInfoTests.java index d36f1f2ce27f..4b5a35295b2a 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/info/SslInfoTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/info/SslInfoTests.java @@ -60,9 +60,9 @@ void validCertificatesShouldProvideSslInfo() { assertThat(bundle.getCertificateChains().get(1).getAlias()).isEqualTo("test-alias"); assertThat(bundle.getCertificateChains().get(1).getCertificates()).hasSize(1); assertThat(bundle.getCertificateChains().get(2).getAlias()).isEqualTo("spring-boot-cert"); - assertThat(bundle.getCertificateChains().get(2).getCertificates()).isEmpty(); + assertThat(bundle.getCertificateChains().get(2).getCertificates()).hasSize(1); assertThat(bundle.getCertificateChains().get(3).getAlias()).isEqualTo("test-alias-cert"); - assertThat(bundle.getCertificateChains().get(3).getCertificates()).isEmpty(); + assertThat(bundle.getCertificateChains().get(3).getCertificates()).hasSize(1); CertificateInfo cert1 = bundle.getCertificateChains().get(0).getCertificates().get(0); assertThat(cert1.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US"); assertThat(cert1.getIssuer()).isEqualTo(cert1.getSubject()); @@ -85,6 +85,7 @@ void validCertificatesShouldProvideSslInfo() { assertThat(cert2.getValidity()).isNotNull(); assertThat(cert2.getValidity().getStatus()).isSameAs(Status.VALID); assertThat(cert2.getValidity().getMessage()).isNull(); + assertThat(bundle.getTrustStoreCertificates()).isEmpty(); } @Test @@ -149,7 +150,7 @@ void multipleBundlesShouldProvideSslInfo() { .flatMap((bundle) -> bundle.getCertificateChains().stream()) .flatMap((certificateChain) -> certificateChain.getCertificates().stream()) .toList(); - assertThat(certs).hasSize(5); + assertThat(certs).hasSize(7); assertThat(certs).allSatisfy((cert) -> { assertThat(cert.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US"); assertThat(cert.getIssuer()).isEqualTo(cert.getSubject()); @@ -188,6 +189,68 @@ void nullKeyStore() { SslInfo sslInfo = new SslInfo(sslBundleRegistry, CLOCK); assertThat(sslInfo.getBundles()).hasSize(1); assertThat(sslInfo.getBundles().get(0).getCertificateChains()).isEmpty(); + assertThat(sslInfo.getBundles().get(0).getTrustStoreCertificates()).isEmpty(); + } + + @Test + @WithPackageResources("test.p12") + void trustStoreCertificatesShouldProvideSslInfo() { + DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); + JksSslStoreDetails trustStoreDetails = JksSslStoreDetails.forLocation("classpath:test.p12") + .withPassword("secret"); + SslStoreBundle sslStoreBundle = new JksSslStoreBundle(null, trustStoreDetails); + sslBundleRegistry.registerBundle("test-trust", SslBundle.of(sslStoreBundle)); + SslInfo sslInfo = new SslInfo(sslBundleRegistry, CLOCK); + assertThat(sslInfo.getBundles()).hasSize(1); + BundleInfo bundle = sslInfo.getBundles().get(0); + assertThat(bundle.getName()).isEqualTo("test-trust"); + assertThat(bundle.getCertificateChains()).isEmpty(); + assertThat(bundle.getTrustStoreCertificates()).hasSize(4); + assertThat(bundle.getTrustStoreCertificates().get(0).getAlias()).isEqualTo("spring-boot"); + assertThat(bundle.getTrustStoreCertificates().get(1).getAlias()).isEqualTo("test-alias"); + } + + @Test + @WithPackageResources("test.p12") + void bothKeyStoreAndTrustStoreCertificatesShouldProvideSslInfo() { + DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); + JksSslStoreDetails storeDetails = JksSslStoreDetails.forLocation("classpath:test.p12").withPassword("secret"); + SslStoreBundle sslStoreBundle = new JksSslStoreBundle(storeDetails, storeDetails); + sslBundleRegistry.registerBundle("test-both", SslBundle.of(sslStoreBundle)); + SslInfo sslInfo = new SslInfo(sslBundleRegistry, CLOCK); + assertThat(sslInfo.getBundles()).hasSize(1); + BundleInfo bundle = sslInfo.getBundles().get(0); + assertThat(bundle.getName()).isEqualTo("test-both"); + assertThat(bundle.getCertificateChains()).hasSize(4); + assertThat(bundle.getTrustStoreCertificates()).hasSize(4); + } + + @Test + @WithPackageResources({ "keystore.jks", "truststore.jks" }) + void separateKeyStoreAndTrustStoreShouldProvideSslInfo() { + DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); + JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation("classpath:keystore.jks") + .withPassword("secret"); + JksSslStoreDetails trustStoreDetails = JksSslStoreDetails.forLocation("classpath:truststore.jks") + .withPassword("secret"); + SslStoreBundle sslStoreBundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails); + sslBundleRegistry.registerBundle("test-separate", SslBundle.of(sslStoreBundle)); + SslInfo sslInfo = new SslInfo(sslBundleRegistry, CLOCK); + assertThat(sslInfo.getBundles()).hasSize(1); + BundleInfo bundle = sslInfo.getBundles().get(0); + assertThat(bundle.getName()).isEqualTo("test-separate"); + // Keystore has 2 PrivateKeyEntry entries + assertThat(bundle.getCertificateChains()).hasSize(2); + assertThat(bundle.getCertificateChains()).allSatisfy((chain) -> { + assertThat(chain.getCertificates()).hasSize(1); + assertThat(chain.getCertificates().get(0).getSubject()).startsWith("CN=localhost"); + }); + // Truststore has 3 trustedCertEntry entries + assertThat(bundle.getTrustStoreCertificates()).hasSize(3); + assertThat(bundle.getTrustStoreCertificates()).allSatisfy((chain) -> { + assertThat(chain.getCertificates()).hasSize(1); + assertThat(chain.getCertificates().get(0).getSubject()).startsWith("CN=localhost"); + }); } private SslInfo createSslInfo(String... locations) { diff --git a/core/spring-boot/src/test/resources/org/springframework/boot/info/keystore.jks b/core/spring-boot/src/test/resources/org/springframework/boot/info/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..4e3e26f8195a6f7c40a7eaa0564fa136a1c6d7ee GIT binary patch literal 2604 zcmZ{lXH?VK7RB=q)dVE;A|RslV(6iS-lex7OsE3Vm0qMI2#AELA`wtPihxvUQY_Tq zNOvNLfM8G{GW23-!}HC0kN4J^5BI~qYn^-U+V}qUIa)bd0f9i^zZU?4_yxMV`dtqU z4v__cfP88$$0QJl1_Z}aAHW%CAriCzH6RXV1^{X-^)l(R(;MxghKdow-eB+-HaeTR z06ItVVhqv5<^Iq`qZeqYOUWQ+H+{kDs%4J`#=hCtUVe2U%-3jI^-Ii;s7A}BVzZ|W zSI^AxshJC8pMOlG&_@S1t7CIOnzD?2jP^141<2W8q3EP_&br=u<=&TR+w-ZR$LYBJ zAA;wJf7;_w_wQ*2S2>DG=E?8}ad2qNq@>bafN_16r8LED-96sfI~oh=%FP(xhiu>Y zUg0hgBKjO^ur%vA&*{~v47!%o-PAr6d01UOLg|NHRzrd2c^<3&Rh!*5QweH~CY@{L5cO9{gogT}TC6UJhO;Llyi0Y%7MNRQ z))_WEMdO&+dmmm+>j`G~nZB)ci6MTUZ}U^B3_6c-n#J(SgJ&ZI2jgb`Z+BuNTYa>k zLV}-cJQDq*gymYgl)47C2E!oB+W0&sViMQZ=<_@`XJe<;KA%Y8ELtO-+bpGApgNVZ z5XpiGIf3F>7?YVez0!>JDl5$&z*RZ?}4DY3osX?FOJ?^%ee33Ik};H{<~1cRtwHPw9uY?{h9ck*f{#t z_fsXAg2^w#jGi5YJE&ahedRIQmG@1kPazsnX+ZfA9=b26ecrg-s5h(MK$r2gB^K8H zyAe-fi>-W7W1+Rer3U=Aw6&3h@t#;3VTWRcC`l{VF1V&OSww@0+9CUMo0~kXsyWM# z#5X>|ti9|}j9IAtE*`m#x}#|A>;~hfaH$B3DAKq$=+#xr$@s49lVT35cC8j?ENI-` zx?oJ}!$Vmu;~p9 z-SE_H)R|9KA{I0TVJbC48$~WEIxwBy?{Qxe;kEZ#Z9Cd6Soha5@56zcJ{tJi689Fh z4NXF=mGttTFENMXJhm@D%cdVXr!(bohJ=kA*c;(DVoKgqSUA@u34)Tg(arQAr+dEv z)NS5qbv@edi!C;kuli!W9>VXiC@ryg^8_Ur`)V?I#vc22Op1q|?Tny>w9~rvLh_x3 zh3s?C{V4^oD^n|h2_se63>n7j@W?gPS^%$wrdOsK8pm2oC9gM7eVIMv;gAfgjt$wm zCrH4VkL!Sl* znD#YWefDJEU{Si;ROwIGUUXD?Hy`Pg7UwUroSZG)?EcvKYtEa8JPt0#h+I)9g^we0 zJKA(qO!dlTR?g2(+6SJWQY2MAPZd+(H2OS?hjj?)c9hPu&QMGq;fKTedq4<*$ zs=5L3DybkuRMsSZd^9z>y9_IwnU>0ynu_66#=`kI8D$mWGO`F6xQv_}!r??Jos$1lh6MyqHvnirR9JxN zB&Gof7z+TPw;i1CmKM014FQ9I%V-h6x~DCREIQn0mcr!^{)Ybcu8Cuas2R_bI6CX; zVMZWTD-oWi^j;LW+NS63J8O1ll0gK6EEjxZ8PU%Ls_IZh!)Nu ziW%Qw@zxE%31UPiHo~nf%Tn0%m)=%B4?DVn<1OTTy@0WUw4x4D@*vL|Z1}xXhaws+ z=96=IBX;ni2*j%7<(r)$);%OHB?3!z#88dM}`4MF;Ox#Uhs4OG*KCOl0VAM`!LL0k1w7QGcsl`JC{6&vEBURn8>SD({ z)t2Q^+<<#7+OQNKJ}@D7P3t#b2W0I4r5hP+hfbn-P1G!j{v=Jtn+;1mTpqxoyNrrP z=HEtn&@h`M@slyXKJuCW4EbL(9@cJqvONaJ4FBJV$Gj>Uiw#H+w))EEw;MFMO^zrR zh;`u=`(w#5jfdazBBq9AF+rq|U^lGok~jV837?7H*lzj5PQjylzeQwuF=}ifhvcrz z$oBSgB0@J4eCjFc?fil73WMxKp{UD>-G+f~$i50>Fq*VC8n~|UQgZC)uy2=SNa4Ff zrYdLe%y?(xB%I3F6X`+IT!$RI0zq14Jfj#oC_f`gOjkU5X8)wfNt)9U*gdKXi&LosY80NocLLc;aM5H<-fAt&5UU3tbjT08y4-eRE zYu`vzoKcB7ePt>`5L^!|pbyS5y_mSI8RG!>mayIJ9o{YL8|rQ{j^lS6HnHf zpIF>^R9PW%muqtNeZP*IJnD_M%he53*%sA@-8gvV_>+t!heIA_h5UNK;a;uQe@)Y? zLnzZFuScm-2b^3G)*|Yb(wzbA2L^XekqLqCe}UiJJAKIn(Q8?gmb| znXZt%ZTaCHuVod!y8T`HKh!!=H==CEpUWa2(!88FmhF97SGVum9LujV6(S5-#3< z&1dTN$8R=knqA0g_nGlo_~oZx3yqG*f3TIb%(G-kx_tTjb-nP}np53;Sg$-(m%Z7y z&e?H7=fb+`-+v`nALHKr@8m|NzN}@wR;Fqi8<(~JTvX#cL%>ceLSV(OA1^FSD|VRu ziL=R)oftVSK}u~_Y-^9ro(oY%%j5e@E?7D?))!`T8}Um1W($ZuTlH;K^DfZ{?zM-6 z33dx`m2*Yv4=btUU+n{DwS2xr%mp4BRFyM!xOh|Nl|HW2{?rdx@*Hz zIHNW&C8--Uo`WXvJbzCI1I8EG(|mL)FWvwws;^Xst z&pC5b+26Fpd(F!#Jm#F?kNtBbwW2$!{c+BPNpF+1jOx`?f0yTd-nIKvtJyuPDr6@! zdoUQdGAXz*P4iW5{pwQICu#n)PKYTzSh2xT{J@jt?VEh~$`9;kQe;?ZoxVze>4w4O z%lX$HA6nqBbU9c3Hk&u+Z}=>ka8u^>KeY0~2ppOc`wZZrIa*$fmKQ^{yqF%cmd9`6 Tp(8$AZm;+!JmEOe_xK|Kwg%hN literal 0 HcmV?d00001 diff --git a/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java b/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java index 0d11d51829b7..db1e2a5c9ee7 100644 --- a/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java +++ b/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java @@ -208,6 +208,40 @@ private ResponseFieldsSnippet sslInfo() { .description("Certificate validity status.") .type(JsonFieldType.STRING), fieldWithPath("bundles[].certificateChains[].certificates[].signatureAlgorithmName") + .description("Signature algorithm name.") + .type(JsonFieldType.STRING), + fieldWithPath("bundles[].trustStoreCertificates").description("Certificate chains in the trust store.") + .type(JsonFieldType.ARRAY), + fieldWithPath("bundles[].trustStoreCertificates[].alias").description("Alias of the certificate chain.") + .type(JsonFieldType.STRING), + fieldWithPath("bundles[].trustStoreCertificates[].certificates") + .description("Certificates in the chain.") + .type(JsonFieldType.ARRAY), + fieldWithPath("bundles[].trustStoreCertificates[].certificates[].subject") + .description("Subject of the certificate.") + .type(JsonFieldType.STRING), + fieldWithPath("bundles[].trustStoreCertificates[].certificates[].version") + .description("Version of the certificate.") + .type(JsonFieldType.STRING), + fieldWithPath("bundles[].trustStoreCertificates[].certificates[].issuer") + .description("Issuer of the certificate.") + .type(JsonFieldType.STRING), + fieldWithPath("bundles[].trustStoreCertificates[].certificates[].validityStarts") + .description("Certificate validity start date.") + .type(JsonFieldType.STRING), + fieldWithPath("bundles[].trustStoreCertificates[].certificates[].serialNumber") + .description("Serial number of the certificate.") + .type(JsonFieldType.STRING), + fieldWithPath("bundles[].trustStoreCertificates[].certificates[].validityEnds") + .description("Certificate validity end date.") + .type(JsonFieldType.STRING), + fieldWithPath("bundles[].trustStoreCertificates[].certificates[].validity") + .description("Certificate validity information.") + .type(JsonFieldType.OBJECT), + fieldWithPath("bundles[].trustStoreCertificates[].certificates[].validity.status") + .description("Certificate validity status.") + .type(JsonFieldType.STRING), + fieldWithPath("bundles[].trustStoreCertificates[].certificates[].signatureAlgorithmName") .description("Signature algorithm name.") .type(JsonFieldType.STRING)); } @@ -259,9 +293,9 @@ JavaInfoContributor javaInfoContributor() { @Bean SslInfo sslInfo() { DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); - JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation("classpath:test.p12") + JksSslStoreDetails storeDetails = JksSslStoreDetails.forLocation("classpath:test.p12") .withPassword("secret"); - SslStoreBundle sslStoreBundle = new JksSslStoreBundle(keyStoreDetails, null); + SslStoreBundle sslStoreBundle = new JksSslStoreBundle(storeDetails, storeDetails); sslBundleRegistry.registerBundle("test-0", SslBundle.of(sslStoreBundle)); return new SslInfo(sslBundleRegistry); } From 27b9402df1b9104421a4aa0d52df0db73e6a166e Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 6 Feb 2026 12:04:31 +0100 Subject: [PATCH 180/930] Polish "Show certificates from truststore in SSL info endpoint" See gh-48967 --- .../springframework/boot/info/SslInfo.java | 33 ++++++++++++------- .../boot/info/SslInfoTests.java | 16 ++++----- .../info/InfoEndpointDocumentationTests.java | 26 ++++++++------- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java b/core/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java index 4c848b0d82e1..b3be703613d4 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java @@ -104,12 +104,12 @@ public final class BundleInfo { private final List certificateChains; - private final List trustStoreCertificates; + private final List trustStoreCertificateChains; private BundleInfo(String name, SslBundle sslBundle) { this.name = name; this.certificateChains = extractCertificateChains(sslBundle.getStores().getKeyStore()); - this.trustStoreCertificates = extractCertificateChains(sslBundle.getStores().getTrustStore()); + this.trustStoreCertificateChains = extractCertificateChains(sslBundle.getStores().getTrustStore()); } private List extractCertificateChains(@Nullable KeyStore keyStore) { @@ -135,8 +135,8 @@ public List getCertificateChains() { return this.certificateChains; } - public List getTrustStoreCertificates() { - return this.trustStoreCertificates; + public List getTrustStoreCertificateChains() { + return this.trustStoreCertificateChains; } } @@ -157,15 +157,13 @@ public final class CertificateChainInfo { private List extractCertificates(KeyStore keyStore, String alias) { try { - // First try to get the certificate chain (works for PrivateKeyEntry) - Certificate[] certificates = keyStore.getCertificateChain(alias); - if (!ObjectUtils.isEmpty(certificates)) { - return Arrays.stream(certificates).map(CertificateInfo::new).toList(); + List certificates = readCertificateChain(keyStore, alias); + if (certificates != null) { + return certificates; } - // Fall back to single certificate (works for trustedCertEntry) - Certificate certificate = keyStore.getCertificate(alias); + List certificate = readCertificate(keyStore, alias); if (certificate != null) { - return List.of(new CertificateInfo(certificate)); + return certificate; } return Collections.emptyList(); } @@ -174,6 +172,19 @@ private List extractCertificates(KeyStore keyStore, String alia } } + private @Nullable List readCertificate(KeyStore keyStore, String alias) + throws KeyStoreException { + Certificate certificate = keyStore.getCertificate(alias); + return (certificate != null) ? List.of(new CertificateInfo(certificate)) : null; + } + + private @Nullable List readCertificateChain(KeyStore keyStore, String alias) + throws KeyStoreException { + Certificate[] certificates = keyStore.getCertificateChain(alias); + return ObjectUtils.isEmpty(certificates) ? null + : Arrays.stream(certificates).map(CertificateInfo::new).toList(); + } + public String getAlias() { return this.alias; } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/info/SslInfoTests.java b/core/spring-boot/src/test/java/org/springframework/boot/info/SslInfoTests.java index 4b5a35295b2a..57b9d3e38fb7 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/info/SslInfoTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/info/SslInfoTests.java @@ -85,7 +85,7 @@ void validCertificatesShouldProvideSslInfo() { assertThat(cert2.getValidity()).isNotNull(); assertThat(cert2.getValidity().getStatus()).isSameAs(Status.VALID); assertThat(cert2.getValidity().getMessage()).isNull(); - assertThat(bundle.getTrustStoreCertificates()).isEmpty(); + assertThat(bundle.getTrustStoreCertificateChains()).isEmpty(); } @Test @@ -189,7 +189,7 @@ void nullKeyStore() { SslInfo sslInfo = new SslInfo(sslBundleRegistry, CLOCK); assertThat(sslInfo.getBundles()).hasSize(1); assertThat(sslInfo.getBundles().get(0).getCertificateChains()).isEmpty(); - assertThat(sslInfo.getBundles().get(0).getTrustStoreCertificates()).isEmpty(); + assertThat(sslInfo.getBundles().get(0).getTrustStoreCertificateChains()).isEmpty(); } @Test @@ -205,9 +205,9 @@ void trustStoreCertificatesShouldProvideSslInfo() { BundleInfo bundle = sslInfo.getBundles().get(0); assertThat(bundle.getName()).isEqualTo("test-trust"); assertThat(bundle.getCertificateChains()).isEmpty(); - assertThat(bundle.getTrustStoreCertificates()).hasSize(4); - assertThat(bundle.getTrustStoreCertificates().get(0).getAlias()).isEqualTo("spring-boot"); - assertThat(bundle.getTrustStoreCertificates().get(1).getAlias()).isEqualTo("test-alias"); + assertThat(bundle.getTrustStoreCertificateChains()).hasSize(4); + assertThat(bundle.getTrustStoreCertificateChains().get(0).getAlias()).isEqualTo("spring-boot"); + assertThat(bundle.getTrustStoreCertificateChains().get(1).getAlias()).isEqualTo("test-alias"); } @Test @@ -222,7 +222,7 @@ void bothKeyStoreAndTrustStoreCertificatesShouldProvideSslInfo() { BundleInfo bundle = sslInfo.getBundles().get(0); assertThat(bundle.getName()).isEqualTo("test-both"); assertThat(bundle.getCertificateChains()).hasSize(4); - assertThat(bundle.getTrustStoreCertificates()).hasSize(4); + assertThat(bundle.getTrustStoreCertificateChains()).hasSize(4); } @Test @@ -246,8 +246,8 @@ void separateKeyStoreAndTrustStoreShouldProvideSslInfo() { assertThat(chain.getCertificates().get(0).getSubject()).startsWith("CN=localhost"); }); // Truststore has 3 trustedCertEntry entries - assertThat(bundle.getTrustStoreCertificates()).hasSize(3); - assertThat(bundle.getTrustStoreCertificates()).allSatisfy((chain) -> { + assertThat(bundle.getTrustStoreCertificateChains()).hasSize(3); + assertThat(bundle.getTrustStoreCertificateChains()).allSatisfy((chain) -> { assertThat(chain.getCertificates()).hasSize(1); assertThat(chain.getCertificates().get(0).getSubject()).startsWith("CN=localhost"); }); diff --git a/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java b/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java index db1e2a5c9ee7..41ee62ec7eb5 100644 --- a/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java +++ b/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java @@ -210,38 +210,40 @@ private ResponseFieldsSnippet sslInfo() { fieldWithPath("bundles[].certificateChains[].certificates[].signatureAlgorithmName") .description("Signature algorithm name.") .type(JsonFieldType.STRING), - fieldWithPath("bundles[].trustStoreCertificates").description("Certificate chains in the trust store.") + fieldWithPath("bundles[].trustStoreCertificateChains") + .description("Certificate chains in the trust store.") .type(JsonFieldType.ARRAY), - fieldWithPath("bundles[].trustStoreCertificates[].alias").description("Alias of the certificate chain.") + fieldWithPath("bundles[].trustStoreCertificateChains[].alias") + .description("Alias of the certificate chain.") .type(JsonFieldType.STRING), - fieldWithPath("bundles[].trustStoreCertificates[].certificates") + fieldWithPath("bundles[].trustStoreCertificateChains[].certificates") .description("Certificates in the chain.") .type(JsonFieldType.ARRAY), - fieldWithPath("bundles[].trustStoreCertificates[].certificates[].subject") + fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].subject") .description("Subject of the certificate.") .type(JsonFieldType.STRING), - fieldWithPath("bundles[].trustStoreCertificates[].certificates[].version") + fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].version") .description("Version of the certificate.") .type(JsonFieldType.STRING), - fieldWithPath("bundles[].trustStoreCertificates[].certificates[].issuer") + fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].issuer") .description("Issuer of the certificate.") .type(JsonFieldType.STRING), - fieldWithPath("bundles[].trustStoreCertificates[].certificates[].validityStarts") + fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].validityStarts") .description("Certificate validity start date.") .type(JsonFieldType.STRING), - fieldWithPath("bundles[].trustStoreCertificates[].certificates[].serialNumber") + fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].serialNumber") .description("Serial number of the certificate.") .type(JsonFieldType.STRING), - fieldWithPath("bundles[].trustStoreCertificates[].certificates[].validityEnds") + fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].validityEnds") .description("Certificate validity end date.") .type(JsonFieldType.STRING), - fieldWithPath("bundles[].trustStoreCertificates[].certificates[].validity") + fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].validity") .description("Certificate validity information.") .type(JsonFieldType.OBJECT), - fieldWithPath("bundles[].trustStoreCertificates[].certificates[].validity.status") + fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].validity.status") .description("Certificate validity status.") .type(JsonFieldType.STRING), - fieldWithPath("bundles[].trustStoreCertificates[].certificates[].signatureAlgorithmName") + fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].signatureAlgorithmName") .description("Signature algorithm name.") .type(JsonFieldType.STRING)); } From 9c188bc7323d79a2ce2f97f7139ab4c83088d860 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 28 Jan 2026 21:51:09 -0800 Subject: [PATCH 181/930] Refine conventions to exclude checking of generated source Closes gh-49153 --- .../springframework/boot/build/JavaConventions.java | 13 +++++++++++++ core/spring-boot/build.gradle | 3 --- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java index 223a492a0491..eff1d527d829 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java @@ -42,6 +42,7 @@ import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.artifacts.Dependency; import org.gradle.api.artifacts.DependencySet; +import org.gradle.api.file.FileTreeElement; import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; @@ -50,6 +51,7 @@ import org.gradle.api.plugins.quality.CheckstylePlugin; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.SourceTask; import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.compile.JavaCompile; @@ -298,6 +300,17 @@ private void configureSpringJavaFormat(Project project) { .add(project.getDependencies().create("com.puppycrawl.tools:checkstyle:" + checkstyle.getToolVersion())); checkstyleDependencies .add(project.getDependencies().create("io.spring.javaformat:spring-javaformat-checkstyle:" + version)); + project.getTasks().withType(CheckFormat.class, this::excludeGeneratedSources); + project.getTasks().withType(Checkstyle.class, this::excludeGeneratedSources); + } + + private SourceTask excludeGeneratedSources(SourceTask task) { + return task.exclude(this::isGeneratedSource); + } + + private boolean isGeneratedSource(FileTreeElement candidate) { + String path = candidate.getFile().getPath(); + return path.contains("/generated/sources/") || path.contains("/generated-source/"); } private void configureDependencyManagement(Project project) { diff --git a/core/spring-boot/build.gradle b/core/spring-boot/build.gradle index 42d13c58b776..b094d7ac387a 100644 --- a/core/spring-boot/build.gradle +++ b/core/spring-boot/build.gradle @@ -82,9 +82,6 @@ def syncJavaTemplates = tasks.register("syncJavaTemplates", Sync) { } tasks.named("checkFormatMain") { - def generatedSources = fileTree("build/generated-sources/main") - // Exclude source generated from the templates as expand(properties) changes line endings on Windows - exclude { candidate -> generatedSources.contains(candidate.file) } // Add the templates to check that the input is correctly formatted source(fileTree("src/main/javaTemplates")) } From 4244dd933a81155c3274e8fc71129bb351fa4d54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sv=C4=9Btl=C3=ADk?= <16480390+osvetlik@users.noreply.github.com> Date: Sat, 7 Feb 2026 00:15:12 +0100 Subject: [PATCH 182/930] Bind `Optional` value object parameters as empty rather than `null` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update `ValueObjectBinder` so that `Optional` parameters are bound as `Option.empty()` rather than `null`. See gh-49152 Signed-off-by: Ondřej Světlík <16480390+osvetlik@users.noreply.github.com> --- .../properties/bind/BindConverter.java | 8 +++++- .../properties/bind/ValueObjectBinder.java | 7 ++++- .../bind/ValueObjectBinderTests.java | 27 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java index 29ac4e3fb232..764b6fa7ffd6 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java @@ -105,7 +105,13 @@ private boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor t new ResolvableTypeDescriptor(targetType, targetAnnotations)); } - private @Nullable Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + @SuppressWarnings("unchecked") + @Nullable T convertNullValue(ResolvableType targetType, Annotation... targetAnnotations) { + return (T) convert(null, null, new ResolvableTypeDescriptor(targetType, targetAnnotations)); + } + + private @Nullable Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, + TypeDescriptor targetType) { ConversionException failure = null; for (ConversionService delegate : this.delegates) { try { diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java index bdaffd389585..667ecfe0837f 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java @@ -132,7 +132,12 @@ public void onUnableToCreateInstance(Bindable target, Context context, Ru return convertDefaultValue(context.getConverter(), defaultValue, type, annotations); } } - return null; + return convertNullValue(context, type); + } + + private @Nullable T convertNullValue(Binder.Context context, ResolvableType type, Annotation... annotations) { + BindConverter converter = context.getConverter(); + return converter.convertNullValue(type, annotations); } private @Nullable T convertDefaultValue(BindConverter converter, String[] defaultValue, ResolvableType type, diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java index 2f1d0bd3b4b9..dff3c83fb396 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java @@ -326,6 +326,25 @@ void bindWhenOptionalParameterWithEmptyDefaultValueShouldReturnEmptyInstance() { assertThat(bound.getOptionalValue()).isEmpty(); } + @Test + void bindWhenOptionalParameterWithNoValueForPrimitiveShouldReturnEmptyInstance() { + RecordPropertiesWithOptional bound = this.binder.bindOrCreate("foo", + Bindable.of(RecordPropertiesWithOptional.class)); + assertThat(bound.property1()).isEmpty(); + } + + @Test + void bindWhenOptionalParameterInNestedWithNoValueForPrimitiveShouldReturnEmptyInstance() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.nested.property2", "str"); + this.sources.add(source); + RecordPropertiesWithOptionalInNestedRecord bound = this.binder.bindOrCreate("foo", + Bindable.of(RecordPropertiesWithOptionalInNestedRecord.class)); + assertThat(bound.nested()).isNotNull(); + assertThat(bound.nested().property1()).isNotNull().isEmpty(); + assertThat(bound.nested().property2()).isEqualTo("str"); + } + @Test void bindWhenEnumParameterWithEmptyDefaultValueShouldThrowException() { assertThatExceptionOfType(BindException.class) @@ -902,6 +921,14 @@ record RecordProperties(@DefaultValue("default-value-1") String property1, @DefaultValue("default-value-2") String property2) { } + record RecordPropertiesWithOptional(Optional property1) { + } + + record RecordPropertiesWithOptionalInNestedRecord(NestedRecord nested) { + record NestedRecord(Optional property1, String property2) { + } + } + static class NonExtractableParameterName { private @Nullable String value; From 764b5588877b5f6bf2cb87990fd7e4edf0f62c50 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 8 Feb 2026 09:47:50 -0800 Subject: [PATCH 183/930] Polish "Bind `Optional` value object parameters as empty rather than `null`" See gh-49152 --- .../context/properties/bind/BindConverter.java | 15 +++++---------- .../properties/bind/ValueObjectBinder.java | 8 ++------ .../properties/bind/ValueObjectBinderTests.java | 1 + 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java index 764b6fa7ffd6..a6fd1c7c8263 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java @@ -98,16 +98,8 @@ private boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor t @SuppressWarnings("unchecked") @Nullable T convert(@Nullable Object source, ResolvableType targetType, Annotation... targetAnnotations) { - if (source == null) { - return null; - } - return (T) convert(source, TypeDescriptor.forObject(source), - new ResolvableTypeDescriptor(targetType, targetAnnotations)); - } - - @SuppressWarnings("unchecked") - @Nullable T convertNullValue(ResolvableType targetType, Annotation... targetAnnotations) { - return (T) convert(null, null, new ResolvableTypeDescriptor(targetType, targetAnnotations)); + TypeDescriptor sourceType = (source != null) ? TypeDescriptor.forObject(source) : null; + return (T) convert(source, sourceType, new ResolvableTypeDescriptor(targetType, targetAnnotations)); } private @Nullable Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, @@ -125,6 +117,9 @@ private boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor t } } } + if (source == null) { + return null; + } throw (failure != null) ? failure : new ConverterNotFoundException(sourceType, targetType); } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java index 667ecfe0837f..919c6fc4fd20 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java @@ -61,6 +61,7 @@ * @author Stephane Nicoll * @author Phillip Webb * @author Scott Frederick + * @author Ondřej Světlík */ class ValueObjectBinder implements DataObjectBinder { @@ -132,12 +133,7 @@ public void onUnableToCreateInstance(Bindable target, Context context, Ru return convertDefaultValue(context.getConverter(), defaultValue, type, annotations); } } - return convertNullValue(context, type); - } - - private @Nullable T convertNullValue(Binder.Context context, ResolvableType type, Annotation... annotations) { - BindConverter converter = context.getConverter(); - return converter.convertNullValue(type, annotations); + return context.getConverter().convert(null, type); } private @Nullable T convertDefaultValue(BindConverter converter, String[] defaultValue, ResolvableType type, diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java index dff3c83fb396..7b1661b3a488 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java @@ -50,6 +50,7 @@ * @author Phillip Webb * @author Pavel Anisimov * @author Yanming Zhou + * @author Ondřej Světlík */ class ValueObjectBinderTests { From dd8ace8d685bd0cf33d3299b20eb074b44cfd3c2 Mon Sep 17 00:00:00 2001 From: aalsanie Date: Fri, 6 Feb 2026 10:00:39 +0300 Subject: [PATCH 184/930] Optimize ContextPairs nested name splitting See gh-48999 Signed-off-by: aalsanie --- .../boot/logging/structured/ContextPairs.java | 35 ++++++++++++++++--- .../logging/structured/ContextPairsTests.java | 13 +++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java index 75c268d72017..69fd6628e375 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java @@ -193,24 +193,49 @@ void nested(T item, BiConsumer pairs) { LinkedHashMap result = new LinkedHashMap<>(); this.addedPairs.forEach((addedPair) -> { addedPair.accept(item, joining((name, value) -> { - List nameParts = List.of(name.split("\\.")); Map destination = result; - for (int i = 0; i < nameParts.size() - 1; i++) { - Object existing = destination.computeIfAbsent(nameParts.get(i), (key) -> new LinkedHashMap<>()); + + int end = trimTrailingDelimiters(name); + if (end == 0) { + return; + } + + int start = 0; + while (true) { + int dot = name.indexOf('.', start); + if (dot == -1 || dot >= end) { + break; + } + + String part = name.substring(start, dot); + + Object existing = destination.computeIfAbsent(part, (key) -> new LinkedHashMap<>()); if (!(existing instanceof Map)) { - String common = String.join(".", nameParts.subList(0, i + 1)); + String common = name.substring(0, dot); throw new IllegalStateException( "Duplicate nested pairs added under '%s'".formatted(common)); } + destination = (Map) existing; + start = dot + 1; } - Object previous = destination.put(nameParts.get(nameParts.size() - 1), value); + + String leaf = name.substring(start, end); + Object previous = destination.put(leaf, value); Assert.state(previous == null, () -> "Duplicate nested pairs added under '%s'".formatted(name)); })); }); result.forEach(pairs); } + private int trimTrailingDelimiters(String name) { + int end = name.length(); + while (end > 0 && name.charAt(end - 1) == '.') { + end--; + } + return end; + } + private BiConsumer joining(BiConsumer pairs) { return (name, value) -> { name = this.joiner.join(ContextPairs.this.prefix, (name != null) ? name : ""); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/structured/ContextPairsTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/structured/ContextPairsTests.java index e0013d0a8689..0b9b79730454 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/structured/ContextPairsTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/structured/ContextPairsTests.java @@ -120,6 +120,19 @@ void nestedExpandsNames() { assertThat(actual).isEqualTo(expected); } + @Test + void nestedWhenNameEndsWithDelimiterDropsTrailingDelimiter() { + ContextPairs contextPairs = new ContextPairs(true, null); + Map map = new LinkedHashMap<>(); + map.put("a1.b1.", "A1B1"); + Map actual = apply(contextPairs.nested((pairs) -> pairs.addMapEntries((item) -> map))); + Map expected = new LinkedHashMap<>(); + Map a1 = new LinkedHashMap<>(); + expected.put("a1", a1); + a1.put("b1", "A1B1"); + assertThat(actual).isEqualTo(expected); + } + @Test void nestedWhenDuplicateInParentThrowsException() { ContextPairs contextPairs = new ContextPairs(true, null); From 0a5e7ec1356cf679c6b7afc7aef63bb8790075f3 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 8 Feb 2026 15:09:41 -0800 Subject: [PATCH 185/930] Polish "Optimize ContextPairs nested name splitting" See gh-48999 --- .../boot/logging/structured/ContextPairs.java | 52 +++++++------------ 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java index 69fd6628e375..d5aebb1c10f9 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java @@ -193,47 +193,35 @@ void nested(T item, BiConsumer pairs) { LinkedHashMap result = new LinkedHashMap<>(); this.addedPairs.forEach((addedPair) -> { addedPair.accept(item, joining((name, value) -> { + StringBuilder part = new StringBuilder(name.length()); + int length = (!name.endsWith(".")) ? name.length() : name.length() - 1; Map destination = result; - - int end = trimTrailingDelimiters(name); - if (end == 0) { - return; - } - - int start = 0; - while (true) { - int dot = name.indexOf('.', start); - if (dot == -1 || dot >= end) { - break; + for (int i = 0; i < length; i++) { + char ch = name.charAt(i); + if (i == length - 1) { + part.append(ch); + Object previous = destination.put(part.toString(), value); + assertNotDuplicateNestedPairs(previous == null, name, length); } - - String part = name.substring(start, dot); - - Object existing = destination.computeIfAbsent(part, (key) -> new LinkedHashMap<>()); - if (!(existing instanceof Map)) { - String common = name.substring(0, dot); - throw new IllegalStateException( - "Duplicate nested pairs added under '%s'".formatted(common)); + else if (ch == '.') { + Object current = destination.computeIfAbsent(part.toString(), + (key) -> new LinkedHashMap<>()); + assertNotDuplicateNestedPairs(current instanceof Map, name, i); + destination = (Map) current; + part.setLength(0); + } + else { + part.append(ch); } - - destination = (Map) existing; - start = dot + 1; } - - String leaf = name.substring(start, end); - Object previous = destination.put(leaf, value); - Assert.state(previous == null, () -> "Duplicate nested pairs added under '%s'".formatted(name)); })); }); result.forEach(pairs); } - private int trimTrailingDelimiters(String name) { - int end = name.length(); - while (end > 0 && name.charAt(end - 1) == '.') { - end--; - } - return end; + private void assertNotDuplicateNestedPairs(boolean expression, String name, int index) { + Assert.state(expression, + () -> "Duplicate nested pairs added under '%s'".formatted(name.substring(0, index))); } private BiConsumer joining(BiConsumer pairs) { From babf9c9fbc126c68f5c01aa01ba76c93270c62b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 9 Feb 2026 14:22:12 +0100 Subject: [PATCH 186/930] Upgrade to Flyway 12.0.0 Closes gh-49161 --- .../boot/flyway/autoconfigure/FlywayPropertiesTests.java | 2 +- platform/spring-boot-dependencies/build.gradle | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/FlywayPropertiesTests.java b/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/FlywayPropertiesTests.java index 3ae12e613eca..74049b8f65ab 100644 --- a/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/FlywayPropertiesTests.java +++ b/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/FlywayPropertiesTests.java @@ -128,7 +128,7 @@ void expectedPropertiesAreManaged() { ignoreProperties(configuration, "resolversAsClassNames", "callbacksAsClassNames", "driver", "modernConfig", "currentResolvedEnvironment", "reportFilename", "reportEnabled", "workingDirectory", "cachedDataSources", "cachedResolvedEnvironments", "currentEnvironmentName", "allEnvironments", - "environmentProvisionMode", "provisionMode", "cleanOnValidationError"); + "environmentProvisionMode", "provisionMode"); // Handled by the conversion service ignoreProperties(configuration, "baselineVersionAsString", "encodingAsString", "locationsAsStrings", "callbackLocationsAsStrings", "targetAsString"); diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0d48330627ff..23e6252976c2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -425,9 +425,10 @@ bom { javadoc("elasticsearch-java", version -> "https://javadoc.io/doc/co.elastic.clients/elasticsearch-java/%s/index.html".formatted(version), "co.elastic.clients.elasticsearch", "co.elastic.clients.transport") } } - library("Flyway", "11.20.3") { + library("Flyway", "12.0.0") { group("org.flywaydb") { modules = [ + "flyway-command-test-connection", "flyway-commandline", "flyway-core", "flyway-database-cassandra", @@ -436,6 +437,8 @@ bom { "flyway-database-hsqldb", "flyway-database-informix", "flyway-database-mongodb", + "flyway-database-nc-couchbase", + "flyway-database-nc-mongodb", "flyway-database-oracle", "flyway-database-postgresql", "flyway-database-redshift", From 996999b79937b360a664d0c6cb8e319cb8329c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 9 Feb 2026 14:38:51 +0100 Subject: [PATCH 187/930] Polish "Upgrade to Flyway 12.0.0" Move Flyway 10.x tests to Flyway 11.x See gh-49161 --- ...urationTests.java => Flyway110AutoConfigurationTests.java} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/{Flyway100AutoConfigurationTests.java => Flyway110AutoConfigurationTests.java} (94%) diff --git a/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/Flyway100AutoConfigurationTests.java b/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/Flyway110AutoConfigurationTests.java similarity index 94% rename from module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/Flyway100AutoConfigurationTests.java rename to module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/Flyway110AutoConfigurationTests.java index 0bb306d98d59..825be0b9584c 100644 --- a/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/Flyway100AutoConfigurationTests.java +++ b/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/Flyway110AutoConfigurationTests.java @@ -34,8 +34,8 @@ * @author Andy Wilkinson */ @ClassPathExclusions({ "flyway-core-*.jar", "flyway-sqlserver-*.jar" }) -@ClassPathOverrides({ "org.flywaydb:flyway-core:10.0.0", "com.h2database:h2:2.1.210" }) -class Flyway100AutoConfigurationTests { +@ClassPathOverrides({ "org.flywaydb:flyway-core:11.0.0" }) +class Flyway110AutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) From 961cd43da9524bd066f270b0073cac122ef63e06 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Mon, 9 Feb 2026 09:19:03 +0800 Subject: [PATCH 188/930] Remove redundant assertion `isNotNull()` is enforced in `isEmpty()`. See gh-49156 Signed-off-by: Yanming Zhou --- .../boot/context/properties/bind/ValueObjectBinderTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java index 7b1661b3a488..35acae7bd453 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java @@ -342,7 +342,7 @@ void bindWhenOptionalParameterInNestedWithNoValueForPrimitiveShouldReturnEmptyIn RecordPropertiesWithOptionalInNestedRecord bound = this.binder.bindOrCreate("foo", Bindable.of(RecordPropertiesWithOptionalInNestedRecord.class)); assertThat(bound.nested()).isNotNull(); - assertThat(bound.nested().property1()).isNotNull().isEmpty(); + assertThat(bound.nested().property1()).isEmpty(); assertThat(bound.nested().property2()).isEqualTo("str"); } From 14fdf493b066165f2f405efc317b3351d45dcc78 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Fri, 23 Jan 2026 20:01:47 -0800 Subject: [PATCH 189/930] Add time, locale, and work dir data to ProcessInfo The new fields are: - uptime - startTime - currentTime - timezone - locale - workingDirectory See gh-48959 --- .../boot/info/ProcessInfo.java | 68 +++++++++++++++++++ .../boot/info/ProcessInfoTests.java | 6 ++ .../info/InfoEndpointDocumentationTests.java | 8 +++ 3 files changed, 82 insertions(+) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java b/core/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java index e66876322363..4b7e9ffe0093 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java @@ -22,7 +22,12 @@ import java.lang.management.MemoryUsage; import java.lang.management.PlatformManagedObject; import java.lang.reflect.Method; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; import java.util.List; +import java.util.Locale; import org.jspecify.annotations.Nullable; @@ -50,11 +55,23 @@ public class ProcessInfo { private final @Nullable String owner; + private final @Nullable Instant startTime; + + private final ZoneId timezone; + + private final Locale locale; + + private final String workingDirectory; + 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); + this.startTime = process.info().startInstant().orElse(null); + this.timezone = ZoneId.systemDefault(); + this.locale = Locale.getDefault(); + this.workingDirectory = Path.of(".").toAbsolutePath().normalize().toString(); } /** @@ -133,6 +150,57 @@ public long getParentPid() { return this.owner; } + /** + * The uptime of the process can be useful to see how long the process has been + * running and to check how long ago the last deployment or restart happened. + * @return duration since the process started if available, null otherwise + */ + public @Nullable Duration getUptime() { + return (this.startTime != null) ? Duration.between(this.startTime, Instant.now()) : null; + } + + /** + * The start time of the process can be useful to see when the process was started and + * to check when the last deployment or restart happened. + * @return the time when the process started if available, null otherwise + */ + public @Nullable Instant getStartTime() { + return this.startTime; + } + + /** + * The current time can be useful to check if there is any clock-skew issue and if the + * current time that the process knows is accurate enough. + * @return the current time of the process + */ + public Instant getCurrentTime() { + return Instant.now(); + } + + /** + * The timezone can help to detect time and timezone related issues. + * @return the timezone of the process, e.g.: America/Los_Angeles + */ + public ZoneId getTimezone() { + return this.timezone; + } + + /** + * The locale can help to detect issues connected to language and country settings. + * @return the locale of the process, e.g.: en_US + */ + public Locale getLocale() { + return this.locale; + } + + /** + * The working directory can help to locate files that the process uses. + * @return the absolute path of the working directory of the process + */ + public String getWorkingDirectory() { + return this.workingDirectory; + } + /** * Virtual threads information. * diff --git a/core/spring-boot/src/test/java/org/springframework/boot/info/ProcessInfoTests.java b/core/spring-boot/src/test/java/org/springframework/boot/info/ProcessInfoTests.java index 317e26db3f41..c7eeb746dc02 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/info/ProcessInfoTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/info/ProcessInfoTests.java @@ -44,6 +44,12 @@ void processInfoIsAvailable() { assertThat(processInfo.getPid()).isEqualTo(ProcessHandle.current().pid()); assertThat(processInfo.getParentPid()) .isEqualTo(ProcessHandle.current().parent().map(ProcessHandle::pid).orElse(null)); + assertThat(processInfo.getUptime()).isPositive(); + assertThat(processInfo.getStartTime()).isInThePast(); + assertThat(processInfo.getCurrentTime()).isAfter(processInfo.getStartTime()); + assertThat(processInfo.getTimezone()).isNotNull(); + assertThat(processInfo.getLocale()).isNotNull(); + assertThat(processInfo.getWorkingDirectory()).isNotBlank(); } @Test diff --git a/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java b/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java index 41ee62ec7eb5..698be0717037 100644 --- a/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java +++ b/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java @@ -103,6 +103,14 @@ private ResponseFieldsSnippet processInfo() { fieldWithPath("pid").description("Process ID.").type(JsonFieldType.NUMBER), fieldWithPath("parentPid").description("Parent Process ID (or -1).").type(JsonFieldType.NUMBER), fieldWithPath("owner").description("Process owner.").type(JsonFieldType.STRING), + fieldWithPath("uptime").description("Process uptime.").type(JsonFieldType.STRING), + fieldWithPath("startTime").description("Process start time.").type(JsonFieldType.STRING), + fieldWithPath("currentTime").description("Current time known by the process.") + .type(JsonFieldType.STRING), + fieldWithPath("timezone").description("Process timezone.").type(JsonFieldType.STRING), + fieldWithPath("locale").description("Process locale.").type(JsonFieldType.STRING), + fieldWithPath("workingDirectory").description("Working directory of the process.") + .type(JsonFieldType.STRING), fieldWithPath("cpus").description("Number of CPUs available to the process.") .type(JsonFieldType.NUMBER), fieldWithPath("memory").description("Memory information."), From 5f797f7af3dfb93aa50c0de22b06d58340019c73 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Feb 2026 13:47:59 +0000 Subject: [PATCH 190/930] Polish "Add time, locale, and work dir data to ProcessInfo" See gh-48959 --- .../boot/info/ProcessInfo.java | 38 ++++++++++++------- .../boot/info/ProcessInfoTests.java | 13 +++++-- .../info/InfoEndpointDocumentationTests.java | 8 +++- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java b/core/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java index 4b7e9ffe0093..1a8151987466 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java @@ -16,6 +16,7 @@ package org.springframework.boot.info; +import java.lang.ProcessHandle.Info; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; @@ -151,51 +152,62 @@ public long getParentPid() { } /** - * The uptime of the process can be useful to see how long the process has been - * running and to check how long ago the last deployment or restart happened. - * @return duration since the process started if available, null otherwise + * Uptime of the process. Can be useful to see how long the process has been running + * and to check how long ago the last deployment or restart happened. + * @return duration since the process started, if available, otherwise {@code null} + * @since 4.1.0 */ public @Nullable Duration getUptime() { return (this.startTime != null) ? Duration.between(this.startTime, Instant.now()) : null; } /** - * The start time of the process can be useful to see when the process was started and - * to check when the last deployment or restart happened. - * @return the time when the process started if available, null otherwise + * Time at which the process started. Can be useful to see when the process was + * started and to check when the last deployment or restart happened. + * @return the time when the process started, if available, otherwise {@code null} + * @since 4.1.0 + * @see Info#startInstant() */ public @Nullable Instant getStartTime() { return this.startTime; } /** - * The current time can be useful to check if there is any clock-skew issue and if the - * current time that the process knows is accurate enough. + * Current time of the process. Can be useful to check if there is any clock-skew + * issue and if the current time that the process knows is accurate enough. * @return the current time of the process + * @since 4.1.0 + * @see Instant#now */ public Instant getCurrentTime() { return Instant.now(); } /** - * The timezone can help to detect time and timezone related issues. - * @return the timezone of the process, e.g.: America/Los_Angeles + * Timezone of the process. Can help to detect time and timezone related issues. + * @return the timezone of the process + * @since 4.1.0 + * @see ZoneId#systemDefault() */ public ZoneId getTimezone() { return this.timezone; } /** - * The locale can help to detect issues connected to language and country settings. - * @return the locale of the process, e.g.: en_US + * Locale of the process. Can help to detect issues connected to language and country + * settings. + * @return the locale of the process + * @since 4.1.0 + * @see Locale#getDefault() */ public Locale getLocale() { return this.locale; } /** - * The working directory can help to locate files that the process uses. + * Working directory of the process. Can help to locate files that the process uses. * @return the absolute path of the working directory of the process + * @since 4.1.0 */ public String getWorkingDirectory() { return this.workingDirectory; diff --git a/core/spring-boot/src/test/java/org/springframework/boot/info/ProcessInfoTests.java b/core/spring-boot/src/test/java/org/springframework/boot/info/ProcessInfoTests.java index c7eeb746dc02..92bb8e03bb0d 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/info/ProcessInfoTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/info/ProcessInfoTests.java @@ -44,9 +44,16 @@ void processInfoIsAvailable() { assertThat(processInfo.getPid()).isEqualTo(ProcessHandle.current().pid()); assertThat(processInfo.getParentPid()) .isEqualTo(ProcessHandle.current().parent().map(ProcessHandle::pid).orElse(null)); - assertThat(processInfo.getUptime()).isPositive(); - assertThat(processInfo.getStartTime()).isInThePast(); - assertThat(processInfo.getCurrentTime()).isAfter(processInfo.getStartTime()); + if (ProcessHandle.current().info().startInstant().isPresent()) { + assertThat(processInfo.getUptime()).isPositive(); + assertThat(processInfo.getStartTime()).isInThePast(); + assertThat(processInfo.getCurrentTime()).isAfter(processInfo.getStartTime()); + } + else { + assertThat(processInfo.getUptime()).isNull(); + assertThat(processInfo.getStartTime()).isNull(); + assertThat(processInfo.getCurrentTime()).isNotNull(); + } assertThat(processInfo.getTimezone()).isNotNull(); assertThat(processInfo.getLocale()).isNotNull(); assertThat(processInfo.getWorkingDirectory()).isNotBlank(); diff --git a/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java b/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java index 698be0717037..1b517c02b2ce 100644 --- a/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java +++ b/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java @@ -103,8 +103,12 @@ private ResponseFieldsSnippet processInfo() { fieldWithPath("pid").description("Process ID.").type(JsonFieldType.NUMBER), fieldWithPath("parentPid").description("Parent Process ID (or -1).").type(JsonFieldType.NUMBER), fieldWithPath("owner").description("Process owner.").type(JsonFieldType.STRING), - fieldWithPath("uptime").description("Process uptime.").type(JsonFieldType.STRING), - fieldWithPath("startTime").description("Process start time.").type(JsonFieldType.STRING), + fieldWithPath("uptime").description("Process uptime, if available.") + .optional() + .type(JsonFieldType.STRING), + fieldWithPath("startTime").description("Process start time, if availabe.") + .optional() + .type(JsonFieldType.STRING), fieldWithPath("currentTime").description("Current time known by the process.") .type(JsonFieldType.STRING), fieldWithPath("timezone").description("Process timezone.").type(JsonFieldType.STRING), From 793f9a3ddbaf8fd9ce9a8f234bf97ae13eaf6db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Tue, 24 Sep 2024 19:29:08 -0600 Subject: [PATCH 191/930] Add RabbitMQ Stream service connection from RabbitMQContainer Add `RabbitStreamConnectionDetails` and support from `RabbitMQContainer` when `rabbitmq_stream` plugin is enabled. See gh-42443 --- ...nectionDetailsFactoryIntegrationTests.java | 129 ++++++++++++++++++ .../RabbitStreamConfiguration.java | 65 +++++++-- .../RabbitStreamConnectionDetails.java | 65 +++++++++ ...reamContainerConnectionDetailsFactory.java | 79 +++++++++++ .../main/resources/META-INF/spring.factories | 4 +- .../RabbitStreamConfigurationTests.java | 62 +++++++-- 6 files changed, 386 insertions(+), 18 deletions(-) create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..53d1117e3993 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import com.rabbitmq.stream.Address; +import com.rabbitmq.stream.Environment; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.TestImage; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; +import org.springframework.rabbit.stream.support.StreamAdmin; +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 RabbitStreamContainerConnectionDetailsFactory}. + * + * @author Eddú Meléndez + */ +@SpringJUnitConfig +@TestPropertySource( + properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) +@Testcontainers(disabledWithoutDocker = true) +class RabbitStreamContainerConnectionDetailsFactoryIntegrationTests { + + private static final int RABBITMQ_STREAMS_PORT = 5552; + + @Container + @ServiceConnection + static final RabbitMQContainer rabbit = getRabbitMqStreamContainer(); + + private static RabbitMQContainer getRabbitMqStreamContainer() { + RabbitMQContainer container = TestImage.container(RabbitMQContainer.class); + container.addExposedPorts(RABBITMQ_STREAMS_PORT); + String enabledPlugins = "[rabbitmq_stream,rabbitmq_prometheus]."; + container.withCopyToContainer(Transferable.of(enabledPlugins), "/etc/rabbitmq/enabled_plugins"); + return container; + } + + @Autowired(required = false) + private RabbitStreamConnectionDetails connectionDetails; + + @Autowired + private RabbitStreamTemplate rabbitStreamTemplate; + + @Autowired + private TestListener listener; + + @Test + void connectionCanBeMadeToRabbitContainer() { + assertThat(this.connectionDetails).isNotNull(); + this.rabbitStreamTemplate.convertAndSend("message"); + Awaitility.waitAtMost(Duration.ofMinutes(4)) + .untilAsserted(() -> assertThat(this.listener.messages).containsExactly("message")); + + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(RabbitAutoConfiguration.class) + static class TestConfiguration { + + @Bean + StreamAdmin streamAdmin(Environment env) { + return new StreamAdmin(env, sc -> { + sc.stream("stream.queue1").create(); + }); + } + + @Bean + EnvironmentBuilderCustomizer environmentBuilderCustomizer() { + return env -> { + Address entrypoint = new Address(rabbit.getHost(), rabbit.getMappedPort(RABBITMQ_STREAMS_PORT)); + env.addressResolver(address -> entrypoint); + }; + } + + @Bean + TestListener testListener() { + return new TestListener(); + } + + } + + static class TestListener { + + private final List messages = new ArrayList<>(); + + @RabbitListener(queues = "stream.queue1") + void processMessage(String message) { + this.messages.add(message); + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java index 316b981b6df8..b06fa1909c7d 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java @@ -22,6 +22,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.amqp.autoconfigure.RabbitProperties.Stream; import org.springframework.boot.amqp.autoconfigure.RabbitProperties.StreamContainer; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -50,6 +51,12 @@ @ConditionalOnClass(StreamRabbitListenerContainerFactory.class) class RabbitStreamConfiguration { + @Bean + @ConditionalOnMissingBean(RabbitStreamConnectionDetails.class) + RabbitStreamConnectionDetails rabbitStreamConnectionDetails(RabbitProperties rabbitProperties) { + return new PropertiesRabbitStreamConnectionDetails(rabbitProperties.getStream()); + } + @Bean(name = "rabbitListenerContainerFactory") @ConditionalOnMissingBean(name = "rabbitListenerContainerFactory") @ConditionalOnProperty(name = "spring.rabbitmq.listener.type", havingValue = "stream") @@ -66,13 +73,16 @@ StreamRabbitListenerContainerFactory streamRabbitListenerContainerFactory(Enviro consumerCustomizer.ifUnique(factory::setConsumerCustomizer); containerCustomizer.ifUnique(factory::setContainerCustomizer); return factory; + } @Bean(name = "rabbitStreamEnvironment") @ConditionalOnMissingBean(name = "rabbitStreamEnvironment") Environment rabbitStreamEnvironment(RabbitProperties properties, RabbitConnectionDetails connectionDetails, + RabbitStreamConnectionDetails streamConnectionDetails, ObjectProvider customizers) { - EnvironmentBuilder builder = configure(Environment.builder(), properties, connectionDetails); + EnvironmentBuilder builder = configure(Environment.builder(), properties, connectionDetails, + streamConnectionDetails); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } @@ -105,20 +115,57 @@ RabbitStreamTemplate rabbitStreamTemplate(Environment rabbitStreamEnvironment, R } static EnvironmentBuilder configure(EnvironmentBuilder builder, RabbitProperties properties, - RabbitConnectionDetails connectionDetails) { - return configure(builder, properties.getStream(), connectionDetails); + RabbitConnectionDetails connectionDetails, RabbitStreamConnectionDetails streamConnectionDetails) { + return configure(builder, properties.getStream(), connectionDetails, streamConnectionDetails); } private static EnvironmentBuilder configure(EnvironmentBuilder builder, RabbitProperties.Stream stream, - RabbitConnectionDetails connectionDetails) { + RabbitConnectionDetails connectionDetails, RabbitStreamConnectionDetails streamConnectionDetails) { builder.lazyInitialization(true); PropertyMapper map = PropertyMapper.get(); - map.from(stream.getHost()).to(builder::host); - map.from(stream.getPort()).to(builder::port); - map.from(stream.getVirtualHost()).orFrom(connectionDetails::getVirtualHost).to(builder::virtualHost); - map.from(stream.getUsername()).orFrom(connectionDetails::getUsername).to(builder::username); - map.from(stream.getPassword()).orFrom(connectionDetails::getPassword).to(builder::password); + map.from(streamConnectionDetails.getHost()).to(builder::host); + map.from(streamConnectionDetails.getPort()).to(builder::port); + map.from(streamConnectionDetails.getVirtualHost()) + .orFrom(connectionDetails::getVirtualHost) + .to(builder::virtualHost); + map.from(streamConnectionDetails.getUsername()).orFrom(connectionDetails::getUsername).to(builder::username); + map.from(streamConnectionDetails.getPassword()).orFrom(connectionDetails::getPassword).to(builder::password); return builder; } + static class PropertiesRabbitStreamConnectionDetails implements RabbitStreamConnectionDetails { + + private final Stream streamProperties; + + PropertiesRabbitStreamConnectionDetails(Stream streamProperties) { + this.streamProperties = streamProperties; + } + + @Override + public String getHost() { + return this.streamProperties.getHost(); + } + + @Override + public int getPort() { + return this.streamProperties.getPort(); + } + + @Override + public String getVirtualHost() { + return this.streamProperties.getVirtualHost(); + } + + @Override + public String getUsername() { + return this.streamProperties.getUsername(); + } + + @Override + public String getPassword() { + return this.streamProperties.getPassword(); + } + + } + } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java new file mode 100644 index 000000000000..de630e6f9d99 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.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.amqp.autoconfigure; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to a RabbitMQ Stream service. + * + * @author Eddú Meléndez + * @since 4.1.0 + */ +public interface RabbitStreamConnectionDetails extends ConnectionDetails { + + /** + * Rabbit server host. + * @return the rabbit server host + */ + String getHost(); + + /** + * Rabbit Stream server port. + * @return the rabbit stream server port + */ + int getPort(); + + /** + * Login user to authenticate to the broker. + * @return the login user to authenticate to the broker or {@code null} + */ + default String getUsername() { + return null; + } + + /** + * Login to authenticate against the broker. + * @return the login to authenticate against the broker or {@code null} + */ + default String getPassword() { + return null; + } + + /** + * Virtual host to use when connecting to the broker. + * @return the virtual host to use when connecting to the broker or {@code null} + */ + default String getVirtualHost() { + return null; + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java new file mode 100644 index 000000000000..fb364a90703c --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.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.amqp.testcontainers; + +import org.testcontainers.containers.RabbitMQContainer; + +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +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 RabbitStreamConnectionDetails} from a + * {@link ServiceConnection @ServiceConnection}-annotated {@link RabbitMQContainer}. + * + * @author Eddú Meléndez + */ +class RabbitStreamContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory { + + RabbitStreamContainerConnectionDetailsFactory() { + super(ANY_CONNECTION_NAME, "org.springframework.rabbit.stream.producer.RabbitStreamTemplate"); + } + + @Override + protected RabbitStreamConnectionDetails getContainerConnectionDetails( + ContainerConnectionSource source) { + return new RabbitMqStreamContainerConnectionDetails(source); + } + + /** + * {@link RabbitStreamConnectionDetails} backed by a + * {@link ContainerConnectionSource}. + */ + private static final class RabbitMqStreamContainerConnectionDetails + extends ContainerConnectionDetails implements RabbitStreamConnectionDetails { + + private RabbitMqStreamContainerConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + @Override + public String getHost() { + return getContainer().getHost(); + } + + @Override + public int getPort() { + return getContainer().getMappedPort(5552); + } + + @Override + public String getUsername() { + return getContainer().getAdminUsername(); + } + + @Override + public String getPassword() { + return getContainer().getAdminPassword(); + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories b/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories index f086b1a88ce8..9f8225b9b3bc 100644 --- a/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories @@ -2,4 +2,6 @@ org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ org.springframework.boot.amqp.docker.compose.RabbitDockerComposeConnectionDetailsFactory,\ org.springframework.boot.amqp.testcontainers.DeprecatedRabbitContainerConnectionDetailsFactory,\ -org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory +org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory,\ +org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory + diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java index b9aa3b32afde..088c31f6ccdb 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.List; +import com.rabbitmq.stream.Address; import com.rabbitmq.stream.BackOffDelayPolicy; import com.rabbitmq.stream.Codec; import com.rabbitmq.stream.Environment; @@ -32,6 +33,7 @@ import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConfiguration.PropertiesRabbitStreamConnectionDetails; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -48,6 +50,7 @@ import org.springframework.rabbit.stream.producer.ProducerCustomizer; import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.then; @@ -146,7 +149,8 @@ void environmentUsesConnectionDetailsByDefault() { EnvironmentBuilder builder = mock(EnvironmentBuilder.class); RabbitProperties properties = new RabbitProperties(); RabbitStreamConfiguration.configure(builder, properties, - new TestRabbitConnectionDetails("guest", "guest", "vhost")); + new TestRabbitConnectionDetails("guest", "guest", "vhost"), + getRabbitStreamConnectionDetails(properties)); then(builder).should().port(5552); then(builder).should().host("localhost"); then(builder).should().virtualHost("vhost"); @@ -162,7 +166,8 @@ void whenStreamPortIsSetThenEnvironmentUsesCustomPort() { RabbitProperties properties = new RabbitProperties(); properties.getStream().setPort(5553); RabbitStreamConfiguration.configure(builder, properties, - new TestRabbitConnectionDetails("guest", "guest", "vhost")); + new TestRabbitConnectionDetails("guest", "guest", "vhost"), + getRabbitStreamConnectionDetails(properties)); then(builder).should().port(5553); } @@ -172,7 +177,8 @@ void whenStreamHostIsSetThenEnvironmentUsesCustomHost() { RabbitProperties properties = new RabbitProperties(); properties.getStream().setHost("stream.rabbit.example.com"); RabbitStreamConfiguration.configure(builder, properties, - new TestRabbitConnectionDetails("guest", "guest", "vhost")); + new TestRabbitConnectionDetails("guest", "guest", "vhost"), + getRabbitStreamConnectionDetails(properties)); then(builder).should().host("stream.rabbit.example.com"); } @@ -182,7 +188,8 @@ void whenStreamVirtualHostIsSetThenEnvironmentUsesCustomVirtualHost() { RabbitProperties properties = new RabbitProperties(); properties.getStream().setVirtualHost("stream-virtual-host"); RabbitStreamConfiguration.configure(builder, properties, - new TestRabbitConnectionDetails("guest", "guest", "vhost")); + new TestRabbitConnectionDetails("guest", "guest", "vhost"), + getRabbitStreamConnectionDetails(properties)); then(builder).should().virtualHost("stream-virtual-host"); } @@ -190,9 +197,10 @@ void whenStreamVirtualHostIsSetThenEnvironmentUsesCustomVirtualHost() { void whenStreamVirtualHostIsNotSetButDefaultVirtualHostIsSetThenEnvironmentUsesDefaultVirtualHost() { EnvironmentBuilder builder = mock(EnvironmentBuilder.class); RabbitProperties properties = new RabbitProperties(); - properties.setVirtualHost("properties-virtual-host"); + properties.setVirtualHost("default-virtual-host"); RabbitStreamConfiguration.configure(builder, properties, - new TestRabbitConnectionDetails("guest", "guest", "default-virtual-host")); + new TestRabbitConnectionDetails("guest", "guest", "default-virtual-host"), + getRabbitStreamConnectionDetails(properties)); then(builder).should().virtualHost("default-virtual-host"); } @@ -203,7 +211,8 @@ void whenStreamCredentialsAreNotSetThenEnvironmentUsesConnectionDetailsCredentia properties.setUsername("alice"); properties.setPassword("secret"); RabbitStreamConfiguration.configure(builder, properties, - new TestRabbitConnectionDetails("bob", "password", "vhost")); + new TestRabbitConnectionDetails("bob", "password", "vhost"), + getRabbitStreamConnectionDetails(properties)); then(builder).should().username("bob"); then(builder).should().password("password"); } @@ -217,7 +226,8 @@ void whenStreamCredentialsAreSetThenEnvironmentUsesStreamCredentials() { properties.getStream().setUsername("bob"); properties.getStream().setPassword("confidential"); RabbitStreamConfiguration.configure(builder, properties, - new TestRabbitConnectionDetails("charlotte", "hidden", "vhost")); + new TestRabbitConnectionDetails("guest", "guest", "vhost"), + getRabbitStreamConnectionDetails(properties)); then(builder).should().username("bob"); then(builder).should().password("confidential"); } @@ -297,6 +307,22 @@ void environmentCreatedByBuilderCanBeCustomized() { }); } + @Test + @SuppressWarnings("unchecked") + void connectionDetailsAreApplied() { + this.contextRunner.withPropertyValues("spring.rabbitmq.stream.name:stream-test") + .withUserConfiguration(CustomConnectionDetails.class) + .run((context) -> assertThat(context.getBean(Environment.class)) + .extracting((environment) -> (List
    ) ReflectionTestUtils.getField(environment, "addresses")) + .extracting((address) -> address.get(0)) + .extracting("host", "port") + .containsExactly("rabbitmq", 5555)); + } + + private RabbitStreamConnectionDetails getRabbitStreamConnectionDetails(RabbitProperties properties) { + return new PropertiesRabbitStreamConnectionDetails(properties.getStream()); + } + @Configuration(proxyBeanMethods = false) static class TestConfiguration { @@ -418,4 +444,24 @@ public List
    getAddresses() { } + @Configuration(proxyBeanMethods = false) + static class CustomConnectionDetails { + + @Bean + RabbitStreamConnectionDetails customRabbitMqStreamConnectionDetails() { + return new RabbitStreamConnectionDetails() { + @Override + public String getHost() { + return "rabbitmq"; + } + + @Override + public int getPort() { + return 5555; + } + }; + } + + } + } From 8df13b4709c26af01c5d693cee3b2822ec08027f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 30 Jan 2026 11:23:09 +0000 Subject: [PATCH 192/930] Polish "Add RabbitMQ Stream service connection from RabbitMQContainer" See gh-42443 --- .../connection/ContainerConnectionSource.java | 7 +- .../pages/testing/testcontainers.adoc | 10 +- ...nectionDetailsFactoryIntegrationTests.java | 1 - ...nectionDetailsFactoryIntegrationTests.java | 32 ++-- ...nectionDetailsFactoryIntegrationTests.java | 167 ++++++++++++++++++ ...nectionDetailsFactoryIntegrationTests.java | 161 +++++++++++++++++ .../RabbitStreamConfiguration.java | 9 +- .../RabbitStreamConnectionDetails.java | 24 +-- ...bbitContainerConnectionDetailsFactory.java | 2 +- ...reamContainerConnectionDetailsFactory.java | 15 +- 10 files changed, 390 insertions(+), 38 deletions(-) create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java diff --git a/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSource.java b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSource.java index f08067721e23..76a23ab82b95 100644 --- a/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSource.java +++ b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSource.java @@ -167,7 +167,12 @@ Supplier getContainerSupplier() { return this.containerSupplier; } - Set> getConnectionDetailsTypes() { + /** + * Returns the requested connection details types. + * @return the requested connection details types. + * @since 4.1.0 + */ + public Set> getConnectionDetailsTypes() { return this.connectionDetailsTypes; } diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc index 4df927c0a0b5..4a71a35fc804 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc @@ -176,6 +176,9 @@ javadoc:org.testcontainers.oracle.OracleContainer[OracleContainer (free)], javad | javadoc:org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails[] | Containers of type javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] +| javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] +| Containers of type javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] when the `@ServiceConnection` `type` attribute includes javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] + | javadoc:org.springframework.boot.data.redis.autoconfigure.DataRedisConnectionDetails[] | Containers of type javadoc:com.redis.testcontainers.RedisContainer[] or javadoc:com.redis.testcontainers.RedisStackContainer[], or containers named "redis", "redis/redis-stack" or "redis/redis-stack-server" @@ -185,10 +188,13 @@ javadoc:org.testcontainers.oracle.OracleContainer[OracleContainer (free)], javad [TIP] ==== -By default all applicable connection details beans will be created for a given javadoc:org.testcontainers.containers.Container[]. +By default, with the exception of javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[], all applicable connection details beans will be created for a given javadoc:org.testcontainers.containers.Container[]. For example, a javadoc:{url-testcontainers-postgresql-javadoc}/org.testcontainers.postgresql.PostgreSQLContainer[] will create both javadoc:org.springframework.boot.jdbc.autoconfigure.JdbcConnectionDetails[] and javadoc:org.springframework.boot.r2dbc.autoconfigure.R2dbcConnectionDetails[]. If you want to create only a subset of the applicable types, you can use the `type` attribute of javadoc:org.springframework.boot.testcontainers.service.connection.ServiceConnection[format=annotation]. + +To create a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] bean from a javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[], you must opt in using the `type` attribute of javadoc:org.springframework.boot.testcontainers.service.connection.ServiceConnection[format=annotation]. +The container must also expose port 5552, the RabbitMQ streams port. ==== By default `Container.getDockerImageName().getRepository()` is used to obtain the name used to find connection details. @@ -229,7 +235,7 @@ The SSL annotations are supported for the following service connections: * Elasticsearch * Kafka * MongoDB -* RabbitMQ +* RabbitMQ (excluding streams) * Redis The `ElasticsearchContainer` additionally supports automatic detection of server side SSL. diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java index 898f9fad3dc5..ea80da8d9bf3 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java @@ -71,7 +71,6 @@ void connectionCanBeMadeToRabbitContainer() { this.rabbitTemplate.convertAndSend("test", "message"); Awaitility.waitAtMost(Duration.ofMinutes(4)) .untilAsserted(() -> assertThat(this.listener.messages).containsExactly("message")); - } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java index 53d1117e3993..1e65d213f2ac 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-present the original author 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,16 +24,19 @@ import com.rabbitmq.stream.Environment; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; -import org.testcontainers.containers.RabbitMQContainer; import org.testcontainers.images.builder.Transferable; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.rabbitmq.RabbitMQContainer; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; @@ -47,9 +50,11 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitStreamContainerConnectionDetailsFactory}. + * Tests for {@link RabbitStreamContainerConnectionDetailsFactory} with a single container + * that's only used for streams. * * @author Eddú Meléndez + * @author Andy Wilkinson */ @SpringJUnitConfig @TestPropertySource( @@ -60,7 +65,7 @@ class RabbitStreamContainerConnectionDetailsFactoryIntegrationTests { private static final int RABBITMQ_STREAMS_PORT = 5552; @Container - @ServiceConnection + @ServiceConnection(type = RabbitStreamConnectionDetails.class) static final RabbitMQContainer rabbit = getRabbitMqStreamContainer(); private static RabbitMQContainer getRabbitMqStreamContainer() { @@ -72,7 +77,10 @@ private static RabbitMQContainer getRabbitMqStreamContainer() { } @Autowired(required = false) - private RabbitStreamConnectionDetails connectionDetails; + private RabbitConnectionDetails connectionDetails; + + @Autowired(required = false) + private RabbitStreamConnectionDetails streamConnectionDetails; @Autowired private RabbitStreamTemplate rabbitStreamTemplate; @@ -82,11 +90,11 @@ private static RabbitMQContainer getRabbitMqStreamContainer() { @Test void connectionCanBeMadeToRabbitContainer() { - assertThat(this.connectionDetails).isNotNull(); + assertThat(this.connectionDetails).isNotInstanceOf(RabbitMqContainerConnectionDetails.class); + assertThat(this.streamConnectionDetails).isInstanceOf(RabbitMqStreamContainerConnectionDetails.class); this.rabbitStreamTemplate.convertAndSend("message"); Awaitility.waitAtMost(Duration.ofMinutes(4)) .untilAsserted(() -> assertThat(this.listener.messages).containsExactly("message")); - } @Configuration(proxyBeanMethods = false) @@ -95,17 +103,13 @@ static class TestConfiguration { @Bean StreamAdmin streamAdmin(Environment env) { - return new StreamAdmin(env, sc -> { - sc.stream("stream.queue1").create(); - }); + return new StreamAdmin(env, (sc) -> sc.stream("stream.queue1").create()); } @Bean EnvironmentBuilderCustomizer environmentBuilderCustomizer() { - return env -> { - Address entrypoint = new Address(rabbit.getHost(), rabbit.getMappedPort(RABBITMQ_STREAMS_PORT)); - env.addressResolver(address -> entrypoint); - }; + return (env) -> env.addressResolver( + (address) -> new Address(rabbit.getHost(), rabbit.getMappedPort(RABBITMQ_STREAMS_PORT))); } @Bean diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..778f6536bd57 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import com.rabbitmq.stream.Address; +import com.rabbitmq.stream.Environment; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.rabbitmq.RabbitMQContainer; + +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.TestImage; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; +import org.springframework.rabbit.stream.support.StreamAdmin; +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 RabbitStreamContainerConnectionDetailsFactory} with two containers, + * one for streams and one for standard messaging. + * + * @author Eddú Meléndez + * @author Andy Wilkinson + */ +@SpringJUnitConfig +@TestPropertySource( + properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) +@Testcontainers(disabledWithoutDocker = true) +class SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests { + + private static final int RABBITMQ_STREAMS_PORT = 5552; + + @Container + @ServiceConnection + static final RabbitMQContainer rabbit = TestImage.container(RabbitMQContainer.class); + + @Container + @ServiceConnection(type = RabbitStreamConnectionDetails.class) + static final RabbitMQContainer rabbitStream = getRabbitMqStreamContainer(); + + private static RabbitMQContainer getRabbitMqStreamContainer() { + RabbitMQContainer container = TestImage.container(RabbitMQContainer.class); + container.addExposedPorts(RABBITMQ_STREAMS_PORT); + String enabledPlugins = "[rabbitmq_stream,rabbitmq_prometheus]."; + container.withCopyToContainer(Transferable.of(enabledPlugins), "/etc/rabbitmq/enabled_plugins"); + return container; + } + + @Autowired(required = false) + private RabbitConnectionDetails connectionDetails; + + @Autowired(required = false) + private RabbitStreamConnectionDetails streamConnectionDetails; + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Autowired + private RabbitStreamTemplate rabbitStreamTemplate; + + @Autowired + private StreamTestListener streamListener; + + @Test + void rabbitConnectionDetailsAreSourcedFromContainer() { + assertThat(this.connectionDetails).isInstanceOf(RabbitMqContainerConnectionDetails.class); + assertThat(this.connectionDetails.getFirstAddress().port()).isEqualTo(rabbit.getAmqpPort()); + } + + @Test + void rabbitStreamConnectionDetailsAreSourcedFromContainer() { + assertThat(this.streamConnectionDetails).isInstanceOf(RabbitMqStreamContainerConnectionDetails.class); + assertThat(this.streamConnectionDetails.getPort()).isEqualTo(rabbitStream.getMappedPort(RABBITMQ_STREAMS_PORT)); + } + + @Test + void connectionCanBeMadeToRabbitContainer() { + this.rabbitTemplate.convertAndSend("test", "message"); + Awaitility.waitAtMost(Duration.ofMinutes(4)) + .untilAsserted(() -> assertThat(this.rabbitTemplate.receive("test")) + .extracting((message) -> new String(message.getBody(), StandardCharsets.UTF_8)) + .isEqualTo("message")); + } + + @Test + void streamConnectionCanBeMadeToRabbitContainer() { + this.rabbitStreamTemplate.convertAndSend("message"); + Awaitility.waitAtMost(Duration.ofMinutes(4)) + .untilAsserted(() -> assertThat(this.streamListener.messages).containsExactly("message")); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(RabbitAutoConfiguration.class) + static class TestConfiguration { + + TestConfiguration(AmqpAdmin amqpAdmin) { + amqpAdmin.declareQueue(new Queue("test")); + } + + @Bean + StreamAdmin streamAdmin(Environment env) { + return new StreamAdmin(env, (sc) -> sc.stream("stream.queue1").create()); + } + + @Bean + EnvironmentBuilderCustomizer environmentBuilderCustomizer() { + return (env) -> env.addressResolver((address) -> new Address(rabbitStream.getHost(), + rabbitStream.getMappedPort(RABBITMQ_STREAMS_PORT))); + } + + @Bean + StreamTestListener streamTestListener() { + return new StreamTestListener(); + } + + } + + static class StreamTestListener { + + private final List messages = new ArrayList<>(); + + @RabbitListener(queues = "stream.queue1") + void processMessage(String message) { + this.messages.add(message); + } + + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..6dc8b12291c3 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,161 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import com.rabbitmq.stream.Address; +import com.rabbitmq.stream.Environment; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.rabbitmq.RabbitMQContainer; + +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.TestImage; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; +import org.springframework.rabbit.stream.support.StreamAdmin; +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 RabbitStreamContainerConnectionDetailsFactory} with a single container + * used for both streams and standard messaging. + * + * @author Eddú Meléndez + * @author Andy Wilkinson + */ +@SpringJUnitConfig +@TestPropertySource( + properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) +@Testcontainers(disabledWithoutDocker = true) +class SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests { + + private static final int RABBITMQ_STREAMS_PORT = 5552; + + @Container + @ServiceConnection(type = { RabbitConnectionDetails.class, RabbitStreamConnectionDetails.class }) + static final RabbitMQContainer rabbit = getRabbitMqStreamContainer(); + + private static RabbitMQContainer getRabbitMqStreamContainer() { + RabbitMQContainer container = TestImage.container(RabbitMQContainer.class); + container.addExposedPorts(RABBITMQ_STREAMS_PORT); + String enabledPlugins = "[rabbitmq_stream,rabbitmq_prometheus]."; + container.withCopyToContainer(Transferable.of(enabledPlugins), "/etc/rabbitmq/enabled_plugins"); + return container; + } + + @Autowired(required = false) + private RabbitConnectionDetails connectionDetails; + + @Autowired(required = false) + private RabbitStreamConnectionDetails streamConnectionDetails; + + @Autowired + private RabbitStreamTemplate rabbitStreamTemplate; + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Autowired + private StreamTestListener streamListener; + + @Test + void rabbitConnectionDetailsAreSourcedFromContainer() { + assertThat(this.connectionDetails).isInstanceOf(RabbitMqContainerConnectionDetails.class); + } + + @Test + void rabbitStreamConnectionDetailsAreSourcedFromContainer() { + assertThat(this.streamConnectionDetails).isInstanceOf(RabbitMqStreamContainerConnectionDetails.class); + } + + @Test + void connectionCanBeMadeToRabbitContainer() { + this.rabbitTemplate.convertAndSend("test", "message"); + Awaitility.waitAtMost(Duration.ofMinutes(4)) + .untilAsserted(() -> assertThat(this.rabbitTemplate.receive("test")) + .extracting((message) -> new String(message.getBody(), StandardCharsets.UTF_8)) + .isEqualTo("message")); + } + + @Test + void streamConnectionCanBeMadeToRabbitContainer() { + this.rabbitStreamTemplate.convertAndSend("message"); + Awaitility.waitAtMost(Duration.ofMinutes(4)) + .untilAsserted(() -> assertThat(this.streamListener.messages).containsExactly("message")); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(RabbitAutoConfiguration.class) + static class TestConfiguration { + + TestConfiguration(AmqpAdmin amqpAdmin) { + amqpAdmin.declareQueue(new Queue("test")); + } + + @Bean + StreamAdmin streamAdmin(Environment env) { + return new StreamAdmin(env, (sc) -> sc.stream("stream.queue1").create()); + } + + @Bean + EnvironmentBuilderCustomizer environmentBuilderCustomizer() { + return (env) -> env.addressResolver( + (address) -> new Address(rabbit.getHost(), rabbit.getMappedPort(RABBITMQ_STREAMS_PORT))); + } + + @Bean + StreamTestListener streamTestListener() { + return new StreamTestListener(); + } + + } + + static class StreamTestListener { + + private final List messages = new ArrayList<>(); + + @RabbitListener(queues = "stream.queue1") + void processMessage(String message) { + this.messages.add(message); + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java index b06fa1909c7d..0b396dd01f7e 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java @@ -18,6 +18,7 @@ import com.rabbitmq.stream.Environment; import com.rabbitmq.stream.EnvironmentBuilder; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.config.ContainerCustomizer; import org.springframework.amqp.support.converter.MessageConverter; @@ -52,7 +53,7 @@ class RabbitStreamConfiguration { @Bean - @ConditionalOnMissingBean(RabbitStreamConnectionDetails.class) + @ConditionalOnMissingBean RabbitStreamConnectionDetails rabbitStreamConnectionDetails(RabbitProperties rabbitProperties) { return new PropertiesRabbitStreamConnectionDetails(rabbitProperties.getStream()); } @@ -152,17 +153,17 @@ public int getPort() { } @Override - public String getVirtualHost() { + public @Nullable String getVirtualHost() { return this.streamProperties.getVirtualHost(); } @Override - public String getUsername() { + public @Nullable String getUsername() { return this.streamProperties.getUsername(); } @Override - public String getPassword() { + public @Nullable String getPassword() { return this.streamProperties.getPassword(); } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java index de630e6f9d99..8b08349167d8 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-present the original author 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.amqp.autoconfigure; +import org.jspecify.annotations.Nullable; + import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; /** @@ -27,30 +29,30 @@ public interface RabbitStreamConnectionDetails extends ConnectionDetails { /** - * Rabbit server host. - * @return the rabbit server host + * Rabbit Stream server host. + * @return the Rabbit Stream server host */ String getHost(); /** * Rabbit Stream server port. - * @return the rabbit stream server port + * @return the Rabbit Stream server port */ int getPort(); /** - * Login user to authenticate to the broker. - * @return the login user to authenticate to the broker or {@code null} + * Username for authentication. + * @return the username for authentication or {@code null} */ - default String getUsername() { + default @Nullable String getUsername() { return null; } /** - * Login to authenticate against the broker. - * @return the login to authenticate against the broker or {@code null} + * Password for authentication. + * @return the password for authentication or {@code null} */ - default String getPassword() { + default @Nullable String getPassword() { return null; } @@ -58,7 +60,7 @@ default String getPassword() { * Virtual host to use when connecting to the broker. * @return the virtual host to use when connecting to the broker or {@code null} */ - default String getVirtualHost() { + default @Nullable String getVirtualHost() { return null; } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java index 8049dd52218c..fd47f98fd478 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java @@ -49,7 +49,7 @@ protected RabbitConnectionDetails getContainerConnectionDetails( /** * {@link RabbitConnectionDetails} backed by a {@link ContainerConnectionSource}. */ - private static final class RabbitMqContainerConnectionDetails extends ContainerConnectionDetails + static final class RabbitMqContainerConnectionDetails extends ContainerConnectionDetails implements RabbitConnectionDetails { private RabbitMqContainerConnectionDetails(ContainerConnectionSource source) { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java index fb364a90703c..8d2bf2bc9dc4 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-present the original author 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,7 @@ package org.springframework.boot.amqp.testcontainers; -import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.rabbitmq.RabbitMQContainer; import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; @@ -37,6 +37,13 @@ class RabbitStreamContainerConnectionDetailsFactory super(ANY_CONNECTION_NAME, "org.springframework.rabbit.stream.producer.RabbitStreamTemplate"); } + @Override + protected boolean sourceAccepts(ContainerConnectionSource source, Class requiredContainerType, + Class requiredConnectionDetailsType) { + return source.getConnectionDetailsTypes().contains(requiredConnectionDetailsType) + && super.sourceAccepts(source, requiredContainerType, requiredConnectionDetailsType); + } + @Override protected RabbitStreamConnectionDetails getContainerConnectionDetails( ContainerConnectionSource source) { @@ -47,8 +54,8 @@ protected RabbitStreamConnectionDetails getContainerConnectionDetails( * {@link RabbitStreamConnectionDetails} backed by a * {@link ContainerConnectionSource}. */ - private static final class RabbitMqStreamContainerConnectionDetails - extends ContainerConnectionDetails implements RabbitStreamConnectionDetails { + static final class RabbitMqStreamContainerConnectionDetails extends ContainerConnectionDetails + implements RabbitStreamConnectionDetails { private RabbitMqStreamContainerConnectionDetails(ContainerConnectionSource source) { super(source); From 12c6292708d60d5a6870a44264908f3d24c22b1b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Feb 2026 17:13:22 +0000 Subject: [PATCH 193/930] Support RabbitMQ Streams with Docker Compose Closes gh-49162 --- .../pages/features/dev-services.adoc | 7 +- ...nectionDetailsFactoryIntegrationTests.java | 68 ++++++++++++ ...nectionDetailsFactoryIntegrationTests.java | 45 ++++++++ ...nectionDetailsFactoryIntegrationTests.java | 61 +++++++++++ ...bbit-stream-separate-services-compose.yaml | 21 ++++ ...-rabbit-stream-single-service-compose.yaml | 15 +++ .../docker/compose/rabbit-stream-compose.yaml | 14 +++ ...DockerComposeConnectionDetailsFactory.java | 18 +-- ...DockerComposeConnectionDetailsFactory.java | 103 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + 10 files changed, 344 insertions(+), 9 deletions(-) create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc index 038d1600964d..3cc2eabfd80a 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc @@ -126,7 +126,10 @@ The following service connections are currently supported: | Containers named "clickhouse/clickhouse-server", "gvenzl/oracle-free", "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres" | javadoc:org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails[] -| Containers named "rabbitmq" +| Containers named "rabbitmq" with container port 5672 mapped + +| javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] +| Containers named "rabbitmq" with container port 5552 mapped | javadoc:org.springframework.boot.data.redis.autoconfigure.DataRedisConnectionDetails[] | Containers named "redis", "redis/redis-stack" or "redis/redis-stack-server" @@ -151,7 +154,7 @@ SSL is supported for the following service connections: * Elasticsearch * Kafka * MongoDB -* RabbitMQ +* RabbitMQ (excluding streams) * Redis To enable SSL support for a service, you can use https://docs.docker.com/reference/compose-file/services/#labels[service labels]. diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..5b1c7797bbac --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.testsupport.container.TestImage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory} and + * {@link RabbitStreamDockerComposeConnectionDetailsFactory} when used in combination. + * + * @author Andy Wilkinson + */ +class RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests { + + @DockerComposeTest(composeFile = "rabbit-and-rabbit-stream-separate-services-compose.yaml", + image = TestImage.RABBITMQ) + void runCreatesConnectionDetailsFromSeparateServices(RabbitConnectionDetails connectionDetails, + RabbitStreamConnectionDetails streamConnectionDetails) { + assertConnectionDetails(connectionDetails); + assertConnectionDetails(streamConnectionDetails); + } + + @DockerComposeTest(composeFile = "rabbit-and-rabbit-stream-single-service-compose.yaml", image = TestImage.RABBITMQ) + void runCreatesConnectionDetailsFromSingleService(RabbitConnectionDetails connectionDetails, + RabbitStreamConnectionDetails streamConnectionDetails) { + assertConnectionDetails(connectionDetails); + assertConnectionDetails(streamConnectionDetails); + } + + private void assertConnectionDetails(RabbitConnectionDetails connectionDetails) { + 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); + } + + private void assertConnectionDetails(RabbitStreamConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getVirtualHost()).isEqualTo("/"); + assertThat(connectionDetails.getHost()).isNotNull(); + assertThat(connectionDetails.getPort()).isGreaterThan(0); + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..1547fbf825c4 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.testsupport.container.TestImage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link RabbitStreamDockerComposeConnectionDetailsFactory}. + * + * @author Andy Wilkinson + */ +class RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests { + + @DockerComposeTest(composeFile = "rabbit-stream-compose.yaml", image = TestImage.RABBITMQ) + void runCreatesConnectionDetails(RabbitStreamConnectionDetails connectionDetails) { + assertConnectionDetails(connectionDetails); + } + + private void assertConnectionDetails(RabbitStreamConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getVirtualHost()).isEqualTo("/"); + assertThat(connectionDetails.getHost()).isNotNull(); + assertThat(connectionDetails.getPort()).isGreaterThan(0); + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..d9f077bc21cd --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.testsupport.container.TestImage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory} and + * {@link RabbitStreamDockerComposeConnectionDetailsFactory} when using a single service + * for standard and stream-based messaging. + * + * @author Andy Wilkinson + */ +class SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests { + + @DockerComposeTest(composeFile = "rabbit-and-rabbit-stream-single-service-compose.yaml", image = TestImage.RABBITMQ) + void runCreatesConnectionDetails(RabbitConnectionDetails connectionDetails, + RabbitStreamConnectionDetails streamConnectionDetails) { + assertConnectionDetails(connectionDetails); + assertConnectionDetails(streamConnectionDetails); + } + + private void assertConnectionDetails(RabbitConnectionDetails connectionDetails) { + 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); + } + + private void assertConnectionDetails(RabbitStreamConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getVirtualHost()).isEqualTo("/"); + assertThat(connectionDetails.getHost()).isNotNull(); + assertThat(connectionDetails.getPort()).isGreaterThan(0); + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml new file mode 100644 index 000000000000..b6ea102d7bb6 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml @@ -0,0 +1,21 @@ +services: + rabbitmq: + image: '{imageName}' + environment: + - 'RABBITMQ_DEFAULT_USER=myuser' + - 'RABBITMQ_DEFAULT_PASS=secret' + ports: + - '5672' + rabbitmq_streams: + image: '{imageName}' + environment: + - 'RABBITMQ_DEFAULT_USER=myuser' + - 'RABBITMQ_DEFAULT_PASS=secret' + configs: + - source: plugins + target: /etc/rabbitmq/enabled_plugins + ports: + - '5552' +configs: + plugins: + content: "[rabbitmq_stream]." diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml new file mode 100644 index 000000000000..71664cd07413 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml @@ -0,0 +1,15 @@ +services: + rabbitmq: + image: '{imageName}' + environment: + - 'RABBITMQ_DEFAULT_USER=myuser' + - 'RABBITMQ_DEFAULT_PASS=secret' + configs: + - source: plugins + target: /etc/rabbitmq/enabled_plugins + ports: + - '5552' + - '5672' +configs: + plugins: + content: "[rabbitmq_stream]." diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml new file mode 100644 index 000000000000..8da0233be107 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml @@ -0,0 +1,14 @@ +services: + rabbitmq: + image: '{imageName}' + environment: + - 'RABBITMQ_DEFAULT_USER=myuser' + - 'RABBITMQ_DEFAULT_PASS=secret' + configs: + - source: plugins + target: /etc/rabbitmq/enabled_plugins + ports: + - '5552' +configs: + plugins: + content: "[rabbitmq_stream]." diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java index 9dd286741483..a5564994cb76 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java @@ -21,18 +21,16 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; 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 RabbitConnectionDetails} - * for a {@code rabbitmq} service. + * {@link DockerComposeConnectionDetailsFactory} to create + * {@link RabbitStreamConnectionDetails} for a {@code rabbitmq} service. * - * @author Moritz Halbritter * @author Andy Wilkinson - * @author Phillip Webb - * @author Scott Frederick */ class RabbitDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { @@ -44,8 +42,14 @@ protected RabbitDockerComposeConnectionDetailsFactory() { } @Override - protected RabbitConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { - return new RabbitDockerComposeConnectionDetails(source.getRunningService()); + protected @Nullable RabbitConnectionDetails getDockerComposeConnectionDetails( + DockerComposeConnectionSource source) { + try { + return new RabbitDockerComposeConnectionDetails(source.getRunningService()); + } + catch (IllegalStateException ex) { + return null; + } } /** diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..e482c04c0220 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +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 RabbitConnectionDetails} + * for a {@code rabbitmq} service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Scott Frederick + */ +class RabbitStreamDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int RABBITMQ_STREAMS_PORT = 5552; + + protected RabbitStreamDockerComposeConnectionDetailsFactory() { + super("rabbitmq"); + } + + @Override + protected @Nullable RabbitStreamConnectionDetails getDockerComposeConnectionDetails( + DockerComposeConnectionSource source) { + try { + return new RabbitStreamDockerComposeConnectionDetails(source.getRunningService()); + } + catch (IllegalStateException ex) { + return null; + } + } + + /** + * {@link RabbitStreamConnectionDetails} backed by a {@code rabbitmq} + * {@link RunningService}. + */ + static class RabbitStreamDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements RabbitStreamConnectionDetails { + + private final RabbitEnvironment environment; + + private final String host; + + private final int port; + + protected RabbitStreamDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new RabbitEnvironment(service.env()); + this.host = service.host(); + this.port = service.ports().get(RABBITMQ_STREAMS_PORT); + } + + @Override + public @Nullable String getUsername() { + return this.environment.getUsername(); + } + + @Override + public @Nullable String getPassword() { + return this.environment.getPassword(); + } + + @Override + public String getVirtualHost() { + return "/"; + } + + @Override + public String getHost() { + return this.host; + } + + @Override + public int getPort() { + return this.port; + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories b/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories index 9f8225b9b3bc..17fb2095415a 100644 --- a/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories @@ -1,6 +1,7 @@ # Connection Details Factories org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ org.springframework.boot.amqp.docker.compose.RabbitDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.amqp.docker.compose.RabbitStreamDockerComposeConnectionDetailsFactory,\ org.springframework.boot.amqp.testcontainers.DeprecatedRabbitContainerConnectionDetailsFactory,\ org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory,\ org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory From be750eb3066720cf3fdd244713612d0dcb4e1984 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Feb 2026 09:06:57 +0000 Subject: [PATCH 194/930] Adapt to deprecation of support for Dynatrace V1 API See gh-49111 --- .../reference/pages/actuator/metrics.adoc | 10 ++++++--- .../export/dynatrace/DynatraceProperties.java | 10 +++++++++ .../DynatracePropertiesConfigAdapter.java | 19 ++++++++++++----- ...DynatracePropertiesConfigAdapterTests.java | 21 ++++++++++++++++--- .../dynatrace/DynatracePropertiesTests.java | 10 ++++++++- 5 files changed, 58 insertions(+), 12 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc index 7a1bd819870d..84e572bbea16 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc @@ -174,9 +174,13 @@ management: Dynatrace offers two metrics ingest APIs, both of which are implemented for {url-micrometer-docs-implementations}/dynatrace[Micrometer]. You can find the Dynatrace documentation on Micrometer metrics ingest {url-dynatrace-docs-shortlink}/micrometer-metrics-ingest[here]. + Configuration properties in the `v1` namespace apply only when exporting to the {url-dynatrace-docs-shortlink}/api-metrics[Timeseries v1 API]. +Support for the V1 API is deprecated. + Configuration properties in the `v2` namespace apply only when exporting to the {url-dynatrace-docs-shortlink}/api-metrics-v2-post-datapoints[Metrics v2 API]. -Note that this integration can export only to either the `v1` or `v2` version of the API at a time, with `v2` being preferred. + +Note that this integration can export only to either the `v1` or `v2` version of the API at a time, with `v2` being strongly recommended due to the deprecation of the v1 API. If the `device-id` (required for v1 but not used in v2) is set in the `v1` namespace, metrics are exported to the `v1` endpoint. Otherwise, `v2` is assumed. @@ -264,13 +268,13 @@ management: [[actuator.metrics.export.dynatrace.v1-api]] -==== v1 API (Legacy) +==== v1 API (Deprecated) The Dynatrace v1 API metrics registry pushes metrics to the configured URI periodically by using the {url-dynatrace-docs-shortlink}/api-metrics[Timeseries v1 API]. For backwards-compatibility with existing setups, when `device-id` is set (required for v1, but not used in v2), metrics are exported to the Timeseries v1 endpoint. To export metrics to {url-micrometer-docs-implementations}/dynatrace[Dynatrace], your API token, device ID, and URI must be provided: -[configprops,yaml] +[configprops%novalidate,yaml] ---- management: dynatrace: diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatraceProperties.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatraceProperties.java index 46b070db1b2e..33d14914a1d4 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatraceProperties.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatraceProperties.java @@ -21,6 +21,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.micrometer.metrics.autoconfigure.export.properties.StepRegistryProperties; /** @@ -34,6 +35,7 @@ @ConfigurationProperties("management.dynatrace.metrics.export") public class DynatraceProperties extends StepRegistryProperties { + @Deprecated(since = "4.1.0", forRemoval = true) private final V1 v1 = new V1(); private final V2 v2 = new V2(); @@ -65,6 +67,7 @@ public void setUri(@Nullable String uri) { this.uri = uri; } + @Deprecated(since = "4.1.0", forRemoval = true) public V1 getV1() { return this.v1; } @@ -73,6 +76,7 @@ public V2 getV2() { return this.v2; } + @Deprecated(since = "4.1.0", forRemoval = true) public static class V1 { /** @@ -92,6 +96,8 @@ public static class V1 { */ private String technologyType = "java"; + @DeprecatedConfigurationProperty(reason = "Dynatrace V1 API is deprecated, use the V2 API instead.", + since = "4.1.0") public @Nullable String getDeviceId() { return this.deviceId; } @@ -100,6 +106,8 @@ public void setDeviceId(@Nullable String deviceId) { this.deviceId = deviceId; } + @DeprecatedConfigurationProperty(reason = "Dynatrace V1 API is deprecated, use the V2 API instead.", + since = "4.1.0") public @Nullable String getGroup() { return this.group; } @@ -108,6 +116,8 @@ public void setGroup(@Nullable String group) { this.group = group; } + @DeprecatedConfigurationProperty(reason = "Dynatrace V1 API is deprecated, use the V2 API instead.", + since = "4.1.0") public String getTechnologyType() { return this.technologyType; } diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatracePropertiesConfigAdapter.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatracePropertiesConfigAdapter.java index bed52c70d538..44838184e111 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatracePropertiesConfigAdapter.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatracePropertiesConfigAdapter.java @@ -22,7 +22,6 @@ import io.micrometer.dynatrace.DynatraceConfig; import org.jspecify.annotations.Nullable; -import org.springframework.boot.micrometer.metrics.autoconfigure.export.dynatrace.DynatraceProperties.V1; import org.springframework.boot.micrometer.metrics.autoconfigure.export.dynatrace.DynatraceProperties.V2; import org.springframework.boot.micrometer.metrics.autoconfigure.export.properties.StepRegistryPropertiesConfigAdapter; @@ -45,18 +44,23 @@ public String prefix() { } @Override + @SuppressWarnings("deprecation") public String apiToken() { return obtain(DynatraceProperties::getApiToken, DynatraceConfig.super::apiToken); } @Override + @SuppressWarnings("removal") + @Deprecated(since = "4.1.0", forRemoval = true) public String deviceId() { - return obtain(v1(V1::getDeviceId), DynatraceConfig.super::deviceId); + return obtain(v1(DynatraceProperties.V1::getDeviceId), DynatraceConfig.super::deviceId); } @Override + @SuppressWarnings("removal") + @Deprecated(since = "4.1.0", forRemoval = true) public String technologyType() { - return obtain(v1(V1::getTechnologyType), DynatraceConfig.super::technologyType); + return obtain(v1(DynatraceProperties.V1::getTechnologyType), DynatraceConfig.super::technologyType); } @Override @@ -65,11 +69,14 @@ public String uri() { } @Override + @SuppressWarnings("removal") + @Deprecated(since = "4.1.0", forRemoval = true) public @Nullable String group() { - return get(v1(V1::getGroup), DynatraceConfig.super::group); + return get(v1(DynatraceProperties.V1::getGroup), DynatraceConfig.super::group); } @Override + @SuppressWarnings({ "deprecation", "removal" }) public DynatraceApiVersion apiVersion() { return obtain((properties) -> (properties.getV1().getDeviceId() != null) ? DynatraceApiVersion.V1 : DynatraceApiVersion.V2, DynatraceConfig.super::apiVersion); @@ -100,7 +107,9 @@ public boolean exportMeterMetadata() { return obtain(v2(V2::isExportMeterMetadata), DynatraceConfig.super::exportMeterMetadata); } - private Getter v1(Getter getter) { + @SuppressWarnings("removal") + @Deprecated(since = "4.1.0", forRemoval = true) + private Getter v1(Getter getter) { return (properties) -> getter.get(properties.getV1()); } diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatracePropertiesConfigAdapterTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatracePropertiesConfigAdapterTests.java index 364c7c169c8c..9605b078159d 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatracePropertiesConfigAdapterTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatracePropertiesConfigAdapterTests.java @@ -53,6 +53,8 @@ void whenPropertiesApiTokenIsSetAdapterApiTokenReturnsIt() { } @Test + @SuppressWarnings("removal") + @Deprecated(since = "4.1.0", forRemoval = true) void whenPropertiesV1DeviceIdIsSetAdapterDeviceIdReturnsIt() { DynatraceProperties properties = new DynatraceProperties(); properties.getV1().setDeviceId("dev-1"); @@ -60,6 +62,8 @@ void whenPropertiesV1DeviceIdIsSetAdapterDeviceIdReturnsIt() { } @Test + @SuppressWarnings("removal") + @Deprecated(since = "4.1.0", forRemoval = true) void whenPropertiesV1TechnologyTypeIsSetAdapterTechnologyTypeReturnsIt() { DynatraceProperties properties = new DynatraceProperties(); properties.getV1().setTechnologyType("tech-1"); @@ -67,6 +71,8 @@ void whenPropertiesV1TechnologyTypeIsSetAdapterTechnologyTypeReturnsIt() { } @Test + @SuppressWarnings("removal") + @Deprecated(since = "4.1.0", forRemoval = true) void whenPropertiesV1GroupIsSetAdapterGroupReturnsIt() { DynatraceProperties properties = new DynatraceProperties(); properties.getV1().setGroup("group-1"); @@ -74,6 +80,8 @@ void whenPropertiesV1GroupIsSetAdapterGroupReturnsIt() { } @Test + @SuppressWarnings("removal") + @Deprecated(since = "4.1.0", forRemoval = true) void whenV1DeviceIdIsSetThenAdapterApiVersionIsV1() { DynatraceProperties properties = new DynatraceProperties(); properties.getV1().setDeviceId("dev-1"); @@ -123,13 +131,20 @@ void defaultValues() { DynatraceProperties properties = new DynatraceProperties(); assertThat(properties.getApiToken()).isNull(); assertThat(properties.getUri()).isNull(); - assertThat(properties.getV1().getDeviceId()).isNull(); - assertThat(properties.getV1().getTechnologyType()).isEqualTo("java"); - assertThat(properties.getV1().getGroup()).isNull(); assertThat(properties.getV2().getMetricKeyPrefix()).isNull(); assertThat(properties.getV2().isEnrichWithDynatraceMetadata()).isTrue(); assertThat(properties.getV2().getDefaultDimensions()).isNull(); assertThat(properties.getV2().isUseDynatraceSummaryInstruments()).isTrue(); } + @Test + @SuppressWarnings("removal") + @Deprecated(forRemoval = true, since = "4.1.0") + void v1DefaultValues() { + DynatraceProperties properties = new DynatraceProperties(); + assertThat(properties.getV1().getDeviceId()).isNull(); + assertThat(properties.getV1().getTechnologyType()).isEqualTo("java"); + assertThat(properties.getV1().getGroup()).isNull(); + } + } diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatracePropertiesTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatracePropertiesTests.java index 1fe51b7a6f89..c73aaf0fb111 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatracePropertiesTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/dynatrace/DynatracePropertiesTests.java @@ -35,10 +35,18 @@ void defaultValuesAreConsistent() { DynatraceProperties properties = new DynatraceProperties(); DynatraceConfig config = (key) -> null; assertStepRegistryDefaultValues(properties, config); - assertThat(properties.getV1().getTechnologyType()).isEqualTo(config.technologyType()); assertThat(properties.getV2().isUseDynatraceSummaryInstruments()) .isEqualTo(config.useDynatraceSummaryInstruments()); assertThat(properties.getV2().isExportMeterMetadata()).isEqualTo(config.exportMeterMetadata()); } + @Test + @SuppressWarnings("removal") + @Deprecated(since = "4.1.0", forRemoval = true) + void v1DefaultValuesAreConsistent() { + DynatraceProperties properties = new DynatraceProperties(); + DynatraceConfig config = (key) -> null; + assertThat(properties.getV1().getTechnologyType()).isEqualTo(config.technologyType()); + } + } From d26b6895efe7e0c2bc69d1b4cd69746a6a26f3f1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 15:17:18 +0000 Subject: [PATCH 195/930] Start building against Spring LDAP 4.1.0-M1 snapshots See gh-49119 --- .../embedded/EmbeddedLdapAutoConfiguration.java | 8 +++++--- .../autoconfigure/embedded/EmbeddedLdapProperties.java | 5 ----- .../boot/ldap/health/LdapHealthIndicator.java | 8 +++++--- .../ldap/autoconfigure/LdapAutoConfigurationTests.java | 4 +--- platform/spring-boot-dependencies/build.gradle | 2 +- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java index a2f51bc14195..f35fe581f3ed 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java @@ -244,9 +244,11 @@ LdapContextSource ldapContextSource(Environment environment, LdapProperties prop EmbeddedLdapProperties embeddedProperties) { LdapContextSource source = new LdapContextSource(); source.setBase(properties.getBase()); - if (embeddedProperties.getCredential().isAvailable()) { - source.setUserDn(embeddedProperties.getCredential().getUsername()); - source.setPassword(embeddedProperties.getCredential().getPassword()); + String username = embeddedProperties.getCredential().getUsername(); + String password = embeddedProperties.getCredential().getPassword(); + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + source.setUserDn(username); + source.setPassword(password); } source.setUrls(properties.determineUrls(environment)); return source; diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java index c43c72237db9..f3a7303ee08a 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java @@ -24,7 +24,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.convert.Delimiter; import org.springframework.core.io.Resource; -import org.springframework.util.StringUtils; /** * Configuration properties for Embedded LDAP. @@ -135,10 +134,6 @@ public void setPassword(@Nullable String password) { this.password = password; } - boolean isAvailable() { - return StringUtils.hasText(this.username) && StringUtils.hasText(this.password); - } - } public static class Ssl { diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/health/LdapHealthIndicator.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/health/LdapHealthIndicator.java index bb3b97477131..df021b8430bb 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/health/LdapHealthIndicator.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/health/LdapHealthIndicator.java @@ -37,7 +37,7 @@ */ public class LdapHealthIndicator extends AbstractHealthIndicator { - private static final ContextExecutor versionContextExecutor = new VersionContextExecutor(); + private static final ContextExecutor<@Nullable String> versionContextExecutor = new VersionContextExecutor(); private final LdapOperations ldapOperations; @@ -50,10 +50,12 @@ public LdapHealthIndicator(LdapOperations ldapOperations) { @Override protected void doHealthCheck(Health.Builder builder) throws Exception { String version = this.ldapOperations.executeReadOnly(versionContextExecutor); - builder.up().withDetail("version", version); + if (version != null) { + builder.up().withDetail("version", version); + } } - private static final class VersionContextExecutor implements ContextExecutor { + private static final class VersionContextExecutor implements ContextExecutor<@Nullable String> { @Override public @Nullable String executeWithContext(DirContext ctx) throws NamingException { diff --git a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/LdapAutoConfigurationTests.java b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/LdapAutoConfigurationTests.java index 01b51af2e2a4..18ae1664608e 100644 --- a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/LdapAutoConfigurationTests.java +++ b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/LdapAutoConfigurationTests.java @@ -272,9 +272,7 @@ static class PooledContextSourceConfig { @Bean @Primary PooledContextSource pooledContextSource(LdapContextSource ldapContextSource) { - PooledContextSource pooledContextSource = new PooledContextSource(new PoolConfig()); - pooledContextSource.setContextSource(ldapContextSource); - return pooledContextSource; + return new PooledContextSource(ldapContextSource, new PoolConfig()); } } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 23e6252976c2..6b32acaf4a9e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2549,7 +2549,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-kafka/releases/tag/v{version}") } } - library("Spring LDAP", "4.0.1") { + library("Spring LDAP", "4.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.ldap") { modules = [ From 9e00a4fee690818d73d64947b8803a1c371e5701 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Feb 2026 10:52:39 +0000 Subject: [PATCH 196/930] Upgrade to Jakarta XML Bind 4.0.5 Closes gh-49170 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6b32acaf4a9e..1704e34da523 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -948,7 +948,7 @@ bom { .formatted(version.major(), version.minor()), "jakarta.ws.rs") } } - library("Jakarta XML Bind", "4.0.4") { + library("Jakarta XML Bind", "4.0.5") { group("jakarta.xml.bind") { modules = [ "jakarta.xml.bind-api" From adfc63a2c7a0c651fc4aa18766f338f43d6ff396 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Feb 2026 10:52:45 +0000 Subject: [PATCH 197/930] Upgrade to Logback 1.5.29 Closes gh-49171 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1704e34da523..5bb74f37339b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1340,7 +1340,7 @@ bom { releaseNotes("https://github.com/apache/logging-log4j2/releases/tag/rel%2F{version}") } } - library("Logback", "1.5.27") { + library("Logback", "1.5.29") { group("ch.qos.logback") { modules = [ "logback-classic", From af83a7bc5a6889898a4d690f800965adbfbad9a4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Feb 2026 10:52:49 +0000 Subject: [PATCH 198/930] Upgrade to Maven Dependency Plugin 3.10.0 Closes gh-49172 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 5bb74f37339b..6483fddc58d1 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1423,7 +1423,7 @@ bom { releaseNotes("https://github.com/apache/maven-compiler-plugin/releases/tag/maven-compiler-plugin-{version}") } } - library("Maven Dependency Plugin", "3.9.0") { + library("Maven Dependency Plugin", "3.10.0") { group("org.apache.maven.plugins") { plugins = [ "maven-dependency-plugin" From 2612490dacf20270d4ece01068ea9eff64eda989 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Feb 2026 10:52:55 +0000 Subject: [PATCH 199/930] Upgrade to Micrometer 1.17.0-M2 Closes gh-49173 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6483fddc58d1..d4c463a81eb3 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1580,7 +1580,7 @@ bom { releaseNotes("https://github.com/apache/maven-war-plugin/releases/tag/maven-war-plugin-{version}") } } - library("Micrometer", "1.17.0-SNAPSHOT") { + library("Micrometer", "1.17.0-M2") { considerSnapshots() group("io.micrometer") { modules = [ From 8f3a7a340cff95d03fbdbebbde079f741f4cf53a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Feb 2026 10:53:01 +0000 Subject: [PATCH 200/930] Upgrade to Micrometer Tracing 1.7.0-M3 Closes gh-49174 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d4c463a81eb3..a8aae30405c0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1603,7 +1603,7 @@ bom { releaseNotes("https://github.com/micrometer-metrics/micrometer/releases/tag/v{version}") } } - library("Micrometer Tracing", "1.7.0-SNAPSHOT") { + library("Micrometer Tracing", "1.7.0-M3") { considerSnapshots() group("io.micrometer") { bom("micrometer-tracing-bom") From 1b86ac411a7f34e0377ed06041c2600e91bc77aa Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Feb 2026 10:53:06 +0000 Subject: [PATCH 201/930] Upgrade to SQLite JDBC 3.51.2.0 Closes gh-49175 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a8aae30405c0..5c82006b5c70 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2652,7 +2652,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ws/releases/tag/v{version}") } } - library("SQLite JDBC", "3.51.1.0") { + library("SQLite JDBC", "3.51.2.0") { group("org.xerial") { modules = [ "sqlite-jdbc" From 17b4cbf6a1bdca4a6ed451f4455272cbc98d1405 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Fri, 23 Jan 2026 17:39:04 -0800 Subject: [PATCH 202/930] Add compression mode OTLP property See https://github.com/micrometer-metrics/micrometer/issues/7009 See gh-48957 --- .../export/otlp/OtlpMetricsProperties.java | 14 ++++++++++++++ .../otlp/OtlpMetricsPropertiesConfigAdapter.java | 6 ++++++ .../OtlpMetricsPropertiesConfigAdapterTests.java | 12 ++++++++++++ .../export/otlp/OtlpMetricsPropertiesTests.java | 1 + 4 files changed, 33 insertions(+) diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java index 0c77d1f60582..e267cc37e32f 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java @@ -21,6 +21,7 @@ import java.util.concurrent.TimeUnit; import io.micrometer.registry.otlp.AggregationTemporality; +import io.micrometer.registry.otlp.CompressionMode; import io.micrometer.registry.otlp.HistogramFlavor; import org.jspecify.annotations.Nullable; @@ -49,6 +50,11 @@ public class OtlpMetricsProperties extends StepRegistryProperties { */ private AggregationTemporality aggregationTemporality = AggregationTemporality.CUMULATIVE; + /** + * Compression mode for metric data sender. + */ + private CompressionMode compressionMode = CompressionMode.NONE; + /** * Headers for the exported metrics. */ @@ -96,6 +102,14 @@ public void setAggregationTemporality(AggregationTemporality aggregationTemporal this.aggregationTemporality = aggregationTemporality; } + public CompressionMode getCompressionMode() { + return this.compressionMode; + } + + public void setCompressionMode(CompressionMode compressionMode) { + this.compressionMode = compressionMode; + } + public @Nullable Map getHeaders() { return this.headers; } diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java index 122aaf1ca2d3..80dfe0f2f7b6 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java @@ -22,6 +22,7 @@ import java.util.concurrent.TimeUnit; import io.micrometer.registry.otlp.AggregationTemporality; +import io.micrometer.registry.otlp.CompressionMode; import io.micrometer.registry.otlp.HistogramFlavor; import io.micrometer.registry.otlp.OtlpConfig; @@ -72,6 +73,11 @@ public AggregationTemporality aggregationTemporality() { return obtain(OtlpMetricsProperties::getAggregationTemporality, OtlpConfig.super::aggregationTemporality); } + @Override + public CompressionMode compressionMode() { + return obtain(OtlpMetricsProperties::getCompressionMode, OtlpConfig.super::compressionMode); + } + @Override public Map resourceAttributes() { Map resourceAttributes = new LinkedHashMap<>(); diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java index 116c37eda323..2fe0e850b739 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java @@ -20,6 +20,7 @@ import java.util.concurrent.TimeUnit; import io.micrometer.registry.otlp.AggregationTemporality; +import io.micrometer.registry.otlp.CompressionMode; import io.micrometer.registry.otlp.HistogramFlavor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -88,6 +89,17 @@ void whenPropertiesAggregationTemporalityIsSetAdapterAggregationTemporalityRetur assertThat(createAdapter().aggregationTemporality()).isSameAs(AggregationTemporality.DELTA); } + @Test + void whenPropertiesCompressionModeIsNotSetAdapterCompressionModeReturnsNone() { + assertThat(createAdapter().compressionMode()).isSameAs(CompressionMode.NONE); + } + + @Test + void whenPropertiesCompressionModeIsSetAdapterCompressionModeReturnsIt() { + this.properties.setCompressionMode(CompressionMode.GZIP); + assertThat(createAdapter().compressionMode()).isSameAs(CompressionMode.GZIP); + } + @Test void whenOpenTelemetryPropertiesResourceAttributesIsSetAdapterResourceAttributesReturnsIt() { this.openTelemetryProperties.setResourceAttributes(Map.of("service.name", "boot-service")); diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesTests.java index 47eeffc0948f..c34f0c39f209 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesTests.java @@ -36,6 +36,7 @@ void defaultValuesAreConsistent() { OtlpConfig config = OtlpConfig.DEFAULT; assertStepRegistryDefaultValues(properties, config); assertThat(properties.getAggregationTemporality()).isSameAs(config.aggregationTemporality()); + assertThat(properties.getCompressionMode()).isSameAs(config.compressionMode()); assertThat(properties.getHistogramFlavor()).isSameAs(config.histogramFlavor()); assertThat(properties.getMaxScale()).isEqualTo(config.maxScale()); assertThat(properties.getMaxBucketCount()).isEqualTo(config.maxBucketCount()); From f67f232e69ae4422dd48c316972075abe6b71ae4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Feb 2026 11:10:26 +0000 Subject: [PATCH 203/930] Polish "Add compression mode OTLP property" See gh-48957 --- .../autoconfigure/export/otlp/OtlpMetricsProperties.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java index e267cc37e32f..62577bdfdcbf 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java @@ -51,7 +51,7 @@ public class OtlpMetricsProperties extends StepRegistryProperties { private AggregationTemporality aggregationTemporality = AggregationTemporality.CUMULATIVE; /** - * Compression mode for metric data sender. + * Compression mode to use when exporting metrics. */ private CompressionMode compressionMode = CompressionMode.NONE; From 89cc1e7a68ace93b98c8958f706c3ae8e38c2648 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 10 Feb 2026 16:28:25 -0800 Subject: [PATCH 204/930] Upgrade to Elasticsearch Client 9.2.5 Closes gh-49177 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 5c82006b5c70..4a9231aafe9a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -401,7 +401,7 @@ bom { releaseNotes("https://github.com/ehcache/ehcache3/releases/tag/v{version}") } } - library("Elasticsearch Client", "9.2.4") { + library("Elasticsearch Client", "9.2.5") { prohibit { contains "-alpha" contains "-beta" From d638e179014ad58f767415f879174874f75a5c33 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 11 Feb 2026 09:19:46 +0000 Subject: [PATCH 205/930] Upgrade to Hibernate 7.2.4.Final Closes gh-49180 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4a9231aafe9a..5ddf99b631c7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -612,7 +612,7 @@ bom { releaseNotes("https://github.com/hazelcast/hazelcast/releases/tag/v{version}") } } - library("Hibernate", "7.2.3.Final") { + library("Hibernate", "7.2.4.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From f4d3283bb24c9763268385fb58765959fc8bac9d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 11 Feb 2026 09:19:51 +0000 Subject: [PATCH 206/930] Upgrade to HttpCore5 5.4.1 Closes gh-49181 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 5ddf99b631c7..9fc10f6c5bfc 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -721,7 +721,7 @@ bom { ] } } - library("HttpCore5", "5.4") { + library("HttpCore5", "5.4.1") { group("org.apache.httpcomponents.core5") { modules = [ "httpcore5", From 40ffe8493e8feda8618a7f98267a30aa00611ff0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 11 Feb 2026 09:19:55 +0000 Subject: [PATCH 207/930] Upgrade to Rabbit Stream Client 1.5.0 Closes gh-49182 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9fc10f6c5bfc..904aff4f38ea 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1984,7 +1984,7 @@ bom { releaseNotes("https://github.com/rabbitmq/rabbitmq-java-client/releases/tag/v{version}") } } - library("Rabbit Stream Client", "1.4.0") { + library("Rabbit Stream Client", "1.5.0") { group("com.rabbitmq") { modules = [ "stream-client" From ef7ec4cae79c72166bb41c0c500ed16c1d780688 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 11 Feb 2026 09:19:56 +0000 Subject: [PATCH 208/930] Upgrade to Reactor Bom 2025.0.3 Closes gh-49113 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 904aff4f38ea..262ab6c47bf2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2002,7 +2002,7 @@ bom { ] } } - library("Reactor Bom", "2025.0.3-SNAPSHOT") { + library("Reactor Bom", "2025.0.3") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From dc0047683087fb82d3ae03b451934f5dcd54afd2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Feb 2026 11:05:53 +0000 Subject: [PATCH 209/930] Upgrade to Artemis 2.51.0 Closes gh-49203 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ff8b92f09c12..1829b744e97d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -65,7 +65,7 @@ bom { releaseNotes("https://github.com/eclipse-ee4j/angus-mail/releases/tag/{version}") } } - library("Artemis", "2.50.0") { + library("Artemis", "2.51.0") { group("org.apache.artemis") { bom("artemis-bom") { permit("org.apache.activemq:artemis-amqp-protocol") From f85eb6edc3310abcfa5761a4630ef7b5369ec56d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Feb 2026 11:05:57 +0000 Subject: [PATCH 210/930] Upgrade to Postgresql 42.7.10 Closes gh-49204 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1829b744e97d..52dd1d14e93f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1825,7 +1825,7 @@ bom { javadoc("https://javadoc.io/doc/org.messaginghub/pooled-jms/{version}", "org.messaginghub.pooled.jms") } } - library("Postgresql", "42.7.9") { + library("Postgresql", "42.7.10") { group("org.postgresql") { modules = [ "postgresql" From 313ebce637f0ee9cd938dea972ab7df2caf646e1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Feb 2026 11:05:58 +0000 Subject: [PATCH 211/930] Upgrade to Spring Framework 7.0.4 Closes gh-49116 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 795a568cc621..a11e61209751 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ mockitoVersion=5.21.0 nativeBuildToolsVersion=0.11.4 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.5 -springFrameworkVersion=7.0.4-SNAPSHOT +springFrameworkVersion=7.0.4 springFramework60xVersion=6.0.23 tomcatVersion=11.0.18 From 918e59b81781bc021fc27c79a03f329ade53ba9a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Feb 2026 11:31:17 +0000 Subject: [PATCH 212/930] Add customizer and property support for configuring Jackson factories Closes gh-34709 --- .../modules/how-to/pages/spring-mvc.adoc | 2 + .../CborFactoryBuilderCustomizer.java | 39 ++++ .../JacksonAutoConfiguration.java | 194 ++++++++++++++++-- .../autoconfigure/JacksonProperties.java | 135 ++++++++++++ .../JsonFactoryBuilderCustomizer.java | 39 ++++ .../XmlFactoryBuilderCustomizer.java | 39 ++++ .../JacksonAutoConfigurationTests.java | 117 ++++++++++- 7 files changed, 542 insertions(+), 23 deletions(-) create mode 100644 module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/CborFactoryBuilderCustomizer.java create mode 100644 module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JsonFactoryBuilderCustomizer.java create mode 100644 module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/XmlFactoryBuilderCustomizer.java diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc index 3f12e6491e13..82a99e7cce93 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc @@ -108,6 +108,8 @@ To ease the migration when working on an application that previously used Jackso The context's javadoc:tools.jackson.databind.json.JsonMapper$Builder[] can be customized by one or more javadoc:org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer[] beans. Such customizer beans can be ordered (Boot's own customizer has an order of 0), letting additional customization be applied both before and after Boot's customization. +Furthermore, the `JsonFactory` used by the builder and the mapper that it creates can be customized by one or more javadoc:org.springframework.boot.jackson.autoconfigure.JsonFactoryBuilderCustomizer[] beans. +Various `spring.jackson.factory` properties can also be used to configure the factory. Any beans of type javadoc:tools.jackson.databind.JacksonModule[] are automatically registered with the auto-configured javadoc:tools.jackson.databind.json.JsonMapper$Builder[] and are applied to any javadoc:tools.jackson.databind.json.JsonMapper[] instances that it creates. This provides an application-wide mechanism for contributing custom modules when you add new features to your application. diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/CborFactoryBuilderCustomizer.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/CborFactoryBuilderCustomizer.java new file mode 100644 index 000000000000..7fb6a14823ef --- /dev/null +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/CborFactoryBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jackson.autoconfigure; + +import tools.jackson.dataformat.cbor.CBORFactory; +import tools.jackson.dataformat.cbor.CBORFactoryBuilder; + +/** + * Callback interface that can be implemented by beans wishing to further customize the + * {@link CBORFactory} through {@link CBORFactoryBuilder} to fine-tune its + * auto-configuration. + * + * @author Andy Wilkinson + * @since 4.1.0 + */ +@FunctionalInterface +public interface CborFactoryBuilderCustomizer { + + /** + * Customize the CBORFactoryBuilder. + * @param cborFactoryBuilder the builder to customize + */ + void customize(CBORFactoryBuilder cborFactoryBuilder); + +} diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java index ff1aebdd0bda..7a0c90417656 100644 --- a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java @@ -32,6 +32,11 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.PropertyAccessor; import org.jspecify.annotations.Nullable; +import tools.jackson.core.StreamReadConstraints; +import tools.jackson.core.StreamWriteConstraints; +import tools.jackson.core.base.DecorableTSFactory.DecorableTSFBuilder; +import tools.jackson.core.json.JsonFactory; +import tools.jackson.core.json.JsonFactoryBuilder; import tools.jackson.databind.JacksonModule; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.PropertyNamingStrategies; @@ -40,7 +45,11 @@ import tools.jackson.databind.cfg.DateTimeFeature; import tools.jackson.databind.cfg.MapperBuilder; import tools.jackson.databind.json.JsonMapper; +import tools.jackson.dataformat.cbor.CBORFactory; +import tools.jackson.dataformat.cbor.CBORFactoryBuilder; import tools.jackson.dataformat.cbor.CBORMapper; +import tools.jackson.dataformat.xml.XmlFactory; +import tools.jackson.dataformat.xml.XmlFactoryBuilder; import tools.jackson.dataformat.xml.XmlMapper; import org.springframework.aot.hint.ReflectionHints; @@ -55,10 +64,14 @@ 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.boot.jackson.JacksonComponentModule; import org.springframework.boot.jackson.JacksonMixinModule; import org.springframework.boot.jackson.JacksonMixinModuleEntries; import org.springframework.boot.jackson.autoconfigure.JacksonProperties.ConstructorDetectorStrategy; +import org.springframework.boot.jackson.autoconfigure.JacksonProperties.Factory.Constraints; +import org.springframework.boot.jackson.autoconfigure.JacksonProperties.Factory.Constraints.Read; +import org.springframework.boot.jackson.autoconfigure.JacksonProperties.Factory.Constraints.Write; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -94,11 +107,21 @@ JacksonComponentModule jsonComponentModule() { return new JacksonComponentModule(); } + @Bean + @ConditionalOnMissingBean + JsonFactory jsonFactory(List customizers) { + JsonFactoryBuilder builder = JsonFactory.builder(); + for (JsonFactoryBuilderCustomizer customizer : customizers) { + customizer.customize(builder); + } + return builder.build(); + } + @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @ConditionalOnMissingBean - JsonMapper.Builder jsonMapperBuilder(List customizers) { - JsonMapper.Builder builder = JsonMapper.builder(); + JsonMapper.Builder jsonMapperBuilder(List customizers, JsonFactory jsonFactory) { + JsonMapper.Builder builder = JsonMapper.builder(jsonFactory); customize(builder, customizers); return builder; } @@ -137,12 +160,36 @@ JacksonMixinModule jacksonMixinModule(ApplicationContext context, JacksonMixinMo @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(JacksonProperties.class) - static class JacksonJsonMapperBuilderCustomizerConfiguration { + static class JacksonJsonCustomizerConfiguration { + + private final JacksonProperties jacksonProperties; + + JacksonJsonCustomizerConfiguration(JacksonProperties jacksonProperties) { + this.jacksonProperties = jacksonProperties; + } @Bean - StandardJsonMapperBuilderCustomizer standardJsonMapperBuilderCustomizer(JacksonProperties jacksonProperties, - ObjectProvider modules) { - return new StandardJsonMapperBuilderCustomizer(jacksonProperties, modules.stream().toList()); + StandardJsonFactoryBuilderCustomizer standardJsonFactoryBuilderCustomizer() { + return new StandardJsonFactoryBuilderCustomizer(this.jacksonProperties); + } + + @Bean + StandardJsonMapperBuilderCustomizer standardJsonMapperBuilderCustomizer(ObjectProvider modules) { + return new StandardJsonMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList()); + } + + static final class StandardJsonFactoryBuilderCustomizer + extends AbstractFactoryBuilderCustomizer implements JsonFactoryBuilderCustomizer { + + StandardJsonFactoryBuilderCustomizer(JacksonProperties jacksonProperties) { + super(jacksonProperties); + } + + @Override + public void customize(JsonFactoryBuilder jsonFactoryBuilder) { + super.customize(jsonFactoryBuilder); + } + } static final class StandardJsonMapperBuilderCustomizer @@ -189,17 +236,27 @@ public void customize(JsonMapper.Builder builder) { @EnableConfigurationProperties(JacksonCborProperties.class) static class CborConfiguration { + private final JacksonProperties jacksonProperties; + + CborConfiguration(JacksonProperties jacksonProperties) { + this.jacksonProperties = jacksonProperties; + } + @Bean @ConditionalOnMissingBean - CBORMapper cborMapper(CBORMapper.Builder builder) { + CBORFactory cborFactory(List customizers) { + CBORFactoryBuilder builder = CBORFactory.builder(); + for (CborFactoryBuilderCustomizer customizer : customizers) { + customizer.customize(builder); + } return builder.build(); } @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @ConditionalOnMissingBean - CBORMapper.Builder cborMapperBuilder(List customizers) { - CBORMapper.Builder builder = CBORMapper.builder(); + CBORMapper.Builder cborMapperBuilder(CBORFactory factory, List customizers) { + CBORMapper.Builder builder = CBORMapper.builder(factory); customize(builder, customizers); return builder; } @@ -211,12 +268,37 @@ private void customize(CBORMapper.Builder builder, List modules, JacksonCborProperties cborProperties) { - return new StandardCborMapperBuilderCustomizer(jacksonProperties, modules.stream().toList(), + @ConditionalOnMissingBean + CBORMapper cborMapper(CBORMapper.Builder builder) { + return builder.build(); + } + + @Bean + StandardCborFactoryBuilderCustomizer standardCborFactoryBuilderCustomizer() { + return new StandardCborFactoryBuilderCustomizer(this.jacksonProperties); + } + + @Bean + StandardCborMapperBuilderCustomizer standardCborMapperBuilderCustomizer(ObjectProvider modules, + JacksonCborProperties cborProperties) { + return new StandardCborMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList(), cborProperties); } + static final class StandardCborFactoryBuilderCustomizer + extends AbstractFactoryBuilderCustomizer implements CborFactoryBuilderCustomizer { + + StandardCborFactoryBuilderCustomizer(JacksonProperties jacksonProperties) { + super(jacksonProperties); + } + + @Override + public void customize(CBORFactoryBuilder cborFactoryBuilder) { + super.customize(cborFactoryBuilder); + } + + } + static class StandardCborMapperBuilderCustomizer extends AbstractMapperBuilderCustomizer implements CborMapperBuilderCustomizer { @@ -244,17 +326,27 @@ public void customize(CBORMapper.Builder builder) { @EnableConfigurationProperties(JacksonXmlProperties.class) static class XmlConfiguration { + private final JacksonProperties jacksonProperties; + + XmlConfiguration(JacksonProperties jacksonProperties) { + this.jacksonProperties = jacksonProperties; + } + @Bean @ConditionalOnMissingBean - XmlMapper xmlMapper(XmlMapper.Builder builder) { + XmlFactory xmlFactory(List customizers) { + XmlFactoryBuilder builder = XmlFactory.builder(); + for (XmlFactoryBuilderCustomizer customizer : customizers) { + customizer.customize(builder); + } return builder.build(); } @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @ConditionalOnMissingBean - XmlMapper.Builder xmlMapperBuilder(List customizers) { - XmlMapper.Builder builder = XmlMapper.builder(); + XmlMapper.Builder xmlMapperBuilder(XmlFactory xmlFactory, List customizers) { + XmlMapper.Builder builder = XmlMapper.builder(xmlFactory); customize(builder, customizers); return builder; } @@ -266,9 +358,21 @@ private void customize(XmlMapper.Builder builder, List modules, JacksonXmlProperties xmlProperties) { - return new StandardXmlMapperBuilderCustomizer(jacksonProperties, modules.stream().toList(), xmlProperties); + @ConditionalOnMissingBean + XmlMapper xmlMapper(XmlMapper.Builder builder) { + return builder.build(); + } + + @Bean + StandardXmlFactoryBuilderCustomizer standardXmlFactoryBuilderCustomizer() { + return new StandardXmlFactoryBuilderCustomizer(this.jacksonProperties); + } + + @Bean + StandardXmlMapperBuilderCustomizer standardXmlMapperBuilderCustomizer(ObjectProvider modules, + JacksonXmlProperties xmlProperties) { + return new StandardXmlMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList(), + xmlProperties); } @Configuration(proxyBeanMethods = false) @@ -291,6 +395,20 @@ public void customize(XmlMapper.Builder builder) { } + static final class StandardXmlFactoryBuilderCustomizer + extends AbstractFactoryBuilderCustomizer implements XmlFactoryBuilderCustomizer { + + StandardXmlFactoryBuilderCustomizer(JacksonProperties jacksonProperties) { + super(jacksonProperties); + } + + @Override + public void customize(XmlFactoryBuilder xmlFactoryBuilder) { + super.customize(xmlFactoryBuilder); + } + + } + static class StandardXmlMapperBuilderCustomizer extends AbstractMapperBuilderCustomizer implements XmlMapperBuilderCustomizer { @@ -344,6 +462,46 @@ private boolean isPropertyNamingStrategyField(Field candidate) { } + abstract static class AbstractFactoryBuilderCustomizer> implements Ordered { + + private final JacksonProperties jacksonProperties; + + AbstractFactoryBuilderCustomizer(JacksonProperties jacksonProperties) { + this.jacksonProperties = jacksonProperties; + } + + @Override + public int getOrder() { + return 0; + } + + protected void customize(B builder) { + Constraints constraints = this.jacksonProperties.getFactory().getConstraints(); + builder.streamReadConstraints(readConstraintsFrom(constraints.getRead())); + builder.streamWriteConstraints(writeConstraintsFrom(constraints.getWrite())); + } + + private StreamReadConstraints readConstraintsFrom(Read read) { + PropertyMapper map = PropertyMapper.get(); + StreamReadConstraints.Builder constraintsBuilder = StreamReadConstraints.builder(); + map.from(read::getMaxDocumentLength).to(constraintsBuilder::maxDocumentLength); + map.from(read::getMaxNameLength).to(constraintsBuilder::maxNameLength); + map.from(read::getMaxNestingDepth).to(constraintsBuilder::maxNestingDepth); + map.from(read::getMaxNumberLength).to(constraintsBuilder::maxNumberLength); + map.from(read::getMaxStringLength).to(constraintsBuilder::maxStringLength); + map.from(read::getMaxTokenCount).to(constraintsBuilder::maxTokenCount); + return constraintsBuilder.build(); + } + + private StreamWriteConstraints writeConstraintsFrom(Write write) { + PropertyMapper map = PropertyMapper.get(); + StreamWriteConstraints.Builder constraintsBuilder = StreamWriteConstraints.builder(); + map.from(write::getMaxNestingDepth).to(constraintsBuilder::maxNestingDepth); + return constraintsBuilder.build(); + } + + } + abstract static class AbstractMapperBuilderCustomizer> implements Ordered { private final JacksonProperties jacksonProperties; diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java index f62083ad76b8..472b68045194 100644 --- a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java @@ -137,6 +137,8 @@ public class JacksonProperties { private final Json json = new Json(); + private final Factory factory = new Factory(); + public @Nullable String getDateFormat() { return this.dateFormat; } @@ -241,6 +243,10 @@ public Json getJson() { return this.json; } + public Factory getFactory() { + return this.factory; + } + public enum ConstructorDetectorStrategy { /** @@ -319,4 +325,133 @@ public Map getWrite() { } + public static class Factory { + + private final Constraints constraints = new Constraints(); + + public Constraints getConstraints() { + return this.constraints; + } + + public static class Constraints { + + private final Read read = new Read(); + + private final Write write = new Write(); + + public Read getRead() { + return this.read; + } + + public Write getWrite() { + return this.write; + } + + public static class Read { + + /** + * Maximum nesting depth. The depth is a count of objects and arrays that + * have not been closed. + */ + private int maxNestingDepth = 500; + + /** + * Maximum allowed document length. A value less than or equal to zero + * indicates that any length is acceptable. + */ + private long maxDocumentLength = -1L; + + /** + * Maximum allowed token count. A value less than or equal to zero + * indicates that any count is acceptable. + */ + private long maxTokenCount = -1L; + + /** + * Maximum number length. + */ + private int maxNumberLength = 1_000; + + /** + * Maximum string length. + */ + private int maxStringLength = 20_000_000; + + /** + * Maximum name length. + */ + private int maxNameLength = 50_000; + + public int getMaxNestingDepth() { + return this.maxNestingDepth; + } + + public void setMaxNestingDepth(int maxNestingDepth) { + this.maxNestingDepth = maxNestingDepth; + } + + public long getMaxDocumentLength() { + return this.maxDocumentLength; + } + + public void setMaxDocumentLength(long maxDocumentLength) { + this.maxDocumentLength = maxDocumentLength; + } + + public long getMaxTokenCount() { + return this.maxTokenCount; + } + + public void setMaxTokenCount(long maxTokenCount) { + this.maxTokenCount = maxTokenCount; + } + + public int getMaxNumberLength() { + return this.maxNumberLength; + } + + public void setMaxNumberLength(int maxNumberLength) { + this.maxNumberLength = maxNumberLength; + } + + public int getMaxStringLength() { + return this.maxStringLength; + } + + public void setMaxStringLength(int maxStringLength) { + this.maxStringLength = maxStringLength; + } + + public int getMaxNameLength() { + return this.maxNameLength; + } + + public void setMaxNameLength(int maxNameLength) { + this.maxNameLength = maxNameLength; + } + + } + + public static class Write { + + /** + * Maximum nesting depth. The depth is a count of objects and arrays that + * have not been closed. + */ + private int maxNestingDepth = 500; + + public int getMaxNestingDepth() { + return this.maxNestingDepth; + } + + public void setMaxNestingDepth(int maxNestingDepth) { + this.maxNestingDepth = maxNestingDepth; + } + + } + + } + + } + } diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JsonFactoryBuilderCustomizer.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JsonFactoryBuilderCustomizer.java new file mode 100644 index 000000000000..4865a1a2b6e0 --- /dev/null +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JsonFactoryBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jackson.autoconfigure; + +import tools.jackson.core.json.JsonFactory; +import tools.jackson.core.json.JsonFactoryBuilder; + +/** + * Callback interface that can be implemented by beans wishing to further customize the + * {@link JsonFactory} through {@link JsonFactoryBuilder} to fine-tune its + * auto-configuration. + * + * @author Andy Wilkinson + * @since 4.1.0 + */ +@FunctionalInterface +public interface JsonFactoryBuilderCustomizer { + + /** + * Customize the JsonFactoryBuilder. + * @param jsonFactoryBuilder the builder to customize + */ + void customize(JsonFactoryBuilder jsonFactoryBuilder); + +} diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/XmlFactoryBuilderCustomizer.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/XmlFactoryBuilderCustomizer.java new file mode 100644 index 000000000000..0e92a6468843 --- /dev/null +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/XmlFactoryBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jackson.autoconfigure; + +import tools.jackson.dataformat.xml.XmlFactory; +import tools.jackson.dataformat.xml.XmlFactoryBuilder; + +/** + * Callback interface that can be implemented by beans wishing to further customize the + * {@link XmlFactory} through {@link XmlFactoryBuilder} to fine-tune its + * auto-configuration. + * + * @author Andy Wilkinson + * @since 4.1.0 + */ +@FunctionalInterface +public interface XmlFactoryBuilderCustomizer { + + /** + * Customize the XmlFactoryBuilder. + * @param xmlFactoryBuilder the builder to customize + */ + void customize(XmlFactoryBuilder xmlFactoryBuilder); + +} diff --git a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java index 02eaa6c0ef4d..96ecbab97481 100644 --- a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java +++ b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java @@ -32,8 +32,12 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import tools.jackson.core.JsonGenerator; +import tools.jackson.core.StreamReadConstraints; import tools.jackson.core.StreamReadFeature; +import tools.jackson.core.StreamWriteConstraints; import tools.jackson.core.StreamWriteFeature; +import tools.jackson.core.TokenStreamFactory; +import tools.jackson.core.json.JsonFactory; import tools.jackson.core.json.JsonReadFeature; import tools.jackson.core.json.JsonWriteFeature; import tools.jackson.databind.DeserializationFeature; @@ -56,7 +60,9 @@ import tools.jackson.databind.json.JsonMapper.Builder; import tools.jackson.databind.module.SimpleModule; import tools.jackson.databind.util.StdDateFormat; +import tools.jackson.dataformat.cbor.CBORFactory; import tools.jackson.dataformat.cbor.CBORMapper; +import tools.jackson.dataformat.xml.XmlFactory; import tools.jackson.dataformat.xml.XmlMapper; import tools.jackson.module.kotlin.KotlinModule; @@ -70,9 +76,12 @@ import org.springframework.boot.jackson.JacksonMixinModule; import org.springframework.boot.jackson.JacksonMixinModuleEntries; import org.springframework.boot.jackson.ObjectValueSerializer; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.CborConfiguration.StandardCborFactoryBuilderCustomizer; import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.CborConfiguration.StandardCborMapperBuilderCustomizer; import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.JacksonAutoConfigurationRuntimeHints; -import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.JacksonJsonMapperBuilderCustomizerConfiguration.StandardJsonMapperBuilderCustomizer; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.JacksonJsonCustomizerConfiguration.StandardJsonFactoryBuilderCustomizer; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.JacksonJsonCustomizerConfiguration.StandardJsonMapperBuilderCustomizer; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.XmlConfiguration.StandardXmlFactoryBuilderCustomizer; import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.XmlConfiguration.StandardXmlMapperBuilderCustomizer; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ApplicationContext; @@ -106,6 +115,12 @@ class JacksonAutoConfigurationTests { protected final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)); + @EnumSource + @ParameterizedTest + void definesFactory(MapperType mapperType) { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(mapperType.factoryClass)); + } + @EnumSource @ParameterizedTest void definesMapper(MapperType mapperType) { @@ -118,6 +133,15 @@ void definesMapperBuilder(MapperType mapperType) { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(mapperType.builderClass)); } + @EnumSource + @ParameterizedTest + void factoryBacksOffWhenCustomFactoryIsDefined(MapperType mapperType) { + this.contextRunner.withBean("customFactory", mapperType.factoryClass).run((context) -> { + assertThat(context).hasSingleBean(mapperType.factoryClass); + assertThat(context).hasBean("customFactory"); + }); + } + @EnumSource @ParameterizedTest void mapperBacksOffWhenCustomMapperIsDefined(MapperType mapperType) { @@ -161,6 +185,24 @@ void standardXmlMapperBuilderCustomizerDoesNotBackOffWhenCustomizerIsDefined() { .run((context) -> assertThat(context).hasSingleBean(StandardXmlMapperBuilderCustomizer.class)); } + @Test + void standardJsonFactoryBuilderCustomizerDoesNotBackOffWhenCustomizerIsDefined() { + this.contextRunner.withBean(JsonFactoryBuilderCustomizer.class, () -> mock(JsonFactoryBuilderCustomizer.class)) + .run((context) -> assertThat(context).hasSingleBean(StandardJsonFactoryBuilderCustomizer.class)); + } + + @Test + void standardCborFactoryBuilderCustomizerDoesNotBackOffWhenCustomizerIsDefined() { + this.contextRunner.withBean(CborFactoryBuilderCustomizer.class, () -> mock(CborFactoryBuilderCustomizer.class)) + .run((context) -> assertThat(context).hasSingleBean(StandardCborFactoryBuilderCustomizer.class)); + } + + @Test + void standardXmlFactoryBuilderCustomizerDoesNotBackOffWhenCustomizerIsDefined() { + this.contextRunner.withBean(XmlFactoryBuilderCustomizer.class, () -> mock(XmlFactoryBuilderCustomizer.class)) + .run((context) -> assertThat(context).hasSingleBean(StandardXmlFactoryBuilderCustomizer.class)); + } + @Test void doubleModuleRegistration() { this.contextRunner.withUserConfiguration(DoubleModulesConfig.class).run((context) -> { @@ -814,6 +856,63 @@ void shouldNotFindAndAddModulesWhenDisabled(MapperType mapperType) { .doesNotHaveAnyElementsOfTypes(KotlinModule.class)); } + @EnumSource + @ParameterizedTest + void defaultStreamReadConstraintsMatchJacksonDefaults(MapperType mapperType) { + this.contextRunner.run((context) -> { + StreamReadConstraints streamReadConstraints = mapperType.getFactory(context).streamReadConstraints(); + assertThat(streamReadConstraints.getMaxDocumentLength()) + .isEqualTo(StreamReadConstraints.DEFAULT_MAX_DOC_LEN); + assertThat(streamReadConstraints.getMaxNameLength()).isEqualTo(StreamReadConstraints.DEFAULT_MAX_NAME_LEN); + assertThat(streamReadConstraints.getMaxNestingDepth()).isEqualTo(StreamReadConstraints.DEFAULT_MAX_DEPTH); + assertThat(streamReadConstraints.getMaxNumberLength()).isEqualTo(StreamReadConstraints.DEFAULT_MAX_NUM_LEN); + assertThat(streamReadConstraints.getMaxStringLength()) + .isEqualTo(StreamReadConstraints.DEFAULT_MAX_STRING_LEN); + assertThat(streamReadConstraints.getMaxTokenCount()) + .isEqualTo(StreamReadConstraints.DEFAULT_MAX_TOKEN_COUNT); + }); + } + + @EnumSource + @ParameterizedTest + void customStreamReadConstraintsAreAppliedToAutoConfiguredFactory(MapperType mapperType) { + this.contextRunner + .withPropertyValues("spring.jackson.factory.constraints.read.max-document-length=1000", + "spring.jackson.factory.constraints.read.max-name-length=1001", + "spring.jackson.factory.constraints.read.max-nesting-depth=1002", + "spring.jackson.factory.constraints.read.max-number-length=1003", + "spring.jackson.factory.constraints.read.max-string-length=1004", + "spring.jackson.factory.constraints.read.max-token-count=1005") + .run((context) -> { + StreamReadConstraints streamReadConstraints = mapperType.getFactory(context).streamReadConstraints(); + assertThat(streamReadConstraints.getMaxDocumentLength()).isEqualTo(1000); + assertThat(streamReadConstraints.getMaxNameLength()).isEqualTo(1001); + assertThat(streamReadConstraints.getMaxNestingDepth()).isEqualTo(1002); + assertThat(streamReadConstraints.getMaxNumberLength()).isEqualTo(1003); + assertThat(streamReadConstraints.getMaxStringLength()).isEqualTo(1004); + assertThat(streamReadConstraints.getMaxTokenCount()).isEqualTo(1005); + }); + } + + @EnumSource + @ParameterizedTest + void customStreamWriteConstraintsAreAppliedToAutoConfiguredFactory(MapperType mapperType) { + this.contextRunner.withPropertyValues("spring.jackson.factory.constraints.write.max-nesting-depth=1000") + .run((context) -> { + StreamWriteConstraints streamWriteConstraints = mapperType.getFactory(context).streamWriteConstraints(); + assertThat(streamWriteConstraints.getMaxNestingDepth()).isEqualTo(1000); + }); + } + + @EnumSource + @ParameterizedTest + void defaultStreamWriteConstraintsMatchJacksonDefaults(MapperType mapperType) { + this.contextRunner.run((context) -> { + StreamWriteConstraints streamWriteConstraints = mapperType.getFactory(context).streamWriteConstraints(); + assertThat(streamWriteConstraints.getMaxNestingDepth()).isEqualTo(StreamReadConstraints.DEFAULT_MAX_DEPTH); + }); + } + static class MyDateFormat extends SimpleDateFormat { MyDateFormat() { @@ -1137,15 +1236,19 @@ static class CircularDependencySerializerConfiguration { enum MapperType { - CBOR(CBORMapper.class, CBORMapper.Builder.class), JSON(JsonMapper.class, JsonMapper.Builder.class), - XML(XmlMapper.class, XmlMapper.Builder.class); + CBOR(CBORFactory.class, CBORMapper.class, CBORMapper.Builder.class), + JSON(JsonFactory.class, JsonMapper.class, JsonMapper.Builder.class), + XML(XmlFactory.class, XmlMapper.class, XmlMapper.Builder.class); + + private final Class factoryClass; private final Class mapperClass; private final Class> builderClass; - > MapperType(Class mapperClass, - Class builderClass) { + > MapperType( + Class factoryClass, Class mapperClass, Class builderClass) { + this.factoryClass = factoryClass; this.mapperClass = mapperClass; this.builderClass = builderClass; } @@ -1154,6 +1257,10 @@ ObjectMapper getMapper(ApplicationContext context) { return context.getBean(this.mapperClass); } + TokenStreamFactory getFactory(ApplicationContext context) { + return context.getBean(this.mapperClass).tokenStreamFactory(); + } + } } From d5d1d7ccb529ec89a91693c681786e62c0cafaf1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Feb 2026 15:18:51 +0000 Subject: [PATCH 213/930] Start building against Spring Framework 7.0.5 snapshots See gh-49218 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a11e61209751..3c197f407c63 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ mockitoVersion=5.21.0 nativeBuildToolsVersion=0.11.4 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.5 -springFrameworkVersion=7.0.4 +springFrameworkVersion=7.0.5-SNAPSHOT springFramework60xVersion=6.0.23 tomcatVersion=11.0.18 From 496818a334519ca2793398e7aee92bbadf184c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 13 Feb 2026 16:52:39 +0100 Subject: [PATCH 214/930] Upgrade to Spring Data Bom 2025.1.3 Closes gh-49115 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ae05732f934f..239fecb61715 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2456,7 +2456,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-batch/releases/tag/v{version}") } } - library("Spring Data Bom", "2025.1.3-SNAPSHOT") { + library("Spring Data Bom", "2025.1.3") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From dada7696d60b9b4e51c60462d093abaea05b8290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 13 Feb 2026 16:52:39 +0100 Subject: [PATCH 215/930] Upgrade to Spring LDAP 4.1.0-M1 Closes gh-49119 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 239fecb61715..7b2c55b84d27 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2554,7 +2554,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-kafka/releases/tag/v{version}") } } - library("Spring LDAP", "4.1.0-SNAPSHOT") { + library("Spring LDAP", "4.1.0-M1") { considerSnapshots() group("org.springframework.ldap") { modules = [ From cc3d819ca2349283b14975f5e790454cfaa26b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 14 Feb 2026 10:02:45 +0100 Subject: [PATCH 216/930] Upgrade to Spring Security 7.1.0-M2 Closes gh-49121 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7b2c55b84d27..adfe0039191b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2604,7 +2604,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-restdocs/releases/tag/v{version}") } } - library("Spring Security", "7.1.0-SNAPSHOT") { + library("Spring Security", "7.1.0-M2") { considerSnapshots() group("org.springframework.security") { bom("spring-security-bom") From 5edae6448c4b8ce59ea78f178b44c90a0d0100fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 14 Feb 2026 10:06:08 +0100 Subject: [PATCH 217/930] Upgrade to Spring Data 2026.0.0-M1 Closes gh-49229 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index adfe0039191b..b0782be14943 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2456,7 +2456,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-batch/releases/tag/v{version}") } } - library("Spring Data Bom", "2025.1.3") { + library("Spring Data Bom", "2026.0.0-M1") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From c6781f30ee66ff8c5d97c4bfd2e3d29b3d28233c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 14 Feb 2026 10:07:19 +0100 Subject: [PATCH 218/930] Upgrade to Couchbase Client 3.11.0 Closes gh-49226 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b0782be14943..dd70cf6ec63c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -312,7 +312,7 @@ bom { site("https://commons.apache.org/proper/commons-pool") } } - library("Couchbase Client", "3.9.2") { + library("Couchbase Client", "3.11.0") { group("com.couchbase.client") { modules = [ "java-client" From 3af358ca05ede436d3a0319704154a40a542fa2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 14 Feb 2026 10:07:22 +0100 Subject: [PATCH 219/930] Upgrade to Jedis 7.2.1 Closes gh-49227 --- .../redis/autoconfigure/JedisConnectionConfiguration.java | 8 ++++---- platform/spring-boot-dependencies/build.gradle | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java index 80d0aba0c692..3ec5e555319f 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java @@ -19,8 +19,8 @@ import javax.net.ssl.SSLParameters; import org.apache.commons.pool2.impl.GenericObjectPool; +import redis.clients.jedis.ConnectionPoolConfig; import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPoolConfig; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -151,11 +151,11 @@ private void applySslIfNeeded(JedisClientConfigurationBuilder builder) { private void applyPooling(DataRedisProperties.Pool pool, JedisClientConfiguration.JedisClientConfigurationBuilder builder) { - builder.usePooling().poolConfig(jedisPoolConfig(pool)); + builder.usePooling().poolConfig(poolConfig(pool)); } - private JedisPoolConfig jedisPoolConfig(DataRedisProperties.Pool pool) { - JedisPoolConfig config = new JedisPoolConfig(); + private ConnectionPoolConfig poolConfig(DataRedisProperties.Pool pool) { + ConnectionPoolConfig config = new ConnectionPoolConfig(); config.setMaxTotal(pool.getMaxActive()); config.setMaxIdle(pool.getMaxIdle()); config.setMinIdle(pool.getMinIdle()); diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index dd70cf6ec63c..c949ce6bad0d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1056,7 +1056,7 @@ bom { releaseNotes("https://github.com/hunterhacker/jdom/releases/tag/JDOM-{version}") } } - library("Jedis", "7.0.0") { + library("Jedis", "7.2.1") { alignWith { property { name "jedis" From 9420a15567fa0a6811f9006d6ab52f2d6ae48760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 14 Feb 2026 10:07:26 +0100 Subject: [PATCH 220/930] Upgrade to Lettuce 7.2.1.RELEASE Closes gh-49228 --- .../redis/autoconfigure/DataRedisProperties.java | 14 -------------- .../LettuceConnectionConfiguration.java | 3 --- .../additional-spring-configuration-metadata.json | 9 +++++++++ platform/spring-boot-dependencies/build.gradle | 2 +- 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisProperties.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisProperties.java index e3dfb3965865..88045e385364 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisProperties.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisProperties.java @@ -558,12 +558,6 @@ public static class Refresh { */ private @Nullable Duration period; - /** - * Whether adaptive topology refreshing using all available refresh - * triggers should be used. - */ - private boolean adaptive; - public boolean isDynamicRefreshSources() { return this.dynamicRefreshSources; } @@ -580,14 +574,6 @@ public void setPeriod(@Nullable Duration period) { this.period = period; } - public boolean isAdaptive() { - return this.adaptive; - } - - public void setAdaptive(boolean adaptive) { - this.adaptive = adaptive; - } - } } diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/LettuceConnectionConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/LettuceConnectionConfiguration.java index 177040323c1b..adf02ebb0eb7 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/LettuceConnectionConfiguration.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/LettuceConnectionConfiguration.java @@ -242,9 +242,6 @@ private ClientOptions.Builder initializeClientOptionsBuilder() { if (refreshProperties.getPeriod() != null) { refreshBuilder.enablePeriodicRefresh(refreshProperties.getPeriod()); } - if (refreshProperties.isAdaptive()) { - refreshBuilder.enableAllAdaptiveRefreshTriggers(); - } return builder.topologyRefreshOptions(refreshBuilder.build()); } return ClientOptions.builder(); diff --git a/module/spring-boot-data-redis/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-data-redis/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 9977e54ee545..b3d2d5212a21 100644 --- a/module/spring-boot-data-redis/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-data-redis/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -7,6 +7,15 @@ "description": "Whether to enable Redis health check.", "defaultValue": true }, + { + "name": "spring.data.redis.lettuce.cluster.refresh.adaptive", + "type": "java.lang.Boolean", + "deprecation": { + "reason": "All adaptive triggers are enabled by default.", + "level": "error", + "since": "4.1.0" + } + }, { "name": "spring.data.redis.repositories.enabled", "type": "java.lang.Boolean", diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c949ce6bad0d..59ecded934ac 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1288,7 +1288,7 @@ bom { releaseNotes("https://github.com/Kotlin/kotlinx.serialization/releases/tag/v{version}") } } - library("Lettuce", "6.8.2.RELEASE") { + library("Lettuce", "7.2.1.RELEASE") { prohibit { contains ".BETA" because "we don't want betas" From 244f5ff45044f180b9005051c501ff9f3f6ffd30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sun, 15 Feb 2026 15:36:08 +0100 Subject: [PATCH 221/930] Upgrade to Byte Buddy 1.18.5 Closes gh-49235 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 59ecded934ac..121f10f72ce2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -186,7 +186,7 @@ bom { releaseNotes("https://github.com/mojohaus/build-helper-maven-plugin/releases/tag/{version}") } } - library("Byte Buddy", "1.18.4") { + library("Byte Buddy", "1.18.5") { group("net.bytebuddy") { modules = [ "byte-buddy", From 6268829e65a56186670d970050a4e0999ef8259c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sun, 15 Feb 2026 15:36:12 +0100 Subject: [PATCH 222/930] Upgrade to JUnit Jupiter 6.0.3 Closes gh-49236 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3c197f407c63..595d891b5200 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ hamcrestVersion=3.0 jackson2Version=2.21.0 jacksonVersion=3.0.4 javaFormatVersion=0.0.47 -junitJupiterVersion=6.0.2 +junitJupiterVersion=6.0.3 kotlinVersion=2.3.10 mavenVersion=3.9.10 mockitoVersion=5.21.0 From 9904ad11c64e74734abedd77d21d5341fe52bd85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sun, 15 Feb 2026 15:36:16 +0100 Subject: [PATCH 223/930] Upgrade to Logback 1.5.31 Closes gh-49237 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 121f10f72ce2..fb46182828f6 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1345,7 +1345,7 @@ bom { releaseNotes("https://github.com/apache/logging-log4j2/releases/tag/rel%2F{version}") } } - library("Logback", "1.5.29") { + library("Logback", "1.5.31") { group("ch.qos.logback") { modules = [ "logback-classic", From 9ca52de36ab601a6fe6b47c8f2bf49c71e4d8d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 17 Feb 2026 19:30:11 +0100 Subject: [PATCH 224/930] Upgrade to Logback 1.5.32 Closes gh-49248 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index fb46182828f6..c892954c753f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1345,7 +1345,7 @@ bom { releaseNotes("https://github.com/apache/logging-log4j2/releases/tag/rel%2F{version}") } } - library("Logback", "1.5.31") { + library("Logback", "1.5.32") { group("ch.qos.logback") { modules = [ "logback-classic", From ee71aff8e999fcb1ae174fc53533096f4fa7853b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 17 Feb 2026 19:30:11 +0100 Subject: [PATCH 225/930] Upgrade to Spring AMQP 4.1.0-M2 Closes gh-49114 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c892954c753f..84105fbd0987 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2426,7 +2426,7 @@ bom { ] } } - library("Spring AMQP", "4.1.0-SNAPSHOT") { + library("Spring AMQP", "4.1.0-M2") { considerSnapshots() group("org.springframework.amqp") { bom("spring-amqp-bom") From 8aaf95d01961b3ac5747967994273415a6f17e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 17 Feb 2026 19:30:12 +0100 Subject: [PATCH 226/930] Upgrade to Spring Kafka 4.1.0-M1 Closes gh-49118 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 84105fbd0987..53836476288a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2536,7 +2536,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-integration/releases/tag/v{version}") } } - library("Spring Kafka", "4.1.0-SNAPSHOT") { + library("Spring Kafka", "4.1.0-M1") { considerSnapshots() group("org.springframework.kafka") { modules = [ From 8d9ab791593d4ab25d19ef6934b3fa59b02bd4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 17 Feb 2026 19:30:12 +0100 Subject: [PATCH 227/930] Upgrade to Spring Pulsar 2.0.3 Closes gh-49120 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 53836476288a..d7bb399ec1de 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2574,7 +2574,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ldap/releases/tag/{version}") } } - library("Spring Pulsar", "2.0.3-SNAPSHOT") { + library("Spring Pulsar", "2.0.3") { considerSnapshots() group("org.springframework.pulsar") { bom("spring-pulsar-bom") From d330f542fd2c3720fd374a55873a826cb0d26962 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 18 Feb 2026 09:36:48 +0100 Subject: [PATCH 228/930] Print CI docker images in system test --- .github/workflows/run-system-tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/run-system-tests.yml b/.github/workflows/run-system-tests.yml index 9dcf62d7f318..d906603fcf87 100644 --- a/.github/workflows/run-system-tests.yml +++ b/.github/workflows/run-system-tests.yml @@ -30,6 +30,10 @@ jobs: id: run-system-tests shell: bash run: ./gradlew systemTest + - name: List docker images + if: always() + shell: bash + run: docker image ls --digests - name: Send Notification if: always() uses: ./.github/actions/send-notification From 3c028759a2243017f5f4dd028ffbf3d10823252a Mon Sep 17 00:00:00 2001 From: Tommy Ludwig <8924140+shakuzen@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:56:47 +0900 Subject: [PATCH 229/930] Add support for configuring publishMaxGaugeForHistograms See https://github.com/micrometer-metrics/micrometer/pull/6159 See gh-49242 --- .../export/otlp/OtlpMetricsProperties.java | 14 ++++++++++++++ .../OtlpMetricsPropertiesConfigAdapter.java | 6 ++++++ ...tlpMetricsPropertiesConfigAdapterTests.java | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java index 62577bdfdcbf..770b824ef3e2 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java @@ -86,6 +86,12 @@ public class OtlpMetricsProperties extends StepRegistryProperties { */ private final Map meter = new LinkedHashMap<>(); + /** + * Whether to publish a separate gauge for the max value of histogram-based meters. A + * null value defers to Micrometer's default. + */ + private @Nullable Boolean publishMaxGaugeForHistograms; + public @Nullable String getUrl() { return this.url; } @@ -154,6 +160,14 @@ public Map getMeter() { return this.meter; } + public @Nullable Boolean getPublishMaxGaugeForHistograms() { + return this.publishMaxGaugeForHistograms; + } + + public void setPublishMaxGaugeForHistograms(@Nullable Boolean publishMaxGaugeForHistograms) { + this.publishMaxGaugeForHistograms = publishMaxGaugeForHistograms; + } + /** * Per-meter settings. */ diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java index 80dfe0f2f7b6..3a6bebeb146d 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java @@ -121,6 +121,12 @@ public TimeUnit baseTimeUnit() { return obtain(OtlpMetricsProperties::getBaseTimeUnit, OtlpConfig.super::baseTimeUnit); } + @Override + public boolean publishMaxGaugeForHistograms() { + return obtain(OtlpMetricsProperties::getPublishMaxGaugeForHistograms, + OtlpConfig.super::publishMaxGaugeForHistograms); + } + private Getter> perMeter(Getter getter) { return (properties) -> { if (CollectionUtils.isEmpty(properties.getMeter())) { diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java index 2fe0e850b739..ba30cf1ec065 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java @@ -219,6 +219,24 @@ void shouldUseDefaultApplicationGroupIfApplicationGroupIsNotSet() { assertThat(createAdapter().resourceAttributes()).doesNotContainKey("service.namespace"); } + @Test + void useDefaultPublishMaxGaugeForHistogramsWhenNotSet() { + assertThat(this.properties.getPublishMaxGaugeForHistograms()).isNull(); + assertThat(createAdapter().publishMaxGaugeForHistograms()).isTrue(); + } + + @Test + void whenDefaultPublishMaxGaugeForHistogramsIsSetAdapterUsesIt() { + this.properties.setPublishMaxGaugeForHistograms(false); + assertThat(createAdapter().publishMaxGaugeForHistograms()).isFalse(); + } + + @Test + void whenAggregationTemporalityIsSetToDeltaThenPublishMaxGaugeForHistogramsDefaultChanges() { + this.properties.setAggregationTemporality(AggregationTemporality.DELTA); + assertThat(createAdapter().publishMaxGaugeForHistograms()).isFalse(); + } + private OtlpMetricsPropertiesConfigAdapter createAdapter() { return new OtlpMetricsPropertiesConfigAdapter(this.properties, this.openTelemetryProperties, this.connectionDetails, this.environment); From 53e7503fd4e0ca8ea973a2da868e5c953ea23a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Feb 2026 13:58:48 +0100 Subject: [PATCH 230/930] Polish "Add support for configuring publishMaxGaugeForHistograms" See gh-49242 --- .../export/otlp/OtlpMetricsProperties.java | 27 +++++++------- .../OtlpMetricsPropertiesConfigAdapter.java | 12 +++---- ...lpMetricsPropertiesConfigAdapterTests.java | 36 +++++++++---------- 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java index 770b824ef3e2..0f4687adbdf4 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java @@ -65,6 +65,11 @@ public class OtlpMetricsProperties extends StepRegistryProperties { */ private HistogramFlavor histogramFlavor = HistogramFlavor.EXPLICIT_BUCKET_HISTOGRAM; + /** + * Whether to publish a separate gauge for the max value of histogram-based meters. + */ + private @Nullable Boolean publishMaxGaugeForHistograms; + /** * Max scale to use for exponential histograms, if configured. */ @@ -86,12 +91,6 @@ public class OtlpMetricsProperties extends StepRegistryProperties { */ private final Map meter = new LinkedHashMap<>(); - /** - * Whether to publish a separate gauge for the max value of histogram-based meters. A - * null value defers to Micrometer's default. - */ - private @Nullable Boolean publishMaxGaugeForHistograms; - public @Nullable String getUrl() { return this.url; } @@ -132,6 +131,14 @@ public void setHistogramFlavor(HistogramFlavor histogramFlavor) { this.histogramFlavor = histogramFlavor; } + public @Nullable Boolean getPublishMaxGaugeForHistograms() { + return this.publishMaxGaugeForHistograms; + } + + public void setPublishMaxGaugeForHistograms(@Nullable Boolean publishMaxGaugeForHistograms) { + this.publishMaxGaugeForHistograms = publishMaxGaugeForHistograms; + } + public int getMaxScale() { return this.maxScale; } @@ -160,14 +167,6 @@ public Map getMeter() { return this.meter; } - public @Nullable Boolean getPublishMaxGaugeForHistograms() { - return this.publishMaxGaugeForHistograms; - } - - public void setPublishMaxGaugeForHistograms(@Nullable Boolean publishMaxGaugeForHistograms) { - this.publishMaxGaugeForHistograms = publishMaxGaugeForHistograms; - } - /** * Per-meter settings. */ diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java index 3a6bebeb146d..4cc7073c0090 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java @@ -106,6 +106,12 @@ public Map maxBucketsPerMeter() { return obtain(perMeter(Meter::getMaxBucketCount), OtlpConfig.super::maxBucketsPerMeter); } + @Override + public boolean publishMaxGaugeForHistograms() { + return obtain(OtlpMetricsProperties::getPublishMaxGaugeForHistograms, + OtlpConfig.super::publishMaxGaugeForHistograms); + } + @Override public int maxScale() { return obtain(OtlpMetricsProperties::getMaxScale, OtlpConfig.super::maxScale); @@ -121,12 +127,6 @@ public TimeUnit baseTimeUnit() { return obtain(OtlpMetricsProperties::getBaseTimeUnit, OtlpConfig.super::baseTimeUnit); } - @Override - public boolean publishMaxGaugeForHistograms() { - return obtain(OtlpMetricsProperties::getPublishMaxGaugeForHistograms, - OtlpConfig.super::publishMaxGaugeForHistograms); - } - private Getter> perMeter(Getter getter) { return (properties) -> { if (CollectionUtils.isEmpty(properties.getMeter())) { diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java index ba30cf1ec065..1ab506cb9e3e 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java @@ -137,6 +137,24 @@ void whenPropertiesHistogramFlavorPerMeterIsSetAdapterHistogramFlavorPerMeterRet HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM); } + @Test + void useDefaultPublishMaxGaugeForHistogramsWhenNotSet() { + assertThat(this.properties.getPublishMaxGaugeForHistograms()).isNull(); + assertThat(createAdapter().publishMaxGaugeForHistograms()).isTrue(); + } + + @Test + void whenDefaultPublishMaxGaugeForHistogramsIsSetAdapterUsesIt() { + this.properties.setPublishMaxGaugeForHistograms(false); + assertThat(createAdapter().publishMaxGaugeForHistograms()).isFalse(); + } + + @Test + void whenAggregationTemporalityIsSetToDeltaThenPublishMaxGaugeForHistogramsDefaultChanges() { + this.properties.setAggregationTemporality(AggregationTemporality.DELTA); + assertThat(createAdapter().publishMaxGaugeForHistograms()).isFalse(); + } + @Test void whenPropertiesMaxScaleIsNotSetAdapterMaxScaleReturns20() { assertThat(createAdapter().maxScale()).isEqualTo(20); @@ -219,24 +237,6 @@ void shouldUseDefaultApplicationGroupIfApplicationGroupIsNotSet() { assertThat(createAdapter().resourceAttributes()).doesNotContainKey("service.namespace"); } - @Test - void useDefaultPublishMaxGaugeForHistogramsWhenNotSet() { - assertThat(this.properties.getPublishMaxGaugeForHistograms()).isNull(); - assertThat(createAdapter().publishMaxGaugeForHistograms()).isTrue(); - } - - @Test - void whenDefaultPublishMaxGaugeForHistogramsIsSetAdapterUsesIt() { - this.properties.setPublishMaxGaugeForHistograms(false); - assertThat(createAdapter().publishMaxGaugeForHistograms()).isFalse(); - } - - @Test - void whenAggregationTemporalityIsSetToDeltaThenPublishMaxGaugeForHistogramsDefaultChanges() { - this.properties.setAggregationTemporality(AggregationTemporality.DELTA); - assertThat(createAdapter().publishMaxGaugeForHistograms()).isFalse(); - } - private OtlpMetricsPropertiesConfigAdapter createAdapter() { return new OtlpMetricsPropertiesConfigAdapter(this.properties, this.openTelemetryProperties, this.connectionDetails, this.environment); From f7196896c4770564f4714ab55f07f105f8019aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Feb 2026 18:18:11 +0100 Subject: [PATCH 231/930] Upgrade to ActiveMQ 6.2.1 Closes gh-49254 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d7bb399ec1de..ea0c84061d19 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -31,7 +31,7 @@ bom { ] } } - library("ActiveMQ", "6.2.0") { + library("ActiveMQ", "6.2.1") { group("org.apache.activemq") { modules = [ "activemq-console", From c9de4559c0cfcd0a2090b1858bb9f59cdf206514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Feb 2026 18:18:17 +0100 Subject: [PATCH 232/930] Upgrade to Flyway 12.0.1 Closes gh-49255 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ea0c84061d19..750f393be5a4 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -425,7 +425,7 @@ bom { javadoc("elasticsearch-java", version -> "https://javadoc.io/doc/co.elastic.clients/elasticsearch-java/%s/index.html".formatted(version), "co.elastic.clients.elasticsearch", "co.elastic.clients.transport") } } - library("Flyway", "12.0.0") { + library("Flyway", "12.0.1") { group("org.flywaydb") { modules = [ "flyway-command-test-connection", From 231a0fb550f6b586a189e7870ebe127a7fed21fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Feb 2026 18:18:21 +0100 Subject: [PATCH 233/930] Upgrade to Prometheus Client 1.5.0 Closes gh-49256 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 750f393be5a4..8ebee30bf40e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1835,7 +1835,7 @@ bom { releaseNotes("https://github.com/pgjdbc/pgjdbc/releases/tag/REL{version}") } } - library("Prometheus Client", "1.4.3") { + library("Prometheus Client", "1.5.0") { group("io.prometheus") { bom("prometheus-metrics-bom") } From 5ee3e3d486d222219816393f8f6e59e013575727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Feb 2026 18:18:25 +0100 Subject: [PATCH 234/930] Upgrade to R2DBC MariaDB 1.4.0 Closes gh-49257 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8ebee30bf40e..538925802dbb 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1900,7 +1900,7 @@ bom { releaseNotes("https://github.com/r2dbc/r2dbc-h2/releases/tag/v{version}") } } - library("R2DBC MariaDB", "1.3.0") { + library("R2DBC MariaDB", "1.4.0") { group("org.mariadb") { modules = [ "r2dbc-mariadb" From e465d55f851f3e3a40049a3fb127875ab926311d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Feb 2026 18:18:25 +0100 Subject: [PATCH 235/930] Upgrade to Spring Framework 7.0.5 Closes gh-49218 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 595d891b5200..828ceae76c13 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ mockitoVersion=5.21.0 nativeBuildToolsVersion=0.11.4 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.5 -springFrameworkVersion=7.0.5-SNAPSHOT +springFrameworkVersion=7.0.5 springFramework60xVersion=6.0.23 tomcatVersion=11.0.18 From 09b2befda1cf36f4631966ac6b524c7a80fb7a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Feb 2026 18:18:26 +0100 Subject: [PATCH 236/930] Upgrade to Spring Integration 7.1.0-M2 Closes gh-49117 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 538925802dbb..bcff9a9bf320 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2521,7 +2521,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-hateoas/releases/tag/{version}") } } - library("Spring Integration", "7.1.0-SNAPSHOT") { + library("Spring Integration", "7.1.0-M2") { considerSnapshots() group("org.springframework.integration") { bom("spring-integration-bom") From 6218ac5ac2ff1426de814a0f93ed3b7d8d36ff4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Feb 2026 18:18:26 +0100 Subject: [PATCH 237/930] Upgrade to Spring Session 4.1.0-M1 Closes gh-49145 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index bcff9a9bf320..90cde0cf2f97 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2619,7 +2619,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-security/releases/tag/{version}") } } - library("Spring Session", "4.1.0-SNAPSHOT") { + library("Spring Session", "4.1.0-M1") { considerSnapshots() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) From b1aff395dff8fb7c760f4ba0eba0cfa85550fb53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Feb 2026 18:22:39 +0100 Subject: [PATCH 238/930] Upgrade to Kafka 4.2.0 Closes gh-49258 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 90cde0cf2f97..cb176a378906 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1213,7 +1213,7 @@ bom { releaseNotes("https://junit.org/junit5/docs/{version}/release-notes") } } - library("Kafka", "4.1.1") { + library("Kafka", "4.2.0") { group("org.apache.kafka") { modules = [ "connect", From 47b9c6d739a306067b0005b9018118aa7a63d234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Feb 2026 18:35:41 +0100 Subject: [PATCH 239/930] Upgrade to Couchbase Client 3.11.1 Closes gh-49252 --- platform/spring-boot-dependencies/build.gradle | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index cb176a378906..3e2e50f401a4 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -312,18 +312,12 @@ bom { site("https://commons.apache.org/proper/commons-pool") } } - library("Couchbase Client", "3.11.0") { + library("Couchbase Client", "3.11.1") { group("com.couchbase.client") { modules = [ "java-client" ] } - alignWith { - version { - from "org.springframework.data:spring-data-couchbase" - managedBy "Spring Data Bom" - } - } links { site("https://docs.couchbase.com/java-sdk/current/hello-world/overview.html") javadoc("https://javadoc.io/doc/com.couchbase.client/java-client/{version}", "com.couchbase.client") From 28f5591a2b88d82f370679463e2b3f753d1c16ad Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 18 Feb 2026 18:13:49 -0800 Subject: [PATCH 240/930] Temporarily switch Docker to overlay2 See gh-49251 --- .github/workflows/run-system-tests.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-system-tests.yml b/.github/workflows/run-system-tests.yml index 3941d2b007ca..c4ec0771883c 100644 --- a/.github/workflows/run-system-tests.yml +++ b/.github/workflows/run-system-tests.yml @@ -2,12 +2,12 @@ name: Run System Tests on: push: branches: - - 'main' + - "main" permissions: contents: read jobs: run-system-tests: - name: 'Java ${{ matrix.java.version}}' + name: "Java ${{ matrix.java.version}}" if: ${{ github.repository == 'spring-projects/spring-boot' }} runs-on: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} strategy: @@ -18,6 +18,11 @@ jobs: - version: 21 toolchain: true steps: + - name: Switch Docker to Overlay2 + shell: bash + run: | + echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json + sudo systemctl restart docker - name: Check Out Code uses: actions/checkout@v6 - name: Prepare Gradle Build From 1af054775bb5ca65358fae5e0d35f443297e44ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 19 Feb 2026 16:04:19 +0100 Subject: [PATCH 241/930] Polish "Upgrade to Flyway 12.0.1" See https://github.com/flyway/flyway/issues/4207 See gh-49255 --- platform/spring-boot-dependencies/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3e2e50f401a4..94474b2aea17 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -430,7 +430,6 @@ bom { "flyway-database-derby", "flyway-database-hsqldb", "flyway-database-informix", - "flyway-database-mongodb", "flyway-database-nc-couchbase", "flyway-database-nc-mongodb", "flyway-database-oracle", From c33e7585acec6fb2be02dc9434487e440d6a05f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 20 Feb 2026 10:41:49 +0100 Subject: [PATCH 242/930] Fix equality check for multiple values This commit fixes the changelog generator to not flag two identical arrays as changed. Previously, any candidate property with an array of default values were flagged incorrectly. Closes gh-49272 --- .../changelog/Difference.java | 2 +- .../changelog/DifferenceTests.java | 98 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/DifferenceTests.java diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java index 35a1867c89b2..c150359006dd 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java @@ -51,7 +51,7 @@ static Difference compute(ConfigurationMetadataProperty oldProperty, Configurati && newProperty.isDeprecated() && newProperty.getDeprecation().getLevel() == Level.ERROR) { return new Difference(DifferenceType.DELETED, oldProperty, newProperty); } - if (!Objects.equals(oldProperty.getDefaultValue(), newProperty.getDefaultValue())) { + if (!Objects.deepEquals(oldProperty.getDefaultValue(), newProperty.getDefaultValue())) { return new Difference(DifferenceType.DEFAULT_CHANGED, oldProperty, newProperty); } return null; diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/DifferenceTests.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/DifferenceTests.java new file mode 100644 index 000000000000..090844a695b4 --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/DifferenceTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Difference}. + * + * @author Stephane Nicoll + */ +class DifferenceTests { + + @Nested + class DefaultChangedTests { + + @Test + void sameValueComputesNoDifference() { + Difference difference = Difference.compute(createProperty("test.id", "test"), + createProperty("test.id", "test")); + assertThat(difference).isNull(); + } + + @Test + void bothNullComputesNoDifference() { + Difference difference = Difference.compute(createProperty("test.id", null), + createProperty("test.id", null)); + assertThat(difference).isNull(); + } + + @Test + void nullThenNotNullDefaultChanged() { + Difference difference = Difference.compute(createProperty("test.id", null), + createProperty("test.id", "test")); + assertThat(difference).isNotNull(); + assertThat(difference.type()).isEqualTo(DifferenceType.DEFAULT_CHANGED); + } + + @Test + void notNullThenNullDefaultChanged() { + Difference difference = Difference.compute(createProperty("test.id", "test"), + createProperty("test.id", null)); + assertThat(difference).isNotNull(); + assertThat(difference.type()).isEqualTo(DifferenceType.DEFAULT_CHANGED); + } + + @Test + void arrayEqualsComputesNoDifference() { + Difference difference = Difference.compute(createProperty("test.id", new Object[] { "one", "two" }), + createProperty("test.id", new Object[] { "one", "two" })); + assertThat(difference).isNull(); + } + + @Test + void arrayOrderChangedComputesDefaultChanged() { + Difference difference = Difference.compute(createProperty("test.id", new Object[] { "one", "two" }), + createProperty("test.id", new Object[] { "two", "one" })); + assertThat(difference).isNotNull(); + assertThat(difference.type()).isEqualTo(DifferenceType.DEFAULT_CHANGED); + } + + @Test + void arrayAdditionalValueComputesDefaultChanged() { + Difference difference = Difference.compute(createProperty("test.id", new Object[] { "one", "two" }), + createProperty("test.id", new Object[] { "one", "two", "three" })); + assertThat(difference).isNotNull(); + assertThat(difference.type()).isEqualTo(DifferenceType.DEFAULT_CHANGED); + } + + } + + private static ConfigurationMetadataProperty createProperty(String id, Object defaultValue) { + ConfigurationMetadataProperty property = new ConfigurationMetadataProperty(); + property.setId(id); + property.setDefaultValue(defaultValue); + return property; + } + +} From 66841ddb3a5e341901c30873f4fdb7cc7d2db0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 20 Feb 2026 14:29:16 +0100 Subject: [PATCH 243/930] Fix checkstyle violation --- .../changelog/DifferenceTests.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/DifferenceTests.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/DifferenceTests.java index 090844a695b4..fbf92da367f4 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/DifferenceTests.java +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/DifferenceTests.java @@ -30,6 +30,13 @@ */ class DifferenceTests { + private static ConfigurationMetadataProperty createProperty(String id, Object defaultValue) { + ConfigurationMetadataProperty property = new ConfigurationMetadataProperty(); + property.setId(id); + property.setDefaultValue(defaultValue); + return property; + } + @Nested class DefaultChangedTests { @@ -88,11 +95,4 @@ void arrayAdditionalValueComputesDefaultChanged() { } - private static ConfigurationMetadataProperty createProperty(String id, Object defaultValue) { - ConfigurationMetadataProperty property = new ConfigurationMetadataProperty(); - property.setId(id); - property.setDefaultValue(defaultValue); - return property; - } - } From cfee3f10f4fc02c9d0bb3a8b8f85b8f4e5d5012d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 20 Feb 2026 14:38:22 +0100 Subject: [PATCH 244/930] Handle Spring Boot 4.1 modules in changelog generation Closes gh-49273 --- .../build.gradle | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle index 5644d3f9251b..367a774e54f8 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle @@ -43,8 +43,7 @@ nullability { } def dependenciesOf(String version) { - if (version.startsWith("4.0")) { - return [ + def springBoot40Modules = [ "spring-boot", "spring-boot-activemq", "spring-boot-actuator", @@ -148,17 +147,23 @@ def dependenciesOf(String version) { "spring-boot-webservices", "spring-boot-webservices-test", "spring-boot-zipkin" - ] + ] + if (version.startsWith("4.0")) { + return springBoot40Modules + } + else if (version.startsWith("4.1")) { + // Alter Spring Boot 4.0 modules if necessary + return springBoot40Modules } return [ - "spring-boot", - "spring-boot-actuator", - "spring-boot-actuator-autoconfigure", - "spring-boot-autoconfigure", - "spring-boot-devtools", - "spring-boot-docker-compose", - "spring-boot-test-autoconfigure", - "spring-boot-testcontainers" + "spring-boot", + "spring-boot-actuator", + "spring-boot-actuator-autoconfigure", + "spring-boot-autoconfigure", + "spring-boot-devtools", + "spring-boot-docker-compose", + "spring-boot-test-autoconfigure", + "spring-boot-testcontainers" ] } From d102bb69b37b8bac09db3dcce8b7cf8ab2081a04 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 20 Feb 2026 16:28:36 -0800 Subject: [PATCH 245/930] Create reusable default TaskScheduler configuration Create a `DefaultTaskSchedulerConfiguration` for use when standard task scheduler beans are required. Closes gh-49280 --- .../DefaultTaskSchedulerConfiguration.java | 68 +++++++++++++++++++ .../task/TaskSchedulingConfigurations.java | 16 +---- .../IntegrationAutoConfiguration.java | 36 +--------- 3 files changed, 72 insertions(+), 48 deletions(-) create mode 100644 core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/DefaultTaskSchedulerConfiguration.java diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/DefaultTaskSchedulerConfiguration.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/DefaultTaskSchedulerConfiguration.java new file mode 100644 index 000000000000..ba13fb6a9970 --- /dev/null +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/DefaultTaskSchedulerConfiguration.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; +import org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder; +import org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder; +import org.springframework.boot.thread.Threading; +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.TaskSchedulerRouter; + +/** + * Configuration that can be imported to expose a standard {@link TaskScheduler} if the + * user has not enabled task scheduling explicitly. A {@link SimpleAsyncTaskScheduler} is + * exposed if the user enables virtual threads via + * {@code spring.threads.virtual.enabled=true}, otherwise {@link ThreadPoolTaskScheduler}. + *

    + * Configurations importing this one should be ordered after + * {@link TaskSchedulingAutoConfiguration}. + * + * @author Phillip Webb + * @since 4.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(name = DefaultTaskSchedulerConfiguration.DEFAULT_TASK_SCHEDULER_BEAN_NAME) +public class DefaultTaskSchedulerConfiguration { + + /** + * The bean name of the default task scheduler. + */ + public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = TaskSchedulerRouter.DEFAULT_TASK_SCHEDULER_BEAN_NAME; + + @Bean(name = DEFAULT_TASK_SCHEDULER_BEAN_NAME) + @ConditionalOnBean(ThreadPoolTaskSchedulerBuilder.class) + @ConditionalOnThreading(Threading.PLATFORM) + ThreadPoolTaskScheduler taskScheduler(ThreadPoolTaskSchedulerBuilder threadPoolTaskSchedulerBuilder) { + return threadPoolTaskSchedulerBuilder.build(); + } + + @Bean(name = DEFAULT_TASK_SCHEDULER_BEAN_NAME) + @ConditionalOnBean(SimpleAsyncTaskSchedulerBuilder.class) + @ConditionalOnThreading(Threading.VIRTUAL) + SimpleAsyncTaskScheduler taskSchedulerVirtualThreads( + SimpleAsyncTaskSchedulerBuilder simpleAsyncTaskSchedulerBuilder) { + return simpleAsyncTaskSchedulerBuilder.build(); + } + +} diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java index b08df1625fb6..d3e31e5d851a 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java @@ -32,11 +32,10 @@ import org.springframework.boot.thread.Threading; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.support.CompositeTaskDecorator; import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.config.TaskManagementConfigUtils; /** @@ -58,20 +57,9 @@ class TaskSchedulingConfigurations { @Configuration(proxyBeanMethods = false) @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME) @ConditionalOnMissingBean({ TaskScheduler.class, ScheduledExecutorService.class }) + @Import(DefaultTaskSchedulerConfiguration.class) static class TaskSchedulerConfiguration { - @Bean(name = "taskScheduler") - @ConditionalOnThreading(Threading.VIRTUAL) - SimpleAsyncTaskScheduler taskSchedulerVirtualThreads(SimpleAsyncTaskSchedulerBuilder builder) { - return builder.build(); - } - - @Bean - @ConditionalOnThreading(Threading.PLATFORM) - ThreadPoolTaskScheduler taskScheduler(ThreadPoolTaskSchedulerBuilder threadPoolTaskSchedulerBuilder) { - return threadPoolTaskSchedulerBuilder.build(); - } - } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-integration/src/main/java/org/springframework/boot/integration/autoconfigure/IntegrationAutoConfiguration.java b/module/spring-boot-integration/src/main/java/org/springframework/boot/integration/autoconfigure/IntegrationAutoConfiguration.java index 57c5be7d4171..a4bd05447cec 100644 --- a/module/spring-boot-integration/src/main/java/org/springframework/boot/integration/autoconfigure/IntegrationAutoConfiguration.java +++ b/module/spring-boot-integration/src/main/java/org/springframework/boot/integration/autoconfigure/IntegrationAutoConfiguration.java @@ -33,19 +33,16 @@ 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.autoconfigure.condition.ConditionalOnThreading; import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; import org.springframework.boot.autoconfigure.jmx.JmxProperties; +import org.springframework.boot.autoconfigure.task.DefaultTaskSchedulerConfiguration; import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; import org.springframework.boot.sql.autoconfigure.init.OnDatabaseInitializationCondition; -import org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder; -import org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder; -import org.springframework.boot.thread.Threading; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; @@ -69,8 +66,6 @@ import org.springframework.messaging.rsocket.RSocketStrategies; import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; import org.springframework.scheduling.Trigger; -import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.support.CronTrigger; import org.springframework.scheduling.support.PeriodicTrigger; import org.springframework.util.StringUtils; @@ -93,6 +88,7 @@ afterName = "org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration") @ConditionalOnClass(EnableIntegration.class) @EnableConfigurationProperties({ IntegrationProperties.class, JmxProperties.class }) +@Import(DefaultTaskSchedulerConfiguration.class) public final class IntegrationAutoConfiguration { @Bean(name = IntegrationContextUtils.INTEGRATION_GLOBAL_PROPERTIES_BEAN_NAME) @@ -174,34 +170,6 @@ private Trigger createPeriodicTrigger(Duration period, @Nullable Duration initia } - /** - * Expose a standard {@link org.springframework.scheduling.TaskScheduler - * TaskScheduler} if the user has not enabled task scheduling explicitly. A - * {@link SimpleAsyncTaskScheduler} is exposed if the user enables virtual threads via - * {@code spring.threads.virtual.enabled=true}, otherwise - * {@link ThreadPoolTaskScheduler}. - */ - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(name = IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME) - protected static class IntegrationTaskSchedulerConfiguration { - - @Bean(name = IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME) - @ConditionalOnBean(ThreadPoolTaskSchedulerBuilder.class) - @ConditionalOnThreading(Threading.PLATFORM) - ThreadPoolTaskScheduler taskScheduler(ThreadPoolTaskSchedulerBuilder threadPoolTaskSchedulerBuilder) { - return threadPoolTaskSchedulerBuilder.build(); - } - - @Bean(name = IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME) - @ConditionalOnBean(SimpleAsyncTaskSchedulerBuilder.class) - @ConditionalOnThreading(Threading.VIRTUAL) - SimpleAsyncTaskScheduler taskSchedulerVirtualThreads( - SimpleAsyncTaskSchedulerBuilder simpleAsyncTaskSchedulerBuilder) { - return simpleAsyncTaskSchedulerBuilder.build(); - } - - } - /** * Spring Integration JMX configuration. */ From 4b11e04e7878ec02462253d23f58c3ea3e814e84 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 24 Feb 2026 16:18:09 +0100 Subject: [PATCH 246/930] Deprecate RestTemplateBuilder#rootUri with replacement This commit deprecates both `RootUriTemplateHandler` and `RootUriBuilderFactory` types, along their usage in places like `RestTemplateBuilder`. `RestTemplateBuilder#rootUri` is now deprecated in favor of `RestTemplateBuilder#baseUri`, which leverages Framework's `DefaultUriBuilderFactory`. Closes gh-48350 --- .../RootUriRequestExpectationManager.java | 16 +++++++++---- ...MockServerRestTemplateCustomizerTests.java | 4 ++-- ...RootUriRequestExpectationManagerTests.java | 16 ++++++++++--- .../AnotherExampleRestTemplateService.java | 2 +- ...thRestTemplateRootUriIntegrationTests.java | 2 +- .../ExampleRestTemplateService.java | 2 +- .../boot/restclient/RestTemplateBuilder.java | 24 +++++++++++++++++++ .../restclient/RootUriBuilderFactory.java | 5 ++++ .../restclient/RootUriTemplateHandler.java | 4 ++++ ...estClientWithRestTemplateBuilderTests.java | 4 ++-- .../restclient/RestTemplateBuilderTests.java | 10 ++++---- .../RootUriBuilderFactoryTests.java | 1 + .../RootUriTemplateHandlerTests.java | 1 + .../boot/resttestclient/TestRestTemplate.java | 6 ++--- .../resttestclient/TestRestTemplateTests.java | 8 +++---- .../service/RemoteVehicleDetailsService.java | 2 +- .../deployment/AbstractDeploymentTests.java | 2 +- 17 files changed, 81 insertions(+), 28 deletions(-) diff --git a/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManager.java b/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManager.java index 579da1775886..486188c2f44c 100644 --- a/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManager.java +++ b/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManager.java @@ -22,7 +22,6 @@ import java.net.URISyntaxException; import java.time.Duration; -import org.springframework.boot.restclient.RootUriTemplateHandler; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.support.HttpRequestWrapper; @@ -36,13 +35,14 @@ import org.springframework.test.web.client.SimpleRequestExpectationManager; import org.springframework.util.Assert; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.UriTemplateHandler; /** * {@link RequestExpectationManager} that strips the specified root URI from the request * before verification. Can be used to simply test declarations when all REST calls start * the same way. For example:

    - * RestTemplate restTemplate = new RestTemplateBuilder().rootUri("https://example.com").build();
    + * RestTemplate restTemplate = new RestTemplateBuilder().baseUri("https://example.com").build();
      * MockRestServiceServer server = RootUriRequestExpectationManager.bindTo(restTemplate);
      * server.expect(requestTo("/hello")).andRespond(withSuccess());
      * restTemplate.getForEntity("/hello", String.class);
    @@ -148,19 +148,25 @@ public static MockRestServiceServer bindTo(RestTemplate restTemplate,
     	/**
     	 * Return {@link RequestExpectationManager} to be used for binding with the specified
     	 * {@link RestTemplate}. If the {@link RestTemplate} is using a
    -	 * {@link RootUriTemplateHandler} then a {@link RootUriRequestExpectationManager} is
    -	 * returned, otherwise the source manager is returned unchanged.
    +	 * {@link org.springframework.boot.restclient.RootUriTemplateHandler} then a
    +	 * {@link RootUriRequestExpectationManager} is returned, otherwise the source manager
    +	 * is returned unchanged.
     	 * @param restTemplate the source REST template
     	 * @param expectationManager the source {@link RequestExpectationManager}
     	 * @return a {@link RequestExpectationManager} to be bound to the template
     	 */
    +	@SuppressWarnings("removal")
     	public static RequestExpectationManager forRestTemplate(RestTemplate restTemplate,
     			RequestExpectationManager expectationManager) {
     		Assert.notNull(restTemplate, "'restTemplate' must not be null");
     		UriTemplateHandler templateHandler = restTemplate.getUriTemplateHandler();
    -		if (templateHandler instanceof RootUriTemplateHandler rootHandler && rootHandler.getRootUri() != null) {
    +		if (templateHandler instanceof org.springframework.boot.restclient.RootUriTemplateHandler rootHandler
    +				&& rootHandler.getRootUri() != null) {
     			return new RootUriRequestExpectationManager(rootHandler.getRootUri(), expectationManager);
     		}
    +		else if (templateHandler instanceof DefaultUriBuilderFactory defaultHandler && defaultHandler.hasBaseUri()) {
    +			return new RootUriRequestExpectationManager(defaultHandler.expand("").toString(), expectationManager);
    +		}
     		return expectationManager;
     	}
     
    diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/MockServerRestTemplateCustomizerTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/MockServerRestTemplateCustomizerTests.java
    index 404f92ed7d63..fe380c19fd26 100644
    --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/MockServerRestTemplateCustomizerTests.java
    +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/MockServerRestTemplateCustomizerTests.java
    @@ -97,7 +97,7 @@ void createShouldUseSupplier() {
     	void detectRootUriShouldDefaultToTrue() {
     		MockServerRestTemplateCustomizer customizer = new MockServerRestTemplateCustomizer(
     				UnorderedRequestExpectationManager.class);
    -		customizer.customize(new RestTemplateBuilder().rootUri("https://example.com").build());
    +		customizer.customize(new RestTemplateBuilder().baseUri("https://example.com").build());
     		assertThat(customizer.getServer()).extracting("expectationManager")
     			.isInstanceOf(RootUriRequestExpectationManager.class);
     	}
    @@ -105,7 +105,7 @@ void detectRootUriShouldDefaultToTrue() {
     	@Test
     	void setDetectRootUriShouldDisableRootUriDetection() {
     		this.customizer.setDetectRootUri(false);
    -		this.customizer.customize(new RestTemplateBuilder().rootUri("https://example.com").build());
    +		this.customizer.customize(new RestTemplateBuilder().baseUri("https://example.com").build());
     		assertThat(this.customizer.getServer()).extracting("expectationManager")
     			.isInstanceOf(SimpleRequestExpectationManager.class);
     	}
    diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManagerTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManagerTests.java
    index acdb2d9fd44c..7e912d342c15 100644
    --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManagerTests.java
    +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManagerTests.java
    @@ -141,6 +141,7 @@ void bindToWithExpectationManagerShouldReturnMockRestServiceServer() {
     	}
     
     	@Test
    +	@SuppressWarnings("removal")
     	void forRestTemplateWhenUsingRootUriTemplateHandlerShouldReturnRootUriRequestExpectationManager() {
     		RestTemplate restTemplate = new RestTemplateBuilder().rootUri(this.uri).build();
     		RequestExpectationManager actual = RootUriRequestExpectationManager.forRestTemplate(restTemplate,
    @@ -149,6 +150,15 @@ void forRestTemplateWhenUsingRootUriTemplateHandlerShouldReturnRootUriRequestExp
     		assertThat(actual).extracting("rootUri").isEqualTo(this.uri);
     	}
     
    +	@Test
    +	void forRestTemplateWhenUsingBaseUriTemplateHandlerShouldReturnRootUriRequestExpectationManager() {
    +		RestTemplate restTemplate = new RestTemplateBuilder().baseUri(this.uri).build();
    +		RequestExpectationManager actual = RootUriRequestExpectationManager.forRestTemplate(restTemplate,
    +				this.delegate);
    +		assertThat(actual).isInstanceOf(RootUriRequestExpectationManager.class);
    +		assertThat(actual).extracting("rootUri").isEqualTo(this.uri);
    +	}
    +
     	@Test
     	void forRestTemplateWhenNotUsingRootUriTemplateHandlerShouldReturnOriginalRequestExpectationManager() {
     		RestTemplate restTemplate = new RestTemplateBuilder().build();
    @@ -158,8 +168,8 @@ void forRestTemplateWhenNotUsingRootUriTemplateHandlerShouldReturnOriginalReques
     	}
     
     	@Test
    -	void boundRestTemplateShouldPrefixRootUri() {
    -		RestTemplate restTemplate = new RestTemplateBuilder().rootUri("https://example.com").build();
    +	void boundRestTemplateShouldPrefixBaseUri() {
    +		RestTemplate restTemplate = new RestTemplateBuilder().baseUri("https://example.com").build();
     		MockRestServiceServer server = RootUriRequestExpectationManager.bindTo(restTemplate);
     		server.expect(requestTo("/hello")).andRespond(withSuccess());
     		restTemplate.getForEntity("/hello", String.class);
    @@ -167,7 +177,7 @@ void boundRestTemplateShouldPrefixRootUri() {
     
     	@Test
     	void boundRestTemplateWhenUrlIncludesDomainShouldNotPrefixRootUri() {
    -		RestTemplate restTemplate = new RestTemplateBuilder().rootUri("https://example.com").build();
    +		RestTemplate restTemplate = new RestTemplateBuilder().baseUri("https://example.com").build();
     		MockRestServiceServer server = RootUriRequestExpectationManager.bindTo(restTemplate);
     		server.expect(requestTo("/hello")).andRespond(withSuccess());
     		assertThatExceptionOfType(AssertionError.class)
    diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AnotherExampleRestTemplateService.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AnotherExampleRestTemplateService.java
    index 511f14730622..7402effc3fd2 100644
    --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AnotherExampleRestTemplateService.java
    +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AnotherExampleRestTemplateService.java
    @@ -33,7 +33,7 @@ public class AnotherExampleRestTemplateService {
     	private final RestTemplate restTemplate;
     
     	public AnotherExampleRestTemplateService(RestTemplateBuilder builder) {
    -		this.restTemplate = builder.rootUri("https://example.com").build();
    +		this.restTemplate = builder.baseUri("https://example.com").build();
     	}
     
     	protected RestTemplate getRestTemplate() {
    diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureMockRestServiceServerWithRestTemplateRootUriIntegrationTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureMockRestServiceServerWithRestTemplateRootUriIntegrationTests.java
    index a02b1c8efc07..ec9de9d2fea1 100644
    --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureMockRestServiceServerWithRestTemplateRootUriIntegrationTests.java
    +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureMockRestServiceServerWithRestTemplateRootUriIntegrationTests.java
    @@ -68,7 +68,7 @@ static class RootUriConfiguration {
     
     		@Bean
     		RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
    -			return restTemplateBuilder.rootUri("/rest").build();
    +			return restTemplateBuilder.baseUri("/rest").build();
     		}
     
     	}
    diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/ExampleRestTemplateService.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/ExampleRestTemplateService.java
    index 4f04ecdd4724..5acc9d7b83dd 100644
    --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/ExampleRestTemplateService.java
    +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/ExampleRestTemplateService.java
    @@ -34,7 +34,7 @@ public class ExampleRestTemplateService {
     	private final RestTemplate restTemplate;
     
     	public ExampleRestTemplateService(RestTemplateBuilder builder) {
    -		this.restTemplate = builder.rootUri("https://example.com").build();
    +		this.restTemplate = builder.baseUri("https://example.com").build();
     	}
     
     	protected RestTemplate getRestTemplate() {
    diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java
    index d39a04aa96d2..37848d57e6ff 100644
    --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java
    +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java
    @@ -45,6 +45,7 @@
     import org.springframework.util.CollectionUtils;
     import org.springframework.web.client.ResponseErrorHandler;
     import org.springframework.web.client.RestTemplate;
    +import org.springframework.web.util.DefaultUriBuilderFactory;
     import org.springframework.web.util.UriTemplateHandler;
     
     /**
    @@ -138,6 +139,24 @@ private RestTemplateBuilder(HttpClientSettings clientSettings, boolean detectReq
     		this.requestCustomizers = requestCustomizers;
     	}
     
    +	/**
    +	 * Set a base URL template that should be applied to each request that starts with
    +	 * {@code '/'}. The base URL will only apply when {@code String} variants of the
    +	 * {@link RestTemplate} methods are used for specifying the request URL.
    +	 * 

    + * This is a specialization of {@link #uriTemplateHandler(UriTemplateHandler)} and + * will override any previously configured uri template handler. + * @param baseUri the base URI + * @return a new builder instance + * @since 4.1.0 + */ + public RestTemplateBuilder baseUri(String baseUri) { + return new RestTemplateBuilder(this.clientSettings, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactoryBuilder, + new DefaultUriBuilderFactory(baseUri), this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); + } + /** * Set if the {@link ClientHttpRequestFactory} should be detected based on the * classpath. Default if {@code true}. @@ -157,7 +176,9 @@ public RestTemplateBuilder detectRequestFactory(boolean detectRequestFactory) { * {@link RestTemplate} methods are used for specifying the request URL. * @param rootUri the root URI or {@code null} * @return a new builder instance + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of {@link #baseUri(String)} */ + @Deprecated(forRemoval = true, since = "4.1.0") public RestTemplateBuilder rootUri(@Nullable String rootUri) { return new RestTemplateBuilder(this.clientSettings, this.detectRequestFactory, rootUri, this.messageConverters, this.interceptors, this.requestFactoryBuilder, this.uriTemplateHandler, this.errorHandler, @@ -339,6 +360,8 @@ public RestTemplateBuilder requestFactoryBuilder(ClientHttpRequestFactoryBuilder /** * Set the {@link UriTemplateHandler} that should be used with the * {@link RestTemplate}. + *

    + * This method will override any {@link #baseUri(String)} previously set. * @param uriTemplateHandler the URI template handler to use * @return a new builder instance */ @@ -635,6 +658,7 @@ public T build(Class restTemplateClass) { * @see RestTemplateBuilder#build() * @see RestTemplateBuilder#build(Class) */ + @SuppressWarnings("removal") public T configure(T restTemplate) { ClientHttpRequestFactory requestFactory = buildRequestFactory(); if (requestFactory != null) { diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RootUriBuilderFactory.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RootUriBuilderFactory.java index 1a65e2bea390..19b9846cca61 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RootUriBuilderFactory.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RootUriBuilderFactory.java @@ -18,6 +18,7 @@ import org.springframework.util.Assert; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.UriBuilder; import org.springframework.web.util.UriBuilderFactory; import org.springframework.web.util.UriComponentsBuilder; @@ -28,7 +29,11 @@ * * @author Scott Frederick * @since 4.0.0 + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link DefaultUriBuilderFactory}. */ +@Deprecated(forRemoval = true, since = "4.1.0") +@SuppressWarnings("removal") public class RootUriBuilderFactory extends RootUriTemplateHandler implements UriBuilderFactory { /** diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RootUriTemplateHandler.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RootUriTemplateHandler.java index 85f4471bff50..19020191490c 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RootUriTemplateHandler.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RootUriTemplateHandler.java @@ -23,6 +23,7 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.UriTemplateHandler; /** @@ -31,7 +32,10 @@ * @author Phillip Webb * @author Scott Frederick * @since 4.0.0 + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link DefaultUriBuilderFactory}. */ +@Deprecated(forRemoval = true, since = "4.1.0") public class RootUriTemplateHandler implements UriTemplateHandler { private final @Nullable String rootUri; diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestClientWithRestTemplateBuilderTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestClientWithRestTemplateBuilderTests.java index e9a28524fdb8..3a6dcb313d67 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestClientWithRestTemplateBuilderTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestClientWithRestTemplateBuilderTests.java @@ -36,7 +36,7 @@ class RestClientWithRestTemplateBuilderTests { @Test void buildUsingRestTemplateBuilderRootUri() { - RestTemplate restTemplate = new RestTemplateBuilder().rootUri("https://resttemplate.example.com").build(); + RestTemplate restTemplate = new RestTemplateBuilder().baseUri("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(); @@ -52,7 +52,7 @@ void buildUsingRestClientBuilderBaseUrl() { @Test void buildRestTemplateBuilderRootUriAndRestClientBuilderBaseUrl() { - RestTemplate restTemplate = new RestTemplateBuilder().rootUri("https://resttemplate.example.com").build(); + RestTemplate restTemplate = new RestTemplateBuilder().baseUri("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(); diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestTemplateBuilderTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestTemplateBuilderTests.java index 84dc48e7ffa1..e23285d1a8aa 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestTemplateBuilderTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestTemplateBuilderTests.java @@ -55,6 +55,7 @@ import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.UriTemplateHandler; import static org.assertj.core.api.Assertions.assertThat; @@ -121,8 +122,8 @@ void detectRequestFactoryWhenFalseShouldDisableDetection() { } @Test - void rootUriShouldApply() { - RestTemplate restTemplate = this.builder.rootUri("https://example.com").build(); + void baseUriShouldApply() { + RestTemplate restTemplate = this.builder.baseUri("https://example.com").build(); MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build(); server.expect(requestTo("https://example.com/hello")).andRespond(withSuccess()); restTemplate.getForEntity("/hello", String.class); @@ -130,6 +131,7 @@ void rootUriShouldApply() { } @Test + @SuppressWarnings("removal") void rootUriShouldApplyAfterUriTemplateHandler() { UriTemplateHandler uriTemplateHandler = mock(UriTemplateHandler.class); RestTemplate template = this.builder.uriTemplateHandler(uriTemplateHandler) @@ -466,14 +468,14 @@ void customizerShouldBeAppliedAtTheEnd() { ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); this.builder.interceptors(this.interceptor) .messageConverters(this.messageConverter) - .rootUri("http://localhost:8080") + .baseUri("http://localhost:8080") .errorHandler(errorHandler) .basicAuthentication("spring", "boot") .requestFactory(() -> requestFactory) .customizers((restTemplate) -> { assertThat(restTemplate.getInterceptors()).hasSize(1); assertThat(restTemplate.getMessageConverters()).contains(this.messageConverter); - assertThat(restTemplate.getUriTemplateHandler()).isInstanceOf(RootUriBuilderFactory.class); + assertThat(restTemplate.getUriTemplateHandler()).isInstanceOf(DefaultUriBuilderFactory.class); assertThat(restTemplate.getErrorHandler()).isEqualTo(errorHandler); ClientHttpRequestFactory actualRequestFactory = restTemplate.getRequestFactory(); assertThat(actualRequestFactory).isInstanceOf(InterceptingClientHttpRequestFactory.class); diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RootUriBuilderFactoryTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RootUriBuilderFactoryTests.java index 7251decdd0b2..b4460c718511 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RootUriBuilderFactoryTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RootUriBuilderFactoryTests.java @@ -33,6 +33,7 @@ * * @author Scott Frederick */ +@SuppressWarnings("removal") class RootUriBuilderFactoryTests { @Test diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RootUriTemplateHandlerTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RootUriTemplateHandlerTests.java index 9140f2e8c82a..b646dcf4480d 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RootUriTemplateHandlerTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RootUriTemplateHandlerTests.java @@ -43,6 +43,7 @@ * @author Phillip Webb */ @ExtendWith(MockitoExtension.class) +@SuppressWarnings("removal") class RootUriTemplateHandlerTests { private URI uri; diff --git a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java index 48118a3680e0..b9988d6d48bb 100644 --- a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java +++ b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java @@ -43,7 +43,6 @@ import org.springframework.boot.http.client.HttpComponentsHttpClientBuilder.TlsSocketStrategyFactory; import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.restclient.RestTemplateBuilder; -import org.springframework.boot.restclient.RootUriTemplateHandler; import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.ssl.SslBundle; import org.springframework.core.ParameterizedTypeReference; @@ -184,13 +183,14 @@ public void setUriTemplateHandler(UriTemplateHandler handler) { } /** - * Returns the root URI applied by {@link RestTemplateBuilder#rootUri(String)} or + * Returns the root URI applied by {@link RestTemplateBuilder#baseUri(String)} or * {@code ""} if the root URI has not been applied. * @return the root URI */ + @SuppressWarnings("removal") public @Nullable String getRootUri() { UriTemplateHandler uriTemplateHandler = this.restTemplate.getUriTemplateHandler(); - if (uriTemplateHandler instanceof RootUriTemplateHandler rootHandler) { + if (uriTemplateHandler instanceof org.springframework.boot.restclient.RootUriTemplateHandler rootHandler) { return rootHandler.getRootUri(); } return uriTemplateHandler.expand("").toString(); diff --git a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java index c59889e850ab..932376a8fcf5 100644 --- a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java +++ b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java @@ -114,10 +114,10 @@ void useTheSameRequestFactoryClassWithBasicAuth() { } @Test - void getRootUriRootUriSetViaRestTemplateBuilder() { - String rootUri = "https://example.com"; - RestTemplateBuilder delegate = new RestTemplateBuilder().rootUri(rootUri); - assertThat(new TestRestTemplate(delegate).getRootUri()).isEqualTo(rootUri); + void getBaseUriRootUriSetViaRestTemplateBuilder() { + String baseUri = "https://example.com"; + RestTemplateBuilder delegate = new RestTemplateBuilder().baseUri(baseUri); + assertThat(new TestRestTemplate(delegate).getRootUri()).isEqualTo(baseUri); } @Test diff --git a/smoke-test/spring-boot-smoke-test-test/src/main/java/smoketest/test/service/RemoteVehicleDetailsService.java b/smoke-test/spring-boot-smoke-test-test/src/main/java/smoketest/test/service/RemoteVehicleDetailsService.java index 7fb5bb87cd30..ef4939d8e1ba 100644 --- a/smoke-test/spring-boot-smoke-test-test/src/main/java/smoketest/test/service/RemoteVehicleDetailsService.java +++ b/smoke-test/spring-boot-smoke-test-test/src/main/java/smoketest/test/service/RemoteVehicleDetailsService.java @@ -40,7 +40,7 @@ public class RemoteVehicleDetailsService implements VehicleDetailsService { private final RestTemplate restTemplate; public RemoteVehicleDetailsService(ServiceProperties properties, RestTemplateBuilder restTemplateBuilder) { - this.restTemplate = restTemplateBuilder.rootUri(properties.getVehicleServiceRootUrl()).build(); + this.restTemplate = restTemplateBuilder.baseUri(properties.getVehicleServiceRootUrl()).build(); } @Override diff --git a/system-test/spring-boot-deployment-system-tests/src/systemTest/java/org/springframework/boot/deployment/AbstractDeploymentTests.java b/system-test/spring-boot-deployment-system-tests/src/systemTest/java/org/springframework/boot/deployment/AbstractDeploymentTests.java index f16cbdb4a5e4..51a7477f10ec 100644 --- a/system-test/spring-boot-deployment-system-tests/src/systemTest/java/org/springframework/boot/deployment/AbstractDeploymentTests.java +++ b/system-test/spring-boot-deployment-system-tests/src/systemTest/java/org/springframework/boot/deployment/AbstractDeploymentTests.java @@ -97,7 +97,7 @@ static final class DeployedApplication { private void test(Consumer consumer) { TestRestTemplate rest = new TestRestTemplate(new RestTemplateBuilder() - .rootUri("http://" + this.container.getHost() + ":" + this.container.getMappedPort(this.port) + .baseUri("http://" + this.container.getHost() + ":" + this.container.getMappedPort(this.port) + "/spring-boot") .requestFactory(() -> new HttpComponentsClientHttpRequestFactory(HttpClients.custom() .setRetryStrategy(new DefaultHttpRequestRetryStrategy(10, TimeValue.of(1, TimeUnit.SECONDS))) From 2182339bb146e9990b3a2c0ccf54535b8ea1f19a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 24 Feb 2026 22:37:34 -0800 Subject: [PATCH 247/930] Add FailureAnalyzedException support Add `FailureAnalyzedException` to reduce the number of internal `FailureAnalizers` required when only a simple "description" and "action" are required. Closes gh-49320 --- .../diagnostics/FailureAnalyzedException.java | 70 +++++++++++++++++++ .../boot/diagnostics/FailureAnalyzers.java | 8 ++- .../diagnostics/FailureAnalyzersTests.java | 49 +++++++++++++ 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzedException.java diff --git a/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzedException.java b/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzedException.java new file mode 100644 index 000000000000..d4110f7e8043 --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzedException.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +import org.jspecify.annotations.Nullable; + +/** + * {@link RuntimeException} that includes a {@link FailureAnalysis}. + * + * @author Phillip Webb + * @since 4.1.0 + */ +public class FailureAnalyzedException extends RuntimeException { + + private final @Nullable String action; + + /** + * Create a new {@link FailureAnalyzedException} instance. + * @param description the {@link FailureAnalysis} description + * @param action the {@link FailureAnalysis} action + */ + public FailureAnalyzedException(String description, @Nullable String action) { + super(description); + this.action = action; + } + + /** + * Create a new {@link FailureAnalyzedException} instance. + * @param description the {@link FailureAnalysis} description + * @param action the {@link FailureAnalysis} action + * @param cause the cause of the exception + */ + public FailureAnalyzedException(String description, @Nullable String action, Throwable cause) { + super(description, cause); + this.action = action; + } + + /** + * Return the {@link FailureAnalysis} to use for this exception. + * @return the failure analysis + */ + public FailureAnalysis analysis() { + return new FailureAnalysis(getMessage(), this.action, this); + } + + static @Nullable FailureAnalysis analyze(Throwable failure) { + while (failure != null) { + if (failure instanceof FailureAnalyzedException failureAnalyzedException) { + return failureAnalyzedException.analysis(); + } + failure = failure.getCause(); + } + return null; + } + +} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java b/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java index 6c967fa3a48e..c56f2129b7ba 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java @@ -16,6 +16,8 @@ package org.springframework.boot.diagnostics; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.apache.commons.logging.Log; @@ -64,8 +66,10 @@ public FailureAnalyzers(@Nullable ConfigurableApplicationContext context) { private static List loadFailureAnalyzers(@Nullable ConfigurableApplicationContext context, SpringFactoriesLoader springFactoriesLoader) { - return springFactoriesLoader.load(FailureAnalyzer.class, getArgumentResolver(context), - FailureHandler.logging(logger)); + List analyzers = new ArrayList<>(springFactoriesLoader.load(FailureAnalyzer.class, + getArgumentResolver(context), FailureHandler.logging(logger))); + analyzers.add(FailureAnalyzedException::analyze); + return Collections.unmodifiableList(analyzers); } private static @Nullable ArgumentResolver getArgumentResolver(@Nullable ConfigurableApplicationContext context) { diff --git a/core/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java b/core/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java index b2ab039bc1f0..13c0b6ad52cf 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java @@ -16,6 +16,9 @@ package org.springframework.boot.diagnostics; +import java.util.ArrayList; +import java.util.List; + import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -47,6 +50,8 @@ class FailureAnalyzersTests { private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + private final List reports = new ArrayList<>(); + @BeforeEach void configureMock() { failureAnalyzer = mock(FailureAnalyzer.class); @@ -87,6 +92,37 @@ void analyzerThatFailsDuringAnalysisDoesNotPreventOtherAnalyzersFromBeingCalled( then(failureAnalyzer).should().analyze(failure); } + @Test + void failureAnalyzedExceptionAreReported() { + FailureAnalyzedException failure = new FailureAnalyzedException("Bad", "Fix it!"); + analyzeAndReport(failure); + assertThat(this.reports).hasSize(1); + FailureAnalysis report = this.reports.get(0); + assertThat(report.getDescription()).isEqualTo("Bad"); + assertThat(report.getAction()).isEqualTo("Fix it!"); + assertThat(report.getCause()).isSameAs(failure); + } + + @Test + void wrappedFailureAnalyzedExceptionAreReported() { + FailureAnalyzedException failure = new FailureAnalyzedException("Bad", "Fix it!"); + analyzeAndReport(new IllegalStateException("the state of this!", failure)); + assertThat(this.reports).hasSize(1); + FailureAnalysis report = this.reports.get(0); + assertThat(report.getDescription()).isEqualTo("Bad"); + assertThat(report.getAction()).isEqualTo("Fix it!"); + assertThat(report.getCause()).isSameAs(failure); + } + + @Test + void whenOtherAnalyzerIsAvailableFailureAnalyzedExceptionAreNotReported() { + FailureAnalyzedException failure = new FailureAnalyzedException("Bad", "Fix it!"); + analyzeAndReport(new IllegalStateException("the state of this!", failure), IllegalStateFailureAnalyzer.class); + assertThat(this.reports).hasSize(1); + FailureAnalysis report = this.reports.get(0); + assertThat(report.getDescription()).isEqualTo("analyzed state"); + } + @SafeVarargs private void analyzeAndReport(Throwable failure, Class... failureAnalyzerClasses) { analyzeAndReport(failure, this.context, failureAnalyzerClasses); @@ -99,6 +135,7 @@ private void analyzeAndReport(Throwable failure, AnnotationConfigApplicationCont for (Class failureAnalyzerClass : failureAnalyzerClasses) { loader.add(FailureAnalyzer.class, failureAnalyzerClass); } + loader.addInstance(FailureAnalysisReporter.class, this.reports::add); new FailureAnalyzers(context, loader).reportException(failure); } @@ -151,4 +188,16 @@ static class EnvironmentConstructorFailureAnalyzer extends BasicFailureAnalyzer } + static class IllegalStateFailureAnalyzer implements FailureAnalyzer { + + @Override + public @Nullable FailureAnalysis analyze(Throwable failure) { + if (failure instanceof IllegalStateException) { + return new FailureAnalysis("analyzed state", null, failure); + } + return null; + } + + } + } From 87e9544327b7036a0ac9cfb1edf67ca13ca4c4fa Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 26 Feb 2026 11:06:43 +0000 Subject: [PATCH 248/930] Upgrade to Jackson 2 Bom 2.21.1 Closes gh-49329 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 828ceae76c13..05f9150b56fb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ checkstyleToolVersion=10.12.4 commonsCodecVersion=1.21.0 graalVersion=25 hamcrestVersion=3.0 -jackson2Version=2.21.0 +jackson2Version=2.21.1 jacksonVersion=3.0.4 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 From 988a012a7b0fddea94dce566c2e74250f26a2628 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 26 Feb 2026 11:06:48 +0000 Subject: [PATCH 249/930] Upgrade to Jackson Bom 3.1.0 Closes gh-49330 --- gradle.properties | 2 +- .../boot/jackson/autoconfigure/JacksonProperties.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 05f9150b56fb..65a4693d8214 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ commonsCodecVersion=1.21.0 graalVersion=25 hamcrestVersion=3.0 jackson2Version=2.21.1 -jacksonVersion=3.0.4 +jacksonVersion=3.1.0 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 kotlinVersion=2.3.10 diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java index 472b68045194..6d3735609d85 100644 --- a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java @@ -375,7 +375,7 @@ public static class Read { /** * Maximum string length. */ - private int maxStringLength = 20_000_000; + private int maxStringLength = 100_000_000; /** * Maximum name length. From 8018829ff5da9a242b084d3853dc4add74c92b46 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 26 Feb 2026 14:22:02 +0100 Subject: [PATCH 250/930] Deprecate LiveReload support for removal Closes gh-48971 --- .../modules/reference/pages/using/devtools.adoc | 10 +++++----- .../autoconfigure/DevToolsProperties.java | 3 +++ .../LocalDevToolsAutoConfiguration.java | 12 +++++++----- .../autoconfigure/OptionalLiveReloadServer.java | 3 +++ .../boot/devtools/livereload/Connection.java | 1 + .../devtools/livereload/LiveReloadServer.java | 2 ++ .../remote/client/DelayedLiveReloadTrigger.java | 1 + .../client/RemoteClientConfiguration.java | 17 +++++++++-------- .../LocalDevToolsAutoConfigurationTests.java | 1 + .../OptionalLiveReloadServerTests.java | 1 + .../livereload/LiveReloadServerTests.java | 1 + .../client/DelayedLiveReloadTriggerTests.java | 1 + .../client/RemoteClientConfigurationTests.java | 3 +++ 13 files changed, 38 insertions(+), 18 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/using/devtools.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/using/devtools.adoc index ca344d9dc593..a0664e59ba63 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/using/devtools.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/using/devtools.adoc @@ -108,10 +108,8 @@ The way in which you cause the classpath to be updated depends on the tool that NOTE: If you are restarting with Maven or Gradle using the build plugin you must leave the `forking` set to `enabled`. If you disable forking, the isolated application classloader used by devtools will not be created and restarts will not operate properly. -TIP: Automatic restart works very well when used with LiveReload. -See the xref:using/devtools.adoc#using.devtools.livereload[] section for details. -If you use JRebel, automatic restarts are disabled in favor of dynamic class reloading. -Other devtools features (such as LiveReload and property overrides) can still be used. +TIP: If you use JRebel, automatic restarts are disabled in favor of dynamic class reloading. +Other devtools features (such as property overrides) can still be used. NOTE: DevTools relies on the application context's shutdown hook to close it during a restart. It does not work correctly if you have disabled the shutdown hook (`SpringApplication.setRegisterShutdownHook(false)`). @@ -287,11 +285,13 @@ If you find such a problem, you need to request a fix with the original authors. [[using.devtools.livereload]] == LiveReload +WARNING: Given its decrease in popularity and support, the LiveReload feature is deprecated as of Spring Boot 4.1.0 with no replacement. + The `spring-boot-devtools` module includes an embedded LiveReload server that can be used to trigger a browser refresh when a resource is changed. LiveReload browser extensions are freely available for Chrome, Firefox and Safari. You can find these extensions by searching 'LiveReload' in the marketplace or store of your chosen browser. -If you want to start the LiveReload server when your application runs, you can set the configprop:spring.devtools.livereload.enabled[] property to `true`. +If you want to start the LiveReload server when your application runs, you can set the `spring.devtools.livereload.enabled` property to `true`. NOTE: You can only run one LiveReload server at a time. Before starting your application, ensure that no other LiveReload servers are running. diff --git a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsProperties.java b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsProperties.java index b7ecb9ebb6bf..5db413c16939 100644 --- a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsProperties.java +++ b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsProperties.java @@ -24,6 +24,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.util.StringUtils; @@ -200,6 +201,7 @@ public static class Livereload { */ private int port = 35729; + @DeprecatedConfigurationProperty(reason = "Deprecated with no replacement", since = "4.1.0") public boolean isEnabled() { return this.enabled; } @@ -208,6 +210,7 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } + @DeprecatedConfigurationProperty(reason = "Deprecated with no replacement", since = "4.1.0") public int getPort() { return this.port; } diff --git a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java index 4392bf46eb4e..add36fcdcea8 100644 --- a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java +++ b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java @@ -37,7 +37,6 @@ import org.springframework.boot.devtools.filewatch.FileSystemWatcher; import org.springframework.boot.devtools.filewatch.FileSystemWatcherFactory; import org.springframework.boot.devtools.filewatch.SnapshotStateRepository; -import org.springframework.boot.devtools.livereload.LiveReloadServer; import org.springframework.boot.devtools.restart.ConditionalOnInitializedRestarter; import org.springframework.boot.devtools.restart.RestartScope; import org.springframework.boot.devtools.restart.Restarter; @@ -71,18 +70,20 @@ public final class LocalDevToolsAutoConfiguration { */ @Configuration(proxyBeanMethods = false) @ConditionalOnBooleanProperty(name = "spring.devtools.livereload.enabled") + @SuppressWarnings("removal") static class LiveReloadConfiguration { @Bean @RestartScope @ConditionalOnMissingBean - LiveReloadServer liveReloadServer(DevToolsProperties properties) { - return new LiveReloadServer(properties.getLivereload().getPort(), - Restarter.getInstance().getThreadFactory()); + org.springframework.boot.devtools.livereload.LiveReloadServer liveReloadServer(DevToolsProperties properties) { + return new org.springframework.boot.devtools.livereload.LiveReloadServer( + properties.getLivereload().getPort(), Restarter.getInstance().getThreadFactory()); } @Bean - OptionalLiveReloadServer optionalLiveReloadServer(LiveReloadServer liveReloadServer) { + OptionalLiveReloadServer optionalLiveReloadServer( + org.springframework.boot.devtools.livereload.LiveReloadServer liveReloadServer) { return new OptionalLiveReloadServer(liveReloadServer); } @@ -160,6 +161,7 @@ private FileSystemWatcher newFileSystemWatcher() { } + @SuppressWarnings("removal") static class LiveReloadServerEventListener implements GenericApplicationListener { private final OptionalLiveReloadServer liveReloadServer; diff --git a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/OptionalLiveReloadServer.java b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/OptionalLiveReloadServer.java index 1931f4fab8f3..1ca0dc0ae5e1 100644 --- a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/OptionalLiveReloadServer.java +++ b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/OptionalLiveReloadServer.java @@ -30,7 +30,10 @@ * * @author Phillip Webb * @since 1.3.0 + * @deprecated since 4.1.0 for removal in 4.3.0 with no replacement. */ +@Deprecated(since = "4.1.0", forRemoval = true) +@SuppressWarnings("removal") public class OptionalLiveReloadServer implements InitializingBean { private static final Log logger = LogFactory.getLog(OptionalLiveReloadServer.class); diff --git a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/Connection.java b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/Connection.java index a4b261982104..8019a19f1c8a 100644 --- a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/Connection.java +++ b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/Connection.java @@ -40,6 +40,7 @@ * @author Phillip Webb * @author Francis Lavoie */ +@SuppressWarnings("removal") class Connection { private static final Log logger = LogFactory.getLog(Connection.class); diff --git a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java index 0e587afb3bd1..4bebcede1b55 100644 --- a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java +++ b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java @@ -42,7 +42,9 @@ * * @author Phillip Webb * @since 1.3.0 + * @deprecated since 4.1.0 for removal in 4.3.0 with no replacement. */ +@Deprecated(since = "4.1.0", forRemoval = true) public class LiveReloadServer { /** diff --git a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/DelayedLiveReloadTrigger.java b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/DelayedLiveReloadTrigger.java index 3ea02bfa16a9..d84cdcbac2b4 100644 --- a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/DelayedLiveReloadTrigger.java +++ b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/DelayedLiveReloadTrigger.java @@ -37,6 +37,7 @@ * * @author Phillip Webb */ +@SuppressWarnings("removal") class DelayedLiveReloadTrigger implements Runnable { private static final long SHUTDOWN_TIME = 1000; diff --git a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/RemoteClientConfiguration.java b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/RemoteClientConfiguration.java index ef4f525f23d2..b01ad14c9fda 100644 --- a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/RemoteClientConfiguration.java +++ b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/RemoteClientConfiguration.java @@ -36,7 +36,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.devtools.autoconfigure.DevToolsProperties; import org.springframework.boot.devtools.autoconfigure.DevToolsProperties.Restart; -import org.springframework.boot.devtools.autoconfigure.OptionalLiveReloadServer; import org.springframework.boot.devtools.autoconfigure.RemoteDevToolsProperties; import org.springframework.boot.devtools.autoconfigure.RemoteDevToolsProperties.Proxy; import org.springframework.boot.devtools.autoconfigure.TriggerFileFilter; @@ -46,7 +45,6 @@ import org.springframework.boot.devtools.classpath.PatternClassPathRestartStrategy; import org.springframework.boot.devtools.filewatch.FileSystemWatcher; import org.springframework.boot.devtools.filewatch.FileSystemWatcherFactory; -import org.springframework.boot.devtools.livereload.LiveReloadServer; import org.springframework.boot.devtools.restart.DefaultRestartInitializer; import org.springframework.boot.devtools.restart.RestartScope; import org.springframework.boot.devtools.restart.Restarter; @@ -71,6 +69,7 @@ */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(DevToolsProperties.class) +@SuppressWarnings("removal") public class RemoteClientConfiguration implements InitializingBean { private static final Log logger = LogFactory.getLog(RemoteClientConfiguration.class); @@ -152,14 +151,14 @@ static class LiveReloadConfiguration { @Bean @RestartScope @ConditionalOnMissingBean - LiveReloadServer liveReloadServer() { - return new LiveReloadServer(this.properties.getLivereload().getPort(), - Restarter.getInstance().getThreadFactory()); + org.springframework.boot.devtools.livereload.LiveReloadServer liveReloadServer() { + return new org.springframework.boot.devtools.livereload.LiveReloadServer( + this.properties.getLivereload().getPort(), Restarter.getInstance().getThreadFactory()); } @Bean ApplicationListener liveReloadTriggeringClassPathChangedEventListener( - OptionalLiveReloadServer optionalLiveReloadServer) { + org.springframework.boot.devtools.autoconfigure.OptionalLiveReloadServer optionalLiveReloadServer) { return (event) -> { String url = this.remoteUrl + this.properties.getRemote().getContextPath(); this.executor.execute( @@ -168,8 +167,10 @@ ApplicationListener liveReloadTriggeringClassPathChangedE } @Bean - OptionalLiveReloadServer optionalLiveReloadServer(ObjectProvider liveReloadServer) { - return new OptionalLiveReloadServer(liveReloadServer.getIfAvailable()); + org.springframework.boot.devtools.autoconfigure.OptionalLiveReloadServer optionalLiveReloadServer( + ObjectProvider liveReloadServer) { + return new org.springframework.boot.devtools.autoconfigure.OptionalLiveReloadServer( + liveReloadServer.getIfAvailable()); } final ExecutorService getExecutor() { diff --git a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java index 1c6ebe2a4dc0..af6d37ba675b 100644 --- a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java +++ b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java @@ -73,6 +73,7 @@ * @author Vladimir Tsanev */ @ExtendWith(MockRestarter.class) +@SuppressWarnings("removal") class LocalDevToolsAutoConfigurationTests { private @Nullable ConfigurableApplicationContext context; diff --git a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/OptionalLiveReloadServerTests.java b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/OptionalLiveReloadServerTests.java index 993e26ddebbd..e284c0dedaf6 100644 --- a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/OptionalLiveReloadServerTests.java +++ b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/OptionalLiveReloadServerTests.java @@ -30,6 +30,7 @@ * * @author Phillip Webb */ +@SuppressWarnings("removal") class OptionalLiveReloadServerTests { @Test diff --git a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/livereload/LiveReloadServerTests.java b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/livereload/LiveReloadServerTests.java index a3d4edc03f06..b32d6f3a8686 100644 --- a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/livereload/LiveReloadServerTests.java +++ b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/livereload/LiveReloadServerTests.java @@ -83,6 +83,7 @@ * @author Phillip Webb * @author Andy Wilkinson */ +@SuppressWarnings("removal") class LiveReloadServerTests { private static final String HANDSHAKE = "{command: 'hello', " diff --git a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/remote/client/DelayedLiveReloadTriggerTests.java b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/remote/client/DelayedLiveReloadTriggerTests.java index 013c6c73cf7b..f3dc5509a43b 100644 --- a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/remote/client/DelayedLiveReloadTriggerTests.java +++ b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/remote/client/DelayedLiveReloadTriggerTests.java @@ -44,6 +44,7 @@ * @author Phillip Webb */ @ExtendWith(MockitoExtension.class) +@SuppressWarnings("removal") class DelayedLiveReloadTriggerTests { private static final String URL = "http://localhost:8080"; diff --git a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/remote/client/RemoteClientConfigurationTests.java b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/remote/client/RemoteClientConfigurationTests.java index a1af0cd6d6a3..37ff6bef3637 100644 --- a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/remote/client/RemoteClientConfigurationTests.java +++ b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/remote/client/RemoteClientConfigurationTests.java @@ -104,6 +104,7 @@ void failIfNoSecret() { } @Test + @SuppressWarnings("removal") void liveReloadOnClassPathChanged() { configure("spring.devtools.livereload.enabled:true"); Set changeSet = new HashSet<>(); @@ -115,6 +116,7 @@ void liveReloadOnClassPathChanged() { } @Test + @SuppressWarnings("removal") void liveReloadDisabledByDefault() { configure(); assertThatExceptionOfType(NoSuchBeanDefinitionException.class) @@ -185,6 +187,7 @@ Dispatcher dispatcher() throws IOException { } @Configuration(proxyBeanMethods = false) + @SuppressWarnings("removal") static class ClientConfig { @Bean From 396bdfb385ffb766b8bcf1216ffa3a40833ba52e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 26 Feb 2026 13:55:39 +0000 Subject: [PATCH 251/930] Make config metadata changelog generation more robust Previously, the list of modules for a particular version of Boot was hard-coded. When this list got of a sync with a version's actual modules, the changelog would become inaccurate as the generator would not have a complete view of all of the version's metadata. This commit addresses this by resolving the list of modules for a particular version from its dependency management declared in spring-boot-dependencies. Closes gh-49336 --- .../build.gradle | 173 +++--------------- 1 file changed, 22 insertions(+), 151 deletions(-) diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle index 367a774e54f8..deedba41ed1d 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle @@ -1,3 +1,5 @@ +import groovy.xml.XmlSlurper + /* * Copyright 2012-present the original author or authors. * @@ -20,15 +22,6 @@ plugins { description = "Spring Boot Configuration Metadata Changelog Generator" -configurations { - oldMetadata { - transitive = false - } - newMetadata { - transitive = false - } -} - dependencies { implementation(enforcedPlatform(project(":platform:spring-boot-dependencies"))) implementation(project(":configuration-metadata:spring-boot-configuration-metadata")) @@ -42,162 +35,40 @@ nullability { requireExplicitNullMarking = false } -def dependenciesOf(String version) { - def springBoot40Modules = [ - "spring-boot", - "spring-boot-activemq", - "spring-boot-actuator", - "spring-boot-actuator-autoconfigure", - "spring-boot-amqp", - "spring-boot-artemis", - "spring-boot-autoconfigure", - "spring-boot-batch", - "spring-boot-batch-jdbc", - "spring-boot-cache", - "spring-boot-cassandra", - "spring-boot-couchbase", - "spring-boot-data-cassandra", - "spring-boot-data-commons", - "spring-boot-data-couchbase", - "spring-boot-data-elasticsearch", - "spring-boot-data-jdbc", - "spring-boot-data-jpa", - "spring-boot-data-ldap", - "spring-boot-data-mongodb", - "spring-boot-data-neo4j", - "spring-boot-data-r2dbc", - "spring-boot-data-redis", - "spring-boot-data-rest", - "spring-boot-devtools", - "spring-boot-docker-compose", - "spring-boot-elasticsearch", - "spring-boot-flyway", - "spring-boot-freemarker", - "spring-boot-graphql", - "spring-boot-groovy-templates", - "spring-boot-gson", - "spring-boot-h2console", - "spring-boot-hateoas", - "spring-boot-hazelcast", - "spring-boot-health", - "spring-boot-hibernate", - "spring-boot-http-client", - "spring-boot-http-codec", - "spring-boot-http-converter", - "spring-boot-integration", - "spring-boot-jackson", - "spring-boot-jackson2", - "spring-boot-jdbc", - "spring-boot-jdbc-test", - "spring-boot-jersey", - "spring-boot-jetty", - "spring-boot-jms", - "spring-boot-jooq", - "spring-boot-jpa", - "spring-boot-kafka", - "spring-boot-ldap", - "spring-boot-liquibase", - "spring-boot-mail", - "spring-boot-micrometer-metrics", - "spring-boot-micrometer-metrics-test", - "spring-boot-micrometer-observation", - "spring-boot-micrometer-tracing", - "spring-boot-micrometer-tracing-brave", - "spring-boot-micrometer-tracing-opentelemetry", - "spring-boot-micrometer-tracing-test", - "spring-boot-mongodb", - "spring-boot-mustache", - "spring-boot-neo4j", - "spring-boot-netty", - "spring-boot-opentelemetry", - "spring-boot-persistence", - "spring-boot-pulsar", - "spring-boot-quartz", - "spring-boot-r2dbc", - "spring-boot-reactor", - "spring-boot-reactor-netty", - "spring-boot-restclient", - "spring-boot-restclient-test", - "spring-boot-restdocs", - "spring-boot-rsocket", - "spring-boot-security", - "spring-boot-security-oauth2-authorization-server", - "spring-boot-security-oauth2-client", - "spring-boot-security-oauth2-resource-server", - "spring-boot-security-saml2", - "spring-boot-sendgrid", - "spring-boot-servlet", - "spring-boot-session", - "spring-boot-session-data-redis", - "spring-boot-session-jdbc", - "spring-boot-sql", - "spring-boot-test-autoconfigure", - "spring-boot-testcontainers", - "spring-boot-thymeleaf", - "spring-boot-tomcat", - "spring-boot-transaction", - "spring-boot-validation", - "spring-boot-web-server", - "spring-boot-web-server-test", - "spring-boot-webclient", - "spring-boot-webflux", - "spring-boot-webflux-test", - "spring-boot-webmvc", - "spring-boot-webmvc-test", - "spring-boot-webservices", - "spring-boot-webservices-test", - "spring-boot-zipkin" - ] - if (version.startsWith("4.0")) { - return springBoot40Modules - } - else if (version.startsWith("4.1")) { - // Alter Spring Boot 4.0 modules if necessary - return springBoot40Modules - } - return [ - "spring-boot", - "spring-boot-actuator", - "spring-boot-actuator-autoconfigure", - "spring-boot-autoconfigure", - "spring-boot-devtools", - "spring-boot-docker-compose", - "spring-boot-test-autoconfigure", - "spring-boot-testcontainers" - ] -} - if (project.hasProperty("oldVersion") && project.hasProperty("newVersion")) { - dependencies { - dependenciesOf(oldVersion).each { - oldMetadata("org.springframework.boot:$it:$oldVersion") + configurations { + oldMetadata { + transitive = false } - dependenciesOf(newVersion).each { - newMetadata("org.springframework.boot:$it:$newVersion") + newMetadata { + transitive = false } } - + configurations.oldMetadata.dependencies.addAllLater(bootArtifactsFromBom(oldVersion)) + configurations.newMetadata.dependencies.addAllLater(bootArtifactsFromBom(newVersion)) def prepareOldMetadata = tasks.register("prepareOldMetadata", Sync) { from(configurations.oldMetadata) - if (project.hasProperty("oldVersion")) { - destinationDir = project.file("build/configuration-metadata-diff/$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") - } + 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.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")] - } + args = [project.file("build/configuration-metadata-diff/$oldVersion"), project.file("build/configuration-metadata-diff/$newVersion"), project.file("build/configuration-metadata-changelog.adoc")] } } + +def bootArtifactsFromBom(def version) { + project.provider { + def bomDependency = project.dependencies.create("org.springframework.boot:spring-boot-dependencies:$version@pom") + def bom = new XmlSlurper().parse(configurations.detachedConfiguration(bomDependency).singleFile) + return bom.dependencyManagement.dependencies.dependency + .findAll { it.groupId == "org.springframework.boot" } + .collect { project.dependencies.create("org.springframework.boot:${it.artifactId}:$version") } + } +} From ac290cdb43478bc5dcce980e6ce6406468a464fc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 26 Feb 2026 15:18:08 +0000 Subject: [PATCH 252/930] Polish See gh-48971 --- .../src/docs/antora/modules/reference/pages/using/devtools.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/using/devtools.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/using/devtools.adoc index a0664e59ba63..1c5ac7dc143f 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/using/devtools.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/using/devtools.adoc @@ -291,7 +291,7 @@ The `spring-boot-devtools` module includes an embedded LiveReload server that ca LiveReload browser extensions are freely available for Chrome, Firefox and Safari. You can find these extensions by searching 'LiveReload' in the marketplace or store of your chosen browser. -If you want to start the LiveReload server when your application runs, you can set the `spring.devtools.livereload.enabled` property to `true`. +If you want to start the LiveReload server when your application runs, you can set the configprop:spring.devtools.livereload.enabled[deprecated] property to `true`. NOTE: You can only run one LiveReload server at a time. Before starting your application, ensure that no other LiveReload servers are running. From 86567aab1fe891f23861bb96575fc441a7e13394 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 26 Feb 2026 21:48:08 -0800 Subject: [PATCH 253/930] Enable auto-configuration of OAuth2 machinery in a non-webapp Refactor `spring-boot-security-oauth2-authorization-server` to separate core authorization server auto-configuration from Spring Security web auto-configuration. This update should allow authorization server auto-configuration outside or a reactive or servlet web application. Closes gh-43978 --- ...2AuthorizationServerAutoConfiguration.java | 2 +- .../JwkSetUriJwtDecoderBuilderCustomizer.java | 39 +++ .../JwtConverterConfiguration.java | 94 +++++++ .../JwtDecoderConfiguration.java | 163 +++++++++++ ...OAuth2ResourceServerAutoConfiguration.java | 15 +- ...OpaqueTokenIntrospectionConfiguration.java | 51 ++++ .../ReactiveJwtConverterConfiguration.java | 94 +++++++ .../ReactiveJwtDecoderConfiguration.java | 170 ++++++++++++ ...OAuth2ResourceServerAutoConfiguration.java | 13 +- ...tiveOAuth2ResourceServerConfiguration.java | 51 ---- ...eOAuth2ResourceServerJwkConfiguration.java | 259 ------------------ ...esourceServerOpaqueTokenConfiguration.java | 76 ----- ...TokenIntrospectionClientConfiguration.java | 52 ++++ .../JwkSetUriJwtDecoderBuilderCustomizer.java | 12 +- .../OAuth2ResourceServerJwtConfiguration.java | 251 ----------------- ...esourceServerOpaqueTokenConfiguration.java | 80 ------ .../Oauth2ResourceServerConfiguration.java | 48 ---- .../autoconfigure/servlet/package-info.java | 2 +- ...rceServerWebSecurityAutoConfiguration.java | 67 +++++ .../autoconfigure/web/package-info.java | 23 ++ ...rceServerWebSecurityAutoConfiguration.java | 76 +++++ .../web/reactive/package-info.java | 23 ++ ...ot.autoconfigure.AutoConfiguration.imports | 4 +- ...lux.test.autoconfigure.WebFluxTest.imports | 1 + ...bmvc.test.autoconfigure.WebMvcTest.imports | 3 +- ...nfigure.AutoConfigureWebTestClient.imports | 3 +- ...2ResourceServerAutoConfigurationTests.java | 80 +----- ...2ResourceServerAutoConfigurationTests.java | 108 ++------ ...AutoConfigurationMvcIntegrationTests.java} | 10 +- ...rverWebSecurityAutoConfigurationTests.java | 130 +++++++++ ...rverWebSecurityAutoConfigurationTests.java | 139 ++++++++++ ...ConfigurationWebFluxIntegrationTests.java} | 5 +- 32 files changed, 1188 insertions(+), 956 deletions(-) create mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwkSetUriJwtDecoderBuilderCustomizer.java create mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterConfiguration.java create mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtDecoderConfiguration.java rename module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/{servlet => }/OAuth2ResourceServerAutoConfiguration.java (67%) create mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OpaqueTokenIntrospectionConfiguration.java create mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtConverterConfiguration.java create mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtDecoderConfiguration.java delete mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerConfiguration.java delete mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java delete mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java create mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java delete mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerJwtConfiguration.java delete mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerOpaqueTokenConfiguration.java delete mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/Oauth2ResourceServerConfiguration.java create mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/OAuth2ResourceServerWebSecurityAutoConfiguration.java create mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/package-info.java create mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/ReactiveOAuth2ResourceServerWebSecurityAutoConfiguration.java create mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/package-info.java rename module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/{servlet => }/OAuth2ResourceServerAutoConfigurationTests.java (90%) rename module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/{servlet/OAuth2ResourceServerWebMvcTestIntegrationTests.java => web/OAuth2ResourceServerWebSecurityAutoConfigurationMvcIntegrationTests.java} (75%) create mode 100644 module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/OAuth2ResourceServerWebSecurityAutoConfigurationTests.java create mode 100644 module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/ReactiveOAuth2ResourceServerWebSecurityAutoConfigurationTests.java rename module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/{reactive/ReactiveOAuth2ResourceServerWebFluxTestIntegrationTests.java => web/reactive/ReactiveOAuth2ResourceServerWebSecurityAutoConfigurationWebFluxIntegrationTests.java} (86%) diff --git a/module/spring-boot-security-oauth2-authorization-server/src/main/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerAutoConfiguration.java b/module/spring-boot-security-oauth2-authorization-server/src/main/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerAutoConfiguration.java index 97980437cb5c..b92169a5d9cd 100644 --- a/module/spring-boot-security-oauth2-authorization-server/src/main/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerAutoConfiguration.java +++ b/module/spring-boot-security-oauth2-authorization-server/src/main/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerAutoConfiguration.java @@ -44,7 +44,7 @@ * @see OAuth2AuthorizationServerJwtAutoConfiguration */ @AutoConfiguration(before = { SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class }, - beforeName = "org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration") + beforeName = "org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration") @ConditionalOnClass(OAuth2Authorization.class) @ConditionalOnWebApplication(type = Type.SERVLET) @Import({ OAuth2AuthorizationServerConfiguration.class, OAuth2AuthorizationServerWebSecurityConfiguration.class }) diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwkSetUriJwtDecoderBuilderCustomizer.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwkSetUriJwtDecoderBuilderCustomizer.java new file mode 100644 index 000000000000..ea3f56be8636 --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwkSetUriJwtDecoderBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.security.oauth2.server.resource.autoconfigure; + +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder; + +/** + * Callback interface for the customization of the {@link JwkSetUriJwtDecoderBuilder} used + * to create the auto-configured {@link JwtDecoder} for a JWK set URI that has been + * configured directly or obtained through an issuer URI. + * + * @author Andy Wilkinson + * @since 4.1.0 + */ +@FunctionalInterface +public interface JwkSetUriJwtDecoderBuilderCustomizer { + + /** + * Customize the given {@code builder}. + * @param builder the {@code builder} to customize + */ + void customize(JwkSetUriJwtDecoderBuilder builder); + +} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterConfiguration.java new file mode 100644 index 000000000000..655284b76aec --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterConfiguration.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.security.oauth2.server.resource.autoconfigure; + +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.autoconfigure.condition.ConditionalOnProperty; +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.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; + +/** + * {@link Configuration @Configuration} for JWT converter beans. + * + * @author Madhura Bhave + * @author Artsiom Yudovin + * @author HaiTao Zhang + * @author Mushtaq Ahmed + * @author Roman Golovin + * @author Yan Kardziyaka + * @author Phillip Webb + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(JwtDecoder.class) +@ConditionalOnMissingBean(JwtAuthenticationConverter.class) +@Conditional(JwtConverterConfiguration.PropertiesCondition.class) +class JwtConverterConfiguration { + + @Bean + JwtAuthenticationConverter jwtAuthenticationConverter(OAuth2ResourceServerProperties properties) { + return jwtAuthenticationConverter(properties.getJwt()); + } + + private JwtAuthenticationConverter jwtAuthenticationConverter(OAuth2ResourceServerProperties.Jwt properties) { + PropertyMapper map = PropertyMapper.get(); + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + map.from(properties::getPrincipalClaimName).to(converter::setPrincipalClaimName); + map.from(properties).as(this::getGrantedAuthoritiesConverter).to(converter::setJwtGrantedAuthoritiesConverter); + return converter; + } + + private JwtGrantedAuthoritiesConverter getGrantedAuthoritiesConverter( + OAuth2ResourceServerProperties.Jwt properties) { + PropertyMapper map = PropertyMapper.get(); + JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter(); + map.from(properties::getAuthorityPrefix).to(converter::setAuthorityPrefix); + map.from(properties::getAuthoritiesClaimDelimiter).to(converter::setAuthoritiesClaimDelimiter); + map.from(properties::getAuthoritiesClaimName).to(converter::setAuthoritiesClaimName); + return converter; + } + + static class PropertiesCondition extends AnyNestedCondition { + + PropertiesCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.authority-prefix") + static class OnAuthorityPrefix { + + } + + @ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.principal-claim-name") + static class OnPrincipalClaimName { + + } + + @ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.authorities-claim-name") + static class OnAuthoritiesClaimName { + + } + + } + +} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtDecoderConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtDecoderConfiguration.java new file mode 100644 index 000000000000..0571e3ef4312 --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtDecoderConfiguration.java @@ -0,0 +1,163 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.security.oauth2.server.resource.autoconfigure; + +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.jspecify.annotations.Nullable; + +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.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtClaimValidator; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtIssuerValidator; +import org.springframework.security.oauth2.jwt.JwtValidators; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.PublicKeyJwtDecoderBuilder; +import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * {@link Configuration @Configuration} for JWT decoder beans. + * + * @author Madhura Bhave + * @author Artsiom Yudovin + * @author HaiTao Zhang + * @author Mushtaq Ahmed + * @author Roman Golovin + * @author Yan Kardziyaka + * @author Phillip Webb + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(JwtDecoder.class) +@ConditionalOnMissingBean(JwtDecoder.class) +class JwtDecoderConfiguration { + + private final OAuth2ResourceServerProperties.Jwt properties; + + private final List> additionalValidators; + + private final ObjectProvider jwkSetUriJwtDecoderBuilderCustomizers; + + JwtDecoderConfiguration(OAuth2ResourceServerProperties properties, + ObjectProvider> additionalValidators, + ObjectProvider jwkSetUriJwtDecoderBuilderCustomizers) { + this.properties = properties.getJwt(); + this.additionalValidators = additionalValidators.orderedStream().toList(); + this.jwkSetUriJwtDecoderBuilderCustomizers = jwkSetUriJwtDecoderBuilderCustomizers; + } + + @Bean + @ConditionalOnPublicKeyJwtDecoder + JwtDecoder jwtDecoderByPublicKeyValue() throws Exception { + PublicKeyJwtDecoderBuilder builder = NimbusJwtDecoder.withPublicKey(getReadPublicKey()); + builder.signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())); + NimbusJwtDecoder decoder = builder.build(); + decoder.setJwtValidator(getValidator()); + return decoder; + } + + private RSAPublicKey getReadPublicKey() throws Exception { + X509EncodedKeySpec spec = new X509EncodedKeySpec(decodeKeyProperty(this.properties.readPublicKey())); + return (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(spec); + } + + private byte[] decodeKeyProperty(String value) { + return Base64.getMimeDecoder() + .decode(value.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "")); + } + + private String exactlyOneAlgorithm() { + List algorithms = this.properties.getJwsAlgorithms(); + Assert.state(algorithms != null && algorithms.size() == 1, + () -> "Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + + algorithms.size() + " were configured"); + return algorithms.get(0); + } + + @Bean + @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") + JwtDecoder jwtDecoderByJwkKeySetUri() { + JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri()); + builder.jwsAlgorithms(this::jwsAlgorithms); + return buildJwkSetUriJwtDecoder(builder); + } + + private void jwsAlgorithms(Set signatureAlgorithms) { + this.properties.getJwsAlgorithms().stream().map(SignatureAlgorithm::from).forEach(signatureAlgorithms::add); + } + + @Bean + @ConditionalOnIssuerLocationJwtDecoder + SupplierJwtDecoder jwtDecoderByIssuerUri() { + return new SupplierJwtDecoder(this::supplyJwtDecoderByIssuerUri); + } + + private JwtDecoder supplyJwtDecoderByIssuerUri() { + String issuerUri = this.properties.getIssuerUri(); + Assert.state(issuerUri != null, "No JWT issuer URI propery specified"); + JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withIssuerLocation(issuerUri); + return buildJwkSetUriJwtDecoder(builder); + } + + private JwtDecoder buildJwkSetUriJwtDecoder(JwkSetUriJwtDecoderBuilder builder) { + this.jwkSetUriJwtDecoderBuilderCustomizers.orderedStream() + .forEach((customizer) -> customizer.customize(builder)); + NimbusJwtDecoder decoder = builder.build(); + decoder.setJwtValidator(getValidator()); + return decoder; + } + + private OAuth2TokenValidator getValidator() { + List> validators = new ArrayList<>(); + if (this.properties.getIssuerUri() != null) { + validators.add(new JwtIssuerValidator(this.properties.getIssuerUri())); + } + if (!CollectionUtils.isEmpty(this.properties.getAudiences())) { + validators.add(audienceValidator(this.properties.getAudiences())); + } + validators.addAll(this.additionalValidators); + return validators.isEmpty() ? JwtValidators.createDefault() + : JwtValidators.createDefaultWithValidators(validators); + } + + private JwtClaimValidator> audienceValidator(List audiences) { + return new JwtClaimValidator<>(JwtClaimNames.AUD, (claim) -> hasElementsInCommon(claim, audiences)); + } + + private boolean hasElementsInCommon(@Nullable List c1, List c2) { + return c1 != null && !Collections.disjoint(c1, c2); + } + +} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerAutoConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfiguration.java similarity index 67% rename from module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerAutoConfiguration.java rename to module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfiguration.java index 5a235ac2a204..69262c93bb8e 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerAutoConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfiguration.java @@ -14,18 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet; +package org.springframework.boot.security.oauth2.server.resource.autoconfigure; 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.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration; import org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration; -import org.springframework.boot.security.autoconfigure.actuate.web.servlet.ManagementWebSecurityAutoConfiguration; -import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties; import org.springframework.context.annotation.Import; import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; @@ -33,15 +29,12 @@ * {@link EnableAutoConfiguration Auto-configuration} for OAuth2 resource server support. * * @author Madhura Bhave - * @since 4.0.0 + * @since 4.1.0 */ -@AutoConfiguration(before = { ManagementWebSecurityAutoConfiguration.class, SecurityAutoConfiguration.class, - UserDetailsServiceAutoConfiguration.class }) +@AutoConfiguration(before = { SecurityAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class }) @EnableConfigurationProperties(OAuth2ResourceServerProperties.class) @ConditionalOnClass(BearerTokenAuthenticationToken.class) -@ConditionalOnWebApplication(type = Type.SERVLET) -@Import({ Oauth2ResourceServerConfiguration.JwtConfiguration.class, - Oauth2ResourceServerConfiguration.OpaqueTokenConfiguration.class }) +@Import({ JwtConverterConfiguration.class, JwtDecoderConfiguration.class, OpaqueTokenIntrospectionConfiguration.class }) public final class OAuth2ResourceServerAutoConfiguration { } diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OpaqueTokenIntrospectionConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OpaqueTokenIntrospectionConfiguration.java new file mode 100644 index 000000000000..d68192277307 --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OpaqueTokenIntrospectionConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.security.oauth2.server.resource.autoconfigure; + +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.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector; +import org.springframework.util.Assert; + +/** + * Configures an {@link OpaqueTokenIntrospector} when a token introspection endpoint is + * available. + * + * @author Madhura Bhave + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(OpaqueTokenIntrospector.class) +class OpaqueTokenIntrospectionConfiguration { + + @Bean + @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri") + SpringOpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServerProperties properties) { + OAuth2ResourceServerProperties.Opaquetoken token = properties.getOpaquetoken(); + Assert.state(token.getClientId() != null, + "No 'spring.security.oauth2.resourceserver.opaquetoken.client-id' property specified"); + Assert.state(token.getClientSecret() != null, + "No 'spring.security.oauth2.resourceserver.opaquetoken.client-secret' property specified"); + return SpringOpaqueTokenIntrospector.withIntrospectionUri(token.getIntrospectionUri()) + .clientId(token.getClientId()) + .clientSecret(token.getClientSecret()) + .build(); + } + +} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtConverterConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtConverterConfiguration.java new file mode 100644 index 000000000000..b287d04fbb21 --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtConverterConfiguration.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive; + +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.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +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; + +/** + * {@link Configuration @Configuration} for JWT converter beans. + * + * @author Madhura Bhave + * @author Artsiom Yudovin + * @author HaiTao Zhang + * @author Anastasiia Losieva + * @author Mushtaq Ahmed + * @author Roman Golovin + * @author Yan Kardziyaka + * @author Phillip Webb + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(ReactiveJwtDecoder.class) +@ConditionalOnMissingBean(ReactiveJwtAuthenticationConverter.class) +@Conditional(ReactiveJwtConverterConfiguration.PropertiesCondition.class) +class ReactiveJwtConverterConfiguration { + + @Bean + ReactiveJwtAuthenticationConverter reactiveJwtAuthenticationConverter(OAuth2ResourceServerProperties properties) { + return reactiveJwtAuthenticationConverter(properties.getJwt()); + } + + private ReactiveJwtAuthenticationConverter reactiveJwtAuthenticationConverter( + OAuth2ResourceServerProperties.Jwt properties1) { + PropertyMapper map = PropertyMapper.get(); + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + map.from(properties1.getAuthorityPrefix()).to(grantedAuthoritiesConverter::setAuthorityPrefix); + map.from(properties1.getAuthoritiesClaimDelimiter()) + .to(grantedAuthoritiesConverter::setAuthoritiesClaimDelimiter); + map.from(properties1.getAuthoritiesClaimName()).to(grantedAuthoritiesConverter::setAuthoritiesClaimName); + ReactiveJwtAuthenticationConverter jwtAuthenticationConverter = new ReactiveJwtAuthenticationConverter(); + map.from(properties1.getPrincipalClaimName()).to(jwtAuthenticationConverter::setPrincipalClaimName); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter( + new ReactiveJwtGrantedAuthoritiesConverterAdapter(grantedAuthoritiesConverter)); + return jwtAuthenticationConverter; + } + + static class PropertiesCondition extends AnyNestedCondition { + + PropertiesCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.authority-prefix") + static class OnAuthorityPrefix { + + } + + @ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.principal-claim-name") + static class OnPrincipalClaimName { + + } + + @ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.authorities-claim-name") + static class OnAuthoritiesClaimName { + + } + + } + +} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtDecoderConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtDecoderConfiguration.java new file mode 100644 index 000000000000..50110a4b954c --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtDecoderConfiguration.java @@ -0,0 +1,170 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive; + +import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.ConditionalOnIssuerLocationJwtDecoder; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.ConditionalOnPublicKeyJwtDecoder; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtClaimValidator; +import org.springframework.security.oauth2.jwt.JwtIssuerValidator; +import org.springframework.security.oauth2.jwt.JwtValidators; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.PublicKeyReactiveJwtDecoderBuilder; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * {@link Configuration @Configuration} for reactive JWT decoder beans. + * + * @author Madhura Bhave + * @author Artsiom Yudovin + * @author HaiTao Zhang + * @author Anastasiia Losieva + * @author Mushtaq Ahmed + * @author Roman Golovin + * @author Yan Kardziyaka + * @author Phillip Webb + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(ReactiveJwtDecoder.class) +class ReactiveJwtDecoderConfiguration { + + private final OAuth2ResourceServerProperties.Jwt properties; + + private final List> additionalValidators; + + private final ObjectProvider jwkSetUriReactiveJwtDecoderBuilderCustomizers; + + ReactiveJwtDecoderConfiguration(OAuth2ResourceServerProperties properties, + ObjectProvider> additionalValidators, + ObjectProvider jwkSetUriReactiveJwtDecoderBuilderCustomizers) { + this.properties = properties.getJwt(); + this.additionalValidators = additionalValidators.orderedStream().toList(); + this.jwkSetUriReactiveJwtDecoderBuilderCustomizers = jwkSetUriReactiveJwtDecoderBuilderCustomizers; + } + + @Bean + @ConditionalOnPublicKeyJwtDecoder + NimbusReactiveJwtDecoder reactiveJwtDecoderByPublicKeyValue() throws Exception { + RSAPublicKey publicKey = getReadPublicKey(); + PublicKeyReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder.withPublicKey(publicKey); + builder.signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())); + NimbusReactiveJwtDecoder decoder = builder.build(); + decoder.setJwtValidator(getValidator()); + return decoder; + } + + private RSAPublicKey getReadPublicKey() throws InvalidKeySpecException, NoSuchAlgorithmException, IOException { + X509EncodedKeySpec spec = new X509EncodedKeySpec(decodeKeyProperty(this.properties.readPublicKey())); + return (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(spec); + } + + private byte[] decodeKeyProperty(String value) { + return Base64.getMimeDecoder() + .decode(value.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "")); + } + + private String exactlyOneAlgorithm() { + List algorithms = this.properties.getJwsAlgorithms(); + Assert.state(algorithms != null && algorithms.size() == 1, + () -> "Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + + algorithms.size() + " were configured"); + return algorithms.get(0); + } + + @Bean + @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") + ReactiveJwtDecoder reactiveJwtDecoderByJwkKeySetUri() { + JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder + .withJwkSetUri(this.properties.getJwkSetUri()); + builder.jwsAlgorithms(this::jwsAlgorithms); + return buildJwkSetUriJwtDecoder(builder); + } + + private void jwsAlgorithms(Set signatureAlgorithms) { + this.properties.getJwsAlgorithms().stream().map(SignatureAlgorithm::from).forEach(signatureAlgorithms::add); + } + + @Bean + @ConditionalOnIssuerLocationJwtDecoder + SupplierReactiveJwtDecoder reactiveJwtDecoderByIssuerUri() { + return new SupplierReactiveJwtDecoder(this::supplyJwtDecoderByIssuerUri); + } + + private ReactiveJwtDecoder supplyJwtDecoderByIssuerUri() { + String issuerUri = this.properties.getIssuerUri(); + Assert.state(issuerUri != null, "No JWT issuer URI propery specified"); + JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder.withIssuerLocation(issuerUri); + return buildJwkSetUriJwtDecoder(builder); + } + + private ReactiveJwtDecoder buildJwkSetUriJwtDecoder(JwkSetUriReactiveJwtDecoderBuilder builder) { + this.jwkSetUriReactiveJwtDecoderBuilderCustomizers.orderedStream() + .forEach((customizer) -> customizer.customize(builder)); + NimbusReactiveJwtDecoder decoder = builder.build(); + decoder.setJwtValidator(getValidator()); + return decoder; + } + + private OAuth2TokenValidator getValidator() { + List> validators = new ArrayList<>(); + if (this.properties.getIssuerUri() != null) { + validators.add(new JwtIssuerValidator(this.properties.getIssuerUri())); + } + if (!CollectionUtils.isEmpty(this.properties.getAudiences())) { + validators.add(audValidator(this.properties.getAudiences())); + } + validators.addAll(this.additionalValidators); + return validators.isEmpty() ? JwtValidators.createDefault() + : JwtValidators.createDefaultWithValidators(validators); + } + + private JwtClaimValidator> audValidator(List audiences) { + return new JwtClaimValidator<>(JwtClaimNames.AUD, (claim) -> hasElementsInCommon(claim, audiences)); + } + + private boolean hasElementsInCommon(@Nullable List c1, List c2) { + return c1 != null && !Collections.disjoint(c1, c2); + } + +} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java index 3d506782604f..71ccaafc594e 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java @@ -16,18 +16,18 @@ package org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive; +import reactor.core.publisher.Mono; + 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.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.security.autoconfigure.ReactiveUserDetailsServiceAutoConfiguration; import org.springframework.boot.security.autoconfigure.actuate.web.reactive.ReactiveManagementWebSecurityAutoConfiguration; import org.springframework.boot.security.autoconfigure.web.reactive.ReactiveWebSecurityAutoConfiguration; import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties; import org.springframework.context.annotation.Import; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; /** * {@link EnableAutoConfiguration Auto-configuration} for Reactive OAuth2 resource server @@ -38,11 +38,10 @@ */ @AutoConfiguration(before = { ReactiveManagementWebSecurityAutoConfiguration.class, ReactiveWebSecurityAutoConfiguration.class, ReactiveUserDetailsServiceAutoConfiguration.class }) +@ConditionalOnClass({ Mono.class, BearerTokenAuthenticationToken.class }) @EnableConfigurationProperties(OAuth2ResourceServerProperties.class) -@ConditionalOnClass({ EnableWebFluxSecurity.class }) -@ConditionalOnWebApplication(type = Type.REACTIVE) -@Import({ ReactiveOAuth2ResourceServerConfiguration.JwtConfiguration.class, - ReactiveOAuth2ResourceServerConfiguration.OpaqueTokenConfiguration.class }) +@Import({ ReactiveJwtDecoderConfiguration.class, ReactiveJwtConverterConfiguration.class, + ReactiveOpaqueTokenIntrospectionClientConfiguration.class }) public final class ReactiveOAuth2ResourceServerAutoConfiguration { } diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerConfiguration.java deleted file mode 100644 index cdff4d704609..000000000000 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerConfiguration.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; -import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; -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. - * - * @author Madhura Bhave - */ -class ReactiveOAuth2ResourceServerConfiguration { - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class }) - @Import({ ReactiveOAuth2ResourceServerJwkConfiguration.JwtConfiguration.class, - ReactiveOAuth2ResourceServerJwkConfiguration.JwtConverterConfiguration.class, - ReactiveOAuth2ResourceServerJwkConfiguration.WebSecurityConfiguration.class }) - static class JwtConfiguration { - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveOpaqueTokenIntrospector.class }) - @Import({ ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class, - ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.WebSecurityConfiguration.class }) - static class OpaqueTokenConfiguration { - - } - -} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java deleted file mode 100644 index 717c9d7e4a83..000000000000 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive; - -import java.security.KeyFactory; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.X509EncodedKeySpec; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -import org.jspecify.annotations.Nullable; - -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; -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.boot.security.oauth2.server.resource.autoconfigure.ConditionalOnIssuerLocationJwtDecoder; -import org.springframework.boot.security.oauth2.server.resource.autoconfigure.ConditionalOnPublicKeyJwtDecoder; -import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.web.server.ServerHttpSecurity; -import org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2ResourceServerSpec; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtClaimNames; -import org.springframework.security.oauth2.jwt.JwtClaimValidator; -import org.springframework.security.oauth2.jwt.JwtIssuerValidator; -import org.springframework.security.oauth2.jwt.JwtValidators; -import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; -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.Assert; -import org.springframework.util.CollectionUtils; - -/** - * Configures a {@link ReactiveJwtDecoder} when a JWK Set URI, OpenID Connect Issuer URI - * or Public Key configuration is available. Also configures a - * {@link SecurityWebFilterChain} if a {@link ReactiveJwtDecoder} bean is found. - * - * @author Madhura Bhave - * @author Artsiom Yudovin - * @author HaiTao Zhang - * @author Anastasiia Losieva - * @author Mushtaq Ahmed - * @author Roman Golovin - * @author Yan Kardziyaka - */ -@Configuration(proxyBeanMethods = false) -class ReactiveOAuth2ResourceServerJwkConfiguration { - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(ReactiveJwtDecoder.class) - static class JwtConfiguration { - - private final OAuth2ResourceServerProperties.Jwt properties; - - private final List> additionalValidators; - - JwtConfiguration(OAuth2ResourceServerProperties properties, - ObjectProvider> additionalValidators) { - this.properties = properties.getJwt(); - this.additionalValidators = additionalValidators.orderedStream().toList(); - } - - @Bean - @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") - ReactiveJwtDecoder jwtDecoder(ObjectProvider customizers) { - String jwkSetUri = this.properties.getJwkSetUri(); - Assert.state(jwkSetUri != null, "'jwkSetUri' must not be null"); - JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri) - .jwsAlgorithms(this::jwsAlgorithms); - customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = builder.build(); - String issuerUri = this.properties.getIssuerUri(); - List> validators = new ArrayList<>(); - if (issuerUri != null) { - validators.add(new JwtIssuerValidator(issuerUri)); - } - validators.addAll(getValidators()); - nimbusReactiveJwtDecoder.setJwtValidator(validators.isEmpty() ? JwtValidators.createDefault() - : JwtValidators.createDefaultWithValidators(validators)); - return nimbusReactiveJwtDecoder; - } - - private void jwsAlgorithms(Set signatureAlgorithms) { - for (String algorithm : this.properties.getJwsAlgorithms()) { - signatureAlgorithms.add(SignatureAlgorithm.from(algorithm)); - } - } - - private List> getValidators() { - List audiences = this.properties.getAudiences(); - if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) { - return Collections.emptyList(); - } - List> validators = new ArrayList<>(); - if (!CollectionUtils.isEmpty(audiences)) { - validators.add(audValidator(audiences)); - } - validators.addAll(this.additionalValidators); - return validators; - } - - private JwtClaimValidator> audValidator(List audiences) { - return new JwtClaimValidator<>(JwtClaimNames.AUD, (aud) -> nullSafeDisjoint(aud, audiences)); - } - - private boolean nullSafeDisjoint(@Nullable List c1, List c2) { - return c1 != null && !Collections.disjoint(c1, c2); - } - - @Bean - @ConditionalOnPublicKeyJwtDecoder - NimbusReactiveJwtDecoder jwtDecoderByPublicKeyValue() throws Exception { - RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") - .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); - NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(publicKey) - .signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())) - .build(); - List> validators = getValidators(); - jwtDecoder.setJwtValidator(validators.isEmpty() ? JwtValidators.createDefault() - : JwtValidators.createDefaultWithValidators(validators)); - return jwtDecoder; - } - - private byte[] getKeySpec(String keyValue) { - keyValue = keyValue.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", ""); - return Base64.getMimeDecoder().decode(keyValue); - } - - private String exactlyOneAlgorithm() { - List algorithms = this.properties.getJwsAlgorithms(); - int count = (algorithms != null) ? algorithms.size() : 0; - if (count != 1) { - throw new IllegalStateException( - "Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + count - + " were configured"); - } - return algorithms.get(0); - } - - @Bean - @ConditionalOnIssuerLocationJwtDecoder - SupplierReactiveJwtDecoder jwtDecoderByIssuerUri( - ObjectProvider customizers) { - return new SupplierReactiveJwtDecoder(() -> { - String issuerUri = this.properties.getIssuerUri(); - Assert.state(issuerUri != null, "'issuerUri' must not be null"); - JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder.withIssuerLocation(issuerUri); - customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - NimbusReactiveJwtDecoder jwtDecoder = builder.build(); - List> validators = new ArrayList<>(); - validators.add(new JwtIssuerValidator(issuerUri)); - validators.addAll(getValidators()); - jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(validators)); - return jwtDecoder; - }); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(ReactiveJwtAuthenticationConverter.class) - @Conditional(JwtConverterPropertiesCondition.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(); - 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 { - - @Bean - @ConditionalOnBean(ReactiveJwtDecoder.class) - SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder jwtDecoder) { - http.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated()); - http.oauth2ResourceServer((server) -> customDecoder(server, jwtDecoder)); - return http.build(); - } - - private void customDecoder(OAuth2ResourceServerSpec server, ReactiveJwtDecoder decoder) { - server.jwt((jwt) -> jwt.jwtDecoder(decoder)); - } - - } - - private static class JwtConverterPropertiesCondition extends AnyNestedCondition { - - JwtConverterPropertiesCondition() { - super(ConfigurationPhase.REGISTER_BEAN); - } - - @ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.authority-prefix") - static class OnAuthorityPrefix { - - } - - @ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.principal-claim-name") - static class OnPrincipalClaimName { - - } - - @ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.authorities-claim-name") - static class OnAuthoritiesClaimName { - - } - - } - -} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java deleted file mode 100644 index d97b249c4cf6..000000000000 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -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.util.Assert; - -import static org.springframework.security.config.Customizer.withDefaults; - -/** - * Configures a {@link ReactiveOpaqueTokenIntrospector} when a token introspection - * endpoint is available. Also configures a {@link SecurityWebFilterChain} if a - * {@link ReactiveOpaqueTokenIntrospector} bean is found. - * - * @author Madhura Bhave - */ -class ReactiveOAuth2ResourceServerOpaqueTokenConfiguration { - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(ReactiveOpaqueTokenIntrospector.class) - static class OpaqueTokenIntrospectionClientConfiguration { - - @Bean - @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri") - SpringReactiveOpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServerProperties properties) { - OAuth2ResourceServerProperties.Opaquetoken opaquetoken = properties.getOpaquetoken(); - String clientId = opaquetoken.getClientId(); - Assert.state(clientId != null, "'clientId' must not be null"); - String clientSecret = opaquetoken.getClientSecret(); - Assert.state(clientSecret != null, "'clientSecret' must not be null"); - return SpringReactiveOpaqueTokenIntrospector.withIntrospectionUri(opaquetoken.getIntrospectionUri()) - .clientId(clientId) - .clientSecret(clientSecret) - .build(); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(SecurityWebFilterChain.class) - static class WebSecurityConfiguration { - - @Bean - @ConditionalOnBean(ReactiveOpaqueTokenIntrospector.class) - SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - http.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated()); - http.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults())); - return http.build(); - } - - } - -} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java new file mode 100644 index 000000000000..2db3421c39d1 --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector; +import org.springframework.util.Assert; + +/** + * Configures an {@link ReactiveOpaqueTokenIntrospector} when a token introspection + * endpoint is available. + * + * @author Madhura Bhave + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(ReactiveOpaqueTokenIntrospector.class) +class ReactiveOpaqueTokenIntrospectionClientConfiguration { + + @Bean + @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri") + SpringReactiveOpaqueTokenIntrospector reactiveOpaqueTokenIntrospector(OAuth2ResourceServerProperties properties) { + OAuth2ResourceServerProperties.Opaquetoken token = properties.getOpaquetoken(); + Assert.state(token.getClientId() != null, + "No 'spring.security.oauth2.resourceserver.opaquetoken.client-id' property specified"); + Assert.state(token.getClientSecret() != null, + "No 'spring.security.oauth2.resourceserver.opaquetoken.client-secret' property specified"); + return SpringReactiveOpaqueTokenIntrospector.withIntrospectionUri(token.getIntrospectionUri()) + .clientId(token.getClientId()) + .clientSecret(token.getClientSecret()) + .build(); + } + +} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/JwkSetUriJwtDecoderBuilderCustomizer.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/JwkSetUriJwtDecoderBuilderCustomizer.java index a7e85e3e2727..20d9318d6fbf 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/JwkSetUriJwtDecoderBuilderCustomizer.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/JwkSetUriJwtDecoderBuilderCustomizer.java @@ -26,14 +26,12 @@ * * @author Andy Wilkinson * @since 4.0.0 + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link org.springframework.boot.security.oauth2.server.resource.autoconfigure.JwkSetUriJwtDecoderBuilderCustomizer} */ @FunctionalInterface -public interface JwkSetUriJwtDecoderBuilderCustomizer { - - /** - * Customize the given {@code builder}. - * @param builder the {@code builder} to customize - */ - void customize(JwkSetUriJwtDecoderBuilder builder); +@Deprecated(since = "4.1.0", forRemoval = true) +public interface JwkSetUriJwtDecoderBuilderCustomizer extends + org.springframework.boot.security.oauth2.server.resource.autoconfigure.JwkSetUriJwtDecoderBuilderCustomizer { } diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerJwtConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerJwtConfiguration.java deleted file mode 100644 index 573c4a9a823c..000000000000 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerJwtConfiguration.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet; - -import java.security.KeyFactory; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.X509EncodedKeySpec; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -import org.jspecify.annotations.Nullable; - -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; -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.boot.security.autoconfigure.web.servlet.ConditionalOnDefaultWebSecurity; -import org.springframework.boot.security.oauth2.server.resource.autoconfigure.ConditionalOnIssuerLocationJwtDecoder; -import org.springframework.boot.security.oauth2.server.resource.autoconfigure.ConditionalOnPublicKeyJwtDecoder; -import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties; -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.builders.HttpSecurity; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtClaimNames; -import org.springframework.security.oauth2.jwt.JwtClaimValidator; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtIssuerValidator; -import org.springframework.security.oauth2.jwt.JwtValidators; -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.Assert; -import org.springframework.util.CollectionUtils; - -import static org.springframework.security.config.Customizer.withDefaults; - -/** - * Configures a {@link JwtDecoder} when a JWK Set URI, OpenID Connect Issuer URI or Public - * Key configuration is available. Also configures a {@link SecurityFilterChain} if a - * {@link JwtDecoder} bean is found. - * - * @author Madhura Bhave - * @author Artsiom Yudovin - * @author HaiTao Zhang - * @author Mushtaq Ahmed - * @author Roman Golovin - * @author Yan Kardziyaka - */ -@Configuration(proxyBeanMethods = false) -class OAuth2ResourceServerJwtConfiguration { - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(JwtDecoder.class) - static class JwtDecoderConfiguration { - - private final OAuth2ResourceServerProperties.Jwt properties; - - private final List> additionalValidators; - - JwtDecoderConfiguration(OAuth2ResourceServerProperties properties, - ObjectProvider> additionalValidators) { - this.properties = properties.getJwt(); - this.additionalValidators = additionalValidators.orderedStream().toList(); - } - - @Bean - @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") - JwtDecoder jwtDecoderByJwkKeySetUri(ObjectProvider customizers) { - JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri()) - .jwsAlgorithms(this::jwsAlgorithms); - customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - NimbusJwtDecoder nimbusJwtDecoder = builder.build(); - String issuerUri = this.properties.getIssuerUri(); - List> validators = new ArrayList<>(); - if (issuerUri != null) { - validators.add(new JwtIssuerValidator(issuerUri)); - } - validators.addAll(getValidators()); - nimbusJwtDecoder.setJwtValidator(validators.isEmpty() ? JwtValidators.createDefault() - : JwtValidators.createDefaultWithValidators(validators)); - return nimbusJwtDecoder; - } - - private void jwsAlgorithms(Set signatureAlgorithms) { - for (String algorithm : this.properties.getJwsAlgorithms()) { - signatureAlgorithms.add(SignatureAlgorithm.from(algorithm)); - } - } - - private List> getValidators() { - List audiences = this.properties.getAudiences(); - if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) { - return Collections.emptyList(); - } - List> validators = new ArrayList<>(); - if (!CollectionUtils.isEmpty(audiences)) { - validators.add(audValidator(audiences)); - } - validators.addAll(this.additionalValidators); - return validators; - } - - private JwtClaimValidator> audValidator(List audiences) { - return new JwtClaimValidator<>(JwtClaimNames.AUD, (aud) -> nullSafeDisjoint(aud, audiences)); - } - - private boolean nullSafeDisjoint(@Nullable List c1, List c2) { - return c1 != null && !Collections.disjoint(c1, c2); - } - - @Bean - @ConditionalOnPublicKeyJwtDecoder - JwtDecoder jwtDecoderByPublicKeyValue() throws Exception { - RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") - .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(publicKey) - .signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())) - .build(); - List> validators = getValidators(); - jwtDecoder.setJwtValidator(validators.isEmpty() ? JwtValidators.createDefault() - : JwtValidators.createDefaultWithValidators(validators)); - return jwtDecoder; - } - - private byte[] getKeySpec(String keyValue) { - keyValue = keyValue.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", ""); - return Base64.getMimeDecoder().decode(keyValue); - } - - private String exactlyOneAlgorithm() { - List algorithms = this.properties.getJwsAlgorithms(); - int count = (algorithms != null) ? algorithms.size() : 0; - if (count != 1) { - throw new IllegalStateException( - "Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + count - + " were configured"); - } - return algorithms.get(0); - } - - @Bean - @ConditionalOnIssuerLocationJwtDecoder - SupplierJwtDecoder jwtDecoderByIssuerUri(ObjectProvider customizers) { - return new SupplierJwtDecoder(() -> { - String issuerUri = this.properties.getIssuerUri(); - Assert.state(issuerUri != null, "'issuerUri' must not be null"); - JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withIssuerLocation(issuerUri); - customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - NimbusJwtDecoder jwtDecoder = builder.build(); - List> validators = new ArrayList<>(); - validators.add(new JwtIssuerValidator(issuerUri)); - validators.addAll(getValidators()); - jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(validators)); - return jwtDecoder; - }); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnDefaultWebSecurity - static class OAuth2SecurityFilterChainConfiguration { - - @Bean - @ConditionalOnBean(JwtDecoder.class) - SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) { - http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); - http.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults())); - return http.build(); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(JwtAuthenticationConverter.class) - @Conditional(JwtConverterPropertiesCondition.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(); - 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; - } - - } - - private static class JwtConverterPropertiesCondition extends AnyNestedCondition { - - JwtConverterPropertiesCondition() { - super(ConfigurationPhase.REGISTER_BEAN); - } - - @ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.authority-prefix") - static class OnAuthorityPrefix { - - } - - @ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.principal-claim-name") - static class OnPrincipalClaimName { - - } - - @ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.authorities-claim-name") - static class OnAuthoritiesClaimName { - - } - - } - -} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerOpaqueTokenConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerOpaqueTokenConfiguration.java deleted file mode 100644 index c9852e8c9f66..000000000000 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerOpaqueTokenConfiguration.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.security.autoconfigure.web.servlet.ConditionalOnDefaultWebSecurity; -import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; -import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.util.Assert; - -import static org.springframework.security.config.Customizer.withDefaults; - -/** - * Configures an {@link OpaqueTokenIntrospector} when a token introspection endpoint is - * available. Also configures a {@link SecurityFilterChain} if a - * {@link OpaqueTokenIntrospector} bean is found. - * - * @author Madhura Bhave - */ -@Configuration(proxyBeanMethods = false) -class OAuth2ResourceServerOpaqueTokenConfiguration { - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(OpaqueTokenIntrospector.class) - static class OpaqueTokenIntrospectionClientConfiguration { - - @Bean - @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri") - SpringOpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServerProperties properties) { - OAuth2ResourceServerProperties.Opaquetoken opaquetoken = properties.getOpaquetoken(); - String introspectionUri = opaquetoken.getIntrospectionUri(); - Assert.state(introspectionUri != null, "'introspectionUri' must not be null"); - String clientId = opaquetoken.getClientId(); - Assert.state(clientId != null, "'clientId' must not be null"); - String clientSecret = opaquetoken.getClientSecret(); - Assert.state(clientSecret != null, "'clientSecret' must not be null"); - return SpringOpaqueTokenIntrospector.withIntrospectionUri(introspectionUri) - .clientId(clientId) - .clientSecret(clientSecret) - .build(); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnDefaultWebSecurity - static class OAuth2SecurityFilterChainConfiguration { - - @Bean - @ConditionalOnBean(OpaqueTokenIntrospector.class) - SecurityFilterChain opaqueTokenSecurityFilterChain(HttpSecurity http) { - http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); - http.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults())); - return http.build(); - } - - } - -} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/Oauth2ResourceServerConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/Oauth2ResourceServerConfiguration.java deleted file mode 100644 index d3f563bb7016..000000000000 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/Oauth2ResourceServerConfiguration.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.security.oauth2.jwt.JwtDecoder; - -/** - * 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 - */ -class Oauth2ResourceServerConfiguration { - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(JwtDecoder.class) - @Import({ OAuth2ResourceServerJwtConfiguration.JwtConverterConfiguration.class, - OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration.class, - OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration.class }) - static class JwtConfiguration { - - } - - @Configuration(proxyBeanMethods = false) - @Import({ OAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class, - OAuth2ResourceServerOpaqueTokenConfiguration.OAuth2SecurityFilterChainConfiguration.class }) - static class OpaqueTokenConfiguration { - - } - -} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/package-info.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/package-info.java index 1debecdde495..b46af1dacce5 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/package-info.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/package-info.java @@ -15,7 +15,7 @@ */ /** - * Auto-configuration for Spring Security's OAuth2 resource server. + * Support for Spring Security's OAuth2 resource server. */ @NullMarked package org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet; diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/OAuth2ResourceServerWebSecurityAutoConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/OAuth2ResourceServerWebSecurityAutoConfiguration.java new file mode 100644 index 000000000000..5f26603870ae --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/OAuth2ResourceServerWebSecurityAutoConfiguration.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.security.oauth2.server.resource.autoconfigure.web; + +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.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration; +import org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration; +import org.springframework.boot.security.autoconfigure.actuate.web.servlet.ManagementWebSecurityAutoConfiguration; +import org.springframework.boot.security.autoconfigure.web.servlet.ConditionalOnDefaultWebSecurity; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.security.web.SecurityFilterChain; + +import static org.springframework.security.config.Customizer.withDefaults; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for OAuth2 resource server servlet + * based web security. + * + * @author Madhura Bhave + * @author Phillip Webb + * @since 4.1.0 + */ +@AutoConfiguration(before = { ManagementWebSecurityAutoConfiguration.class, SecurityAutoConfiguration.class, + UserDetailsServiceAutoConfiguration.class }, after = OAuth2ResourceServerAutoConfiguration.class) +@ConditionalOnWebApplication(type = Type.SERVLET) +@ConditionalOnDefaultWebSecurity +public final class OAuth2ResourceServerWebSecurityAutoConfiguration { + + @Bean + @ConditionalOnBean(JwtDecoder.class) + SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) { + http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); + http.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults())); + return http.build(); + } + + @Bean + @ConditionalOnBean(OpaqueTokenIntrospector.class) + SecurityFilterChain opaqueTokenSecurityFilterChain(HttpSecurity http) { + http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); + http.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults())); + return http.build(); + } + +} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/package-info.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/package-info.java new file mode 100644 index 000000000000..de450151abe6 --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 OAuth2 resource server web security. + */ +@NullMarked +package org.springframework.boot.security.oauth2.server.resource.autoconfigure.web; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/ReactiveOAuth2ResourceServerWebSecurityAutoConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/ReactiveOAuth2ResourceServerWebSecurityAutoConfiguration.java new file mode 100644 index 000000000000..92db179a1cff --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/ReactiveOAuth2ResourceServerWebSecurityAutoConfiguration.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.security.oauth2.server.resource.autoconfigure.web.reactive; + +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.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.security.autoconfigure.ReactiveUserDetailsServiceAutoConfiguration; +import org.springframework.boot.security.autoconfigure.actuate.web.reactive.ReactiveManagementWebSecurityAutoConfiguration; +import org.springframework.boot.security.autoconfigure.web.reactive.ReactiveWebSecurityAutoConfiguration; +import org.springframework.boot.security.autoconfigure.web.servlet.ConditionalOnDefaultWebSecurity; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive.ReactiveOAuth2ResourceServerAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.Customizer; +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.JwtSpec; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; +import org.springframework.security.web.server.SecurityWebFilterChain; + +import static org.springframework.security.config.Customizer.withDefaults; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for OAuth2 resource server reactive + * based web security. + * + * @author Madhura Bhave + * @author Phillip Webb + * @since 4.1.0 + */ +@AutoConfiguration( + before = { ReactiveManagementWebSecurityAutoConfiguration.class, ReactiveWebSecurityAutoConfiguration.class, + ReactiveUserDetailsServiceAutoConfiguration.class }, + after = ReactiveOAuth2ResourceServerAutoConfiguration.class) +@ConditionalOnWebApplication(type = Type.REACTIVE) +@ConditionalOnDefaultWebSecurity +@ConditionalOnClass({ EnableWebFluxSecurity.class }) +public final class ReactiveOAuth2ResourceServerWebSecurityAutoConfiguration { + + @Bean + @ConditionalOnBean(ReactiveJwtDecoder.class) + SecurityWebFilterChain reactiveJwtSecurityFilterChain(ServerHttpSecurity http, + ReactiveJwtDecoder reactiveJwtDecoder) { + Customizer jwtCustomizer = (jwt) -> jwt.jwtDecoder(reactiveJwtDecoder); + http.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated()); + http.oauth2ResourceServer((server) -> server.jwt(jwtCustomizer)); + return http.build(); + } + + @Bean + @ConditionalOnBean(ReactiveOpaqueTokenIntrospector.class) + SecurityWebFilterChain reactiveOpaqueTokenSecurityWebFilterChain(ServerHttpSecurity http) { + http.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated()); + http.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults())); + return http.build(); + } + +} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/package-info.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/package-info.java new file mode 100644 index 000000000000..bc2cd00d0161 --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 OAuth2 resource server reactive web security. + */ +@NullMarked +package org.springframework.boot.security.oauth2.server.resource.autoconfigure.web.reactive; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 36abf455cc47..85bc0948edaf 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,2 +1,4 @@ +org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive.ReactiveOAuth2ResourceServerAutoConfiguration -org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration +org.springframework.boot.security.oauth2.server.resource.autoconfigure.web.OAuth2ResourceServerWebSecurityAutoConfiguration +org.springframework.boot.security.oauth2.server.resource.autoconfigure.web.reactive.ReactiveOAuth2ResourceServerWebSecurityAutoConfiguration diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.webflux.test.autoconfigure.WebFluxTest.imports b/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.webflux.test.autoconfigure.WebFluxTest.imports index 6eca008c3d5e..37cd8df5930e 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.webflux.test.autoconfigure.WebFluxTest.imports +++ b/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.webflux.test.autoconfigure.WebFluxTest.imports @@ -1 +1,2 @@ org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive.ReactiveOAuth2ResourceServerAutoConfiguration +org.springframework.boot.security.oauth2.server.resource.autoconfigure.web.reactive.ReactiveOAuth2ResourceServerWebSecurityAutoConfiguration diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest.imports b/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest.imports index 759200960efb..8c9c7939a325 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest.imports +++ b/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest.imports @@ -1 +1,2 @@ -org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration +org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration +org.springframework.boot.security.oauth2.server.resource.autoconfigure.web.OAuth2ResourceServerWebSecurityAutoConfiguration diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient.imports b/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient.imports index 6eca008c3d5e..8c9c7939a325 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient.imports +++ b/module/spring-boot-security-oauth2-resource-server/src/main/resources/META-INF/spring/org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient.imports @@ -1 +1,2 @@ -org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive.ReactiveOAuth2ResourceServerAutoConfiguration +org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration +org.springframework.boot.security.oauth2.server.resource.autoconfigure.web.OAuth2ResourceServerWebSecurityAutoConfiguration diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java similarity index 90% rename from module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerAutoConfigurationTests.java rename to module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java index 31beee478411..4e96d251b1eb 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet; +package org.springframework.boot.security.oauth2.server.resource.autoconfigure; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -33,7 +33,6 @@ import java.util.function.Supplier; import com.nimbusds.jose.JWSAlgorithm; -import jakarta.servlet.Filter; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.assertj.core.api.InstanceOfAssertFactories; @@ -47,10 +46,6 @@ import tools.jackson.databind.json.JsonMapper; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration; -import org.springframework.boot.security.autoconfigure.actuate.web.servlet.ManagementWebSecurityAutoConfiguration; -import org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration; -import org.springframework.boot.security.oauth2.server.resource.autoconfigure.JwtConverterCustomizationsArgumentsProvider; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; @@ -63,7 +58,6 @@ 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; @@ -80,10 +74,7 @@ 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; -import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.test.util.ReflectionTestUtils; @@ -104,8 +95,6 @@ */ class OAuth2ResourceServerAutoConfigurationTests { - private static final String MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN = "managementSecurityFilterChain"; - private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(OAuth2ResourceServerAutoConfiguration.class)) .withUserConfiguration(TestConfig.class); @@ -131,7 +120,6 @@ void autoConfigurationShouldConfigureResourceServer() { .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") .run((context) -> { assertThat(context).hasSingleBean(JwtDecoder.class); - assertThat(getBearerTokenFilter(context)).isNotNull(); assertJwkSetUriJwtDecoderBuilderCustomization(context); }); } @@ -168,7 +156,6 @@ void autoConfigurationShouldConfigureResourceServerWithSingleJwsAlgorithm() { assertThat(jwtDecoder).extracting("jwtProcessor.jwsKeySelector.jwsAlgs") .asInstanceOf(InstanceOfAssertFactories.collection(JWSAlgorithm.class)) .containsExactlyInAnyOrder(JWSAlgorithm.RS384); - assertThat(getBearerTokenFilter(context)).isNotNull(); }); } @@ -182,7 +169,6 @@ void autoConfigurationShouldConfigureResourceServerWithMultipleJwsAlgorithms() { assertThat(jwtDecoder).extracting("jwtProcessor.jwsKeySelector.jwsAlgs") .asInstanceOf(InstanceOfAssertFactories.collection(JWSAlgorithm.class)) .containsExactlyInAnyOrder(JWSAlgorithm.RS256, JWSAlgorithm.RS384, JWSAlgorithm.RS512); - assertThat(getBearerTokenFilter(context)).isNotNull(); }); } @@ -305,10 +291,7 @@ void autoConfigurationShouldConfigureResourceServerUsingPublicKeyValue() throws this.contextRunner .withPropertyValues( "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location") - .run((context) -> { - assertThat(context).hasSingleBean(JwtDecoder.class); - assertThat(getBearerTokenFilter(context)).isNotNull(); - }); + .run((context) -> assertThat(context).hasSingleBean(JwtDecoder.class)); } @Test @@ -342,7 +325,6 @@ void autoConfigurationWhenSetUriKeyLocationAndIssuerUriPresentShouldUseSetUri() "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") .run((context) -> { assertThat(context).hasSingleBean(JwtDecoder.class); - assertThat(getBearerTokenFilter(context)).isNotNull(); assertThat(context.containsBean("jwtDecoderByJwkKeySetUri")).isTrue(); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isFalse(); }); @@ -362,14 +344,13 @@ void autoConfigurationWhenKeyLocationAndIssuerUriPresentShouldUseIssuerUri() thr "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location") .run((context) -> { assertThat(context).hasSingleBean(JwtDecoder.class); - assertThat(getBearerTokenFilter(context)).isNotNull(); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); }); } @Test void autoConfigurationWhenJwkSetUriNullShouldNotFail() { - this.contextRunner.run((context) -> assertThat(getBearerTokenFilter(context)).isNull()); + this.contextRunner.run((context) -> assertThat(context).hasNotFailed()); } @Test @@ -377,7 +358,7 @@ void jwtDecoderByJwkSetUriIsConditionalOnMissingBean() { this.contextRunner .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") .withUserConfiguration(JwtDecoderConfig.class) - .run((context) -> assertThat(getBearerTokenFilter(context)).isNotNull()); + .run((context) -> assertThat(context).hasSingleBean(JwtDecoder.class)); } @Test @@ -386,7 +367,7 @@ void jwtDecoderByOidcIssuerUriIsConditionalOnMissingBean() { .withPropertyValues( "spring.security.oauth2.resourceserver.jwt.issuer-uri=https://jwk-oidc-issuer-location.com") .withUserConfiguration(JwtDecoderConfig.class) - .run((context) -> assertThat(getBearerTokenFilter(context)).isNotNull()); + .run((context) -> assertThat(context).hasSingleBean(JwtDecoder.class)); } @Test @@ -395,10 +376,7 @@ void autoConfigurationShouldBeConditionalOnResourceServerClass() { .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") .withUserConfiguration(JwtDecoderConfig.class) .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) - .run((context) -> { - assertThat(context).doesNotHaveBean(OAuth2ResourceServerAutoConfiguration.class); - assertThat(getBearerTokenFilter(context)).isNull(); - }); + .run((context) -> assertThat(context).doesNotHaveBean(OAuth2ResourceServerAutoConfiguration.class)); } @Test @@ -407,10 +385,7 @@ void autoConfigurationForJwtShouldBeConditionalOnJwtDecoderClass() { .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") .withUserConfiguration(JwtDecoderConfig.class) .withClassLoader(new FilteredClassLoader(JwtDecoder.class)) - .run((context) -> { - assertThat(context).hasSingleBean(OAuth2ResourceServerAutoConfiguration.class); - assertThat(getBearerTokenFilter(context)).isNull(); - }); + .run((context) -> assertThat(context).hasSingleBean(OAuth2ResourceServerAutoConfiguration.class)); } @Test @@ -419,10 +394,7 @@ void jwtSecurityFilterShouldBeConditionalOnSecurityFilterChainClass() { .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") .withUserConfiguration(JwtDecoderConfig.class) .withClassLoader(new FilteredClassLoader(SecurityFilterChain.class)) - .run((context) -> { - assertThat(context).hasSingleBean(OAuth2ResourceServerAutoConfiguration.class); - assertThat(getBearerTokenFilter(context)).isNull(); - }); + .run((context) -> assertThat(context).hasSingleBean(OAuth2ResourceServerAutoConfiguration.class)); } @Test @@ -433,10 +405,7 @@ void opaqueTokenSecurityFilterShouldBeConditionalOnSecurityFilterChainClass() { "spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id", "spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret") .withClassLoader(new FilteredClassLoader(SecurityFilterChain.class)) - .run((context) -> { - assertThat(context).hasSingleBean(OAuth2ResourceServerAutoConfiguration.class); - assertThat(getBearerTokenFilter(context)).isNull(); - }); + .run((context) -> assertThat(context).hasSingleBean(OAuth2ResourceServerAutoConfiguration.class)); } @Test @@ -449,9 +418,6 @@ void autoConfigurationWhenJwkSetUriAndIntrospectionUriAvailable() { .run((context) -> { assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class); assertThat(context).hasSingleBean(JwtDecoder.class); - assertThat(getBearerTokenFilter(context)).extracting("authenticationManagerResolver.arg$1.providers") - .asInstanceOf(InstanceOfAssertFactories.LIST) - .hasAtLeastOneElementOfType(JwtAuthenticationProvider.class); }); } @@ -462,10 +428,7 @@ void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionC "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", "spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id", "spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret") - .run((context) -> { - assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class); - assertThat(getBearerTokenFilter(context)).isNotNull(); - }); + .run((context) -> assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class)); } @Test @@ -474,7 +437,7 @@ void opaqueTokenIntrospectorIsConditionalOnMissingBean() { .withPropertyValues( "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com") .withUserConfiguration(OpaqueTokenIntrospectorConfig.class) - .run((context) -> assertThat(getBearerTokenFilter(context)).isNotNull()); + .run((context) -> assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class)); } @Test @@ -684,7 +647,6 @@ void autoConfigurationShouldConfigureResourceServerWithJwtConverterCustomization .extracting(GrantedAuthority::getAuthority) .containsExactlyInAnyOrder(expectedAuthorities); assertThat(context).hasSingleBean(JwtDecoder.class); - assertThat(getBearerTokenFilter(context)).isNotNull(); }); } @@ -731,22 +693,9 @@ void jwtAuthenticationConverterByJwtConfigIsConditionalOnMissingBean() { .isEqualTo(userConfigPrincipalValue) .isNotEqualTo(propertiesPrincipalValue); assertThat(context).hasSingleBean(JwtDecoder.class); - assertThat(getBearerTokenFilter(context)).isNotNull(); }); } - @Test - void causesManagementWebSecurityAutoConfigurationToBackOff() { - WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ManagementWebSecurityAutoConfiguration.class, - OAuth2ResourceServerAutoConfiguration.class, SecurityAutoConfiguration.class, - ServletWebSecurityAutoConfiguration.class, WebMvcAutoConfiguration.class)); - contextRunner.run((context) -> assertThat(context).hasSingleBean(ManagementWebSecurityAutoConfiguration.class)); - contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://authserver") - .run((context) -> assertThat(context).doesNotHaveBean(ManagementWebSecurityAutoConfiguration.class) - .doesNotHaveBean(MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN)); - } - @Test @SuppressWarnings("unchecked") void customTypeValidatorCanReplaceDefaultWhenUsingIssuerUri() throws Exception { @@ -800,13 +749,6 @@ void customTypeValidatorCanReplaceDefaultWhenUsingJwkSetUri() throws Exception { }); } - private @Nullable Filter getBearerTokenFilter(AssertableWebApplicationContext context) { - FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); - List filterChains = filterChain.getFilterChains(); - List filters = filterChains.get(0).getFilters(); - return filters.stream().filter((f) -> f instanceof BearerTokenAuthenticationFilter).findFirst().orElse(null); - } - private String cleanIssuerPath(String issuer) { if (issuer.endsWith("/")) { return issuer.substring(0, issuer.length() - 1); diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index 824656ae063b..d1dd3d19da67 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -32,7 +32,6 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; -import java.util.stream.Stream; import com.nimbusds.jose.JWSAlgorithm; import okhttp3.mockwebserver.MockResponse; @@ -49,14 +48,11 @@ import tools.jackson.databind.json.JsonMapper; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.security.autoconfigure.actuate.web.reactive.ReactiveManagementWebSecurityAutoConfiguration; -import org.springframework.boot.security.autoconfigure.web.reactive.ReactiveWebSecurityAutoConfiguration; import org.springframework.boot.security.oauth2.server.resource.autoconfigure.JwtConverterCustomizationsArgumentsProvider; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.testsupport.classpath.resources.WithResource; -import org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @@ -64,7 +60,6 @@ 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; @@ -82,16 +77,10 @@ import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder; 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; -import org.springframework.security.web.server.authentication.AuthenticationWebFilter; import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilter; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -112,14 +101,14 @@ */ class ReactiveOAuth2ResourceServerAutoConfigurationTests { + private static final Duration TIMEOUT = Duration.ofSeconds(5000000); + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(ReactiveOAuth2ResourceServerAutoConfiguration.class)) .withUserConfiguration(TestConfig.class); private @Nullable MockWebServer server; - private static final Duration TIMEOUT = Duration.ofSeconds(5000000); - private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\"," + "\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGm" + "uLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtd" @@ -139,7 +128,7 @@ void autoConfigurationShouldConfigureResourceServer() { .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") .run((context) -> { assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class); - assertFilterConfiguredWithJwtAuthenticationManager(context); + assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); }); } @@ -225,8 +214,8 @@ void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws I + this.server.getHostName() + ":" + this.server.getPort() + "/" + path) .run((context) -> { assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class); - assertFilterConfiguredWithJwtAuthenticationManager(context); - assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); + assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); + assertThat(context.containsBean("reactiveJwtDecoderByIssuerUri")).isTrue(); // Trigger calls to the issuer by decoding a token decodeJwt(context); assertJwkSetUriReactiveJwtDecoderBuilderCustomization(context); @@ -265,8 +254,8 @@ void autoConfigurationShouldConfigureResourceServerUsingOidcRfc8414IssuerUri() t + this.server.getHostName() + ":" + this.server.getPort()) .run((context) -> { assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class); - assertFilterConfiguredWithJwtAuthenticationManager(context); - assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); + assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); + assertThat(context.containsBean("reactiveJwtDecoderByIssuerUri")).isTrue(); // Trigger calls to the issuer by decoding a token decodeJwt(context); // assertJwkSetUriReactiveJwtDecoderBuilderCustomization(context); @@ -287,8 +276,8 @@ void autoConfigurationShouldConfigureResourceServerUsingOAuthIssuerUri() throws + this.server.getHostName() + ":" + this.server.getPort()) .run((context) -> { assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class); - assertFilterConfiguredWithJwtAuthenticationManager(context); - assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); + assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); + assertThat(context.containsBean("reactiveJwtDecoderByIssuerUri")).isTrue(); // Trigger calls to the issuer by decoding a token decodeJwt(context); assertJwkSetUriReactiveJwtDecoderBuilderCustomization(context); @@ -305,7 +294,7 @@ void autoConfigurationShouldConfigureResourceServerUsingPublicKeyValue() { "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location") .run((context) -> { assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class); - assertFilterConfiguredWithJwtAuthenticationManager(context); + assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); }); } @@ -328,9 +317,9 @@ void autoConfigurationWhenSetUriKeyLocationIssuerUriPresentShouldUseSetUri() { "spring.security.oauth2.resourceserver.jwt.issuer-uri=https://jwk-oidc-issuer-location.com") .run((context) -> { assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class); - assertFilterConfiguredWithJwtAuthenticationManager(context); - assertThat(context.containsBean("jwtDecoder")).isTrue(); - assertThat(context.containsBean("jwtDecoderByIssuerUri")).isFalse(); + assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); + assertThat(context.containsBean("reactiveJwtDecoderByJwkKeySetUri")).isTrue(); + assertThat(context.containsBean("reactiveJwtDecoderByIssuerUri")).isFalse(); }); } @@ -348,8 +337,8 @@ void autoConfigurationWhenKeyLocationAndIssuerUriPresentShouldUseIssuerUri() thr "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location") .run((context) -> { assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class); - assertFilterConfiguredWithJwtAuthenticationManager(context); - assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); + assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); + assertThat(context.containsBean("reactiveJwtDecoderByIssuerUri")).isTrue(); }); } @@ -363,7 +352,7 @@ void jwtDecoderBeanIsConditionalOnMissingBean() { this.contextRunner .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") .withUserConfiguration(JwtDecoderConfig.class) - .run((this::assertFilterConfiguredWithJwtAuthenticationManager)); + .run((context) -> assertThat(context).hasSingleBean(ReactiveJwtDecoder.class)); } @Test @@ -372,7 +361,7 @@ void jwtDecoderByIssuerUriBeanIsConditionalOnMissingBean() { .withPropertyValues( "spring.security.oauth2.resourceserver.jwt.issuer-uri=https://jwk-oidc-issuer-location.com") .withUserConfiguration(JwtDecoderConfig.class) - .run((this::assertFilterConfiguredWithJwtAuthenticationManager)); + .run((context) -> assertThat(context).hasSingleBean(ReactiveJwtDecoder.class)); } @Test @@ -411,10 +400,7 @@ void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionC "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", "spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id", "spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret") - .run((context) -> { - assertThat(context).hasSingleBean(ReactiveOpaqueTokenIntrospector.class); - assertFilterConfiguredWithOpaqueTokenAuthenticationManager(context); - }); + .run((context) -> assertThat(context).hasSingleBean(ReactiveOpaqueTokenIntrospector.class)); } @Test @@ -427,7 +413,7 @@ void autoConfigurationWhenJwkSetUriAndIntrospectionUriAvailable() { .run((context) -> { assertThat(context).hasSingleBean(ReactiveOpaqueTokenIntrospector.class); assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); - assertFilterConfiguredWithJwtAuthenticationManager(context); + assertThat(context).hasSingleBean(ReactiveOpaqueTokenIntrospector.class); }); } @@ -437,7 +423,7 @@ void opaqueTokenIntrospectorIsConditionalOnMissingBean() { .withPropertyValues( "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com") .withUserConfiguration(OpaqueTokenIntrospectorConfig.class) - .run((this::assertFilterConfiguredWithOpaqueTokenAuthenticationManager)); + .run((context) -> assertThat(context).hasSingleBean(ReactiveOpaqueTokenIntrospector.class)); } @Test @@ -687,7 +673,7 @@ void autoConfigurationShouldConfigureResourceServerWithJwtConverterCustomization assertThat(token.getAuthorities()).extracting(GrantedAuthority::getAuthority) .containsExactlyInAnyOrder(expectedAuthorities); assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class); - assertFilterConfiguredWithJwtAuthenticationManager(context); + assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); }); } @@ -712,23 +698,10 @@ void jwtAuthenticationConverterByJwtConfigIsConditionalOnMissingBean() { .isEqualTo(userConfigPrincipalValue) .isNotEqualTo(propertiesPrincipalValue); assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class); - assertFilterConfiguredWithJwtAuthenticationManager(context); + assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); }); } - @Test - void causesReactiveManagementWebSecurityAutoConfigurationToBackOff() { - ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ReactiveManagementWebSecurityAutoConfiguration.class, - ReactiveOAuth2ResourceServerAutoConfiguration.class, ReactiveWebSecurityAutoConfiguration.class, - WebFluxAutoConfiguration.class)); - contextRunner - .run((context) -> assertThat(context).hasSingleBean(ReactiveManagementWebSecurityAutoConfiguration.class)); - contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://authserver") - .run((context) -> assertThat(context) - .doesNotHaveBean(ReactiveManagementWebSecurityAutoConfiguration.class)); - } - @Test @SuppressWarnings("unchecked") void customTypeValidatorCanReplaceDefaultWhenUsingIssuerUri() throws Exception { @@ -783,43 +756,6 @@ void customTypeValidatorCanReplaceDefaultWhenUsingJwkSetUri() throws Exception { }); } - @SuppressWarnings("unchecked") - private void assertFilterConfiguredWithJwtAuthenticationManager(AssertableReactiveWebApplicationContext context) { - MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context - .getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); - Stream filters = filterChain.getWebFilters().toStream(); - AuthenticationWebFilter webFilter = (AuthenticationWebFilter) filters - .filter((f) -> f instanceof AuthenticationWebFilter) - .findFirst() - .orElse(null); - assertThat(webFilter).isNotNull(); - ReactiveAuthenticationManagerResolver authenticationManagerResolver = (ReactiveAuthenticationManagerResolver) ReflectionTestUtils - .getField(webFilter, "authenticationManagerResolver"); - assertThat(authenticationManagerResolver).isNotNull(); - Object authenticationManager = authenticationManagerResolver.resolve(mock(ServerWebExchange.class)) - .block(TIMEOUT); - assertThat(authenticationManager).isInstanceOf(JwtReactiveAuthenticationManager.class); - } - - @SuppressWarnings("unchecked") - private void assertFilterConfiguredWithOpaqueTokenAuthenticationManager( - AssertableReactiveWebApplicationContext context) { - MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context - .getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); - Stream filters = filterChain.getWebFilters().toStream(); - AuthenticationWebFilter webFilter = (AuthenticationWebFilter) filters - .filter((f) -> f instanceof AuthenticationWebFilter) - .findFirst() - .orElse(null); - assertThat(webFilter).isNotNull(); - ReactiveAuthenticationManagerResolver authenticationManagerResolver = (ReactiveAuthenticationManagerResolver) ReflectionTestUtils - .getField(webFilter, "authenticationManagerResolver"); - assertThat(authenticationManagerResolver).isNotNull(); - Object authenticationManager = authenticationManagerResolver.resolve(mock(ServerWebExchange.class)) - .block(TIMEOUT); - assertThat(authenticationManager).isInstanceOf(OpaqueTokenReactiveAuthenticationManager.class); - } - private String cleanIssuerPath(String issuer) { if (issuer.endsWith("/")) { return issuer.substring(0, issuer.length() - 1); diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerWebMvcTestIntegrationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/OAuth2ResourceServerWebSecurityAutoConfigurationMvcIntegrationTests.java similarity index 75% rename from module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerWebMvcTestIntegrationTests.java rename to module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/OAuth2ResourceServerWebSecurityAutoConfigurationMvcIntegrationTests.java index 0fdcd116234a..2713e62bd6e9 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerWebMvcTestIntegrationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/OAuth2ResourceServerWebSecurityAutoConfigurationMvcIntegrationTests.java @@ -14,12 +14,13 @@ * limitations under the License. */ -package org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet; +package org.springframework.boot.security.oauth2.server.resource.autoconfigure.web; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration; import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; import org.springframework.context.ApplicationContext; @@ -33,14 +34,15 @@ * @author Andy Wilkinson */ @WebMvcTest -class OAuth2ResourceServerWebMvcTestIntegrationTests { +class OAuth2ResourceServerWebSecurityAutoConfigurationMvcIntegrationTests { @Autowired - private ApplicationContext applicationContext; + private ApplicationContext context; @Test void oauth2ResourceServerAutoConfigurationWasImported() { - assertThat(this.applicationContext).has(importedAutoConfiguration(OAuth2ResourceServerAutoConfiguration.class)); + assertThat(this.context).has(importedAutoConfiguration(OAuth2ResourceServerAutoConfiguration.class)); + assertThat(this.context).has(importedAutoConfiguration(OAuth2ResourceServerWebSecurityAutoConfiguration.class)); } @SpringBootConfiguration diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/OAuth2ResourceServerWebSecurityAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/OAuth2ResourceServerWebSecurityAutoConfigurationTests.java new file mode 100644 index 000000000000..8086c212cec6 --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/OAuth2ResourceServerWebSecurityAutoConfigurationTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.security.oauth2.server.resource.autoconfigure.web; + +import java.util.List; + +import jakarta.servlet.Filter; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration; +import org.springframework.boot.security.autoconfigure.actuate.web.servlet.ManagementWebSecurityAutoConfiguration; +import org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; +import org.springframework.context.ApplicationContext; +import org.springframework.security.authentication.AuthenticationManagerResolver; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.BeanIds; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Test for {@link OAuth2ResourceServerWebSecurityAutoConfiguration}. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +class OAuth2ResourceServerWebSecurityAutoConfigurationTests { + + private static final String JWK_SET_URI_PROPERTY = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://authserver"; + + private static final String MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN = "managementSecurityFilterChain"; + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ManagementWebSecurityAutoConfiguration.class, + OAuth2ResourceServerAutoConfiguration.class, OAuth2ResourceServerWebSecurityAutoConfiguration.class, + SecurityAutoConfiguration.class, ServletWebSecurityAutoConfiguration.class, + WebMvcAutoConfiguration.class)); + + @Test + void causesManagementWebSecurityAutoConfigurationToBackOff() { + this.contextRunner + .run((context) -> assertThat(context).hasSingleBean(ManagementWebSecurityAutoConfiguration.class)); + this.contextRunner.withPropertyValues(JWK_SET_URI_PROPERTY) + .run((context) -> assertThat(context).doesNotHaveBean(ManagementWebSecurityAutoConfiguration.class) + .doesNotHaveBean(MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN)); + } + + @Test + void whenNoJwtDecoderDoesNotAddFilterChain() { + this.contextRunner.run((context) -> assertThat(getBearerTokenFilter(context)).isNull()); + } + + @Test + void whenHasJwtDecoderAddsFilterChain() { + this.contextRunner.withPropertyValues(JWK_SET_URI_PROPERTY).run((context) -> { + Filter bearerTokenFilter = getBearerTokenFilter(context); + assertThat(bearerTokenFilter).isNotNull(); + assertThat(getAuthenticationProviders(bearerTokenFilter)) + .hasAtLeastOneElementOfType(JwtAuthenticationProvider.class); + }); + } + + @Test + void whenNoOpaqueTokenIntrospectorDoesNotAddFilterChain() { + this.contextRunner.run((context) -> assertThat(getBearerTokenFilter(context)).isNull()); + } + + @Test + void whenHasOpaqueTokenIntrospectorAddsFilterChain() { + this.contextRunner + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://authserver", + "spring.security.oauth2.resourceserver.opaquetoken.client-id=test", + "spring.security.oauth2.resourceserver.opaquetoken.client-secret=shh") + .run((context) -> { + Filter bearerTokenFilter = getBearerTokenFilter(context); + assertThat(bearerTokenFilter).isNotNull(); + assertThat(getAuthenticationProviders(bearerTokenFilter)) + .hasAtLeastOneElementOfType(OpaqueTokenAuthenticationProvider.class); + }); + } + + private @Nullable Filter getBearerTokenFilter(ApplicationContext context) { + return getFilters(context).stream() + .filter(BearerTokenAuthenticationFilter.class::isInstance) + .findFirst() + .orElse(null); + } + + private List getFilters(ApplicationContext context) { + FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); + List filterChains = filterChain.getFilterChains(); + return filterChains.get(0).getFilters(); + } + + private List getAuthenticationProviders(Filter bearerTokenFilter) { + AuthenticationManagerResolver resolver = (AuthenticationManagerResolver) ReflectionTestUtils + .getField(bearerTokenFilter, "authenticationManagerResolver"); + assertThat(resolver).isNotNull(); + ProviderManager providerManager = (ProviderManager) resolver.resolve(mock()); + return providerManager.getProviders(); + } + +} diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/ReactiveOAuth2ResourceServerWebSecurityAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/ReactiveOAuth2ResourceServerWebSecurityAutoConfigurationTests.java new file mode 100644 index 000000000000..1b2e17f1bc58 --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/ReactiveOAuth2ResourceServerWebSecurityAutoConfigurationTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.security.oauth2.server.resource.autoconfigure.web.reactive; + +import java.time.Duration; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.security.autoconfigure.actuate.web.reactive.ReactiveManagementWebSecurityAutoConfiguration; +import org.springframework.boot.security.autoconfigure.web.reactive.ReactiveWebSecurityAutoConfiguration; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive.ReactiveOAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration; +import org.springframework.context.ApplicationContext; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager; +import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenReactiveAuthenticationManager; +import org.springframework.security.web.server.MatcherSecurityWebFilterChain; +import org.springframework.security.web.server.authentication.AuthenticationWebFilter; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.server.ServerWebExchange; + +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 ReactiveOAuth2ResourceServerWebSecurityAutoConfiguration}. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +class ReactiveOAuth2ResourceServerWebSecurityAutoConfigurationTests { + + private static final Duration TIMEOUT = Duration.ofSeconds(5000000); + + private static final String JWK_SET_URI_PROPERTY = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://authserver"; + + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactiveManagementWebSecurityAutoConfiguration.class, + ReactiveOAuth2ResourceServerAutoConfiguration.class, + ReactiveOAuth2ResourceServerWebSecurityAutoConfiguration.class, + ReactiveWebSecurityAutoConfiguration.class, WebFluxAutoConfiguration.class)); + + @Test + void causesReactiveManagementWebSecurityAutoConfigurationToBackOff() { + this.contextRunner + .run((context) -> assertThat(context).hasSingleBean(ReactiveManagementWebSecurityAutoConfiguration.class)); + this.contextRunner.withPropertyValues(JWK_SET_URI_PROPERTY) + .run((context) -> assertThat(context) + .doesNotHaveBean(ReactiveManagementWebSecurityAutoConfiguration.class)); + } + + @Test + void whenNoReactiveJwtDecoderDoesNotAddFilterChain() { + this.contextRunner.run((context) -> { + ReactiveAuthenticationManager authenticationManager = getAuthenticationManager(context); + assertThatExceptionOfType(UsernameNotFoundException.class) + .isThrownBy(authenticationManager.authenticate(mock())::block); + }); + } + + @Test + void whenHasReactiveJwtDecoderAddsFilterChain() { + this.contextRunner.withPropertyValues(JWK_SET_URI_PROPERTY).run((context) -> { + ReactiveAuthenticationManager authenticationManager = getAuthenticationManager(context); + assertThat(authenticationManager).isInstanceOf(JwtReactiveAuthenticationManager.class); + }); + } + + @Test + void whenNoReactiveOpaqueTokenIntrospectorDoesNotAddFilterChain() { + this.contextRunner.run((context) -> { + ReactiveAuthenticationManager authenticationManager = getAuthenticationManager(context); + assertThatExceptionOfType(UsernameNotFoundException.class) + .isThrownBy(authenticationManager.authenticate(mock())::block); + }); + } + + @Test + void whenHasReactiveOpaqueTokenIntrospectorAddsFilterChain() { + this.contextRunner + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://authserver", + "spring.security.oauth2.resourceserver.opaquetoken.client-id=test", + "spring.security.oauth2.resourceserver.opaquetoken.client-secret=shh") + .run((context) -> { + ReactiveAuthenticationManager authenticationManager = getAuthenticationManager(context); + assertThat(authenticationManager).isInstanceOf(OpaqueTokenReactiveAuthenticationManager.class); + }); + } + + private ReactiveAuthenticationManager getAuthenticationManager(AssertableReactiveWebApplicationContext context) { + AuthenticationWebFilter authenticationWebFilter = getAuthenticationWebFilter(context); + assertThat(authenticationWebFilter).isNotNull(); + return getAuthenticationManager(authenticationWebFilter); + } + + private @Nullable AuthenticationWebFilter getAuthenticationWebFilter(ApplicationContext context) { + MatcherSecurityWebFilterChain filterChain = context.getBean(MatcherSecurityWebFilterChain.class); + return (AuthenticationWebFilter) filterChain.getWebFilters() + .toStream() + .filter(AuthenticationWebFilter.class::isInstance) + .findFirst() + .orElse(null); + } + + @SuppressWarnings("unchecked") + private ReactiveAuthenticationManager getAuthenticationManager(AuthenticationWebFilter webFilter) { + ReactiveAuthenticationManagerResolver authenticationManagerResolver = (ReactiveAuthenticationManagerResolver) ReflectionTestUtils + .getField(webFilter, "authenticationManagerResolver"); + assertThat(authenticationManagerResolver).isNotNull(); + ReactiveAuthenticationManager authenticationManager = authenticationManagerResolver + .resolve(mock(ServerWebExchange.class)) + .block(TIMEOUT); + assertThat(authenticationManager).isNotNull(); + return authenticationManager; + } + +} diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerWebFluxTestIntegrationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/ReactiveOAuth2ResourceServerWebSecurityAutoConfigurationWebFluxIntegrationTests.java similarity index 86% rename from module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerWebFluxTestIntegrationTests.java rename to module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/ReactiveOAuth2ResourceServerWebSecurityAutoConfigurationWebFluxIntegrationTests.java index cd3bc1703ebb..b53446a4de47 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerWebFluxTestIntegrationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/web/reactive/ReactiveOAuth2ResourceServerWebSecurityAutoConfigurationWebFluxIntegrationTests.java @@ -14,12 +14,13 @@ * limitations under the License. */ -package org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive; +package org.springframework.boot.security.oauth2.server.resource.autoconfigure.web.reactive; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive.ReactiveOAuth2ResourceServerAutoConfiguration; import org.springframework.boot.webflux.test.autoconfigure.WebFluxTest; import org.springframework.context.ApplicationContext; @@ -33,7 +34,7 @@ * @author Andy Wilkinson */ @WebFluxTest -class ReactiveOAuth2ResourceServerWebFluxTestIntegrationTests { +class ReactiveOAuth2ResourceServerWebSecurityAutoConfigurationWebFluxIntegrationTests { @Autowired private ApplicationContext applicationContext; From fed1098410c1ab1ca0b57827f3e79aa536968980 Mon Sep 17 00:00:00 2001 From: Emanuel Rabina Date: Mon, 2 Mar 2026 20:55:46 +1300 Subject: [PATCH 254/930] Upgrade to Thymeleaf Layout Dialect 4.0.0 See gh-49361 Signed-off-by: Emanuel Rabina --- .../autoconfigure/ThymeleafReactiveAutoConfigurationTests.java | 2 +- .../autoconfigure/ThymeleafServletAutoConfigurationTests.java | 2 +- platform/spring-boot-dependencies/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/module/spring-boot-thymeleaf/src/test/java/org/springframework/boot/thymeleaf/autoconfigure/ThymeleafReactiveAutoConfigurationTests.java b/module/spring-boot-thymeleaf/src/test/java/org/springframework/boot/thymeleaf/autoconfigure/ThymeleafReactiveAutoConfigurationTests.java index 3e94c5feb4ee..590cd933c11d 100644 --- a/module/spring-boot-thymeleaf/src/test/java/org/springframework/boot/thymeleaf/autoconfigure/ThymeleafReactiveAutoConfigurationTests.java +++ b/module/spring-boot-thymeleaf/src/test/java/org/springframework/boot/thymeleaf/autoconfigure/ThymeleafReactiveAutoConfigurationTests.java @@ -261,7 +261,7 @@ static class LayoutDialectConfiguration { @Bean LayoutDialect layoutDialect() { - return new LayoutDialect(new GroupingStrategy()); + return new LayoutDialect().withSortingStrategy(new GroupingStrategy()); } } diff --git a/module/spring-boot-thymeleaf/src/test/java/org/springframework/boot/thymeleaf/autoconfigure/ThymeleafServletAutoConfigurationTests.java b/module/spring-boot-thymeleaf/src/test/java/org/springframework/boot/thymeleaf/autoconfigure/ThymeleafServletAutoConfigurationTests.java index e5493bb6ed54..0c738654874c 100644 --- a/module/spring-boot-thymeleaf/src/test/java/org/springframework/boot/thymeleaf/autoconfigure/ThymeleafServletAutoConfigurationTests.java +++ b/module/spring-boot-thymeleaf/src/test/java/org/springframework/boot/thymeleaf/autoconfigure/ThymeleafServletAutoConfigurationTests.java @@ -374,7 +374,7 @@ static class LayoutDialectConfiguration { @Bean LayoutDialect layoutDialect() { - return new LayoutDialect(new GroupingStrategy()); + return new LayoutDialect().withSortingStrategy(new GroupingStrategy()); } } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 94474b2aea17..e73aa2d86cc7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2732,7 +2732,7 @@ bom { ] } } - library("Thymeleaf Layout Dialect", "3.4.0") { + library("Thymeleaf Layout Dialect", "4.0.0") { group("nz.net.ultraq.thymeleaf") { modules = [ "thymeleaf-layout-dialect" From d865935652c96cc92837d6faa22cfc93df152604 Mon Sep 17 00:00:00 2001 From: hojooo Date: Thu, 18 Sep 2025 00:19:09 +0900 Subject: [PATCH 255/930] Add support for Log4j2 rolling policy configuration See gh-47260 Signed-off-by: hojooo --- .../boot/logging/AbstractLoggingSystem.java | 6 +- .../logging/log4j2/Log4J2LoggingSystem.java | 7 + .../log4j2/Log4j2LoggingSystemProperties.java | 125 ++++++++++++ .../Log4j2RollingPolicySystemProperty.java | 95 +++++++++ .../log4j2/SpringBootTriggeringPolicy.java | 182 ++++++++++++++++++ ...itional-spring-configuration-metadata.json | 48 +++++ .../boot/logging/log4j2/log4j2-file.xml | 14 +- .../logging/AbstractLoggingSystemTests.java | 4 + .../log4j2/Log4J2LoggingSystemTests.java | 162 +++++++++++++--- .../Log4j2LoggingSystemPropertiesTests.java | 122 ++++++++++++ 10 files changed, 731 insertions(+), 34 deletions(-) create mode 100644 core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemProperties.java create mode 100644 core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2RollingPolicySystemProperty.java create mode 100644 core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java create mode 100644 core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemPropertiesTests.java diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java index 1bc91f4bd8d7..52d822475df3 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java @@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; import org.springframework.util.Assert; @@ -180,7 +181,10 @@ protected final String getPackagedConfigFile(String fileName) { } protected final void applySystemProperties(Environment environment, @Nullable LogFile logFile) { - new LoggingSystemProperties(environment, getDefaultValueResolver(environment), null).apply(logFile); + LoggingSystemProperties systemProperties = (environment instanceof ConfigurableEnvironment configurableEnvironment) + ? getSystemProperties(configurableEnvironment) + : new LoggingSystemProperties(environment, getDefaultValueResolver(environment), null); + systemProperties.apply(logFile); } /** diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java index cea74711abc3..e1aebc202366 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java @@ -58,8 +58,10 @@ import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.LoggingSystemFactory; +import org.springframework.boot.logging.LoggingSystemProperties; import org.springframework.core.Conventions; import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; @@ -128,6 +130,11 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { this.loggerContext = loggerContext; } + @Override + public LoggingSystemProperties getSystemProperties(ConfigurableEnvironment environment) { + return new Log4j2LoggingSystemProperties(environment, getDefaultValueResolver(environment), null); + } + @Override protected String[] getStandardConfigLocations() { // With Log4J2 we use the ConfigurationFactory diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemProperties.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemProperties.java new file mode 100644 index 000000000000..1313cd72bac2 --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemProperties.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.function.BiConsumer; +import java.util.function.Function; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.logging.LogFile; +import org.springframework.boot.logging.LoggingSystemProperties; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertyResolver; +import org.springframework.util.unit.DataSize; + +/** + * {@link LoggingSystemProperties} for Log4j2. + * + * @author HoJoo Moon + * @since 4.0.0 + * @see Log4j2RollingPolicySystemProperty + */ +public class Log4j2LoggingSystemProperties extends LoggingSystemProperties { + + public Log4j2LoggingSystemProperties(Environment environment) { + super(environment); + } + + /** + * Create a new {@link Log4j2LoggingSystemProperties} instance. + * @param environment the source environment + * @param setter setter used to apply the property + */ + public Log4j2LoggingSystemProperties(Environment environment, + @Nullable BiConsumer setter) { + super(environment, setter); + } + + /** + * Create a new {@link Log4j2LoggingSystemProperties} 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 + */ + public Log4j2LoggingSystemProperties(Environment environment, + Function<@Nullable String, @Nullable String> defaultValueResolver, + @Nullable BiConsumer setter) { + super(environment, defaultValueResolver, setter); + } + + @Override + protected void apply(@Nullable LogFile logFile, PropertyResolver resolver) { + super.apply(logFile, resolver); + applyRollingPolicyProperties(resolver); + } + + private void applyRollingPolicyProperties(PropertyResolver resolver) { + applyRollingPolicy(Log4j2RollingPolicySystemProperty.STRATEGY, resolver); + applyRollingPolicy(Log4j2RollingPolicySystemProperty.TIME_INTERVAL, resolver, Integer.class); + applyRollingPolicy(Log4j2RollingPolicySystemProperty.TIME_MODULATE, resolver, Boolean.class); + applyRollingPolicy(Log4j2RollingPolicySystemProperty.CRON_SCHEDULE, resolver); + applyRollingPolicy(Log4j2RollingPolicySystemProperty.FILE_NAME_PATTERN, resolver); + applyRollingPolicy(Log4j2RollingPolicySystemProperty.MAX_FILE_SIZE, resolver, DataSize.class); + applyRollingPolicy(Log4j2RollingPolicySystemProperty.MAX_HISTORY, resolver); + } + + private void applyRollingPolicy(Log4j2RollingPolicySystemProperty property, PropertyResolver resolver) { + applyRollingPolicy(property, resolver, String.class); + } + + private void applyRollingPolicy(Log4j2RollingPolicySystemProperty property, PropertyResolver resolver, + Class type) { + T value = getProperty(resolver, property.getApplicationPropertyName(), type); + if (value == null && property.getDeprecatedApplicationPropertyName() != null) { + value = getProperty(resolver, property.getDeprecatedApplicationPropertyName(), type); + } + if (value != null) { + String stringValue = String.valueOf((value instanceof DataSize dataSize) ? dataSize.toBytes() : value); + setSystemProperty(property.getEnvironmentVariableName(), stringValue); + } + } + + @SuppressWarnings("unchecked") + private @Nullable T getProperty(PropertyResolver resolver, String key, Class type) { + try { + return resolver.getProperty(key, type); + } + catch (ConversionFailedException | ConverterNotFoundException ex) { + if (type != DataSize.class) { + throw ex; + } + // Fallback for Log4j2 compatibility - try parsing as string if DataSize + // conversion fails + String value = resolver.getProperty(key); + if (value != null) { + try { + return (T) DataSize.parse(value); + } + catch (Exception parseEx) { + ex.addSuppressed(parseEx); + throw ex; + } + } + return null; + } + } + +} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2RollingPolicySystemProperty.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2RollingPolicySystemProperty.java new file mode 100644 index 000000000000..e777b835abf8 --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2RollingPolicySystemProperty.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.jspecify.annotations.Nullable; + +/** + * Log4j2 rolling policy system properties that can later be used by log configuration + * files. + * + * @author HoJoo Moon + * @since 4.0.0 + * @see Log4j2LoggingSystemProperties + */ +public enum Log4j2RollingPolicySystemProperty { + + /** + * 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 file log max size. + */ + MAX_FILE_SIZE("max-file-size", "logging.file.max-size"), + + /** + * Logging system property for the file log max history. + */ + MAX_HISTORY("max-history", "logging.file.max-history"), + + /** + * Logging system property for the rolling policy strategy. + */ + STRATEGY("strategy", null), + + /** + * Logging system property for the rolling policy time interval. + */ + TIME_INTERVAL("time-based.interval", null), + + /** + * Logging system property for the rolling policy time modulate flag. + */ + TIME_MODULATE("time-based.modulate", null), + + /** + * Logging system property for the cron based schedule. + */ + CRON_SCHEDULE("cron.schedule", null); + + private final String environmentVariableName; + + private final String applicationPropertyName; + + private final @Nullable String deprecatedApplicationPropertyName; + + Log4j2RollingPolicySystemProperty(String applicationPropertyName, + @Nullable String deprecatedApplicationPropertyName) { + this.environmentVariableName = "LOG4J2_ROLLINGPOLICY_" + name(); + this.applicationPropertyName = "logging.log4j2.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; + } + + @Nullable String getDeprecatedApplicationPropertyName() { + return this.deprecatedApplicationPropertyName; + } + +} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java new file mode 100644 index 000000000000..35383b1ae340 --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java @@ -0,0 +1,182 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.appender.rolling.CompositeTriggeringPolicy; +import org.apache.logging.log4j.core.appender.rolling.CronTriggeringPolicy; +import org.apache.logging.log4j.core.appender.rolling.RollingFileManager; +import org.apache.logging.log4j.core.appender.rolling.SizeBasedTriggeringPolicy; +import org.apache.logging.log4j.core.appender.rolling.TimeBasedTriggeringPolicy; +import org.apache.logging.log4j.core.appender.rolling.TriggeringPolicy; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; +import org.apache.logging.log4j.core.util.Builder; +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; + +/** + * Factory for creating a standard Log4j2 {@link TriggeringPolicy} based on configuration + * attributes. The supported strategies are {@code size}, {@code time}, + * {@code size-and-time}, and {@code cron}. + * + * @author HoJoo Moon + * @since 4.0.0 + */ +@Plugin(name = "SpringBootTriggeringPolicy", category = Node.CATEGORY, elementType = "TriggeringPolicy", + deferChildren = true, printObject = true) +public abstract class SpringBootTriggeringPolicy implements TriggeringPolicy { + + private SpringBootTriggeringPolicy() { + } + + @Override + public void initialize(RollingFileManager manager) { + throw new UnsupportedOperationException("This class should not be instantiated"); + } + + @Override + public boolean isTriggeringEvent(LogEvent logEvent) { + throw new UnsupportedOperationException("This class should not be instantiated"); + } + + @PluginBuilderFactory + public static SpringBootTriggeringPolicyBuilder newBuilder() { + return new SpringBootTriggeringPolicyBuilder(); + } + + /** + * Builder for creating a {@link TriggeringPolicy}. + */ + public static class SpringBootTriggeringPolicyBuilder implements Builder { + + private static final String DEFAULT_STRATEGY = "size"; + + private static final String DEFAULT_MAX_FILE_SIZE = "10MB"; + + private static final int DEFAULT_TIME_INTERVAL = 1; + + private static final String DEFAULT_CRON_EXPRESSION = "0 0 0 * * ?"; + + @PluginAttribute("strategy") + private @Nullable String strategy; + + @PluginAttribute("maxFileSize") + private @Nullable String maxFileSize; + + @PluginAttribute("timeInterval") + private @Nullable Integer timeInterval; + + @PluginAttribute("timeModulate") + private @Nullable Boolean timeModulate; + + @PluginAttribute("cronExpression") + private @Nullable String cronExpression; + + @PluginConfiguration + private @Nullable Configuration configuration; + + @Override + public TriggeringPolicy build() { + // Read strategy from system properties first, then from attributes + String resolvedStrategy = System.getProperty("LOG4J2_ROLLINGPOLICY_STRATEGY"); + if (resolvedStrategy == null) { + resolvedStrategy = (this.strategy != null) ? this.strategy : DEFAULT_STRATEGY; + } + return switch (resolvedStrategy) { + case "time" -> createTimePolicy(); + case "size-and-time" -> CompositeTriggeringPolicy.createPolicy(createSizePolicy(), createTimePolicy()); + case "cron" -> createCronPolicy(); + case "size" -> createSizePolicy(); + default -> throw new IllegalArgumentException( + "Unsupported rolling policy strategy '%s'".formatted(resolvedStrategy)); + }; + } + + private TriggeringPolicy createSizePolicy() { + // Read from system properties first, then from attributes + String size = System.getProperty("LOG4J2_ROLLINGPOLICY_MAX_FILE_SIZE"); + if (size == null) { + size = (this.maxFileSize != null) ? this.maxFileSize : DEFAULT_MAX_FILE_SIZE; + } + return SizeBasedTriggeringPolicy.createPolicy(size); + } + + private TriggeringPolicy createTimePolicy() { + // Read from system properties first, then from attributes + String intervalStr = System.getProperty("LOG4J2_ROLLINGPOLICY_TIME_INTERVAL"); + int interval = (intervalStr != null) ? Integer.parseInt(intervalStr) + : (this.timeInterval != null) ? this.timeInterval : DEFAULT_TIME_INTERVAL; + + String modulateStr = System.getProperty("LOG4J2_ROLLINGPOLICY_TIME_MODULATE"); + boolean modulate = (modulateStr != null) ? Boolean.parseBoolean(modulateStr) + : (this.timeModulate != null) ? this.timeModulate : false; + + return TimeBasedTriggeringPolicy.newBuilder().withInterval(interval).withModulate(modulate).build(); + } + + private TriggeringPolicy createCronPolicy() { + Assert.notNull(this.configuration, "configuration must not be null"); + Configuration configuration = this.configuration; + + // Read from system properties first, then from attributes + String schedule = System.getProperty("LOG4J2_ROLLINGPOLICY_CRON_SCHEDULE"); + if (schedule == null) { + schedule = (this.cronExpression != null) ? this.cronExpression : DEFAULT_CRON_EXPRESSION; + } + + return CronTriggeringPolicy.createPolicy(configuration, null, schedule); + } + + SpringBootTriggeringPolicyBuilder setStrategy(@Nullable String strategy) { + this.strategy = strategy; + return this; + } + + SpringBootTriggeringPolicyBuilder setMaxFileSize(@Nullable String maxFileSize) { + this.maxFileSize = maxFileSize; + return this; + } + + SpringBootTriggeringPolicyBuilder setTimeInterval(@Nullable Integer timeInterval) { + this.timeInterval = timeInterval; + return this; + } + + SpringBootTriggeringPolicyBuilder setTimeModulate(@Nullable Boolean timeModulate) { + this.timeModulate = timeModulate; + return this; + } + + SpringBootTriggeringPolicyBuilder setCronExpression(@Nullable String cronExpression) { + this.cronExpression = cronExpression; + return this; + } + + SpringBootTriggeringPolicyBuilder setConfiguration(Configuration configuration) { + this.configuration = configuration; + return this; + } + + } + +} diff --git a/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 651ab994f32d..fd6f141ef871 100644 --- a/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -131,6 +131,54 @@ "type": "java.util.List", "description": "Overriding configuration files used to create a composite configuration. Can be prefixed with 'optional:' to only load the override if it exists." }, + { + "name": "logging.log4j2.rollingpolicy.cron.schedule", + "type": "java.lang.String", + "description": "Cron expression used when the strategy is 'cron'.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener" + }, + { + "name": "logging.log4j2.rollingpolicy.file-name-pattern", + "type": "java.lang.String", + "description": "Pattern for rolled-over log file names.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": "${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz" + }, + { + "name": "logging.log4j2.rollingpolicy.max-file-size", + "type": "org.springframework.util.unit.DataSize", + "description": "Maximum log file size.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": "10MB" + }, + { + "name": "logging.log4j2.rollingpolicy.max-history", + "type": "java.lang.Integer", + "description": "Maximum number of archive log files to keep.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": 7 + }, + { + "name": "logging.log4j2.rollingpolicy.strategy", + "type": "java.lang.String", + "description": "Rolling policy strategy. Supported values are 'size', 'time', 'size-and-time', and 'cron'.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": "size" + }, + { + "name": "logging.log4j2.rollingpolicy.time-based.interval", + "type": "java.lang.Integer", + "description": "Time based triggering interval when the strategy is 'time' or 'size-and-time'.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": 1 + }, + { + "name": "logging.log4j2.rollingpolicy.time-based.modulate", + "type": "java.lang.Boolean", + "description": "Whether to align the next rollover time to occur at the top of the interval when the strategy is time based.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": false + }, { "name": "logging.logback.rollingpolicy.clean-history-on-start", "type": "java.lang.Boolean", diff --git a/core/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml b/core/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml index c81eca81ddd0..73e2b2240484 100644 --- a/core/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml +++ b/core/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml @@ -21,7 +21,8 @@ - + @@ -34,13 +34,14 @@ - - - + + + diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/AbstractLoggingSystemTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/AbstractLoggingSystemTests.java index 5ec109c0e6b3..a0575ebe6b82 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/AbstractLoggingSystemTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/AbstractLoggingSystemTests.java @@ -28,7 +28,6 @@ import org.junit.jupiter.api.io.TempDir; import org.slf4j.MDC; -import org.springframework.boot.logging.log4j2.Log4j2RollingPolicySystemProperty; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.contentOf; @@ -65,9 +64,6 @@ void clear() { for (LoggingSystemProperty property : LoggingSystemProperty.values()) { System.getProperties().remove(property.getEnvironmentVariableName()); } - for (Log4j2RollingPolicySystemProperty property : Log4j2RollingPolicySystemProperty.values()) { - System.getProperties().remove(property.getEnvironmentVariableName()); - } MDC.clear(); } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java index c5fd03464e82..cc80db3db75c 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java @@ -76,6 +76,7 @@ import org.springframework.mock.env.MockEnvironment; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; +import org.springframework.util.unit.DataSize; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -94,6 +95,7 @@ * @author Ben Hale * @author Madhura Bhave * @author Piotr P. Karwasz + * @author Stephane Nicoll */ @ExtendWith(OutputCaptureExtension.class) @ClassPathExclusions("logback-*.jar") @@ -110,6 +112,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @BeforeEach void setup(TestInfo testInfo) { + clearRollingPolicySystemProperties(); PluginRegistry.getInstance().clear(); this.loggingSystem = new TestLog4J2LoggingSystem(testInfo.getDisplayName()); this.environment = new MockEnvironment(); @@ -125,6 +128,12 @@ void cleanUp() { clearRollingPolicySystemProperties(); } + private void clearRollingPolicySystemProperties() { + for (RollingPolicySystemProperty property : RollingPolicySystemProperty.values()) { + System.clearProperty(property.getEnvironmentVariableName()); + } + } + @Test void noFile(CapturedOutput output) { this.loggingSystem.beforeInitialize(); @@ -137,12 +146,6 @@ void noFile(CapturedOutput output) { assertThat(configuration.getConfigurationSource().getFile()).isNotNull(); } - private void clearRollingPolicySystemProperties() { - for (Log4j2RollingPolicySystemProperty property : Log4j2RollingPolicySystemProperty.values()) { - System.clearProperty(property.getEnvironmentVariableName()); - } - } - @Test void withFile(CapturedOutput output) { this.loggingSystem.beforeInitialize(); @@ -785,6 +788,7 @@ void rollingPolicySystemPropertiesAreApplied() { "${LOG_FILE}.%d{yyyy-MM-dd}.%i.log"); File file = new File(tmpDir(), "log4j2-test.log"); LogFile logFile = getLogFile(file.getPath(), null); + this.loggingSystem.getSystemProperties(this.environment).apply(logFile); this.loggingSystem.beforeInitialize(); this.loggingSystem.initialize(this.initializationContext, null, logFile); String maxFileSize = System.getProperty("LOG4J2_ROLLINGPOLICY_MAX_FILE_SIZE"); @@ -796,52 +800,83 @@ void rollingPolicySystemPropertiesAreApplied() { } @Test - void rollingPolicyDeprecatedPropertiesAreApplied() { - this.environment.setProperty("logging.file.max-size", "20MB"); - this.environment.setProperty("logging.file.max-history", "15"); - File file = new File(tmpDir(), "log4j2-test.log"); - LogFile logFile = getLogFile(file.getPath(), null); - this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(this.initializationContext, null, logFile); - String maxFileSize = System.getProperty("LOG4J2_ROLLINGPOLICY_MAX_FILE_SIZE"); - String maxHistory = System.getProperty("LOG4J2_ROLLINGPOLICY_MAX_HISTORY"); - assertThat(maxFileSize).isEqualTo(String.valueOf(20 * 1024 * 1024)); - assertThat(maxHistory).isEqualTo("15"); + void rollingPolicyAppliesDefaults() { + TriggeringPolicy policy = getTriggeringPolicy(); + SizeBasedTriggeringPolicy sizePolicy = findPolicy(policy, SizeBasedTriggeringPolicy.class); + assertThat(sizePolicy).isNotNull(); + assertThat(sizePolicy.getMaxFileSize()).isEqualTo(DataSize.ofMegabytes(10).toBytes()); + } + + @Test + void rollingPolicySizeStrategyIsApplied() { + this.environment.setProperty("logging.log4j2.rollingpolicy.strategy", "size"); + this.environment.setProperty("logging.log4j2.rollingpolicy.max-file-size", "50MB"); + TriggeringPolicy policy = getTriggeringPolicy(); + SizeBasedTriggeringPolicy sizePolicy = findPolicy(policy, SizeBasedTriggeringPolicy.class); + assertThat(sizePolicy).isNotNull(); + assertThat(sizePolicy.getMaxFileSize()).isEqualTo(DataSize.ofMegabytes(50).toBytes()); + } + + @Test + void rollingPolicyTimeStrategyAppliesDefaults() { + this.environment.setProperty("logging.log4j2.rollingpolicy.strategy", "time"); + TriggeringPolicy policy = getTriggeringPolicy(); + TimeBasedTriggeringPolicy timePolicy = findPolicy(policy, TimeBasedTriggeringPolicy.class); + assertThat(timePolicy).isNotNull(); + assertThat(timePolicy.getInterval()).isEqualTo(1); + assertThat(timePolicy).hasFieldOrPropertyWithValue("modulate", false); } @Test void rollingPolicyTimeStrategyIsApplied() { this.environment.setProperty("logging.log4j2.rollingpolicy.strategy", "time"); - this.environment.setProperty("logging.log4j2.rollingpolicy.time-based.interval", "2"); + this.environment.setProperty("logging.log4j2.rollingpolicy.time-interval", "2"); + this.environment.setProperty("logging.log4j2.rollingpolicy.time-modulate", "true"); TriggeringPolicy policy = getTriggeringPolicy(); TimeBasedTriggeringPolicy timePolicy = findPolicy(policy, TimeBasedTriggeringPolicy.class); assertThat(timePolicy).isNotNull(); assertThat(timePolicy.getInterval()).isEqualTo(2); + assertThat(timePolicy).hasFieldOrPropertyWithValue("modulate", true); } @Test void rollingPolicySizeAndTimeStrategyIsApplied() { this.environment.setProperty("logging.log4j2.rollingpolicy.strategy", "size-and-time"); this.environment.setProperty("logging.log4j2.rollingpolicy.max-file-size", "5MB"); - this.environment.setProperty("logging.log4j2.rollingpolicy.time-based.interval", "3"); + this.environment.setProperty("logging.log4j2.rollingpolicy.time-interval", "3"); + this.environment.setProperty("logging.log4j2.rollingpolicy.time-modulate", "true"); TriggeringPolicy policy = getTriggeringPolicy(); - assertThat(findPolicy(policy, TimeBasedTriggeringPolicy.class)).isNotNull(); - assertThat(findPolicy(policy, SizeBasedTriggeringPolicy.class)).isNotNull(); + assertThat(findPolicy(policy, TimeBasedTriggeringPolicy.class)).satisfies((timePolicy) -> { + assertThat(timePolicy.getInterval()).isEqualTo(3); + assertThat(timePolicy).hasFieldOrPropertyWithValue("modulate", true); + }); + assertThat(findPolicy(policy, SizeBasedTriggeringPolicy.class)).satisfies( + (sizePolicy) -> assertThat(sizePolicy.getMaxFileSize()).isEqualTo(DataSize.ofMegabytes(5).toBytes())); } @Test - void rollingPolicyCronStrategyIsApplied() { + void rollingPolicyCronStrategyAppliesDefault() { this.environment.setProperty("logging.log4j2.rollingpolicy.strategy", "cron"); - this.environment.setProperty("logging.log4j2.rollingpolicy.cron.schedule", "0 0 0 * * ?"); TriggeringPolicy policy = getTriggeringPolicy(); CronTriggeringPolicy cronPolicy = findPolicy(policy, CronTriggeringPolicy.class); assertThat(cronPolicy).isNotNull(); assertThat(cronPolicy.getCronExpression().getCronExpression()).isEqualTo("0 0 0 * * ?"); } + @Test + void rollingPolicyCronStrategyIsApplied() { + this.environment.setProperty("logging.log4j2.rollingpolicy.strategy", "cron"); + this.environment.setProperty("logging.log4j2.rollingpolicy.cron", "0 0 1 * * ?"); + TriggeringPolicy policy = getTriggeringPolicy(); + CronTriggeringPolicy cronPolicy = findPolicy(policy, CronTriggeringPolicy.class); + assertThat(cronPolicy).isNotNull(); + assertThat(cronPolicy.getCronExpression().getCronExpression()).isEqualTo("0 0 1 * * ?"); + } + private TriggeringPolicy getTriggeringPolicy() { File file = new File(tmpDir(), "target-file.log"); LogFile logFile = getLogFile(file.getPath(), null); + this.loggingSystem.getSystemProperties(this.environment).apply(logFile); this.loggingSystem.beforeInitialize(); this.loggingSystem.initialize(this.initializationContext, "classpath:org/springframework/boot/logging/log4j2/log4j2-file.xml", logFile); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemPropertiesTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemPropertiesTests.java index e703afa5be5e..a15703d5b5fa 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemPropertiesTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemPropertiesTests.java @@ -65,9 +65,9 @@ void appliesLog4j2RollingPolicyProperties() { this.environment.setProperty("logging.log4j2.rollingpolicy.max-history", "30"); this.environment.setProperty("logging.log4j2.rollingpolicy.file-name-pattern", "test.%d{yyyy-MM-dd}.%i.log"); this.environment.setProperty("logging.log4j2.rollingpolicy.strategy", "time"); - this.environment.setProperty("logging.log4j2.rollingpolicy.time-based.interval", "2"); - this.environment.setProperty("logging.log4j2.rollingpolicy.time-based.modulate", "true"); - this.environment.setProperty("logging.log4j2.rollingpolicy.cron.schedule", "0 0 0 * * ?"); + this.environment.setProperty("logging.log4j2.rollingpolicy.time-interval", "2"); + this.environment.setProperty("logging.log4j2.rollingpolicy.time-modulate", "true"); + this.environment.setProperty("logging.log4j2.rollingpolicy.cron", "0 0 1 * * ?"); new Log4j2LoggingSystemProperties(this.environment).apply(null); @@ -78,7 +78,7 @@ void appliesLog4j2RollingPolicyProperties() { .containsEntry("LOG4J2_ROLLINGPOLICY_STRATEGY", "time") .containsEntry("LOG4J2_ROLLINGPOLICY_TIME_INTERVAL", "2") .containsEntry("LOG4J2_ROLLINGPOLICY_TIME_MODULATE", "true") - .containsEntry("LOG4J2_ROLLINGPOLICY_CRON_SCHEDULE", "0 0 0 * * ?"); + .containsEntry("LOG4J2_ROLLINGPOLICY_CRON", "0 0 1 * * ?"); } @Test @@ -88,28 +88,6 @@ void appliesLog4j2RollingPolicyPropertiesWithDefaults() { .noneMatch((key) -> ((String) key).startsWith("LOG4J2_ROLLINGPOLICY")); } - @Test - void appliesDeprecatedProperties() { - this.environment.setProperty("logging.file.max-size", "20MB"); - this.environment.setProperty("logging.file.max-history", "15"); - new Log4j2LoggingSystemProperties(this.environment).apply(null); - assertThat(System.getProperties()) - .containsEntry("LOG4J2_ROLLINGPOLICY_MAX_FILE_SIZE", String.valueOf(DataSize.ofMegabytes(20).toBytes())) - .containsEntry("LOG4J2_ROLLINGPOLICY_MAX_HISTORY", "15"); - } - - @Test - void newPropertiesOverrideDeprecatedProperties() { - this.environment.setProperty("logging.log4j2.rollingpolicy.max-file-size", "100MB"); - this.environment.setProperty("logging.file.max-size", "20MB"); - this.environment.setProperty("logging.log4j2.rollingpolicy.max-history", "50"); - this.environment.setProperty("logging.file.max-history", "15"); - new Log4j2LoggingSystemProperties(this.environment).apply(null); - assertThat(System.getProperties()) - .containsEntry("LOG4J2_ROLLINGPOLICY_MAX_FILE_SIZE", String.valueOf(DataSize.ofMegabytes(100).toBytes())) - .containsEntry("LOG4J2_ROLLINGPOLICY_MAX_HISTORY", "50"); - } - @Test void appliesWithLogFile() { this.environment.setProperty("logging.log4j2.rollingpolicy.max-file-size", "25MB"); diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc index 04a38aea5a6d..04c37a199390 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc @@ -178,10 +178,10 @@ As a result, specific configuration keys (such as `logback.configurationFile` fo [[features.logging.file-rotation]] == File Rotation -If you are using the Logback, it is possible to fine-tune log rotation settings using your `application.properties` or `application.yaml` file. -For all other logging system, you will need to configure rotation settings directly yourself (for example, if you use Log4j2 then you could add a `log4j2.xml` or `log4j2-spring.xml` file). +If you are using Logback of Log4J2, it is possible to fine-tune log rotation settings using your `application.properties` or `application.yaml` file. +For all other logging system, you will need to configure rotation settings directly yourself. -The following rotation policy properties are supported: +The following rotation policy properties are supported for Logback: |=== | Name | Description @@ -202,6 +202,33 @@ The following rotation policy properties are supported: | The maximum number of archive log files to keep (defaults to 7). |=== +If you are using Log4j2, the following rotation policy properties are available: + +|=== +| Name | Description + +| configprop:logging.log4j2.rollingpolicy.file-name-pattern[] +| The filename pattern used to create log archives. + +| configprop:logging.log4j2.rollingpolicy.max-file-size[] +| The maximum size of log file before it is archived (defaults to 10MB). + +| configprop:logging.log4j2.rollingpolicy.max-history[] +| The maximum number of archive log files to keep (defaults to 7). + +| configprop:logging.log4j2.rollingpolicy.strategy[] +| Rolling policy strategy (defaults to 'size'). + +| configprop:logging.log4j2.rollingpolicy.cron[] +| Cron expression used when the strategy is 'cron' (default to every day at midnight). + +| configprop:logging.log4j2.rollingpolicy.time-interval[] +| Time based triggering interval when the strategy is 'time' or 'size-and-time' (default to 1). + +| configprop:logging.log4j2.rollingpolicy.time-modulate[] +| Whether to align the next rollover time to occur at the top of the interval when the strategy is time based (defaults to `false`). + +|=== [[features.logging.log-levels]] From ad0538f6d967adb06b3c600b32203b12e0788f43 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Mar 2026 17:42:09 +0000 Subject: [PATCH 257/930] Log Docker Compose logs when up or start fails Closes gh-49423 --- .../compose/core/DefaultDockerCompose.java | 5 +++ .../boot/docker/compose/core/DockerCli.java | 4 +- .../docker/compose/core/DockerCliCommand.java | 20 +++++++-- .../docker/compose/core/DockerCompose.java | 7 ++++ .../DockerComposeLifecycleManager.java | 31 +++++++++++++- .../compose/core/DockerCliCommandTests.java | 32 ++++++++++---- .../DockerComposeLifecycleManagerTests.java | 42 +++++++++++++++++++ 7 files changed, 126 insertions(+), 15 deletions(-) diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java index 19f922763b64..e2ca2e753d79 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java @@ -88,6 +88,11 @@ public void stop(Duration timeout, List arguments) { this.cli.run(new DockerCliCommand.ComposeStop(timeout, arguments)); } + @Override + public String logs() { + return this.cli.run(new DockerCliCommand.ComposeLogs()); + } + @Override public boolean hasDefinedServices() { return !this.cli.run(new DockerCliCommand.ComposeConfig()).services().isEmpty(); diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java index 194f2da41adf..1a9fb9d99895 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java @@ -79,8 +79,8 @@ R run(DockerCliCommand dockerCommand) { List command = createCommand(dockerCommand.getType()); command.addAll(dockerCommand.getCommand(this.composeVersion)); Consumer outputConsumer = createOutputConsumer(dockerCommand.getLogLevel()); - String json = this.processRunner.run(outputConsumer, command.toArray(new String[0])); - return dockerCommand.deserialize(json); + String response = this.processRunner.run(outputConsumer, command.toArray(new String[0])); + return dockerCommand.convert(response); } private @Nullable Consumer createOutputConsumer(@Nullable LogLevel logLevel) { diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliCommand.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliCommand.java index 86fedba694a1..4662295c2c43 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliCommand.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliCommand.java @@ -77,12 +77,15 @@ List getCommand(ComposeVersion composeVersion) { } @SuppressWarnings("unchecked") - R deserialize(String json) { + R convert(String response) { if (this.responseType == None.class) { return (R) None.INSTANCE; } - return (R) ((!this.listResponse) ? DockerJson.deserialize(json, this.responseType) - : DockerJson.deserializeToList(json, this.responseType)); + if (this.responseType == String.class) { + return (R) response; + } + return (R) ((!this.listResponse) ? DockerJson.deserialize(response, this.responseType) + : DockerJson.deserializeToList(response, this.responseType)); } @Override @@ -251,6 +254,17 @@ private static String[] getCommand(Duration timeout, List arguments) { } + /** + * The {@code docker compose logs} command. + */ + static final class ComposeLogs extends DockerCliCommand { + + ComposeLogs() { + super(Type.DOCKER_COMPOSE, String.class, false, "logs"); + } + + } + /** * Command Types. */ diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java index 4c82f793ef37..619b6c7de7f3 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java @@ -105,6 +105,13 @@ public interface DockerCompose { */ void stop(Duration timeout, List arguments); + /** + * Run {@code docker compose logs} to view Docker Compose's recent logs. + * @return the recent logs + * @since 4.1.0 + */ + String logs(); + /** * Return if services have been defined in the {@link DockerComposeFile} for the * active profiles. diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java index d21f0ed37a41..f0fabb042731 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java @@ -19,6 +19,7 @@ import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Set; import org.apache.commons.logging.Log; @@ -128,7 +129,13 @@ void start() { logger.info(skip.getLogMessage()); } else { - start.getCommand().applyTo(dockerCompose, start.getLogLevel(), start.getArguments()); + try { + start.getCommand().applyTo(dockerCompose, start.getLogLevel(), start.getArguments()); + } + catch (RuntimeException ex) { + logDockerComposeLogs(dockerCompose, start); + throw ex; + } runningServices = dockerCompose.getRunningServices(); if (wait == Wait.ONLY_IF_STARTED) { wait = Wait.ALWAYS; @@ -147,6 +154,28 @@ void start() { publishEvent(new DockerComposeServicesReadyEvent(this.applicationContext, relevantServices)); } + private void logDockerComposeLogs(DockerCompose dockerCompose, Start start) { + String command = start.getCommand().name().toLowerCase(Locale.ROOT); + String logs = retrieveLogsIfPossible(dockerCompose); + if (logs == null) { + start.getLogLevel() + .log(logger, "docker compose %s failed and its logs were unavailable".formatted(command)); + } + else { + start.getLogLevel() + .log(logger, "docker compose %s failed with the following logs:%n%n%s".formatted(command, logs)); + } + } + + private @Nullable String retrieveLogsIfPossible(DockerCompose dockerCompose) { + try { + return dockerCompose.logs(); + } + catch (Exception ex) { + return null; + } + } + protected DockerComposeFile getComposeFile() { DockerComposeFile composeFile = (CollectionUtils.isEmpty(this.properties.getFile())) ? DockerComposeFile.find(this.workingDirectory) : DockerComposeFile.of(this.properties.getFile()); diff --git a/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliCommandTests.java b/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliCommandTests.java index 7aaeb7968122..f1d1126fa57c 100644 --- a/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliCommandTests.java +++ b/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliCommandTests.java @@ -43,7 +43,7 @@ void context() { DockerCliCommand command = new DockerCliCommand.Context(); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("context", "ls", "--format={{ json . }}"); - assertThat(command.deserialize("[]")).isInstanceOf(List.class); + assertThat(command.convert("[]")).isInstanceOf(List.class); } @Test @@ -52,7 +52,7 @@ void inspect() { assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("inspect", "--format={{ json . }}", "123", "345"); - assertThat(command.deserialize("[]")).isInstanceOf(List.class); + assertThat(command.convert("[]")).isInstanceOf(List.class); } @Test @@ -60,7 +60,7 @@ void composeConfig() { DockerCliCommand command = new DockerCliCommand.ComposeConfig(); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("config", "--format=json"); - assertThat(command.deserialize("{}")).isInstanceOf(DockerCliComposeConfigResponse.class); + assertThat(command.convert("{}")).isInstanceOf(DockerCliComposeConfigResponse.class); } @Test @@ -68,7 +68,7 @@ void composePs() { DockerCliCommand command = new DockerCliCommand.ComposePs(); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("ps", "--orphans=false", "--format=json"); - assertThat(command.deserialize("[]")).isInstanceOf(List.class); + assertThat(command.convert("[]")).isInstanceOf(List.class); } @Test @@ -76,7 +76,7 @@ void composePsWhenLessThanV224() { DockerCliCommand command = new DockerCliCommand.ComposePs(); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getCommand(ComposeVersion.of("2.23"))).containsExactly("ps", "--format=json"); - assertThat(command.deserialize("[]")).isInstanceOf(List.class); + assertThat(command.convert("[]")).isInstanceOf(List.class); } @Test @@ -86,7 +86,7 @@ void composeUp() { assertThat(command.getLogLevel()).isEqualTo(LogLevel.INFO); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("up", "--no-color", "--detach", "--wait", "--renew-anon-volumes"); - assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE); + assertThat(command.convert("[]")).isSameAs(None.INSTANCE); } @Test @@ -95,7 +95,7 @@ void composeDown() { List.of("--remove-orphans")); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("down", "--timeout", "1", "--remove-orphans"); - assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE); + assertThat(command.convert("[]")).isSameAs(None.INSTANCE); } @Test @@ -104,7 +104,7 @@ void composeStart() { assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getLogLevel()).isEqualTo(LogLevel.INFO); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("start", "--dry-run"); - assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE); + assertThat(command.convert("[]")).isSameAs(None.INSTANCE); } @Test @@ -112,7 +112,7 @@ void composeStop() { DockerCliCommand command = new DockerCliCommand.ComposeStop(Duration.ofSeconds(1), List.of("--dry-run")); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("stop", "--timeout", "1", "--dry-run"); - assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE); + assertThat(command.convert("[]")).isSameAs(None.INSTANCE); } @Test @@ -130,4 +130,18 @@ void composeVersionTests() { assertThat(versionWithPrefix.minor()).isEqualTo(31); } + @Test + void composeLogs() { + DockerCliCommand command = new DockerCliCommand.ComposeLogs(); + assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); + assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("logs"); + assertThat(command.convert(""" + multi + line + logs""")).isEqualTo(""" + multi + line + logs"""); + } + } diff --git a/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java b/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java index b6a978e1de78..027780383b97 100644 --- a/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java +++ b/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java @@ -50,10 +50,12 @@ import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 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.BDDMockito.willThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -434,6 +436,46 @@ void shouldStartIfSkipModeIsNeverAndServicesAreAlreadyRunning() { then(this.dockerCompose).should().up(any(), any()); } + @Test + void whenStartFailsLogsAreRetrieved(CapturedOutput output) { + given(this.dockerCompose.hasDefinedServices()).willReturn(true); + given(this.dockerCompose.logs()).willReturn(""" + multi + line + logs"""); + willThrow(RuntimeException.class).given(this.dockerCompose).up(any(), any()); + assertThatExceptionOfType(RuntimeException.class).isThrownBy(this.lifecycleManager::start); + assertThat(output).contains("docker compose up failed with the following logs:").contains(""" + multi + line + logs"""); + } + + @Test + void whenStartUsesStartAndItFailsLogsAreRetrieved(CapturedOutput output) { + this.properties.getStart().setCommand(StartCommand.START); + given(this.dockerCompose.hasDefinedServices()).willReturn(true); + given(this.dockerCompose.logs()).willReturn(""" + multi + line + logs"""); + willThrow(RuntimeException.class).given(this.dockerCompose).start(any(), any()); + assertThatExceptionOfType(RuntimeException.class).isThrownBy(this.lifecycleManager::start); + assertThat(output).contains("docker compose start failed with the following logs:").contains(""" + multi + line + logs"""); + } + + @Test + void whenLogsAreUnavailableFailureIsHandled(CapturedOutput output) { + given(this.dockerCompose.hasDefinedServices()).willReturn(true); + given(this.dockerCompose.logs()).willThrow(RuntimeException.class); + willThrow(RuntimeException.class).given(this.dockerCompose).up(any(), any()); + assertThatExceptionOfType(RuntimeException.class).isThrownBy(this.lifecycleManager::start); + assertThat(output).contains("docker compose up failed and its logs were unavailable"); + } + private void setUpRunningServices() { setUpRunningServices(true); } From 1d1d3efef5c8ab2bbc0e4775210ce0d8da4fa584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 07:58:17 +0100 Subject: [PATCH 258/930] Start building against Micrometer 1.17.0-M3 snapshots See gh-49435 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e73aa2d86cc7..ad16fd5b9c5d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1578,7 +1578,7 @@ bom { releaseNotes("https://github.com/apache/maven-war-plugin/releases/tag/maven-war-plugin-{version}") } } - library("Micrometer", "1.17.0-M2") { + library("Micrometer", "1.17.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ From 205fa87d09b0a2ea8c14cc77c8831a7b0f6a5fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 07:58:21 +0100 Subject: [PATCH 259/930] Start building against Micrometer Tracing 1.7.0-M4 snapshots See gh-49436 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ad16fd5b9c5d..8a91a3793c0f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1601,7 +1601,7 @@ bom { releaseNotes("https://github.com/micrometer-metrics/micrometer/releases/tag/v{version}") } } - library("Micrometer Tracing", "1.7.0-M3") { + library("Micrometer Tracing", "1.7.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { bom("micrometer-tracing-bom") From 04646fc5adc58b53c25fe55a2edd192611a55e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 07:58:25 +0100 Subject: [PATCH 260/930] Start building against Reactor Bom 2025.0.4 snapshots See gh-49437 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8a91a3793c0f..88048a8b556a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2000,7 +2000,7 @@ bom { ] } } - library("Reactor Bom", "2025.0.3") { + library("Reactor Bom", "2025.0.4-SNAPSHOT") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From c56a383495cfbdd3fc4f1517041f964199d05d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 07:58:29 +0100 Subject: [PATCH 261/930] Start building against Spring AMQP 4.1.0-M3 snapshots See gh-49438 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 88048a8b556a..565663c3d794 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2419,7 +2419,7 @@ bom { ] } } - library("Spring AMQP", "4.1.0-M2") { + library("Spring AMQP", "4.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.amqp") { bom("spring-amqp-bom") From 3b43d07f8a528a58d52e513e650ebf15b444df66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 07:58:33 +0100 Subject: [PATCH 262/930] Start building against Spring Batch 6.0.3 snapshots See gh-49439 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 565663c3d794..d25749d366b3 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2434,7 +2434,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-amqp/releases/tag/v{version}") } } - library("Spring Batch", "6.0.2") { + library("Spring Batch", "6.0.3-SNAPSHOT") { considerSnapshots() group("org.springframework.batch") { bom("spring-batch-bom") From ce16bb8a495fdb1af948915272d76aa99350d067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 07:58:37 +0100 Subject: [PATCH 263/930] Start building against Spring Data Bom 2026.0.0-M2 snapshots See gh-49440 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d25749d366b3..1c873ca8d752 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2449,7 +2449,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-batch/releases/tag/v{version}") } } - library("Spring Data Bom", "2026.0.0-M1") { + library("Spring Data Bom", "2026.0.0-SNAPSHOT") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From 494e86b56e16f766862102dbfeb93ff3f5d1fc54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 07:58:41 +0100 Subject: [PATCH 264/930] Start building against Spring Framework 7.0.6 snapshots See gh-49441 --- .../logback/SpringBootJoranConfigurator.java | 6 ++-- ...backConfigurationAotContributionTests.java | 30 +++++++------------ .../advanced/customhints/MyRuntimeHints.java | 6 ++-- gradle.properties | 2 +- 4 files changed, 18 insertions(+), 26 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java index e53d63235db1..ac1d5cc0bd3c 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java @@ -56,7 +56,7 @@ import org.springframework.aot.generate.GeneratedFiles.Kind; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; -import org.springframework.aot.hint.SerializationHints; +import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; @@ -187,8 +187,8 @@ private void writeTo(GenerationContext generationContext) { .handleFile(Kind.RESOURCE, MODEL_RESOURCE_LOCATION, new RequireNewOrMatchingContentFileHandler(serializedModel)); generationContext.getRuntimeHints().resources().registerPattern(MODEL_RESOURCE_LOCATION); - SerializationHints serializationHints = generationContext.getRuntimeHints().serialization(); - serializationTypes(this.model).forEach(serializationHints::registerType); + ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection(); + serializationTypes(this.model).forEach(reflectionHints::registerJavaSerialization); reflectionTypes(this.model).forEach((type) -> generationContext.getRuntimeHints() .reflection() .registerType(TypeReference.of(type), MemberCategory.INVOKE_PUBLIC_METHODS, diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackConfigurationAotContributionTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackConfigurationAotContributionTests.java index 81b18c77131d..0e521989db0b 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackConfigurationAotContributionTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackConfigurationAotContributionTests.java @@ -26,7 +26,6 @@ import java.util.Properties; import java.util.function.Consumer; import java.util.function.Predicate; -import java.util.stream.Stream; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.encoder.PatternLayoutEncoder; @@ -50,10 +49,9 @@ import org.springframework.aot.generate.GeneratedFiles.Kind; import org.springframework.aot.generate.InMemoryGeneratedFiles; -import org.springframework.aot.hint.JavaSerializationHint; import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.RuntimeHints; -import org.springframework.aot.hint.SerializationHints; import org.springframework.aot.hint.TypeHint; import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; @@ -89,12 +87,11 @@ void contributionOfBasicModel() { InMemoryGeneratedFiles generatedFiles = generationContext.getGeneratedFiles(); assertThat(generatedFiles).has(resource("META-INF/spring/logback-model")); assertThat(generatedFiles).has(resource("META-INF/spring/logback-pattern-rules")); - SerializationHints serializationHints = generationContext.getRuntimeHints().serialization(); - assertThat(serializationHints.javaSerializationHints() - .map(JavaSerializationHint::getType) - .map(TypeReference::getName)) - .containsExactlyInAnyOrder(namesOf(Model.class, ArrayList.class, Boolean.class, Integer.class)); - assertThat(generationContext.getRuntimeHints().reflection().typeHints()).isEmpty(); + ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection(); + assertThat(reflectionHints.typeHints().map(TypeHint::getType)).containsExactlyInAnyOrderElementsOf( + TypeReference.listOf(Model.class, ArrayList.class, Boolean.class, Integer.class)); + assertThat(reflectionHints.typeHints().map(TypeHint::hasJavaSerialization).distinct()).singleElement() + .isEqualTo(true); InputStreamSource generatedFile = generatedFiles.getGeneratedFile(Kind.RESOURCE, "META-INF/spring/logback-pattern-rules"); assertThat(generatedFile).isNotNull(); @@ -111,12 +108,11 @@ void contributionOfBasicModelThatMatchesExistingModel() { InMemoryGeneratedFiles generatedFiles = generationContext.getGeneratedFiles(); assertThat(generatedFiles).has(resource("META-INF/spring/logback-model")); assertThat(generatedFiles).has(resource("META-INF/spring/logback-pattern-rules")); - SerializationHints serializationHints = generationContext.getRuntimeHints().serialization(); - assertThat(serializationHints.javaSerializationHints() - .map(JavaSerializationHint::getType) - .map(TypeReference::getName)) - .containsExactlyInAnyOrder(namesOf(Model.class, ArrayList.class, Boolean.class, Integer.class)); - assertThat(generationContext.getRuntimeHints().reflection().typeHints()).isEmpty(); + ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection(); + assertThat(reflectionHints.typeHints().map(TypeHint::getType)).containsExactlyInAnyOrderElementsOf( + TypeReference.listOf(Model.class, ArrayList.class, Boolean.class, Integer.class)); + assertThat(reflectionHints.typeHints().map(TypeHint::hasJavaSerialization).distinct()).singleElement() + .isEqualTo(true); InputStreamSource generatedFile = generatedFiles.getGeneratedFile(Kind.RESOURCE, "META-INF/spring/logback-pattern-rules"); assertThat(generatedFile).isNotNull(); @@ -330,10 +326,6 @@ private void applyContribution(Model model, Consumer contextCusto contribution.applyTo(generationContext, mock(BeanFactoryInitializationCode.class)); } - private String[] namesOf(Class... types) { - return Stream.of(types).map(Class::getName).toArray(String[]::new); - } - private void withSystemProperty(String name, String value, Runnable action) { System.setProperty(name, value); try { diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/packaging/nativeimage/advanced/customhints/MyRuntimeHints.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/packaging/nativeimage/advanced/customhints/MyRuntimeHints.java index 0e4ef3bb2682..b9b98a31ff21 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/packaging/nativeimage/advanced/customhints/MyRuntimeHints.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/packaging/nativeimage/advanced/customhints/MyRuntimeHints.java @@ -31,12 +31,12 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) { Method method = ReflectionUtils.findMethod(MyClass.class, "sayHello", String.class); hints.reflection().registerMethod(method, ExecutableMode.INVOKE); + // Register type for java serialization + hints.reflection().registerJavaSerialization(MySerializableClass.class); + // Register resources hints.resources().registerPattern("my-resource.txt"); - // Register serialization - hints.serialization().registerType(MySerializableClass.class); - // Register proxy hints.proxies().registerJdkProxy(MyInterface.class); } diff --git a/gradle.properties b/gradle.properties index 65a4693d8214..ed68b8c29c69 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ mockitoVersion=5.21.0 nativeBuildToolsVersion=0.11.4 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.5 -springFrameworkVersion=7.0.5 +springFrameworkVersion=7.0.6-SNAPSHOT springFramework60xVersion=6.0.23 tomcatVersion=11.0.18 From ea2c0ec01ba79ca65ae89e9e0dac7c9b1a26180e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 07:58:45 +0100 Subject: [PATCH 265/930] Start building against Spring Integration 7.1.0-M3 snapshots See gh-49442 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1c873ca8d752..2393b62da4d9 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2514,7 +2514,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-hateoas/releases/tag/{version}") } } - library("Spring Integration", "7.1.0-M2") { + library("Spring Integration", "7.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.integration") { bom("spring-integration-bom") From cdf5da9e8b17d2b232ba8e6c341f923ab707e8fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 07:58:49 +0100 Subject: [PATCH 266/930] Start building against Spring Kafka 4.1.0-M2 snapshots See gh-49443 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 2393b62da4d9..4382b4234d2d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2529,7 +2529,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-integration/releases/tag/v{version}") } } - library("Spring Kafka", "4.1.0-M1") { + library("Spring Kafka", "4.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.kafka") { modules = [ From 6aadbab4930a23befceb13184a2962c0b66e266e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 07:58:53 +0100 Subject: [PATCH 267/930] Start building against Spring LDAP 4.1.0-M2 snapshots See gh-49444 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4382b4234d2d..e3705a26c965 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2547,7 +2547,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-kafka/releases/tag/v{version}") } } - library("Spring LDAP", "4.1.0-M1") { + library("Spring LDAP", "4.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.ldap") { modules = [ From 03791e5c9e0b2a008e9b38d8d897bd82dfcb32a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 07:58:57 +0100 Subject: [PATCH 268/930] Start building against Spring Pulsar 2.0.4 snapshots See gh-49445 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e3705a26c965..5aabe377c848 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2567,7 +2567,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ldap/releases/tag/{version}") } } - library("Spring Pulsar", "2.0.3") { + library("Spring Pulsar", "2.0.4-SNAPSHOT") { considerSnapshots() group("org.springframework.pulsar") { bom("spring-pulsar-bom") From 61ec1f2f22d780a7b3f9561b72b2a4ff9a62e032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 10:12:47 +0100 Subject: [PATCH 269/930] Upgrade to Elasticsearch Client 9.3.1 Closes gh-49447 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 5aabe377c848..02b46b98de5e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -395,7 +395,7 @@ bom { releaseNotes("https://github.com/ehcache/ehcache3/releases/tag/v{version}") } } - library("Elasticsearch Client", "9.2.5") { + library("Elasticsearch Client", "9.3.1") { prohibit { contains "-alpha" contains "-beta" From d5fb86239e72c8345d43d0f9a2256a3f2fd0b376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 10:12:51 +0100 Subject: [PATCH 270/930] Upgrade to Jedis 7.3.0 Closes gh-49448 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 02b46b98de5e..4a7c652d2d86 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1049,7 +1049,7 @@ bom { releaseNotes("https://github.com/hunterhacker/jdom/releases/tag/JDOM-{version}") } } - library("Jedis", "7.2.1") { + library("Jedis", "7.3.0") { alignWith { property { name "jedis" From 916acba2d9fdf98bea7218368620dd555d6368ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 10:12:55 +0100 Subject: [PATCH 271/930] Upgrade to Lettuce 7.5.0.RELEASE Closes gh-49449 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4a7c652d2d86..f8aa4d89fa19 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1281,7 +1281,7 @@ bom { releaseNotes("https://github.com/Kotlin/kotlinx.serialization/releases/tag/v{version}") } } - library("Lettuce", "7.2.1.RELEASE") { + library("Lettuce", "7.5.0.RELEASE") { prohibit { contains ".BETA" because "we don't want betas" From a68e3a56a507c00e7c64b298ee4a9afa400e9823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 10:12:59 +0100 Subject: [PATCH 272/930] Upgrade to MongoDB 5.7.0-beta1 Closes gh-49450 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index f8aa4d89fa19..6a3a2d191fd6 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1623,7 +1623,7 @@ bom { releaseNotes("https://github.com/mockito/mockito/releases/tag/v{version}") } } - library("MongoDB", "5.6.3") { + library("MongoDB", "5.7.0-beta1") { alignWith { version { of "org.mongodb:mongodb-driver-core" From d5e8375b9f4ebebdf0a77ec8a4984467cbb266ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 10:13:03 +0100 Subject: [PATCH 273/930] Upgrade to Neo4j Java Driver 6.0.3 Closes gh-49451 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6a3a2d191fd6..0e5106722fce 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1694,7 +1694,7 @@ bom { ] } } - library("Neo4j Java Driver", "6.0.2") { + library("Neo4j Java Driver", "6.0.3") { alignWith { version { of "org.neo4j.driver:neo4j-java-driver" From ec107e2f9df94edd96fe37f7763afc5e2cb1283e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Mar 2026 10:13:07 +0100 Subject: [PATCH 274/930] Upgrade to OpenTelemetry 1.59.0 Closes gh-49452 --- .../zipkin/ZipkinWithOpenTelemetryTracingAutoConfiguration.java | 1 + .../ZipkinWithOpenTelemetryTracingAutoConfigurationTests.java | 1 + platform/spring-boot-dependencies/build.gradle | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfiguration.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfiguration.java index e89c1e8967c3..b2af5b87f83d 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfiguration.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfiguration.java @@ -41,6 +41,7 @@ * @author Phillip Webb * @since 4.0.0 */ +@SuppressWarnings("deprecation") @AutoConfiguration(afterName = "org.springframework.boot.zipkin.autoconfigure.ZipkinAutoConfiguration") @ConditionalOnClass({ ZipkinSpanExporter.class, Span.class }) public final class ZipkinWithOpenTelemetryTracingAutoConfiguration { diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfigurationTests.java index fce94731ce7e..fc02119cc9fb 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfigurationTests.java @@ -38,6 +38,7 @@ * * @author Moritz Halbritter */ +@SuppressWarnings("deprecation") class ZipkinWithOpenTelemetryTracingAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0e5106722fce..02bee9dc26df 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1732,7 +1732,7 @@ bom { javadoc(version -> "https://netty.io/%s.%s/api".formatted(version.major(), version.minor()), "io.netty") } } - library("OpenTelemetry", "1.58.0") { + library("OpenTelemetry", "1.59.0") { group("io.opentelemetry") { bom("opentelemetry-bom") } From a677b44860b9240e67412e71d54156b56573420b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 6 Mar 2026 15:15:16 +0000 Subject: [PATCH 275/930] Polish See gh-49466 --- platform/spring-boot-dependencies/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6dffeb3b86fb..c600db693559 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1755,7 +1755,6 @@ bom { version { of "io.opentelemetry:opentelemetry-api" from "io.micrometer:micrometer-tracing-bridge-otel" - managedBy "Micrometer Tracing" } } links { From 24683045d11036cf3fb4844d4e33d76048ee5941 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 6 Mar 2026 15:35:01 +0000 Subject: [PATCH 276/930] Improve diagnostics for @DockerComposeTest failures Closes gh-49477 --- .../test/DockerComposeTestExtension.java | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/core/spring-boot-docker-compose/src/testFixtures/java/org/springframework/boot/docker/compose/service/connection/test/DockerComposeTestExtension.java b/core/spring-boot-docker-compose/src/testFixtures/java/org/springframework/boot/docker/compose/service/connection/test/DockerComposeTestExtension.java index 6d97671042df..aa6a83f4ece7 100644 --- a/core/spring-boot-docker-compose/src/testFixtures/java/org/springframework/boot/docker/compose/service/connection/test/DockerComposeTestExtension.java +++ b/core/spring-boot-docker-compose/src/testFixtures/java/org/springframework/boot/docker/compose/service/connection/test/DockerComposeTestExtension.java @@ -60,18 +60,31 @@ class DockerComposeTestExtension implements BeforeTestExecutionCallback, AfterTe @Override public void beforeTestExecution(ExtensionContext context) throws Exception { + Path logFile = Files.createTempFile("docker-compose-test", ".log"); Store store = context.getStore(NAMESPACE); Path workspace = Files.createTempDirectory("DockerComposeTestExtension-"); store.put(STORE_KEY_WORKSPACE, workspace); try { Path composeFile = prepareComposeFile(workspace, context); copyAdditionalResources(workspace, context); - SpringApplication application = prepareApplication(composeFile); + SpringApplication application = prepareApplication(composeFile, logFile); store.put(STORE_KEY_APPLICATION_CONTEXT, application.run()); } catch (Exception ex) { cleanUp(context); - throw ex; + throw new DockerComposeApplicationRunException(readLogs(logFile)); + } + finally { + Files.delete(logFile); + } + } + + private String readLogs(Path logFile) { + try { + return Files.readString(logFile); + } + catch (IOException ex) { + return "Unavailable: " + ex.getMessage(); } } @@ -115,7 +128,7 @@ private void copyAdditionalResource(Path workspace, Resource resource) { } } - private SpringApplication prepareApplication(Path composeFile) { + private SpringApplication prepareApplication(Path composeFile, Path logFile) { SpringApplication application = new SpringApplication(Config.class); Map properties = new LinkedHashMap<>(); properties.put("spring.docker.compose.skip.in-tests", "false"); @@ -123,6 +136,7 @@ private SpringApplication prepareApplication(Path composeFile) { properties.put("spring.docker.compose.stop.command", "down"); properties.put("spring.docker.compose.start.arguments[0]", "--wait-timeout"); properties.put("spring.docker.compose.start.arguments[1]", "120"); + properties.put("logging.file.name", logFile.toString()); application.setDefaultProperties(properties); return application; } @@ -170,4 +184,12 @@ static class Config { } + static class DockerComposeApplicationRunException extends RuntimeException { + + DockerComposeApplicationRunException(String logs) { + super("Docker Compose failed. Application logs:%n%n%s".formatted(logs)); + } + + } + } From 5bd0e84157a74d780d96a65443ee0ab4ac32640e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 6 Mar 2026 16:27:41 +0000 Subject: [PATCH 277/930] Make Elasticsearch Docker tests run with Logback available See gh-49477 --- module/spring-boot-elasticsearch/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/module/spring-boot-elasticsearch/build.gradle b/module/spring-boot-elasticsearch/build.gradle index fffdde5a003a..d5023fc7b06c 100644 --- a/module/spring-boot-elasticsearch/build.gradle +++ b/module/spring-boot-elasticsearch/build.gradle @@ -48,6 +48,8 @@ dependencies { dockerTestImplementation(testFixtures(project(":core:spring-boot-docker-compose"))) dockerTestImplementation("org.testcontainers:testcontainers-elasticsearch") dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") + + dockerTestRuntimeOnly("ch.qos.logback:logback-classic") testImplementation(project(":core:spring-boot-test")) testImplementation(project(":test-support:spring-boot-test-support")) From 1fea218f822117030b21523d7681c04d49419538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 09:30:35 +0100 Subject: [PATCH 278/930] Upgrade to Infinispan 16.1.0 Closes gh-49484 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c600db693559..3d3ebcdf486b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -723,7 +723,7 @@ bom { ] } } - library("Infinispan", "15.2.6.Final") { + library("Infinispan", "16.1.0") { group("org.infinispan") { bom("infinispan-bom") } From e180e6068d5e3ff3e4740f88afd7f88badc59040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:12:26 +0100 Subject: [PATCH 279/930] Upgrade to Artemis 2.52.0 Closes gh-49485 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3d3ebcdf486b..ddd8ad5e1eb4 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -65,7 +65,7 @@ bom { releaseNotes("https://github.com/eclipse-ee4j/angus-mail/releases/tag/{version}") } } - library("Artemis", "2.51.0") { + library("Artemis", "2.52.0") { group("org.apache.artemis") { bom("artemis-bom") { permit("org.apache.activemq:artemis-amqp-protocol") From a83497624e67cbac2620e8c49482b2b76d282cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:12:30 +0100 Subject: [PATCH 280/930] Upgrade to Byte Buddy 1.18.7 Closes gh-49486 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ddd8ad5e1eb4..e641efe0a4ba 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -186,7 +186,7 @@ bom { releaseNotes("https://github.com/mojohaus/build-helper-maven-plugin/releases/tag/{version}") } } - library("Byte Buddy", "1.18.5") { + library("Byte Buddy", "1.18.7") { group("net.bytebuddy") { modules = [ "byte-buddy", From 08e21c5621ea120064ce57194f450bc054e99139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:12:34 +0100 Subject: [PATCH 281/930] Upgrade to Flyway 12.0.3 Closes gh-49487 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e641efe0a4ba..38a007b7011e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -419,7 +419,7 @@ bom { javadoc("elasticsearch-java", version -> "https://javadoc.io/doc/co.elastic.clients/elasticsearch-java/%s/index.html".formatted(version), "co.elastic.clients.elasticsearch", "co.elastic.clients.transport") } } - library("Flyway", "12.0.1") { + library("Flyway", "12.0.3") { group("org.flywaydb") { modules = [ "flyway-command-test-connection", From 4355744c8ad5a6da1d8aae37444362acc452f6ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:12:38 +0100 Subject: [PATCH 282/930] Upgrade to Hibernate 7.2.6.Final Closes gh-49488 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 38a007b7011e..a65c435fc55c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -605,7 +605,7 @@ bom { releaseNotes("https://github.com/hazelcast/hazelcast/releases/tag/v{version}") } } - library("Hibernate", "7.2.4.Final") { + library("Hibernate", "7.2.6.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From ecc46cbc672a6a8cc454cc94b6a862c53bf12fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:12:42 +0100 Subject: [PATCH 283/930] Upgrade to HttpCore5 5.4.2 Closes gh-49489 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a65c435fc55c..750e511cfa69 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -714,7 +714,7 @@ bom { ] } } - library("HttpCore5", "5.4.1") { + library("HttpCore5", "5.4.2") { group("org.apache.httpcomponents.core5") { modules = [ "httpcore5", From 43fcaf1bc0fd81026e0b91f06b0728c14d4f46d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:12:46 +0100 Subject: [PATCH 284/930] Upgrade to Jakarta XML WS 4.0.3 Closes gh-49490 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 750e511cfa69..c9a2b9689f41 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -968,7 +968,7 @@ bom { releaseNotes("https://github.com/jakartaee/saaj-api/releases/tag/{version}") } } - library("Jakarta XML WS", "4.0.2") { + library("Jakarta XML WS", "4.0.3") { group("jakarta.xml.ws") { modules = [ "jakarta.xml.ws-api" From 37c8d5689f89b7eb7382a8265a2389a513cfdb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:12:50 +0100 Subject: [PATCH 285/930] Upgrade to Jetty 12.1.7 Closes gh-49491 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c9a2b9689f41..dfa032c4de24 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1084,7 +1084,7 @@ bom { ] } } - library("Jetty", "12.1.6") { + library("Jetty", "12.1.7") { prohibit { contains ".alpha" because "we don't want alpha dependencies" From 07384ea2114b878cc930aaa1cd85072ab33e7682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:12:54 +0100 Subject: [PATCH 286/930] Upgrade to Liquibase 5.0.2 Closes gh-49492 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index dfa032c4de24..af34014efd19 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1305,7 +1305,7 @@ bom { releaseNotes("https://github.com/redis/lettuce/releases/tag/{version}") } } - library("Liquibase", "5.0.1") { + library("Liquibase", "5.0.2") { group("org.liquibase") { modules = [ "liquibase-cdi", From bc471adad70f12b7f313c17dab08cc41a2c469c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:12:58 +0100 Subject: [PATCH 287/930] Upgrade to Maven Failsafe Plugin 3.5.5 Closes gh-49493 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index af34014efd19..5815288952e5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1455,7 +1455,7 @@ bom { releaseNotes("https://github.com/apache/maven-enforcer/releases/tag/enforcer-{version}") } } - library("Maven Failsafe Plugin", "3.5.4") { + library("Maven Failsafe Plugin", "3.5.5") { group("org.apache.maven.plugins") { plugins = [ "maven-failsafe-plugin" From 7a0d4e7fbe0d54f11f380d5e5fca00065c531cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:13:02 +0100 Subject: [PATCH 288/930] Upgrade to Maven Resources Plugin 3.5.0 Closes gh-49494 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 5815288952e5..2ba1f012ef73 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1520,7 +1520,7 @@ bom { releaseNotes("https://github.com/apache/maven-javadoc-plugin/releases/tag/maven-javadoc-plugin-{version}") } } - library("Maven Resources Plugin", "3.4.0") { + library("Maven Resources Plugin", "3.5.0") { prohibit { contains "-beta-" because "we don't want betas" From 430414fe6fc6a19aa26feff072c11e5750085d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:13:06 +0100 Subject: [PATCH 289/930] Upgrade to Maven Shade Plugin 3.6.2 Closes gh-49495 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 2ba1f012ef73..71d416b2cb90 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1534,7 +1534,7 @@ bom { releaseNotes("https://github.com/apache/maven-resources-plugin/releases/tag/maven-resources-plugin-{version}") } } - library("Maven Shade Plugin", "3.6.1") { + library("Maven Shade Plugin", "3.6.2") { group("org.apache.maven.plugins") { plugins = [ "maven-shade-plugin" From 36b9f910b41e68b97aca883e65fb222a5c56d907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:13:10 +0100 Subject: [PATCH 290/930] Upgrade to Maven Surefire Plugin 3.5.5 Closes gh-49496 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 71d416b2cb90..2e737d39183a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1558,7 +1558,7 @@ bom { releaseNotes("https://github.com/apache/maven-source-plugin/releases/tag/maven-source-plugin-{version}") } } - library("Maven Surefire Plugin", "3.5.4") { + library("Maven Surefire Plugin", "3.5.5") { group("org.apache.maven.plugins") { plugins = [ "maven-surefire-plugin" From f0ff1b819712103f93f8ec6f01671ec63e25c109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:13:14 +0100 Subject: [PATCH 291/930] Upgrade to Mockito 5.22.0 Closes gh-49497 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ed68b8c29c69..9ee8a7f7ae98 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 kotlinVersion=2.3.10 mavenVersion=3.9.10 -mockitoVersion=5.21.0 +mockitoVersion=5.22.0 nativeBuildToolsVersion=0.11.4 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.5 From fde18f4523caa047530dd140f9fb6f7e5067d650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:13:18 +0100 Subject: [PATCH 292/930] Upgrade to Native Build Tools Plugin 0.11.5 Closes gh-49498 --- ...rary-and-framework-list-schema-v1.0.0.json | 100 +++++++++++++++++ .../metadata-library-index-schema-v1.0.0.json | 106 ++++++++++++++++++ .../metadata-root-index-schema-v1.0.0.json | 67 +++++++++++ gradle.properties | 2 +- 4 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/library-and-framework-list-schema-v1.0.0.json create mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v1.0.0.json create mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-root-index-schema-v1.0.0.json diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/library-and-framework-list-schema-v1.0.0.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/library-and-framework-list-schema-v1.0.0.json new file mode 100644 index 000000000000..0c6861ec0f85 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/library-and-framework-list-schema-v1.0.0.json @@ -0,0 +1,100 @@ +{ + "$id": "https://raw.githubusercontent.com/oracle/graalvm-reachability-metadata/master/metadata/schemas/library-and-framework-support-schema-v1.0.0.json", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "default": [], + "examples": [ + { + "artifact": "io.netty:netty5-parent", + "details": [ + { + "minimum_version": "4.1", + "metadata_locations": [ + "https://github.com/netty/netty/tree/main/common/src/main/resources/META-INF/native-image" + ], + "tests_locations": [ + "https://github.com/netty/netty/actions" + ], + "test_level": "fully-tested" + } + ] + } + ], + "items": { + "properties": { + "artifact": { + "default": "", + "description": "The artifact name in the groupId:artifactId format", + "pattern": "^[a-zA-Z0-9._-]+:[a-zA-Z0-9._*-]+$", + "title": "The name of the artifact", + "type": "string" + }, + "description": { + "default": "", + "title": "Short description of the library or framework", + "type": "string" + }, + "details": { + "default": [], + "items": { + "default": {}, + "properties": { + "maximal_version": { + "default": "", + "description": "Maximum version for which this entry applies. If not defined, it is assumed to be supported for all versions starting from the minimum_version", + "title": "Maximal version for which this entry applies", + "type": "string" + }, + "metadata_locations": { + "default": [], + "items": { + "default": "", + "type": "string" + }, + "title": "Web URLs of provided metadata", + "type": "array" + }, + "minimum_version": { + "default": "", + "title": "Minimal version for which this entry applies", + "type": "string" + }, + "test_level": { + "default": "Untested", + "enum": [ + "untested", + "community-tested", + "fully-tested" + ], + "title": "Testing level of reachability metadata for the library or framework", + "type": "string" + }, + "tests_locations": { + "default": [], + "items": { + "default": "", + "type": "string" + }, + "title": "Web URLs to tests (sources, CI dashboards/configurations, ...)", + "type": "array" + } + }, + "required": [ + "minimum_version", + "metadata_locations", + "tests_locations", + "test_level" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "artifact", + "details" + ], + "type": "object" + }, + "title": "Schema for the 'library-and-framework-list.json'", + "type": "array" +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v1.0.0.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v1.0.0.json new file mode 100644 index 000000000000..2461661166eb --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v1.0.0.json @@ -0,0 +1,106 @@ +{ + "$id": "https://raw.githubusercontent.com/oracle/graalvm-reachability-metadata/master/metadata/schemas/metadata-library-index-schema-v1.0.0.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Schema for metadata///index.json. Each entry describes a metadata bundle for a range of library versions.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "module", + "metadata-version", + "tested-versions" + ], + "properties": { + "module": { + "$ref": "#/$defs/moduleCoordinate", + "description": "Maven coordinates in the form ':'." + }, + "metadata-version": { + "type": "string", + "minLength": 1, + "description": "Subdirectory name where the metadata files for this entry reside, e.g. '7.1.0.Final'." + }, + "tested-versions": { + "type": "array", + "description": "Explicitly tested upstream library versions that this metadata is known to support.", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "test-version": { + "type": "string", + "minLength": 1, + "description": "Overrides 'metadata-version' for use in test directories. This allows one metadata entry to share tests defined for a different version's metadata directory." + }, + "latest": { + "type": "boolean", + "description": "Marks this entry as the latest/default metadata for currently supported versions." + }, + "default-for": { + "type": "string", + "description": "Java regular expression describing the version range for which this entry should be used by default (e.g. '7\\\\.1\\\\..*')." + }, + "override": { + "type": "boolean", + "description": "When true, expresses the intent to exclude outdated built-in metadata shipped with Native Image for the matched versions." + }, + "skipped-versions": { + "type": "array", + "description": "Versions explicitly excluded from support, each with a reason.", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ "version", "reason" ], + "properties": { + "version": { + "type": "string", + "minLength": 1 + }, + "reason": { + "type": "string", + "minLength": 1 + } + } + } + } + } + }, + "$defs": { + "moduleCoordinate": { + "type": "string", + "pattern": "^[^:]+:[^:]+$" + } + }, + "examples": [ + [ + { + "metadata-version": "0.0.1", + "module": "org.example:library", + "tested-versions": [ "0.0.1", "0.0.2" ], + "default-for": "0\\.0\\..*", + "test-version": "0.0.1" + }, + { + "latest": true, + "metadata-version": "1.0.0", + "module": "org.example:library", + "tested-versions": [ "1.0.0", "1.1.0" , "1.2.0"] + }, + { + "metadata-version": "1.19.0", + "module": "io.opentelemetry:opentelemetry-sdk-trace", + "tested-versions": [ "1.19.0" ], + "override": true, + "skipped-versions": [ + { "version": "1.34.0", "reason": "Dependency io.opentelemetry:opentelemetry-api:1.34.0 provides reflect-config.json which does not parse." } + ] + } + ] + ] +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-root-index-schema-v1.0.0.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-root-index-schema-v1.0.0.json new file mode 100644 index 000000000000..7087aaa1d29c --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-root-index-schema-v1.0.0.json @@ -0,0 +1,67 @@ +{ + "$id": "https://raw.githubusercontent.com/oracle/graalvm-reachability-metadata/master/metadata/schemas/metadata-root-index-schema-v1.0.0.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Schema for metadata/index.json. This file lists modules known to the repository and optionally the directory where their metadata is stored, the allowed package prefixes for metadata, and inter-module requirements. See docs/CONTRIBUTING.md (Metadata structure).", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["module"], + "properties": { + "module": { + "$ref": "#/$defs/moduleCoordinate", + "description": "Maven coordinates for the module in the form ':'." + }, + "directory": { + "type": "string", + "minLength": 1, + "pattern": "^[^\\s].*$", + "description": "Repository-relative path under 'metadata/' containing this module's metadata (e.g. 'org.example/library'). If omitted, the entry may reference requirements only." + }, + "allowed-packages": { + "type": "array", + "description": "List of package (or fully-qualified name) prefixes considered valid sources of metadata entries for this module. Used to filter-in relevant JSON entries.", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "requires": { + "type": "array", + "description": "Optional list of module coordinates this module depends on. Each item is ':'.", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/moduleCoordinate" + } + } + } + }, + "$defs": { + "moduleCoordinate": { + "type": "string", + "pattern": "^[^:]+:[^:]+$" + } + }, + "examples": [ + [ + { + "directory": "org.example/library", + "module": "org.example:library" + }, + { + "allowed-packages": ["org.package.name"], + "module": "org.example:dependant-library", + "requires": ["org.example:library"] + }, + { + "allowed-packages" : [ "org.hibernate", "jakarta" ], + "directory" : "org.hibernate.orm/hibernate-envers", + "module" : "org.hibernate.orm:hibernate-envers", + "requires" : [ "org.hibernate.orm:hibernate-core" ] + } + ] + ] +} diff --git a/gradle.properties b/gradle.properties index 9ee8a7f7ae98..29bfd05cefe5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ junitJupiterVersion=6.0.3 kotlinVersion=2.3.10 mavenVersion=3.9.10 mockitoVersion=5.22.0 -nativeBuildToolsVersion=0.11.4 +nativeBuildToolsVersion=0.11.5 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.5 springFrameworkVersion=7.0.6-SNAPSHOT From a35a6ff6bf654f1e16b791663deebd4d9ffa5599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:13:22 +0100 Subject: [PATCH 293/930] Upgrade to Pulsar 4.1.3 Closes gh-49499 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 2e737d39183a..9c43721045a8 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1862,7 +1862,7 @@ bom { releaseNotes("https://github.com/prometheus/client_java/releases/tag/parent-{version}") } } - library("Pulsar", "4.1.2") { + library("Pulsar", "4.1.3") { group("org.apache.pulsar") { bom("pulsar-bom") } From 7ebfa232cea4c3d829deb57fe5fbb443a3dac991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:13:26 +0100 Subject: [PATCH 294/930] Upgrade to Rabbit AMQP Client 5.29.0 Closes gh-49500 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9c43721045a8..3f978b38fec8 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1984,7 +1984,7 @@ bom { releaseNotes("https://github.com/r2dbc/r2dbc-spi/releases/tag/v{version}") } } - library("Rabbit AMQP Client", "5.28.0") { + library("Rabbit AMQP Client", "5.29.0") { group("com.rabbitmq") { modules = [ "amqp-client" From c7f443cd7825fd0a65462f4fa3d2bac5ec300622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:13:30 +0100 Subject: [PATCH 295/930] Upgrade to Selenium 4.41.0 Closes gh-49501 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3f978b38fec8..26f7445f79fd 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2369,7 +2369,7 @@ bom { ] } } - library("Selenium", "4.40.0") { + library("Selenium", "4.41.0") { group("org.seleniumhq.selenium") { bom("selenium-bom") } From 18e694c2c05e041e5bbcf7eb5364dcb6948a2aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:13:34 +0100 Subject: [PATCH 296/930] Upgrade to Selenium HtmlUnit 4.41.0 Closes gh-49502 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 26f7445f79fd..1dbb5c134690 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2379,7 +2379,7 @@ bom { releaseNotes("https://github.com/SeleniumHQ/selenium/releases/tag/selenium-{version}") } } - library("Selenium HtmlUnit", "4.40.0") { + library("Selenium HtmlUnit", "4.41.0") { group("org.seleniumhq.selenium") { modules = [ "htmlunit3-driver" From 5dd220a06fcd89f28a3eb92c7c47616fcb0765fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 7 Mar 2026 11:13:38 +0100 Subject: [PATCH 297/930] Upgrade to SnakeYAML 2.6 Closes gh-49503 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 29bfd05cefe5..314d9b497c33 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,7 +20,7 @@ mavenVersion=3.9.10 mockitoVersion=5.22.0 nativeBuildToolsVersion=0.11.5 nullabilityPluginVersion=0.0.11 -snakeYamlVersion=2.5 +snakeYamlVersion=2.6 springFrameworkVersion=7.0.6-SNAPSHOT springFramework60xVersion=6.0.23 tomcatVersion=11.0.18 From c54d05ca8adc9770ce93f30b68897e36eced9693 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Mar 2026 11:33:29 +0000 Subject: [PATCH 298/930] Use vendor adapter to configure JTA Platform and Bean Container Closes gh-48969 --- .../HibernateJpaConfiguration.java | 94 ++----------------- .../HibernateJpaAutoConfigurationTests.java | 4 +- 2 files changed, 10 insertions(+), 88 deletions(-) diff --git a/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaConfiguration.java b/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaConfiguration.java index 3edbcc7fffa0..3a032ca00179 100644 --- a/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaConfiguration.java +++ b/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaConfiguration.java @@ -17,7 +17,6 @@ package org.springframework.boot.hibernate.autoconfigure; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; @@ -27,12 +26,9 @@ import javax.sql.DataSource; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.hibernate.boot.model.naming.ImplicitNamingStrategy; import org.hibernate.boot.model.naming.PhysicalNamingStrategy; import org.hibernate.boot.model.naming.PhysicalNamingStrategySnakeCaseImpl; -import org.hibernate.cfg.ManagedBeanSettings; import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.MemberCategory; @@ -42,11 +38,9 @@ import org.springframework.aot.hint.TypeHint.Builder; import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.hibernate.SpringImplicitNamingStrategy; -import org.springframework.boot.hibernate.SpringJtaPlatform; import org.springframework.boot.hibernate.autoconfigure.HibernateJpaConfiguration.HibernateRuntimeHints; import org.springframework.boot.jdbc.SchemaManagementProvider; import org.springframework.boot.jdbc.metadata.CompositeDataSourcePoolMetadataProvider; @@ -57,12 +51,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.jdbc.support.SQLExceptionTranslator; -import org.springframework.jndi.JndiLocatorDelegate; -import org.springframework.orm.jpa.hibernate.SpringBeanContainer; -import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.jta.JtaTransactionManager; -import org.springframework.util.ClassUtils; /** * {@link JpaBaseConfiguration} implementation for Hibernate. @@ -80,10 +70,6 @@ @ImportRuntimeHints(HibernateRuntimeHints.class) class HibernateJpaConfiguration extends JpaBaseConfiguration { - private static final Log logger = LogFactory.getLog(HibernateJpaConfiguration.class); - - private static final String JTA_PLATFORM = "hibernate.transaction.jta.platform"; - private static final String PROVIDER_DISABLES_AUTOCOMMIT = "hibernate.connection.provider_disables_autocommit"; /** @@ -104,8 +90,7 @@ class HibernateJpaConfiguration extends JpaBaseConfiguration { private final List hibernatePropertiesCustomizers; HibernateJpaConfiguration(DataSource dataSource, JpaProperties jpaProperties, - ConfigurableListableBeanFactory beanFactory, ObjectProvider jtaTransactionManager, - HibernateProperties hibernateProperties, + ObjectProvider jtaTransactionManager, HibernateProperties hibernateProperties, ObjectProvider> metadataProviders, ObjectProvider providers, ObjectProvider physicalNamingStrategy, @@ -118,20 +103,15 @@ class HibernateJpaConfiguration extends JpaBaseConfiguration { this.poolMetadataProvider = new CompositeDataSourcePoolMetadataProvider(metadataProviders.getIfAvailable()); this.sqlExceptionTranslator = sqlExceptionTranslator; this.hibernatePropertiesCustomizers = determineHibernatePropertiesCustomizers( - physicalNamingStrategy.getIfAvailable(), implicitNamingStrategy.getIfAvailable(), beanFactory, + physicalNamingStrategy.getIfAvailable(), implicitNamingStrategy.getIfAvailable(), hibernatePropertiesCustomizers.orderedStream().toList()); } private List determineHibernatePropertiesCustomizers( @Nullable PhysicalNamingStrategy physicalNamingStrategy, - @Nullable ImplicitNamingStrategy implicitNamingStrategy, ConfigurableListableBeanFactory beanFactory, + @Nullable ImplicitNamingStrategy implicitNamingStrategy, List hibernatePropertiesCustomizers) { List customizers = new ArrayList<>(); - if (ClassUtils.isPresent("org.hibernate.resource.beans.container.spi.BeanContainer", - getClass().getClassLoader())) { - customizers.add((properties) -> properties.put(ManagedBeanSettings.BEAN_CONTAINER, - new SpringBeanContainer(beanFactory))); - } if (physicalNamingStrategy != null || implicitNamingStrategy != null) { customizers .add(new NamingStrategiesHibernatePropertiesCustomizer(physicalNamingStrategy, implicitNamingStrategy)); @@ -141,9 +121,13 @@ private List determineHibernatePropertiesCustomiz } @Override - protected AbstractJpaVendorAdapter createJpaVendorAdapter() { + protected HibernateJpaVendorAdapter createJpaVendorAdapter() { HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter(); this.sqlExceptionTranslator.ifUnique(adapter.getJpaDialect()::setJdbcExceptionTranslator); + JtaTransactionManager jtaTransactionManager = getJtaTransactionManager(); + if (jtaTransactionManager != null) { + adapter.setJtaTransactionManager(jtaTransactionManager); + } return adapter; } @@ -158,27 +142,11 @@ protected Map getVendorProperties(DataSource dataSource) { @Override protected void customizeVendorProperties(Map vendorProperties) { super.customizeVendorProperties(vendorProperties); - if (!vendorProperties.containsKey(JTA_PLATFORM)) { - configureJtaPlatform(vendorProperties); - } if (!vendorProperties.containsKey(PROVIDER_DISABLES_AUTOCOMMIT)) { configureProviderDisablesAutocommit(vendorProperties); } } - private void configureJtaPlatform(Map vendorProperties) throws LinkageError { - JtaTransactionManager jtaTransactionManager = getJtaTransactionManager(); - // Make sure Hibernate doesn't attempt to auto-detect a JTA platform - if (jtaTransactionManager == null) { - vendorProperties.put(JTA_PLATFORM, getNoJtaPlatformManager()); - } - // As of Hibernate 5.2, Hibernate can fully integrate with the WebSphere - // transaction manager on its own. - else if (!runningOnWebSphere()) { - configureSpringJtaPlatform(vendorProperties, jtaTransactionManager); - } - } - private void configureProviderDisablesAutocommit(Map vendorProperties) { if (isDataSourceAutoCommitDisabled() && !isJta()) { vendorProperties.put(PROVIDER_DISABLES_AUTOCOMMIT, "true"); @@ -190,52 +158,6 @@ private boolean isDataSourceAutoCommitDisabled() { return poolMetadata != null && Boolean.FALSE.equals(poolMetadata.getDefaultAutoCommit()); } - private boolean runningOnWebSphere() { - return ClassUtils.isPresent("com.ibm.websphere.jtaextensions.ExtendedJTATransaction", - getClass().getClassLoader()); - } - - private void configureSpringJtaPlatform(Map vendorProperties, - JtaTransactionManager jtaTransactionManager) { - try { - vendorProperties.put(JTA_PLATFORM, new SpringJtaPlatform(jtaTransactionManager)); - } - catch (LinkageError ex) { - // NoClassDefFoundError can happen if Hibernate 4.2 is used and some - // containers (e.g. JBoss EAP 6) wrap it in the superclass LinkageError - if (!isUsingJndi()) { - throw new IllegalStateException( - "Unable to set Hibernate JTA platform, are you using the correct version of Hibernate?", ex); - } - // Assume that Hibernate will use JNDI - if (logger.isDebugEnabled()) { - logger.debug("Unable to set Hibernate JTA platform : " + ex.getMessage()); - } - } - } - - private boolean isUsingJndi() { - try { - return JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable(); - } - catch (Error ex) { - return false; - } - } - - private Object getNoJtaPlatformManager() { - for (String candidate : NO_JTA_PLATFORM_CLASSES) { - try { - return Class.forName(candidate).getDeclaredConstructor().newInstance(); - } - catch (Exception ex) { - // Continue searching - } - } - throw new IllegalStateException( - "No available JtaPlatform candidates amongst " + Arrays.toString(NO_JTA_PLATFORM_CLASSES)); - } - private static class NamingStrategiesHibernatePropertiesCustomizer implements HibernatePropertiesCustomizer { private final @Nullable PhysicalNamingStrategy physicalNamingStrategy; diff --git a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java index e3ee44b7d3e6..406bee31d408 100644 --- a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java +++ b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java @@ -69,7 +69,6 @@ import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration; import org.springframework.boot.hibernate.SpringImplicitNamingStrategy; -import org.springframework.boot.hibernate.SpringJtaPlatform; import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfigurationTests.JpaUsingApplicationListenerConfiguration.EventCapturingApplicationListener; import org.springframework.boot.hibernate.autoconfigure.HibernateJpaConfiguration.HibernateRuntimeHints; import org.springframework.boot.hibernate.autoconfigure.mapping.NonAnnotatedEntity; @@ -109,6 +108,7 @@ import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.hibernate.ConfigurableJtaPlatform; import org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager; import org.springframework.orm.jpa.persistenceunit.ManagedClassNameFilter; import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; @@ -547,7 +547,7 @@ private ContextConsumer assertJpaVendorAdapter( @Test void jtaDefaultPlatform() { this.contextRunner.withUserConfiguration(JtaTransactionManagerConfiguration.class) - .run(assertJtaPlatform(SpringJtaPlatform.class)); + .run(assertJtaPlatform(ConfigurableJtaPlatform.class)); } @Test From 78c080997a439bba22fd2e142191dcd05cfe5baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 9 Mar 2026 14:19:32 +0100 Subject: [PATCH 299/930] Restore dependency alignment checks for Couchbase Client Closes gh-49259 --- platform/spring-boot-dependencies/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 252853a44145..48deaa384e4d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -318,6 +318,12 @@ bom { "java-client" ] } + alignWith { + version { + from "org.springframework.data:spring-data-couchbase" + managedBy "Spring Data Bom" + } + } links { site("https://docs.couchbase.com/java-sdk/current/hello-world/overview.html") javadoc("https://javadoc.io/doc/com.couchbase.client/java-client/{version}", "com.couchbase.client") From 735d67f768875e5c47c677c452c9fcf3d2f7ab8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 9 Mar 2026 17:24:05 +0100 Subject: [PATCH 300/930] Start building against Spring WS 5.0.1 snapshots See gh-49532 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 48deaa384e4d..524a529b73d5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2656,7 +2656,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-session/releases/tag/{version}") } } - library("Spring WS", "5.0.0") { + library("Spring WS", "5.0.1-SNAPSHOT") { considerSnapshots() group("org.springframework.ws") { bom("spring-ws-bom") From 10659f0d67da57e9ed9847e6fc4abb7738f794ba Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Mar 2026 16:18:51 +0000 Subject: [PATCH 301/930] Revert "Disable HTTP client's built-in decompression" This reverts commit c9246cec4fc745323a1e18d2d9a06c9c81564315. Closes gh-48928 --- .../ElasticsearchRestClientConfigurations.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchRestClientConfigurations.java b/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchRestClientConfigurations.java index 0d4621d03a92..27c330826c40 100644 --- a/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchRestClientConfigurations.java +++ b/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/autoconfigure/ElasticsearchRestClientConfigurations.java @@ -106,10 +106,8 @@ Rest5ClientBuilder elasticsearchRestClientBuilder(ElasticsearchConnectionDetails builder.setDefaultHeaders( new Header[] { new BasicHeader("Authorization", "ApiKey " + connectionDetails.getApiKey()) }); } - builder.setHttpClientConfigCallback((httpClientBuilder) -> { - httpClientBuilder.disableContentCompression(); - builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder)); - }); + builder.setHttpClientConfigCallback((httpClientBuilder) -> builderCustomizers.orderedStream() + .forEach((customizer) -> customizer.customize(httpClientBuilder))); builder.setConnectionManagerCallback((connectionManagerBuilder) -> builderCustomizers.orderedStream() .forEach((customizer) -> customizer.customize(connectionManagerBuilder))); builder.setConnectionConfigCallback((connectionConfigBuilder) -> builderCustomizers.orderedStream() From cc1578aecb5509444cde5f72ffe328672025ae48 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 9 Mar 2026 20:03:05 -0700 Subject: [PATCH 302/930] Allow empty objects to be loaded from YAML Update `OriginTrackedYamlLoader` so that empty objects are retained in the resulting `PropertySource`. Closes gh-48958 --- .../boot/env/OriginTrackedYamlLoader.java | 2 +- .../boot/env/OriginTrackedYamlLoaderTests.java | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedYamlLoader.java b/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedYamlLoader.java index 258181225336..82f1811acf28 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedYamlLoader.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedYamlLoader.java @@ -82,7 +82,7 @@ private Yaml createYaml(LoaderOptions loaderOptions) { List> load() { List> result = new ArrayList<>(); - process((properties, map) -> result.add(getFlattenedMap(map))); + process((properties, map) -> result.add(getFlattenedMap(map, true, ""))); return result; } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedYamlLoaderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedYamlLoaderTests.java index 29e223007942..0c0296bd800a 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedYamlLoaderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedYamlLoaderTests.java @@ -39,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link OriginTrackedYamlLoader}. @@ -154,9 +155,8 @@ void processEmptyAndNullValues() { @Test @WithTestYamlResource - void emptyMapsAreDropped() { - Object emptyMap = getValue("emptymap"); - assertThat(emptyMap).isNull(); + void emptyMapsOrObjectsAreNotDropped() { + assertThat(getFirstResult()).contains(entry("emptymap", "")); } @Test @@ -250,10 +250,15 @@ void canLoadFilesBiggerThan3Mb() { @SuppressWarnings("unchecked") private @Nullable T getValue(String name) { + return (T) getFirstResult().get(name); + } + + private Map getFirstResult() { if (this.result == null) { this.result = this.loader.load(); } - return (T) this.result.get(0).get(name); + Map map = this.result.get(0); + return map; } private String getLocation(OriginTrackedValue value) { From 677211e02e285c1eb6e0268efc9491cd4569e21a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Mar 2026 09:07:31 +0000 Subject: [PATCH 303/930] Switch back to using Micrometer Tracing's bom See gh-49466 and micrometer/micrometer-tracing#1354 --- platform/spring-boot-dependencies/build.gradle | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 524a529b73d5..b85e6014e068 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1609,23 +1609,8 @@ bom { } library("Micrometer Tracing", "1.7.0-SNAPSHOT") { considerSnapshots() - alignWith { - dependencyManagementDeclaredIn("io.micrometer:micrometer-tracing-bom") { - excluding { id -> - !id.artifactId().startsWith("micrometer-tracing") && !id.artifactId().equals("docs") - } - } - } group("io.micrometer") { - modules = [ - "docs", - "micrometer-tracing", - "micrometer-tracing-bridge-brave", - "micrometer-tracing-bridge-otel", - "micrometer-tracing-integration-test", - "micrometer-tracing-reporter-wavefront", - "micrometer-tracing-test" - ] + bom("micrometer-tracing-bom") } links { site("https://micrometer.io") @@ -1761,6 +1746,7 @@ bom { version { of "io.opentelemetry:opentelemetry-api" from "io.micrometer:micrometer-tracing-bridge-otel" + managedBy "Micrometer Tracing" } } links { From 050d0683c5e71f0e20c7811e7e4f5d8b3c6a4749 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Mar 2026 09:38:38 +0000 Subject: [PATCH 304/930] Start building against Spring Security 7.1.0-M3 snapshots See gh-49446 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b85e6014e068..941dee781ec6 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2604,7 +2604,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-restdocs/releases/tag/v{version}") } } - library("Spring Security", "7.1.0-M2") { + library("Spring Security", "7.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { bom("spring-security-bom") From 2ff451d502d8ba1ae8839cf046ace66307353adc Mon Sep 17 00:00:00 2001 From: itsmevichu Date: Sun, 1 Feb 2026 16:07:53 +0530 Subject: [PATCH 305/930] Add customizers for opaque token introspector builders See gh-49029 Signed-off-by: itsmevichu --- ...OpaqueTokenIntrospectionConfiguration.java | 12 ++++-- ...queTokenIntrospectorBuilderCustomizer.java | 39 +++++++++++++++++++ ...TokenIntrospectionClientConfiguration.java | 12 ++++-- ...queTokenIntrospectorBuilderCustomizer.java | 39 +++++++++++++++++++ ...2ResourceServerAutoConfigurationTests.java | 28 ++++++++++++- ...2ResourceServerAutoConfigurationTests.java | 30 +++++++++++++- 6 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/SpringOpaqueTokenIntrospectorBuilderCustomizer.java create mode 100644 module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer.java diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OpaqueTokenIntrospectionConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OpaqueTokenIntrospectionConfiguration.java index d68192277307..402eaab2e54b 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OpaqueTokenIntrospectionConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OpaqueTokenIntrospectionConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.boot.security.oauth2.server.resource.autoconfigure; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; @@ -36,16 +37,19 @@ class OpaqueTokenIntrospectionConfiguration { @Bean @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri") - SpringOpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServerProperties properties) { + SpringOpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServerProperties properties, + ObjectProvider customizers) { OAuth2ResourceServerProperties.Opaquetoken token = properties.getOpaquetoken(); Assert.state(token.getClientId() != null, "No 'spring.security.oauth2.resourceserver.opaquetoken.client-id' property specified"); Assert.state(token.getClientSecret() != null, "No 'spring.security.oauth2.resourceserver.opaquetoken.client-secret' property specified"); - return SpringOpaqueTokenIntrospector.withIntrospectionUri(token.getIntrospectionUri()) + SpringOpaqueTokenIntrospector.Builder builder = SpringOpaqueTokenIntrospector + .withIntrospectionUri(token.getIntrospectionUri()) .clientId(token.getClientId()) - .clientSecret(token.getClientSecret()) - .build(); + .clientSecret(token.getClientSecret()); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder.build(); } } diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/SpringOpaqueTokenIntrospectorBuilderCustomizer.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/SpringOpaqueTokenIntrospectorBuilderCustomizer.java new file mode 100644 index 000000000000..381e1e85a95f --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/SpringOpaqueTokenIntrospectorBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.security.oauth2.server.resource.autoconfigure; + +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector; + +/** + * Callback interface for the customization of the + * {@link SpringOpaqueTokenIntrospector.Builder} used to create the auto-configured + * {@link OpaqueTokenIntrospector}. + * + * @author Vishnutheep B + * @since 4.x.x + */ +@FunctionalInterface +public interface SpringOpaqueTokenIntrospectorBuilderCustomizer { + + /** + * Customize the given {@code builder}. + * @param builder the {@code builder} to customize + */ + void customize(SpringOpaqueTokenIntrospector.Builder builder); + +} diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java index 2db3421c39d1..9ea71025b939 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties; @@ -37,16 +38,19 @@ class ReactiveOpaqueTokenIntrospectionClientConfiguration { @Bean @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri") - SpringReactiveOpaqueTokenIntrospector reactiveOpaqueTokenIntrospector(OAuth2ResourceServerProperties properties) { + SpringReactiveOpaqueTokenIntrospector reactiveOpaqueTokenIntrospector(OAuth2ResourceServerProperties properties, + ObjectProvider customizers) { OAuth2ResourceServerProperties.Opaquetoken token = properties.getOpaquetoken(); Assert.state(token.getClientId() != null, "No 'spring.security.oauth2.resourceserver.opaquetoken.client-id' property specified"); Assert.state(token.getClientSecret() != null, "No 'spring.security.oauth2.resourceserver.opaquetoken.client-secret' property specified"); - return SpringReactiveOpaqueTokenIntrospector.withIntrospectionUri(token.getIntrospectionUri()) + SpringReactiveOpaqueTokenIntrospector.Builder builder = SpringReactiveOpaqueTokenIntrospector + .withIntrospectionUri(token.getIntrospectionUri()) .clientId(token.getClientId()) - .clientSecret(token.getClientSecret()) - .build(); + .clientSecret(token.getClientSecret()); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder.build(); } } diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer.java new file mode 100644 index 000000000000..d09fc335d692 --- /dev/null +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive; + +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector; + +/** + * Callback interface for the customization of the + * {@link SpringReactiveOpaqueTokenIntrospector.Builder} used to create the + * auto-configured {@link ReactiveOpaqueTokenIntrospector}. + * + * @author Vishnutheep B + * @since 4.x.x + */ +@FunctionalInterface +public interface SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer { + + /** + * Customize the given {@code builder}. + * @param builder the {@code builder} to customize + */ + void customize(SpringReactiveOpaqueTokenIntrospector.Builder builder); + +} diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java index 4e96d251b1eb..442fe082524c 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java @@ -428,7 +428,21 @@ void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionC "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", "spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id", "spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret") - .run((context) -> assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class)); + .run((context) -> { + assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class); + assertSpringOpaqueTokenIntrospectorBuilderCustomization(context); + }); + } + + private void assertSpringOpaqueTokenIntrospectorBuilderCustomization(AssertableWebApplicationContext context) { + SpringOpaqueTokenIntrospectorBuilderCustomizer customizer = context + .getBean("opaqueTokenIntrospectorBuilderCustomizer", SpringOpaqueTokenIntrospectorBuilderCustomizer.class); + SpringOpaqueTokenIntrospectorBuilderCustomizer anotherCustomizer = context.getBean( + "anotherOpaqueTokenIntrospectorBuilderCustomizer", + SpringOpaqueTokenIntrospectorBuilderCustomizer.class); + InOrder inOrder = inOrder(customizer, anotherCustomizer); + inOrder.verify(customizer).customize(any()); + inOrder.verify(anotherCustomizer).customize(any()); } @Test @@ -854,6 +868,18 @@ JwkSetUriJwtDecoderBuilderCustomizer anotherDecoderBuilderCustomizer() { return mock(JwkSetUriJwtDecoderBuilderCustomizer.class); } + @Bean + @Order(1) + SpringOpaqueTokenIntrospectorBuilderCustomizer opaqueTokenIntrospectorBuilderCustomizer() { + return mock(SpringOpaqueTokenIntrospectorBuilderCustomizer.class); + } + + @Bean + @Order(2) + SpringOpaqueTokenIntrospectorBuilderCustomizer anotherOpaqueTokenIntrospectorBuilderCustomizer() { + return mock(SpringOpaqueTokenIntrospectorBuilderCustomizer.class); + } + } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index d1dd3d19da67..e9b2e3bb26fa 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -400,7 +400,23 @@ void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionC "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", "spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id", "spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret") - .run((context) -> assertThat(context).hasSingleBean(ReactiveOpaqueTokenIntrospector.class)); + .run((context) -> { + assertThat(context).hasSingleBean(ReactiveOpaqueTokenIntrospector.class); + assertSpringReactiveOpaqueTokenIntrospectorBuilderCustomization(context); + }); + } + + private void assertSpringReactiveOpaqueTokenIntrospectorBuilderCustomization( + AssertableReactiveWebApplicationContext context) { + SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer customizer = context.getBean( + "reactiveOpaqueTokenIntrospectorBuilderCustomizer", + SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer.class); + SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer anotherCustomizer = context.getBean( + "anotherReactiveOpaqueTokenIntrospectorBuilderCustomizer", + SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer.class); + InOrder inOrder = inOrder(customizer, anotherCustomizer); + inOrder.verify(customizer).customize(any()); + inOrder.verify(anotherCustomizer).customize(any()); } @Test @@ -865,6 +881,18 @@ JwkSetUriReactiveJwtDecoderBuilderCustomizer anotherDecoderBuilderCustomizer() { return mock(JwkSetUriReactiveJwtDecoderBuilderCustomizer.class); } + @Bean + @Order(1) + SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer reactiveOpaqueTokenIntrospectorBuilderCustomizer() { + return mock(SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer.class); + } + + @Bean + @Order(2) + SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer anotherReactiveOpaqueTokenIntrospectorBuilderCustomizer() { + return mock(SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer.class); + } + } @Configuration(proxyBeanMethods = false) From 632a19ad6391a8a8e80a77cbb817f95cd9fdd961 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Mar 2026 09:59:58 +0000 Subject: [PATCH 306/930] Polish "Add customizers for opaque token introspector builders" See gh-49029 --- .../modules/reference/pages/web/spring-security.adoc | 6 +++++- ...SpringOpaqueTokenIntrospectorBuilderCustomizer.java | 10 +++++----- ...activeOpaqueTokenIntrospectorBuilderCustomizer.java | 10 +++++----- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/spring-security.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/spring-security.adoc index 0216cd13d344..ccbcc6c6a27c 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/spring-security.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/spring-security.adoc @@ -299,7 +299,11 @@ spring: ---- Again, the same properties are applicable for both servlet and reactive applications. -Alternatively, you can define your own javadoc:org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector[] bean for servlet applications or a javadoc:org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector[] for reactive applications. + +The result is an auto-configured introspector. Either a javadoc:org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector[] or, in a reactive application, a javadoc:org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector[]. +These auto-configured introspectors can be customized using javadoc:org.springframework.boot.security.oauth2.server.resource.autoconfigure.SpringOpaqueTokenIntrospectorBuilderCustomizer[] and javadoc:org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive.SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer[] beans respectively. + +To take complete control of the introspection, define your own javadoc:org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector[] or javadoc:org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector[] bean. diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/SpringOpaqueTokenIntrospectorBuilderCustomizer.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/SpringOpaqueTokenIntrospectorBuilderCustomizer.java index 381e1e85a95f..51796966efc0 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/SpringOpaqueTokenIntrospectorBuilderCustomizer.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/SpringOpaqueTokenIntrospectorBuilderCustomizer.java @@ -16,16 +16,16 @@ package org.springframework.boot.security.oauth2.server.resource.autoconfigure; -import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector.Builder; /** - * Callback interface for the customization of the - * {@link SpringOpaqueTokenIntrospector.Builder} used to create the auto-configured - * {@link OpaqueTokenIntrospector}. + * Callback interface for the customization of the {@link Builder + * SpringOpaqueTokenIntrospector.Builder} used to create the auto-configured + * {@link SpringOpaqueTokenIntrospector}. * * @author Vishnutheep B - * @since 4.x.x + * @since 4.1.0 */ @FunctionalInterface public interface SpringOpaqueTokenIntrospectorBuilderCustomizer { diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer.java index d09fc335d692..bed93dd0e9b5 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer.java @@ -16,16 +16,16 @@ package org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive; -import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector.Builder; /** - * Callback interface for the customization of the - * {@link SpringReactiveOpaqueTokenIntrospector.Builder} used to create the - * auto-configured {@link ReactiveOpaqueTokenIntrospector}. + * Callback interface for the customization of the {@link Builder + * SpringReactiveOpaqueTokenIntrospector.Builder} used to create the auto-configured + * {@link SpringReactiveOpaqueTokenIntrospector}. * * @author Vishnutheep B - * @since 4.x.x + * @since 4.1.0 */ @FunctionalInterface public interface SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer { From 27723a11ab611a49dac9fd10be8161b62e30b3a1 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 10 Mar 2026 14:18:38 +0100 Subject: [PATCH 307/930] Add properties to configure BatchLogRecordProcessor Closes gh-49543 --- .../reference/pages/actuator/loggers.adoc | 12 +++ ...OpenTelemetryLoggingAutoConfiguration.java | 16 ++- .../OpenTelemetryLoggingProperties.java | 98 +++++++++++++++++++ ...elemetryLoggingAutoConfigurationTests.java | 22 +++++ 4 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingProperties.java diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/loggers.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/loggers.adoc index 07f0a500df45..7e4976690442 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/loggers.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/loggers.adoc @@ -49,6 +49,18 @@ management: endpoint: "https://otlp.example.com:4318/v1/logs" ---- +The `management.opentelemetry.logging.export.*` configuration properties can be used to configure the javadoc:io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor[]. +For example, to change the export interval to 15 seconds: + +[configprops,yaml] +---- +management: + opentelemetry: + logging: + export: + schedule-delay: "15s" +---- + NOTE: The OpenTelemetry Logback appender and Log4j appender are not part of Spring Boot. For more details, see the https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/logback/logback-appender-1.0/library[OpenTelemetry Logback appender] or the https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/log4j/log4j-appender-2.17/library[OpenTelemetry Log4j2 appender] in the https://github.com/open-telemetry/opentelemetry-java-instrumentation[OpenTelemetry Java instrumentation GitHub repository]. diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfiguration.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfiguration.java index 56da453b7aac..b2329ce95d82 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfiguration.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfiguration.java @@ -29,7 +29,9 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration; +import org.springframework.boot.opentelemetry.autoconfigure.logging.OpenTelemetryLoggingProperties.Export; import org.springframework.context.annotation.Bean; /** @@ -40,16 +42,26 @@ */ @AutoConfiguration(after = OpenTelemetrySdkAutoConfiguration.class) @ConditionalOnClass(SdkLoggerProvider.class) +@EnableConfigurationProperties(OpenTelemetryLoggingProperties.class) public final class OpenTelemetryLoggingAutoConfiguration { - OpenTelemetryLoggingAutoConfiguration() { + private final OpenTelemetryLoggingProperties properties; + + OpenTelemetryLoggingAutoConfiguration(OpenTelemetryLoggingProperties properties) { + this.properties = properties; } @Bean @ConditionalOnMissingBean BatchLogRecordProcessor openTelemetryBatchLogRecordProcessor(ObjectProvider logRecordExporters) { LogRecordExporter exporter = LogRecordExporter.composite(logRecordExporters.orderedStream().toList()); - return BatchLogRecordProcessor.builder(exporter).build(); + Export export = this.properties.getExport(); + return BatchLogRecordProcessor.builder(exporter) + .setExporterTimeout(export.getTimeout()) + .setMaxQueueSize(export.getMaxQueueSize()) + .setScheduleDelay(export.getScheduleDelay()) + .setMaxExportBatchSize(export.getMaxBatchSize()) + .build(); } @Bean diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingProperties.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingProperties.java new file mode 100644 index 000000000000..f405a1a38ee9 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingProperties.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.opentelemetry.autoconfigure.logging; + +import java.time.Duration; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for logging with OpenTelemetry. + * + * @author Moritz Halbritter + * @since 4.1.0 + */ +@ConfigurationProperties("management.opentelemetry.logging") +public class OpenTelemetryLoggingProperties { + + /** + * Logs export configuration. + */ + private final Export export = new Export(); + + public Export getExport() { + return this.export; + } + + public static class Export { + + /** + * Maximum time an export will be allowed to run before being cancelled. + */ + private Duration timeout = Duration.ofSeconds(30); + + /** + * Maximum batch size for each export. This must be less than or equal to + * 'maxQueueSize'. + */ + private int maxBatchSize = 512; + + /** + * Maximum number of logs that are kept in the queue before they will be dropped. + */ + private int maxQueueSize = 2048; + + /** + * The delay interval between two consecutive exports. + */ + private Duration scheduleDelay = Duration.ofSeconds(1); + + public Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + + public int getMaxBatchSize() { + return this.maxBatchSize; + } + + public void setMaxBatchSize(int maxBatchSize) { + this.maxBatchSize = maxBatchSize; + } + + public int getMaxQueueSize() { + return this.maxQueueSize; + } + + public void setMaxQueueSize(int maxQueueSize) { + this.maxQueueSize = maxQueueSize; + } + + public Duration getScheduleDelay() { + return this.scheduleDelay; + } + + public void setScheduleDelay(Duration scheduleDelay) { + this.scheduleDelay = scheduleDelay; + } + + } + +} diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java index d12e3eb2b580..3438b1670620 100644 --- a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java @@ -16,7 +16,9 @@ package org.springframework.boot.opentelemetry.autoconfigure.logging; +import java.time.Duration; import java.util.Collection; +import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; import io.opentelemetry.context.Context; @@ -123,6 +125,26 @@ void whenHasMultipleSdkLoggerProviderBuilderCustomizersCallsCustomizeMethod() { }); } + @Test + void shouldConfigureBatchLogRecordProcessorWithProperties() { + this.contextRunner + .withPropertyValues("management.opentelemetry.logging.export.timeout=45s", + "management.opentelemetry.logging.export.max-batch-size=256", + "management.opentelemetry.logging.export.max-queue-size=4096", + "management.opentelemetry.logging.export.schedule-delay=15s") + .run((context) -> { + assertThat(context).hasSingleBean(BatchLogRecordProcessor.class); + BatchLogRecordProcessor processor = context.getBean(BatchLogRecordProcessor.class); + assertThat(processor).extracting("worker") + .hasFieldOrPropertyWithValue("exporterTimeoutNanos", Duration.ofSeconds(45).toNanos()) + .hasFieldOrPropertyWithValue("maxExportBatchSize", 256) + .hasFieldOrPropertyWithValue("scheduleDelayNanos", Duration.ofSeconds(15).toNanos()) + .extracting("queue") + .isInstanceOfSatisfying(ArrayBlockingQueue.class, + (queue) -> assertThat(queue.remainingCapacity()).isEqualTo(4096)); + }); + } + @Configuration(proxyBeanMethods = false) static class UserConfiguration { From 951d37a25ce0e671f0725f8c3ed7630bbe6e42c8 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 10 Mar 2026 15:08:11 +0100 Subject: [PATCH 308/930] Add properties to select OpenTelemetry's sampler Closes gh-49548 --- .../reference/pages/actuator/tracing.adoc | 33 +++++++++ ...OpenTelemetryTracingAutoConfiguration.java | 18 +++-- .../OpenTelemetryTracingProperties.java | 23 +++++++ ...elemetryTracingAutoConfigurationTests.java | 69 ++++++++++++++++++- 4 files changed, 135 insertions(+), 8 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc index 83bdcdb36f55..de30d41e9ef4 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc @@ -150,6 +150,39 @@ Use the `management.tracing.export.zipkin.*` configuration properties to configu +[[actuator.micrometer-tracing.sampling]] +== Sampling + +By default, Spring Boot samples only 10% of requests to prevent overwhelming the trace backend. +The configprop:management.tracing.sampling.probability[] property can be used to configure this. + +When using OpenTelemetry, you can also configure which sampler is used via the configprop:management.opentelemetry.tracing.sampler[] property. +The following samplers are supported: + +|=== +|Sampler |Description + +|`always-on` +|Samples every trace. + +|`always-off` +|Discards every trace. + +|`trace-id-ratio` +|Samples a fraction of traces based on configprop:management.tracing.sampling.probability[]. + +|`parent-based-always-on` +|If the parent span is sampled, samples the child span. If there is no parent, samples every trace. + +|`parent-based-always-off` +|If the parent span is sampled, samples the child span. If there is no parent, discards every trace. + +|`parent-based-trace-id-ratio` (default) +|If the parent span is sampled, samples the child span. If there is no parent, samples a fraction of traces based on configprop:management.tracing.sampling.probability[]. +|=== + + + [[actuator.micrometer-tracing.micrometer-observation]] == Integration with Micrometer Observation diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfiguration.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfiguration.java index 8b2b23229640..f2d18faa818c 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfiguration.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfiguration.java @@ -58,6 +58,9 @@ import org.springframework.boot.micrometer.tracing.autoconfigure.MicrometerTracingAutoConfiguration; import org.springframework.boot.micrometer.tracing.autoconfigure.NoopTracerAutoConfiguration; import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties; +import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryPropagationConfigurations.NoPropagation; +import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryPropagationConfigurations.PropagationWithBaggage; +import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryPropagationConfigurations.PropagationWithoutBaggage; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryTracingProperties.Export; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; @@ -74,9 +77,7 @@ @AutoConfiguration(before = { MicrometerTracingAutoConfiguration.class, NoopTracerAutoConfiguration.class }) @ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class }) @EnableConfigurationProperties({ TracingProperties.class, OpenTelemetryTracingProperties.class }) -@Import({ OpenTelemetryPropagationConfigurations.PropagationWithoutBaggage.class, - OpenTelemetryPropagationConfigurations.PropagationWithBaggage.class, - OpenTelemetryPropagationConfigurations.NoPropagation.class }) +@Import({ PropagationWithoutBaggage.class, PropagationWithBaggage.class, NoPropagation.class }) public final class OpenTelemetryTracingAutoConfiguration { private static final Log logger = LogFactory.getLog(OpenTelemetryTracingAutoConfiguration.class); @@ -113,8 +114,15 @@ ContextPropagators otelContextPropagators(ObjectProvider text @Bean @ConditionalOnMissingBean Sampler otelSampler() { - Sampler rootSampler = Sampler.traceIdRatioBased(this.tracingProperties.getSampling().getProbability()); - return Sampler.parentBased(rootSampler); + return switch (this.openTelemetryTracingProperties.getSampler()) { + case ALWAYS_ON -> Sampler.alwaysOn(); + case ALWAYS_OFF -> Sampler.alwaysOff(); + case TRACE_ID_RATIO -> Sampler.traceIdRatioBased(this.tracingProperties.getSampling().getProbability()); + case PARENT_BASED_ALWAYS_ON -> Sampler.parentBased(Sampler.alwaysOn()); + case PARENT_BASED_ALWAYS_OFF -> Sampler.parentBased(Sampler.alwaysOff()); + case PARENT_BASED_TRACE_ID_RATIO -> + Sampler.parentBased(Sampler.traceIdRatioBased(this.tracingProperties.getSampling().getProbability())); + }; } @Bean diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingProperties.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingProperties.java index ecf80b850ef3..ffeeeb15f282 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingProperties.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingProperties.java @@ -34,10 +34,23 @@ public class OpenTelemetryTracingProperties { */ private final Export export = new Export(); + /** + * Sampler to use. + */ + private Sampler sampler = Sampler.PARENT_BASED_TRACE_ID_RATIO; + public Export getExport() { return this.export; } + public Sampler getSampler() { + return this.sampler; + } + + public void setSampler(Sampler sampler) { + this.sampler = sampler; + } + public static class Export { /** @@ -108,4 +121,14 @@ public void setScheduleDelay(Duration scheduleDelay) { } + /** + * Supported samplers. + */ + public enum Sampler { + + ALWAYS_ON, ALWAYS_OFF, TRACE_ID_RATIO, PARENT_BASED_ALWAYS_ON, PARENT_BASED_ALWAYS_OFF, + PARENT_BASED_TRACE_ID_RATIO + + } + } diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java index c09a8ca31567..762aa0eaec31 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java @@ -118,14 +118,77 @@ void shouldSupplyBeans() { } @Test - void samplerIsParentBased() { + void samplerIsParentBasedTraceIdRatioByDefault() { this.contextRunner.run((context) -> { Sampler sampler = context.getBean(Sampler.class); - assertThat(sampler).isNotNull(); - assertThat(sampler.getDescription()).startsWith("ParentBased{"); + assertThat(sampler.getDescription()).startsWith("ParentBased{root:TraceIdRatioBased{"); }); } + @Test + void samplerUsesCustomProbability() { + this.contextRunner.withPropertyValues("management.tracing.sampling.probability=0.5").run((context) -> { + Sampler sampler = context.getBean(Sampler.class); + assertThat(sampler.getDescription()).contains("0.5"); + }); + } + + @Test + void samplerCanBeSetToAlwaysOn() { + this.contextRunner.withPropertyValues("management.opentelemetry.tracing.sampler=always-on").run((context) -> { + Sampler sampler = context.getBean(Sampler.class); + assertThat(sampler.getDescription()).isEqualTo("AlwaysOnSampler"); + }); + } + + @Test + void samplerCanBeSetToAlwaysOff() { + this.contextRunner.withPropertyValues("management.opentelemetry.tracing.sampler=always-off").run((context) -> { + Sampler sampler = context.getBean(Sampler.class); + assertThat(sampler.getDescription()).isEqualTo("AlwaysOffSampler"); + }); + } + + @Test + void samplerCanBeSetToTraceIdRatio() { + this.contextRunner + .withPropertyValues("management.opentelemetry.tracing.sampler=trace-id-ratio", + "management.tracing.sampling.probability=0.3") + .run((context) -> { + Sampler sampler = context.getBean(Sampler.class); + assertThat(sampler.getDescription()).startsWith("TraceIdRatioBased{").contains("0.3"); + }); + } + + @Test + void samplerCanBeSetToParentBasedAlwaysOn() { + this.contextRunner.withPropertyValues("management.opentelemetry.tracing.sampler=parent-based-always-on") + .run((context) -> { + Sampler sampler = context.getBean(Sampler.class); + assertThat(sampler.getDescription()).startsWith("ParentBased{root:AlwaysOnSampler"); + }); + } + + @Test + void samplerCanBeSetToParentBasedAlwaysOff() { + this.contextRunner.withPropertyValues("management.opentelemetry.tracing.sampler=parent-based-always-off") + .run((context) -> { + Sampler sampler = context.getBean(Sampler.class); + assertThat(sampler.getDescription()).startsWith("ParentBased{root:AlwaysOffSampler"); + }); + } + + @Test + void samplerCanBeSetToParentBasedTraceIdRatio() { + this.contextRunner + .withPropertyValues("management.opentelemetry.tracing.sampler=parent-based-trace-id-ratio", + "management.tracing.sampling.probability=0.7") + .run((context) -> { + Sampler sampler = context.getBean(Sampler.class); + assertThat(sampler.getDescription()).startsWith("ParentBased{root:TraceIdRatioBased{").contains("0.7"); + }); + } + @ParameterizedTest @ValueSource(strings = { "io.micrometer.tracing.otel", "io.opentelemetry.sdk", "io.opentelemetry.api" }) void shouldNotSupplyBeansIfDependencyIsMissing(String packageName) { From 4a01cfccbb3ab88d47ee7993e8e355bded4ab8f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 10 Mar 2026 15:03:55 +0100 Subject: [PATCH 309/930] Upgrade to Commons Logging 1.3.6 Closes gh-49549 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 941dee781ec6..3906e7a5dc45 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -284,7 +284,7 @@ bom { releaseNotes("https://commons.apache.org/proper/commons-lang/changes.html#a{version}") } } - library("Commons Logging", "1.3.5") { + library("Commons Logging", "1.3.6") { group("commons-logging") { modules = [ "commons-logging" From 2fc236c62278a167b282d703393067cce94cc568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 10 Mar 2026 15:03:59 +0100 Subject: [PATCH 310/930] Upgrade to DB2 JDBC 12.1.4.0 Closes gh-49550 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3906e7a5dc45..9abd0ae379ca 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -347,7 +347,7 @@ bom { releaseNotes("https://github.com/CycloneDX/cyclonedx-maven-plugin/releases/tag/cyclonedx-maven-plugin-{version}") } } - library("DB2 JDBC", "12.1.3.0") { + library("DB2 JDBC", "12.1.4.0") { group("com.ibm.db2") { modules = [ "jcc" From cd12000f3d4825b131066cc73f0cbf786d39b90f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 10 Mar 2026 15:04:05 +0100 Subject: [PATCH 311/930] Upgrade to Flyway 12.1.0 Closes gh-49551 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9abd0ae379ca..2c2ce759f003 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -425,7 +425,7 @@ bom { javadoc("elasticsearch-java", version -> "https://javadoc.io/doc/co.elastic.clients/elasticsearch-java/%s/index.html".formatted(version), "co.elastic.clients.elasticsearch", "co.elastic.clients.transport") } } - library("Flyway", "12.0.3") { + library("Flyway", "12.1.0") { group("org.flywaydb") { modules = [ "flyway-command-test-connection", From 87514ebc9708e0c03523ba9053fb7c16a82ad3a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 10 Mar 2026 15:04:05 +0100 Subject: [PATCH 312/930] Upgrade to Micrometer 1.17.0-M3 Closes gh-49435 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 2c2ce759f003..aa507581da65 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1584,7 +1584,7 @@ bom { releaseNotes("https://github.com/apache/maven-war-plugin/releases/tag/maven-war-plugin-{version}") } } - library("Micrometer", "1.17.0-SNAPSHOT") { + library("Micrometer", "1.17.0-M3") { considerSnapshots() group("io.micrometer") { modules = [ From 250277a575324068569530613372fd4604268ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 10 Mar 2026 15:04:05 +0100 Subject: [PATCH 313/930] Upgrade to Micrometer Tracing 1.7.0-M4 Closes gh-49436 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index aa507581da65..e914c6e85d2a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1607,7 +1607,7 @@ bom { releaseNotes("https://github.com/micrometer-metrics/micrometer/releases/tag/v{version}") } } - library("Micrometer Tracing", "1.7.0-SNAPSHOT") { + library("Micrometer Tracing", "1.7.0-M4") { considerSnapshots() group("io.micrometer") { bom("micrometer-tracing-bom") From 2681259a8a37a362f7a03a936507fd1524e6aaeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 10 Mar 2026 15:04:06 +0100 Subject: [PATCH 314/930] Upgrade to Reactor Bom 2025.0.4 Closes gh-49437 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e914c6e85d2a..d367439f55b2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2006,7 +2006,7 @@ bom { ] } } - library("Reactor Bom", "2025.0.4-SNAPSHOT") { + library("Reactor Bom", "2025.0.4") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From 5789f9bdcabf56be6a753f8059e65ace68f99f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 10 Mar 2026 16:14:10 +0100 Subject: [PATCH 315/930] Add missing dependency management for httpclient5-observation Closes gh-49553 --- platform/spring-boot-dependencies/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d367439f55b2..b84575390a16 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -708,7 +708,8 @@ bom { modules = [ "httpclient5", "httpclient5-cache", - "httpclient5-fluent" + "httpclient5-fluent", + "httpclient5-observation" ] } } From a2997f34a44f1112c4858abba88206c0a2fceb0e Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 10 Mar 2026 16:44:07 +0100 Subject: [PATCH 316/930] Add support for OpenTelemetry's SpanLimits and LogLimits Closes gh-49554 --- ...OpenTelemetryTracingAutoConfiguration.java | 23 ++++- .../OpenTelemetryTracingProperties.java | 94 +++++++++++++++++++ ...elemetryTracingAutoConfigurationTests.java | 39 ++++++++ .../OpenTelemetryTracingPropertiesTests.java | 43 +++++++++ ...OpenTelemetryLoggingAutoConfiguration.java | 19 +++- .../OpenTelemetryLoggingProperties.java | 42 +++++++++ ...elemetryLoggingAutoConfigurationTests.java | 32 +++++++ .../OpenTelemetryLoggingPropertiesTests.java | 39 ++++++++ 8 files changed, 326 insertions(+), 5 deletions(-) create mode 100644 module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingPropertiesTests.java create mode 100644 module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingPropertiesTests.java diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfiguration.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfiguration.java index f2d18faa818c..490f40ad318d 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfiguration.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfiguration.java @@ -40,6 +40,7 @@ import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.SpanLimits; import io.opentelemetry.sdk.trace.SpanProcessor; import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; import io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder; @@ -62,6 +63,7 @@ import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryPropagationConfigurations.PropagationWithBaggage; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryPropagationConfigurations.PropagationWithoutBaggage; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryTracingProperties.Export; +import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryTracingProperties.Limits; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.util.CollectionUtils; @@ -98,13 +100,30 @@ public final class OpenTelemetryTracingAutoConfiguration { @Bean @ConditionalOnMissingBean SdkTracerProvider otelSdkTracerProvider(Resource resource, SpanProcessors spanProcessors, Sampler sampler, - ObjectProvider customizers) { - SdkTracerProviderBuilder builder = SdkTracerProvider.builder().setSampler(sampler).setResource(resource); + ObjectProvider customizers, SpanLimits spanLimits) { + SdkTracerProviderBuilder builder = SdkTracerProvider.builder() + .setSampler(sampler) + .setResource(resource) + .setSpanLimits(spanLimits); spanProcessors.forEach(builder::addSpanProcessor); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } + @Bean + @ConditionalOnMissingBean + SpanLimits otelSpanLimits() { + Limits limits = this.openTelemetryTracingProperties.getLimits(); + return SpanLimits.builder() + .setMaxAttributeValueLength(limits.getMaxAttributeValueLength()) + .setMaxNumberOfAttributes(limits.getMaxAttributes()) + .setMaxNumberOfEvents(limits.getMaxEvents()) + .setMaxNumberOfLinks(limits.getMaxLinks()) + .setMaxNumberOfAttributesPerEvent(limits.getMaxAttributesPerEvent()) + .setMaxNumberOfAttributesPerLink(limits.getMaxAttributesPerLink()) + .build(); + } + @Bean @ConditionalOnMissingBean ContextPropagators otelContextPropagators(ObjectProvider textMapPropagators) { diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingProperties.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingProperties.java index ffeeeb15f282..b1a4af4d541d 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingProperties.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingProperties.java @@ -34,6 +34,11 @@ public class OpenTelemetryTracingProperties { */ private final Export export = new Export(); + /** + * Span limit configuration. + */ + private final Limits limits = new Limits(); + /** * Sampler to use. */ @@ -43,6 +48,10 @@ public Export getExport() { return this.export; } + public Limits getLimits() { + return this.limits; + } + public Sampler getSampler() { return this.sampler; } @@ -121,6 +130,91 @@ public void setScheduleDelay(Duration scheduleDelay) { } + /** + * Span limits. + */ + public static class Limits { + + /** + * Maximum number of characters for string attribute values. + */ + private int maxAttributeValueLength = Integer.MAX_VALUE; + + /** + * Maximum number of attributes per span. + */ + private int maxAttributes = 128; + + /** + * Maximum number of events per span. + */ + private int maxEvents = 128; + + /** + * Maximum number of links per span. + */ + private int maxLinks = 128; + + /** + * Maximum number of attributes per event. + */ + private int maxAttributesPerEvent = 128; + + /** + * Maximum number of attributes per link. + */ + private int maxAttributesPerLink = 128; + + public int getMaxAttributeValueLength() { + return this.maxAttributeValueLength; + } + + public void setMaxAttributeValueLength(int maxAttributeValueLength) { + this.maxAttributeValueLength = maxAttributeValueLength; + } + + public int getMaxAttributes() { + return this.maxAttributes; + } + + public void setMaxAttributes(int maxAttributes) { + this.maxAttributes = maxAttributes; + } + + public int getMaxEvents() { + return this.maxEvents; + } + + public void setMaxEvents(int maxEvents) { + this.maxEvents = maxEvents; + } + + public int getMaxLinks() { + return this.maxLinks; + } + + public void setMaxLinks(int maxLinks) { + this.maxLinks = maxLinks; + } + + public int getMaxAttributesPerEvent() { + return this.maxAttributesPerEvent; + } + + public void setMaxAttributesPerEvent(int maxAttributesPerEvent) { + this.maxAttributesPerEvent = maxAttributesPerEvent; + } + + public int getMaxAttributesPerLink() { + return this.maxAttributesPerLink; + } + + public void setMaxAttributesPerLink(int maxAttributesPerLink) { + this.maxAttributesPerLink = maxAttributesPerLink; + } + + } + /** * Supported samplers. */ diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java index 762aa0eaec31..4dfaa732612e 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java @@ -114,6 +114,7 @@ void shouldSupplyBeans() { assertThat(context).hasSingleBean(OtelSpanCustomizer.class); assertThat(context).hasSingleBean(SpanProcessors.class); assertThat(context).hasSingleBean(SpanExporters.class); + assertThat(context).hasSingleBean(SpanLimits.class); }); } @@ -387,6 +388,34 @@ void shouldConfigureRemoteAndTaggedFields() { }); } + @Test + void spanLimitsShouldBeConfiguredWithCustomProperties() { + this.contextRunner + .withPropertyValues("management.opentelemetry.tracing.limits.max-attribute-value-length=256", + "management.opentelemetry.tracing.limits.max-attributes=64", + "management.opentelemetry.tracing.limits.max-events=32", + "management.opentelemetry.tracing.limits.max-links=16", + "management.opentelemetry.tracing.limits.max-attributes-per-event=8", + "management.opentelemetry.tracing.limits.max-attributes-per-link=4") + .run((context) -> { + SpanLimits spanLimits = context.getBean(SpanLimits.class); + assertThat(spanLimits.getMaxAttributeValueLength()).isEqualTo(256); + assertThat(spanLimits.getMaxNumberOfAttributes()).isEqualTo(64); + assertThat(spanLimits.getMaxNumberOfEvents()).isEqualTo(32); + assertThat(spanLimits.getMaxNumberOfLinks()).isEqualTo(16); + assertThat(spanLimits.getMaxNumberOfAttributesPerEvent()).isEqualTo(8); + assertThat(spanLimits.getMaxNumberOfAttributesPerLink()).isEqualTo(4); + }); + } + + @Test + void shouldBackOffOnCustomSpanLimitsBean() { + this.contextRunner.withUserConfiguration(CustomSpanLimitsConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(SpanLimits.class); + assertThat(context).hasBean("customSpanLimits"); + }); + } + @Test void shouldCustomizeSdkTracerProvider() { this.contextRunner.withUserConfiguration(SdkTracerProviderCustomizationConfiguration.class).run((context) -> { @@ -621,6 +650,16 @@ Propagator customPropagator() { } + @Configuration(proxyBeanMethods = false) + private static final class CustomSpanLimitsConfiguration { + + @Bean + SpanLimits customSpanLimits() { + return SpanLimits.builder().setMaxNumberOfEvents(99).build(); + } + + } + @Configuration(proxyBeanMethods = false) private static final class SdkTracerProviderCustomizationConfiguration { diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingPropertiesTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingPropertiesTests.java new file mode 100644 index 000000000000..95c30b897962 --- /dev/null +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingPropertiesTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure; + +import io.opentelemetry.sdk.trace.SpanLimits; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OpenTelemetryTracingProperties}. + * + * @author Moritz Halbritter + */ +class OpenTelemetryTracingPropertiesTests { + + @Test + void defaultValuesAreConsistent() { + OpenTelemetryTracingProperties.Limits limits = new OpenTelemetryTracingProperties.Limits(); + SpanLimits defaults = SpanLimits.getDefault(); + assertThat(limits.getMaxAttributeValueLength()).isEqualTo(defaults.getMaxAttributeValueLength()); + assertThat(limits.getMaxAttributes()).isEqualTo(defaults.getMaxNumberOfAttributes()); + assertThat(limits.getMaxEvents()).isEqualTo(defaults.getMaxNumberOfEvents()); + assertThat(limits.getMaxLinks()).isEqualTo(defaults.getMaxNumberOfLinks()); + assertThat(limits.getMaxAttributesPerEvent()).isEqualTo(defaults.getMaxNumberOfAttributesPerEvent()); + assertThat(limits.getMaxAttributesPerLink()).isEqualTo(defaults.getMaxNumberOfAttributesPerLink()); + } + +} diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfiguration.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfiguration.java index b2329ce95d82..5863e2e1bd5d 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfiguration.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.boot.opentelemetry.autoconfigure.logging; +import io.opentelemetry.sdk.logs.LogLimits; import io.opentelemetry.sdk.logs.LogRecordProcessor; import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; @@ -32,6 +33,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration; import org.springframework.boot.opentelemetry.autoconfigure.logging.OpenTelemetryLoggingProperties.Export; +import org.springframework.boot.opentelemetry.autoconfigure.logging.OpenTelemetryLoggingProperties.Limits; import org.springframework.context.annotation.Bean; /** @@ -69,12 +71,23 @@ BatchLogRecordProcessor openTelemetryBatchLogRecordProcessor(ObjectProvider logRecordProcessors, - ObjectProvider customizers) { - SdkLoggerProviderBuilder builder = SdkLoggerProvider.builder(); - builder.setResource(openTelemetryResource); + ObjectProvider customizers, LogLimits logLimits) { + SdkLoggerProviderBuilder builder = SdkLoggerProvider.builder() + .setResource(openTelemetryResource) + .setLogLimits(() -> logLimits); logRecordProcessors.orderedStream().forEach(builder::addLogRecordProcessor); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } + @Bean + @ConditionalOnMissingBean + LogLimits openTelemetryLogLimits() { + Limits limits = this.properties.getLimits(); + return LogLimits.builder() + .setMaxAttributeValueLength(limits.getMaxAttributeValueLength()) + .setMaxNumberOfAttributes(limits.getMaxAttributes()) + .build(); + } + } diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingProperties.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingProperties.java index f405a1a38ee9..293d673a5664 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingProperties.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingProperties.java @@ -34,10 +34,19 @@ public class OpenTelemetryLoggingProperties { */ private final Export export = new Export(); + /** + * Log limits configuration. + */ + private final Limits limits = new Limits(); + public Export getExport() { return this.export; } + public Limits getLimits() { + return this.limits; + } + public static class Export { /** @@ -95,4 +104,37 @@ public void setScheduleDelay(Duration scheduleDelay) { } + /** + * Log limits. + */ + public static class Limits { + + /** + * Maximum number of characters for string attribute values. + */ + private int maxAttributeValueLength = Integer.MAX_VALUE; + + /** + * Maximum number of attributes per log record. + */ + private int maxAttributes = 128; + + public int getMaxAttributeValueLength() { + return this.maxAttributeValueLength; + } + + public void setMaxAttributeValueLength(int maxAttributeValueLength) { + this.maxAttributeValueLength = maxAttributeValueLength; + } + + public int getMaxAttributes() { + return this.maxAttributes; + } + + public void setMaxAttributes(int maxAttributes) { + this.maxAttributes = maxAttributes; + } + + } + } diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java index 3438b1670620..c964911f8e74 100644 --- a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java @@ -23,6 +23,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logs.LogLimits; import io.opentelemetry.sdk.logs.LogRecordProcessor; import io.opentelemetry.sdk.logs.ReadWriteLogRecord; import io.opentelemetry.sdk.logs.SdkLoggerProvider; @@ -65,6 +66,7 @@ void providesBeans() { this.contextRunner.run((context) -> { assertThat(context).hasSingleBean(BatchLogRecordProcessor.class); assertThat(context).hasSingleBean(SdkLoggerProvider.class); + assertThat(context).hasSingleBean(LogLimits.class); }); } @@ -125,6 +127,26 @@ void whenHasMultipleSdkLoggerProviderBuilderCustomizersCallsCustomizeMethod() { }); } + @Test + void logLimitsShouldBeConfiguredWithCustomProperties() { + this.contextRunner + .withPropertyValues("management.opentelemetry.logging.limits.max-attribute-value-length=256", + "management.opentelemetry.logging.limits.max-attributes=64") + .run((context) -> { + LogLimits logLimits = context.getBean(LogLimits.class); + assertThat(logLimits.getMaxAttributeValueLength()).isEqualTo(256); + assertThat(logLimits.getMaxNumberOfAttributes()).isEqualTo(64); + }); + } + + @Test + void shouldBackOffOnCustomLogLimitsBean() { + this.contextRunner.withUserConfiguration(CustomLogLimitsConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(LogLimits.class); + assertThat(context).hasBean("customLogLimits"); + }); + } + @Test void shouldConfigureBatchLogRecordProcessorWithProperties() { this.contextRunner @@ -205,6 +227,16 @@ SdkLoggerProviderBuilderCustomizer customSdkLoggerProviderBuilderCustomizer2() { } + @Configuration(proxyBeanMethods = false) + static class CustomLogLimitsConfiguration { + + @Bean + LogLimits customLogLimits() { + return LogLimits.builder().setMaxNumberOfAttributes(99).build(); + } + + } + static class NoopLogRecordExporter implements LogRecordExporter { @Override diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingPropertiesTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingPropertiesTests.java new file mode 100644 index 000000000000..f3088a60861e --- /dev/null +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingPropertiesTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.opentelemetry.autoconfigure.logging; + +import io.opentelemetry.sdk.logs.LogLimits; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OpenTelemetryLoggingProperties}. + * + * @author Moritz Halbritter + */ +class OpenTelemetryLoggingPropertiesTests { + + @Test + void defaultValuesAreConsistent() { + OpenTelemetryLoggingProperties.Limits limits = new OpenTelemetryLoggingProperties.Limits(); + LogLimits defaults = LogLimits.getDefault(); + assertThat(limits.getMaxAttributeValueLength()).isEqualTo(defaults.getMaxAttributeValueLength()); + assertThat(limits.getMaxAttributes()).isEqualTo(defaults.getMaxNumberOfAttributes()); + } + +} From 81d04f2c4da87831fc8c2fcabb1c590a3e1a0249 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 10 Mar 2026 17:06:49 +0100 Subject: [PATCH 317/930] Add documentation for the new span and log limits See gh-49554 --- .../reference/pages/actuator/loggers.adoc | 15 +++++++++++++ .../reference/pages/actuator/tracing.adoc | 22 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/loggers.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/loggers.adoc index 7e4976690442..4158839d638f 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/loggers.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/loggers.adoc @@ -61,6 +61,21 @@ management: schedule-delay: "15s" ---- +The `management.opentelemetry.logging.limits.*` configuration properties can be used to configure log record limits. +For example, to limit the number of attributes per log record to 64 and the maximum attribute value length to 256 characters: + +[configprops,yaml] +---- +management: + opentelemetry: + logging: + limits: + max-attributes: 64 + max-attribute-value-length: 256 +---- + +If you need full control, you can register a custom javadoc:io.opentelemetry.sdk.logs.LogLimits[] bean. + NOTE: The OpenTelemetry Logback appender and Log4j appender are not part of Spring Boot. For more details, see the https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/logback/logback-appender-1.0/library[OpenTelemetry Logback appender] or the https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/log4j/log4j-appender-2.17/library[OpenTelemetry Log4j2 appender] in the https://github.com/open-telemetry/opentelemetry-java-instrumentation[OpenTelemetry Java instrumentation GitHub repository]. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc index de30d41e9ef4..d8bdf29ec672 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc @@ -183,6 +183,28 @@ The following samplers are supported: +[[actuator.micrometer-tracing.span-limits]] +== Span Limits + +When using OpenTelemetry, you can configure span limits via the `management.opentelemetry.tracing.limits.*` configuration properties. +These allow you to control the maximum number of attributes, events, and links per span, as well as the maximum length of string attribute values. + +For example, to limit the number of attributes per span to 64 and the maximum attribute value length to 256 characters: + +[configprops,yaml] +---- +management: + opentelemetry: + tracing: + limits: + max-attributes: 64 + max-attribute-value-length: 256 +---- + +If you need full control, you can register a custom javadoc:io.opentelemetry.sdk.trace.SpanLimits[] bean. + + + [[actuator.micrometer-tracing.micrometer-observation]] == Integration with Micrometer Observation From d4521bb91b3f0db39f317a7f0701805665d5b672 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Tue, 3 Mar 2026 14:53:49 -0800 Subject: [PATCH 318/930] Add auto-configuration for otlp exemplars See gh-49538 --- .../OtlpMetricsExportAutoConfiguration.java | 17 +- ...lpMetricsExportAutoConfigurationTests.java | 48 ++++++ .../build.gradle | 2 + .../OtlpExemplarsAutoConfigurationTests.java | 142 +++++++++++++++++ .../build.gradle | 1 + .../otlp/OtlpExemplarsAutoConfiguration.java | 92 +++++++++++ .../autoconfigure/otlp/package-info.java | 23 +++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...zyTracingExemplarContextProviderTests.java | 149 ++++++++++++++++++ 9 files changed, 470 insertions(+), 5 deletions(-) create mode 100644 module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java create mode 100644 module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java create mode 100644 module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/package-info.java create mode 100644 module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/LazyTracingExemplarContextProviderTests.java diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfiguration.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfiguration.java index e509785bca0c..832715f6bbff 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfiguration.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp; import io.micrometer.core.instrument.Clock; +import io.micrometer.registry.otlp.ExemplarContextProvider; import io.micrometer.registry.otlp.OtlpConfig; import io.micrometer.registry.otlp.OtlpMeterRegistry; import io.micrometer.registry.otlp.OtlpMetricsSender; @@ -80,23 +81,29 @@ OtlpConfig otlpConfig(OpenTelemetryProperties openTelemetryProperties, @ConditionalOnMissingBean @ConditionalOnThreading(Threading.PLATFORM) OtlpMeterRegistry otlpMeterRegistry(OtlpConfig otlpConfig, Clock clock, - ObjectProvider metricsSender) { - return builder(otlpConfig, clock, metricsSender).build(); + ObjectProvider metricsSender, + ObjectProvider exemplarContextProvider) { + return builder(otlpConfig, clock, metricsSender, exemplarContextProvider).build(); } @Bean @ConditionalOnMissingBean @ConditionalOnThreading(Threading.VIRTUAL) OtlpMeterRegistry otlpMeterRegistryVirtualThreads(OtlpConfig otlpConfig, Clock clock, - ObjectProvider metricsSender) { + ObjectProvider metricsSender, + ObjectProvider exemplarContextProvider) { VirtualThreadTaskExecutor executor = new VirtualThreadTaskExecutor("otlp-meter-registry-"); - return builder(otlpConfig, clock, metricsSender).threadFactory(executor.getVirtualThreadFactory()).build(); + return builder(otlpConfig, clock, metricsSender, exemplarContextProvider) + .threadFactory(executor.getVirtualThreadFactory()) + .build(); } private OtlpMeterRegistry.Builder builder(OtlpConfig otlpConfig, Clock clock, - ObjectProvider metricsSender) { + ObjectProvider metricsSender, + ObjectProvider exemplarContextProvider) { OtlpMeterRegistry.Builder builder = OtlpMeterRegistry.builder(otlpConfig).clock(clock); metricsSender.ifAvailable(builder::metricsSender); + exemplarContextProvider.ifAvailable(builder::exemplarContextProvider); return builder; } diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java index 6316a67c63b9..2dcc4ead368a 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java @@ -19,6 +19,7 @@ import java.util.concurrent.ScheduledExecutorService; import io.micrometer.core.instrument.Clock; +import io.micrometer.registry.otlp.ExemplarContextProvider; import io.micrometer.registry.otlp.OtlpConfig; import io.micrometer.registry.otlp.OtlpMeterRegistry; import io.micrometer.registry.otlp.OtlpMetricsSender; @@ -142,6 +143,13 @@ void allowsCustomMetricsSenderToBeUsed() { .run(this::assertHasCustomMetricsSender); } + @Test + void allowsCustomExemplarContextProviderToBeUsed() { + this.contextRunner + .withUserConfiguration(BaseConfiguration.class, CustomExemplarContextProviderConfiguration.class) + .run(this::assertHasCustomExemplarContextProvider); + } + @Test @EnabledForJreRange(min = JRE.JAVA_21) void allowsCustomMetricsSenderToBeUsedWithVirtualThreads() { @@ -150,6 +158,25 @@ void allowsCustomMetricsSenderToBeUsedWithVirtualThreads() { .run(this::assertHasCustomMetricsSender); } + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void allowsCustomExemplarContextProviderToBeUsedWithVirtualThreads() { + this.contextRunner + .withUserConfiguration(BaseConfiguration.class, CustomExemplarContextProviderConfiguration.class) + .withPropertyValues("spring.threads.virtual.enabled=true") + .run(this::assertHasCustomExemplarContextProvider); + } + + @Test + void exemplarContextProviderShouldBeMissingIfNoExemplarContextProviderBeanPresent() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class).run((context) -> { + assertThat(context).doesNotHaveBean(ExemplarContextProvider.class); + assertThat(context).hasSingleBean(OtlpMeterRegistry.class); + OtlpMeterRegistry registry = context.getBean(OtlpMeterRegistry.class); + assertThat(registry).extracting("exemplarSamplerFactory").isNull(); + }); + } + @Test void shouldBackOffIfSpringBootOpenTelemetryIsMissing() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) @@ -164,6 +191,15 @@ private void assertHasCustomMetricsSender(AssertableApplicationContext context) .satisfies((sender) -> assertThat(sender).isSameAs(CustomMetricsSenderConfiguration.customMetricsSender)); } + private void assertHasCustomExemplarContextProvider(AssertableApplicationContext context) { + assertThat(context).hasSingleBean(OtlpMeterRegistry.class); + OtlpMeterRegistry registry = context.getBean(OtlpMeterRegistry.class); + assertThat(registry).extracting("exemplarSamplerFactory") + .extracting("exemplarContextProvider") + .satisfies((sender) -> assertThat(sender) + .isSameAs(CustomExemplarContextProviderConfiguration.customExemplarContextProvider)); + } + @Configuration(proxyBeanMethods = false) static class BaseConfiguration { @@ -219,4 +255,16 @@ OtlpMetricsSender customMetricsSender() { } + @Configuration(proxyBeanMethods = false) + static class CustomExemplarContextProviderConfiguration { + + static ExemplarContextProvider customExemplarContextProvider = () -> null; + + @Bean + ExemplarContextProvider customExemplarContextProvider() { + return customExemplarContextProvider; + } + + } + } diff --git a/module/spring-boot-micrometer-tracing-brave/build.gradle b/module/spring-boot-micrometer-tracing-brave/build.gradle index 147a619c6834..8f707d751768 100644 --- a/module/spring-boot-micrometer-tracing-brave/build.gradle +++ b/module/spring-boot-micrometer-tracing-brave/build.gradle @@ -40,8 +40,10 @@ dependencies { testImplementation(project(":core:spring-boot-test")) testImplementation(project(":test-support:spring-boot-test-support")) testImplementation(project(":module:spring-boot-micrometer-metrics")) + testImplementation(project(":module:spring-boot-opentelemetry")) testImplementation("io.micrometer:micrometer-registry-prometheus") testImplementation("io.prometheus:prometheus-metrics-exposition-formats") + testImplementation("io.micrometer:micrometer-registry-otlp") testRuntimeOnly("ch.qos.logback:logback-classic") } diff --git a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java new file mode 100644 index 000000000000..0e69f3b074c5 --- /dev/null +++ b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.micrometer.tracing.brave.autoconfigure; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.observation.Observation; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.registry.otlp.ExemplarContextProvider; +import io.micrometer.registry.otlp.OtlpMeterRegistry; +import io.micrometer.registry.otlp.OtlpMetricsSender; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.TracingAwareMeterObservationHandler; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; +import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsExportAutoConfiguration; +import org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration; +import org.springframework.boot.micrometer.tracing.autoconfigure.MicrometerTracingAutoConfiguration; +import org.springframework.boot.micrometer.tracing.autoconfigure.otlp.OtlpExemplarsAutoConfiguration; +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 OtlpExemplarsAutoConfiguration}. + * + * @author Jonatan Ivanov + */ +class OtlpExemplarsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("management.tracing.sampling.probability=1.0", + "management.metrics.distribution.percentiles-histogram.all=true", + "management.metrics.use-global-registry=false") + .withConfiguration( + AutoConfigurations.of(MetricsAutoConfiguration.class, OtlpMetricsExportAutoConfiguration.class, + OtlpExemplarsAutoConfiguration.class, ObservationAutoConfiguration.class, + BraveAutoConfiguration.class, MicrometerTracingAutoConfiguration.class)); + + @Test + void shouldNotSupplyBeansIfOtlpSupportIsMissing() { + this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.registry.otlp")) + .run((context) -> assertThat(context).doesNotHaveBean(ExemplarContextProvider.class)); + } + + @Test + void shouldNotSupplyBeansIfMicrometerTracingIsMissing() { + this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.tracing")) + .run((context) -> assertThat(context).doesNotHaveBean(ExemplarContextProvider.class)); + } + + @Test + void shouldSupplyCustomBeans() { + this.contextRunner.withUserConfiguration(CustomConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(ExemplarContextProvider.class) + .getBean(ExemplarContextProvider.class) + .isSameAs(CustomConfiguration.CONTEXT_PROVIDER)); + } + + @Test + void otlpOutputShouldContainExemplars() { + this.contextRunner.withUserConfiguration(TracingConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ExemplarContextProvider.class); + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("test.observation", observationRegistry).stop(); + OtlpMeterRegistry otlpMeterRegistry = context.getBean(OtlpMeterRegistry.class); + TestOtlpMetricsSender metricsSender = context.getBean(TestOtlpMetricsSender.class); + otlpMeterRegistry.close(); + + assertThat(metricsSender.getOtlpRequest()).containsOnlyOnce("name: \"test.observation\"") + .containsOnlyOnce("exemplars") + .containsOnlyOnce("span_id") + .containsOnlyOnce("trace_id"); + }); + } + + @Configuration(proxyBeanMethods = false) + private static final class CustomConfiguration { + + static final ExemplarContextProvider CONTEXT_PROVIDER = mock(ExemplarContextProvider.class); + + @Bean + ExemplarContextProvider customContextProvider() { + return CONTEXT_PROVIDER; + } + + } + + @Configuration(proxyBeanMethods = false) + static class TracingConfiguration { + + @Bean + TracingAwareMeterObservationHandler tracingAwareMeterObservationHandler(MeterRegistry meterRegistry, + Tracer tracer) { + DefaultMeterObservationHandler delegate = new DefaultMeterObservationHandler(meterRegistry); + return new TracingAwareMeterObservationHandler<>(delegate, tracer); + } + + @Bean + TestOtlpMetricsSender testOtlpMetricsSender() { + return new TestOtlpMetricsSender(); + } + + } + + static class TestOtlpMetricsSender implements OtlpMetricsSender { + + private String request = ""; + + @Override + public void send(Request request) throws Exception { + this.request = request.toString(); + } + + String getOtlpRequest() { + return this.request; + } + + } + +} diff --git a/module/spring-boot-micrometer-tracing/build.gradle b/module/spring-boot-micrometer-tracing/build.gradle index c6a4b3d4e3d5..be1dace653be 100644 --- a/module/spring-boot-micrometer-tracing/build.gradle +++ b/module/spring-boot-micrometer-tracing/build.gradle @@ -34,6 +34,7 @@ dependencies { optional(project(":core:spring-boot-autoconfigure")) optional(project(":module:spring-boot-micrometer-metrics")) optional("io.prometheus:prometheus-metrics-tracer-common") + optional("io.micrometer:micrometer-registry-otlp") optional("org.aspectj:aspectjweaver") testImplementation(project(":core:spring-boot-test")) diff --git a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java new file mode 100644 index 000000000000..848efd2b9577 --- /dev/null +++ b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.micrometer.tracing.autoconfigure.otlp; + +import io.micrometer.registry.otlp.ExemplarContextProvider; +import io.micrometer.registry.otlp.OtlpExemplarContext; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import org.jspecify.annotations.Nullable; + +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.micrometer.tracing.autoconfigure.MicrometerTracingAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.lang.Contract; +import org.springframework.util.function.SingletonSupplier; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for OTLP (OpenTelemetry Protocol) + * Exemplars with Micrometer Tracing. + * + * @author Jonatan Ivanov + * @since 4.1.0 + */ +@AutoConfiguration( + beforeName = "org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsExportAutoConfiguration", + after = MicrometerTracingAutoConfiguration.class) +@ConditionalOnBean(Tracer.class) +@ConditionalOnClass({ Tracer.class, ExemplarContextProvider.class }) +public final class OtlpExemplarsAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + ExemplarContextProvider exemplarContextProvider(ObjectProvider tracerProvider) { + return new LazyTracingExemplarContextProvider(tracerProvider); + } + + /** + * Since the MeterRegistry can depend on the {@link Tracer} (Exemplars) and the + * {@link Tracer} can depend on the MeterRegistry (recording metrics), this + * {@link ExemplarContextProvider} breaks the cycle by lazily loading the + * {@link Tracer}. + */ + static class LazyTracingExemplarContextProvider implements ExemplarContextProvider { + + private final SingletonSupplier tracer; + + LazyTracingExemplarContextProvider(ObjectProvider tracerProvider) { + this.tracer = SingletonSupplier.of(tracerProvider::getObject); + } + + @Override + public @Nullable OtlpExemplarContext getExemplarContext() { + Span span = this.tracer.obtain().currentSpan(); + if (isSampled(span)) { + TraceContext context = span.context(); + return new OtlpExemplarContext(context.traceId(), context.spanId()); + } + return null; + } + + @Contract("null -> false") + private boolean isSampled(@Nullable Span span) { + if (span == null) { + return false; + } + Boolean sampled = span.context().sampled(); + return sampled != null && sampled; + } + + } + +} diff --git a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/package-info.java b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/package-info.java new file mode 100644 index 000000000000..428aa86b754f --- /dev/null +++ b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 OTLP (OpenTelemetry Protocol) Exemplars with Micrometer Tracing. + */ +@NullMarked +package org.springframework.boot.micrometer.tracing.autoconfigure.otlp; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-micrometer-tracing/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-micrometer-tracing/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index b3a62b71f7b2..46801fd46b4f 100644 --- a/module/spring-boot-micrometer-tracing/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/module/spring-boot-micrometer-tracing/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,4 @@ org.springframework.boot.micrometer.tracing.autoconfigure.MicrometerTracingAutoConfiguration org.springframework.boot.micrometer.tracing.autoconfigure.NoopTracerAutoConfiguration +org.springframework.boot.micrometer.tracing.autoconfigure.otlp.OtlpExemplarsAutoConfiguration org.springframework.boot.micrometer.tracing.autoconfigure.prometheus.PrometheusExemplarsAutoConfiguration diff --git a/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/LazyTracingExemplarContextProviderTests.java b/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/LazyTracingExemplarContextProviderTests.java new file mode 100644 index 000000000000..3c7ae921983c --- /dev/null +++ b/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/LazyTracingExemplarContextProviderTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.micrometer.tracing.autoconfigure.otlp; + +import io.micrometer.tracing.Span; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.micrometer.tracing.autoconfigure.otlp.OtlpExemplarsAutoConfiguration.LazyTracingExemplarContextProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LazyTracingExemplarContextProvider}. + * + * @author Jonatan Ivanov + */ +class LazyTracingExemplarContextProviderTests { + + private final Tracer tracer = mock(Tracer.class); + + private final ObjectProvider objectProvider = new ObjectProvider<>() { + @Override + public Tracer getObject() throws BeansException { + return LazyTracingExemplarContextProviderTests.this.tracer; + } + + @Override + public Tracer getObject(@Nullable Object... args) throws BeansException { + return LazyTracingExemplarContextProviderTests.this.tracer; + } + + @Override + public Tracer getIfAvailable() throws BeansException { + return LazyTracingExemplarContextProviderTests.this.tracer; + } + + @Override + public Tracer getIfUnique() throws BeansException { + return LazyTracingExemplarContextProviderTests.this.tracer; + } + + }; + + private final LazyTracingExemplarContextProvider contextProvider = new LazyTracingExemplarContextProvider( + this.objectProvider); + + @Test + void whenCurrentSpanIsNullThenExemplarContextIsNull() { + assertThat(this.contextProvider.getExemplarContext()).isNull(); + } + + @Test + void whenCurrentSpanHasSpanIdThenSpanIdIsFromSpan() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(true); + given(traceContext.spanId()).willReturn("span-id"); + given(span.context()).willReturn(traceContext); + assertThat(this.contextProvider.getExemplarContext()).isNotNull(); + assertThat(this.contextProvider.getExemplarContext().getSpanId()).isEqualTo("span-id"); + } + + @Test + void whenCurrentSpanHasTraceIdThenTraceIdIsFromSpan() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(true); + given(traceContext.traceId()).willReturn("trace-id"); + given(span.context()).willReturn(traceContext); + assertThat(this.contextProvider.getExemplarContext()).isNotNull(); + assertThat(this.contextProvider.getExemplarContext().getTraceId()).isEqualTo("trace-id"); + } + + @Test + void whenCurrentSpanHasNoSpanIdThenSpanIdIsNull() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(true); + given(span.context()).willReturn(traceContext); + assertThat(this.contextProvider.getExemplarContext()).isNotNull(); + assertThat(this.contextProvider.getExemplarContext().getSpanId()).isNull(); + } + + @Test + void whenCurrentSpanHasNoTraceIdThenTraceIdIsNull() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(true); + given(span.context()).willReturn(traceContext); + assertThat(this.contextProvider.getExemplarContext()).isNotNull(); + assertThat(this.contextProvider.getExemplarContext().getTraceId()).isNull(); + } + + @Test + void whenCurrentSpanIsSampledThenExemplarContextIsNotNull() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(true); + given(span.context()).willReturn(traceContext); + assertThat(this.contextProvider.getExemplarContext()).isNotNull(); + } + + @Test + void whenCurrentSpanIsNotSampledThenExemplarContextIsNull() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(false); + given(span.context()).willReturn(traceContext); + assertThat(this.contextProvider.getExemplarContext()).isNull(); + } + + @Test + void whenCurrentSpanHasDeferredSamplingThenExemplarContextIsNull() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(null); + given(span.context()).willReturn(traceContext); + assertThat(this.contextProvider.getExemplarContext()).isNull(); + } + +} From 2a8ad44cefcb34a1fb73c321e1a6f4753d25881f Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 11 Mar 2026 09:11:09 +0100 Subject: [PATCH 319/930] Polish "Add auto-configuration for otlp exemplars" See gh-49538 --- .../docs/antora/modules/reference/pages/actuator/metrics.adoc | 4 ++++ .../export/otlp/OtlpMetricsExportAutoConfigurationTests.java | 2 +- .../autoconfigure/OtlpExemplarsAutoConfigurationTests.java | 1 - 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc index 84e572bbea16..7b9b7573d4bb 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc @@ -527,6 +527,10 @@ Custom headers, for example for authentication, can also be provided using confi If an `OtlpMetricsSender` bean is available, it will be configured on the `OtlpMeterRegistry` that Spring Boot auto-configures. +https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exemplars[OTLP Exemplars] are also supported. +To enable this feature, an javadoc:io.micrometer.registry.otlp.ExemplarContextProvider[] bean should be present. +If you use xref:actuator/tracing.adoc[Micrometer Tracing], this will be auto-configured for you. + [[actuator.metrics.export.prometheus]] diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java index 2dcc4ead368a..95f5e002660a 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java @@ -196,7 +196,7 @@ private void assertHasCustomExemplarContextProvider(AssertableApplicationContext OtlpMeterRegistry registry = context.getBean(OtlpMeterRegistry.class); assertThat(registry).extracting("exemplarSamplerFactory") .extracting("exemplarContextProvider") - .satisfies((sender) -> assertThat(sender) + .satisfies((provider) -> assertThat(provider) .isSameAs(CustomExemplarContextProviderConfiguration.customExemplarContextProvider)); } diff --git a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java index 0e69f3b074c5..ac3f0161a8b2 100644 --- a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java @@ -87,7 +87,6 @@ void otlpOutputShouldContainExemplars() { OtlpMeterRegistry otlpMeterRegistry = context.getBean(OtlpMeterRegistry.class); TestOtlpMetricsSender metricsSender = context.getBean(TestOtlpMetricsSender.class); otlpMeterRegistry.close(); - assertThat(metricsSender.getOtlpRequest()).containsOnlyOnce("name: \"test.observation\"") .containsOnlyOnce("exemplars") .containsOnlyOnce("span_id") From e7c0bec8ab37af3f3eeb94c9de3b4aa6c9a30d71 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 11 Mar 2026 09:59:21 +0000 Subject: [PATCH 320/930] Improve input/output configuration of DSL-created bootBuildInfo task There are two problems with the current approach: 1. The DSL uses dependsOn to try to inform Gradle of the dependency relationship between tasks. This isn't recommended and inputs and outputs should be used instead. 2. The generated build-info.properties file doesn't appear on the classpath when running an application in an IDE, unless that IDE delegates the execution to Gradle. This commit addresses both of these shortcomings by configuring the bootBuildInfo task's output to be a src dir of the main source set's resources. This gives Gradle the preferred input- and output-based view of the tasks' interdependencies. It also causes the generated build-info.properties to appear on the classpath in IDEs that don't delegate to Gradle. For example, the Eclipse Buildship plugin automatically configures build/bootBuildInfo as a source folder on the project's build path. Closes gh-49547 --- .../pages/integrating-with-actuator.adoc | 5 +- .../boot/gradle/dsl/SpringBootExtension.java | 47 +++++++------------ .../gradle/tasks/buildinfo/BuildInfo.java | 30 ++++++++---- .../tasks/buildinfo/BuildInfoProperties.java | 2 +- ...gratingWithActuatorDocumentationTests.java | 8 ++-- .../dsl/BuildInfoDslIntegrationTests.java | 35 +++++++++----- .../buildinfo/BuildInfoIntegrationTests.java | 5 +- .../tasks/buildinfo/BuildInfoTests.java | 14 +++++- ...ildInfoDslIntegrationTests-basicJar.gradle | 3 ++ ...ildInfoDslIntegrationTests-basicWar.gradle | 1 + 10 files changed, 90 insertions(+), 60 deletions(-) diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/integrating-with-actuator.adoc b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/integrating-with-actuator.adoc index 4cf5ea38402d..c3f4dbfb7205 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/integrating-with-actuator.adoc +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/integrating-with-actuator.adoc @@ -26,8 +26,9 @@ include::example$integrating-with-actuator/build-info-basic.gradle.kts[tags=buil ---- ====== -This will configure a {apiref-gradle-plugin-boot-build-info}[`BuildInfo`] task named `bootBuildInfo` and, if it exists, make the Java plugin's `classes` task depend upon it. -The task's destination directory will be `META-INF` in the output directory of the main source set's resources (typically `build/resources/main`). +This will configure a {apiref-gradle-plugin-boot-build-info}[`BuildInfo`] task named `bootBuildInfo`. +The task's destination directory will be `build/bootBuildInfo` and its filename will be `META-INF/build-info.properties`. +If the main source set exists, its resources are configured to use the output of the `bootBuildInfo` task as a src dir. By default, the generated build information is derived from the project: diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/dsl/SpringBootExtension.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/dsl/SpringBootExtension.java index e6f42863de59..67b6940dd454 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/dsl/SpringBootExtension.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/dsl/SpringBootExtension.java @@ -16,10 +16,9 @@ package org.springframework.boot.gradle.dsl; -import java.io.File; - import org.gradle.api.Action; import org.gradle.api.Project; +import org.gradle.api.file.SourceDirectorySet; import org.gradle.api.plugins.BasePlugin; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; @@ -31,7 +30,6 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.gradle.tasks.buildinfo.BuildInfo; -import org.springframework.util.Assert; /** * Entry point to Spring Boot's Gradle DSL. @@ -66,25 +64,25 @@ public Property getMainClass() { } /** - * Creates a new {@link BuildInfo} task named {@code bootBuildInfo} and configures the - * Java plugin's {@code classes} task to depend upon it. + * Creates a new {@link BuildInfo} task named {@code bootBuildInfo} and adds its + * output as a {@link SourceDirectorySet#srcDir source directory} of the main source + * set's {@link SourceSet#getResources resources}. *

    - * By default, the task's destination dir will be a directory named {@code META-INF} - * beneath the main source set's resources output directory, and the task's project - * artifact will be the base name of the {@code bootWar} or {@code bootJar} task. + * By default, the task's project artifact will be the archive base name of the + * {@code bootWar} or {@code bootJar} task. */ public void buildInfo() { buildInfo(null); } /** - * Creates a new {@link BuildInfo} task named {@code bootBuildInfo} and configures the - * Java plugin's {@code classes} task to depend upon it. The task is passed to the - * given {@code configurer} for further configuration. + * Creates a new {@link BuildInfo} task named {@code bootBuildInfo} and adds its + * output as a {@link SourceDirectorySet#srcDir source directory} of the main source + * set's {@link SourceSet#getResources resources}. The task is passed to the given + * {@code configurer} for further configuration. *

    - * By default, the task's destination dir will be a directory named {@code META-INF} - * beneath the main source set's resources output directory, and the task's project - * artifact will be the base name of the {@code bootWar} or {@code bootJar} task. + * By default, the task's project artifact will be the archive base name of the + * {@code bootWar} or {@code bootJar} task. * @param configurer the task configurer */ public void buildInfo(@Nullable Action configurer) { @@ -92,10 +90,15 @@ public void buildInfo(@Nullable Action configurer) { TaskProvider bootBuildInfo = tasks.register("bootBuildInfo", BuildInfo.class, this::configureBuildInfoTask); this.project.getPlugins().withType(JavaPlugin.class, (plugin) -> { - tasks.named(JavaPlugin.CLASSES_TASK_NAME).configure((task) -> task.dependsOn(bootBuildInfo)); bootBuildInfo.configure((buildInfo) -> buildInfo.getProperties() .getArtifact() .convention(this.project.provider(this::determineArtifactBaseName))); + this.project.getExtensions() + .getByType(JavaPluginExtension.class) + .getSourceSets() + .getByName(SourceSet.MAIN_SOURCE_SET_NAME) + .getResources() + .srcDir(bootBuildInfo); }); if (configurer != null) { bootBuildInfo.configure(configurer); @@ -105,20 +108,6 @@ public void buildInfo(@Nullable Action configurer) { private void configureBuildInfoTask(BuildInfo task) { task.setGroup(BasePlugin.BUILD_GROUP); task.setDescription("Generates a META-INF/build-info.properties file."); - task.getDestinationDir() - .convention(this.project.getLayout() - .dir(this.project.provider(() -> new File(determineMainSourceSetResourcesOutputDir(), "META-INF")))); - } - - private File determineMainSourceSetResourcesOutputDir() { - File resourcesDir = this.project.getExtensions() - .getByType(JavaPluginExtension.class) - .getSourceSets() - .getByName(SourceSet.MAIN_SOURCE_SET_NAME) - .getOutput() - .getResourcesDir(); - Assert.state(resourcesDir != null, "'resourcesDir' must not be null"); - return resourcesDir; } private @Nullable String determineArtifactBaseName() { diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.java index d09b99d1bf9e..07742f197882 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.java @@ -23,7 +23,9 @@ import org.gradle.api.DefaultTask; import org.gradle.api.Task; import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.provider.Property; import org.gradle.api.provider.SetProperty; +import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.OutputDirectory; @@ -35,8 +37,8 @@ import org.springframework.boot.loader.tools.BuildPropertiesWriter.ProjectDetails; /** - * {@link Task} for generating a {@code build-info.properties} file from a - * {@code Project}. + * {@link Task} for generating a build info properties file from + * {@link BuildInfoProperties}. * * @author Andy Wilkinson * @since 2.0.0 @@ -49,6 +51,7 @@ public abstract class BuildInfo extends DefaultTask { public BuildInfo() { this.properties = getProject().getObjects().newInstance(BuildInfoProperties.class, getExcludes()); getDestinationDir().convention(getProject().getLayout().getBuildDirectory().dir(getName())); + getFilename().convention("META-INF/build-info.properties"); } /** @@ -60,8 +63,17 @@ public BuildInfo() { public abstract SetProperty getExcludes(); /** - * Generates the {@code build-info.properties} file in the configured - * {@link #getDestinationDir destination}. + * Returns the name of the file that is written to the {@link #getDestinationDir + * destination dir}. Convention is {@code META-INF/build-info.properties}. + * @return the name of the written file + * @since 4.1.0 + */ + @Input + public abstract Property getFilename(); + + /** + * Generates the build info properties file in the configured + * {@link #getDestinationDir destination dir}. */ @TaskAction public void generateBuildProperties() { @@ -70,8 +82,8 @@ public void generateBuildProperties() { this.properties.getArtifactIfNotExcluded(), this.properties.getVersionIfNotExcluded(), this.properties.getNameIfNotExcluded(), this.properties.getTimeIfNotExcluded(), this.properties.getAdditionalIfNotExcluded()); - new BuildPropertiesWriter(new File(getDestinationDir().get().getAsFile(), "build-info.properties")) - .writeBuildProperties(details); + File outputFile = new File(getDestinationDir().getAsFile().get(), getFilename().get()); + new BuildPropertiesWriter(outputFile).writeBuildProperties(details); } catch (IOException ex) { throw new TaskExecutionException(this, ex); @@ -79,8 +91,8 @@ public void generateBuildProperties() { } /** - * Returns the directory to which the {@code build-info.properties} file will be - * written. + * Returns the directory to which the build info file will be written. Convention is + * build/${taskName}. * @return the destination directory */ @OutputDirectory @@ -88,7 +100,7 @@ public void generateBuildProperties() { /** * Returns the {@link BuildInfoProperties properties} that will be included in the - * {@code build-info.properties} file. + * written file. * @return the properties */ @Nested diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java index 4029f248f0d7..c9ba61878404 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java @@ -38,7 +38,7 @@ import org.jspecify.annotations.Nullable; /** - * The properties that are written into the {@code build-info.properties} file. + * The properties that are written into the {@code META-INF/build-info.properties} file. * * @author Andy Wilkinson * @since 2.0.0 diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/IntegratingWithActuatorDocumentationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/IntegratingWithActuatorDocumentationTests.java index d09620860135..a5d39037f7b9 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/IntegratingWithActuatorDocumentationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/IntegratingWithActuatorDocumentationTests.java @@ -44,7 +44,7 @@ class IntegratingWithActuatorDocumentationTests { @TestTemplate void basicBuildInfo() { this.gradleBuild.script(Examples.DIR + "integrating-with-actuator/build-info-basic").build("bootBuildInfo"); - assertThat(new File(this.gradleBuild.getProjectDir(), "build/resources/main/META-INF/build-info.properties")) + assertThat(new File(this.gradleBuild.getProjectDir(), "build/bootBuildInfo/META-INF/build-info.properties")) .isFile(); } @@ -52,7 +52,7 @@ void basicBuildInfo() { void buildInfoCustomValues() { this.gradleBuild.script(Examples.DIR + "integrating-with-actuator/build-info-custom-values") .build("bootBuildInfo"); - File file = new File(this.gradleBuild.getProjectDir(), "build/resources/main/META-INF/build-info.properties"); + File file = new File(this.gradleBuild.getProjectDir(), "build/bootBuildInfo/META-INF/build-info.properties"); assertThat(file).isFile(); Properties properties = buildInfoProperties(file); assertThat(properties).containsEntry("build.artifact", "example-app"); @@ -66,7 +66,7 @@ void buildInfoCustomValues() { void buildInfoAdditional() { this.gradleBuild.script(Examples.DIR + "integrating-with-actuator/build-info-additional") .build("bootBuildInfo"); - File file = new File(this.gradleBuild.getProjectDir(), "build/resources/main/META-INF/build-info.properties"); + File file = new File(this.gradleBuild.getProjectDir(), "build/bootBuildInfo/META-INF/build-info.properties"); assertThat(file).isFile(); Properties properties = buildInfoProperties(file); assertThat(properties).containsEntry("build.a", "alpha"); @@ -77,7 +77,7 @@ void buildInfoAdditional() { void buildInfoExcludeTime() { this.gradleBuild.script(Examples.DIR + "integrating-with-actuator/build-info-exclude-time") .build("bootBuildInfo"); - File file = new File(this.gradleBuild.getProjectDir(), "build/resources/main/META-INF/build-info.properties"); + File file = new File(this.gradleBuild.getProjectDir(), "build/bootBuildInfo/META-INF/build-info.properties"); assertThat(file).isFile(); Properties properties = buildInfoProperties(file); assertThat(properties).doesNotContainKey("build.time"); diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests.java index a0bfed5f8945..d354fcb175d1 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests.java @@ -20,6 +20,8 @@ import java.io.FileReader; import java.io.IOException; import java.util.Properties; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; import org.gradle.testkit.runner.BuildTask; import org.gradle.testkit.runner.TaskOutcome; @@ -44,8 +46,8 @@ class BuildInfoDslIntegrationTests { GradleBuild gradleBuild; @TestTemplate - void basicJar() { - BuildTask task = this.gradleBuild.build("bootBuildInfo", "--stacktrace").task(":bootBuildInfo"); + void basicJar() throws IOException { + BuildTask task = this.gradleBuild.build("bootJar").task(":bootBuildInfo"); assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Properties properties = buildInfoProperties(); @@ -53,6 +55,14 @@ void basicJar() { assertThat(properties).containsEntry("build.artifact", this.gradleBuild.getProjectDir().getName()); assertThat(properties).containsEntry("build.group", "com.example"); assertThat(properties).containsEntry("build.version", "1.0"); + File jar = new File(this.gradleBuild.getProjectDir(), "build/libs/").listFiles()[0]; + try (JarFile jarFile = new JarFile(jar)) { + JarEntry entry = jarFile.getJarEntry("META-INF/build-info.properties"); + assertThat(entry).isNotNull(); + Properties jarProperties = new Properties(); + jarProperties.load(jarFile.getInputStream(entry)); + assertThat(jarProperties).isEqualTo(properties); + } } @TestTemplate @@ -68,8 +78,8 @@ void jarWithCustomName() { } @TestTemplate - void basicWar() { - BuildTask task = this.gradleBuild.build("bootBuildInfo", "--stacktrace").task(":bootBuildInfo"); + void basicWar() throws IOException { + BuildTask task = this.gradleBuild.build("bootWar").task(":bootBuildInfo"); assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Properties properties = buildInfoProperties(); @@ -77,6 +87,14 @@ void basicWar() { assertThat(properties).containsEntry("build.artifact", this.gradleBuild.getProjectDir().getName()); assertThat(properties).containsEntry("build.group", "com.example"); assertThat(properties).containsEntry("build.version", "1.0"); + File war = new File(this.gradleBuild.getProjectDir(), "build/libs/").listFiles()[0]; + try (JarFile warFile = new JarFile(war)) { + JarEntry entry = warFile.getJarEntry("WEB-INF/classes/META-INF/build-info.properties"); + assertThat(entry).isNotNull(); + Properties jarProperties = new Properties(); + jarProperties.load(warFile.getInputStream(entry)); + assertThat(jarProperties).isEqualTo(properties); + } } @TestTemplate @@ -105,15 +123,8 @@ void additionalProperties() { assertThat(properties).containsEntry("build.b", "bravo"); } - @TestTemplate - void classesDependency() { - BuildTask task = this.gradleBuild.build("classes", "--stacktrace").task(":bootBuildInfo"); - assertThat(task).isNotNull(); - assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - } - private Properties buildInfoProperties() { - File file = new File(this.gradleBuild.getProjectDir(), "build/resources/main/META-INF/build-info.properties"); + File file = new File(this.gradleBuild.getProjectDir(), "build/bootBuildInfo/META-INF/build-info.properties"); assertThat(file).isFile(); Properties properties = new Properties(); try (FileReader reader = new FileReader(file)) { diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.java index f87b87dc5e09..136f9462436f 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.java @@ -133,7 +133,8 @@ void reproducibleOutputWithFixedTime() throws IOException, InterruptedException BuildTask task = this.gradleBuild.build("buildInfo", "-PnullTime").task(":buildInfo"); assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - File buildInfoProperties = new File(this.gradleBuild.getProjectDir(), "build/buildInfo/build-info.properties"); + File buildInfoProperties = new File(this.gradleBuild.getProjectDir(), + "build/buildInfo/META-INF/build-info.properties"); String firstHash = FileUtils.sha1Hash(buildInfoProperties); assertThat(buildInfoProperties.delete()).isTrue(); Thread.sleep(1500); @@ -157,7 +158,7 @@ void excludeProperties() { } private Properties buildInfoProperties() { - File file = new File(this.gradleBuild.getProjectDir(), "build/buildInfo/build-info.properties"); + File file = new File(this.gradleBuild.getProjectDir(), "build/buildInfo/META-INF/build-info.properties"); assertThat(file).isFile(); Properties properties = new Properties(); try (FileReader reader = new FileReader(file)) { diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java index 16cfa6140aef..a2048f3dc51c 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java @@ -171,6 +171,17 @@ void nullAdditionalPropertyProducesInformativeFailure() { .withMessage("Cannot add an entry with a null value to a property of type Map."); } + @Test + void filenameCanBeCustomized() { + Project project = createProject("test"); + BuildInfo task = createTask(project); + task.getFilename().set("custom-location-build-info.properties"); + task.generateBuildProperties(); + assertThat(new File(project.getLayout().getBuildDirectory().dir("testBuildInfo").get().getAsFile(), + "custom-location-build-info.properties")) + .exists(); + } + private Project createProject(String projectName) { File projectDir = new File(this.temp, projectName); return GradleProjectBuilder.builder().withProjectDir(projectDir).withName(projectName).build(); @@ -182,7 +193,8 @@ private BuildInfo createTask(Project project) { private Properties buildInfoProperties(BuildInfo task) { task.generateBuildProperties(); - return buildInfoProperties(new File(task.getDestinationDir().get().getAsFile(), "build-info.properties")); + return buildInfoProperties( + new File(task.getDestinationDir().get().getAsFile(), "META-INF/build-info.properties")); } private Properties buildInfoProperties(File file) { diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicJar.gradle b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicJar.gradle index 87c0f81f7770..1662e236179f 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicJar.gradle +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicJar.gradle @@ -24,4 +24,7 @@ version = '1.0' springBoot { buildInfo() + mainClass = "com.example.Main" } + + diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicWar.gradle b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicWar.gradle index 718df89f3373..8afe5dcf19d9 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicWar.gradle +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicWar.gradle @@ -24,4 +24,5 @@ version = '1.0' springBoot { buildInfo() + mainClass = "com.example.Main" } From afb201d8711051a0eeb81171b3ac95e704c1ee07 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 11 Mar 2026 12:19:54 +0000 Subject: [PATCH 321/930] Upgrade to Jedis 7.4.0 Closes gh-49566 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b84575390a16..972f5c35b7d4 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1056,7 +1056,7 @@ bom { releaseNotes("https://github.com/hunterhacker/jdom/releases/tag/JDOM-{version}") } } - library("Jedis", "7.3.0") { + library("Jedis", "7.4.0") { alignWith { property { name "jedis" From bd663c155165ee58550ce67c93d1cff287a712a5 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 11 Mar 2026 12:41:13 +0100 Subject: [PATCH 322/930] Add property to disable the OpenTelemetry SDK This uses no-op implementations for the SdkTracerProvider, SdkLoggerProvider and SdkMeterProvider, but still configures the propagators. Additionally, it doesn't create the tracing and logging beans which would be superfluous for a disabled SDK. Closes gh-49564 --- .../pages/actuator/observability.adoc | 11 ++++ ...OpenTelemetryTracingAutoConfiguration.java | 7 +++ ...elemetryTracingAutoConfigurationTests.java | 25 +++++++++ .../ConditionalOnEnabledOpenTelemetry.java | 42 +++++++++++++++ .../OpenTelemetryProperties.java | 15 ++++++ .../OpenTelemetrySdkAutoConfiguration.java | 25 +++++++++ ...OpenTelemetryLoggingAutoConfiguration.java | 2 + ...penTelemetrySdkAutoConfigurationTests.java | 51 +++++++++++++++++++ ...elemetryLoggingAutoConfigurationTests.java | 9 ++++ 9 files changed, 187 insertions(+) create mode 100644 module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/ConditionalOnEnabledOpenTelemetry.java diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc index 0b04549a57f0..b7d5be43b32c 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc @@ -125,6 +125,17 @@ Exporting OpenTelemetry traces is only auto-configured when used together with x +[[actuator.observability.opentelemetry.disabling]] +=== Disabling OpenTelemetry + +The OpenTelemetry support can be disabled by setting the configprop:management.opentelemetry.enabled[] property to `false`. +This behaves similarly to the https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration[`OTEL_SDK_DISABLED`] environment variable (but negated): when the SDK is disabled, metrics, traces, and logging will use no-op implementations. +Context propagators are not affected and continue to function normally. + +NOTE: Keep in mind that Spring Boot doesn't use OpenTelemetry's metrics functionality, so metrics might still be enabled even when disabling OpenTelemetry. + + + [[actuator.observability.opentelemetry.environment-variables]] === Environment variables diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfiguration.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfiguration.java index 490f40ad318d..c2bd6e6ed3d2 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfiguration.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfiguration.java @@ -64,6 +64,7 @@ import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryPropagationConfigurations.PropagationWithoutBaggage; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryTracingProperties.Export; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryTracingProperties.Limits; +import org.springframework.boot.opentelemetry.autoconfigure.ConditionalOnEnabledOpenTelemetry; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.util.CollectionUtils; @@ -99,6 +100,7 @@ public final class OpenTelemetryTracingAutoConfiguration { @Bean @ConditionalOnMissingBean + @ConditionalOnEnabledOpenTelemetry SdkTracerProvider otelSdkTracerProvider(Resource resource, SpanProcessors spanProcessors, Sampler sampler, ObjectProvider customizers, SpanLimits spanLimits) { SdkTracerProviderBuilder builder = SdkTracerProvider.builder() @@ -112,6 +114,7 @@ SdkTracerProvider otelSdkTracerProvider(Resource resource, SpanProcessors spanPr @Bean @ConditionalOnMissingBean + @ConditionalOnEnabledOpenTelemetry SpanLimits otelSpanLimits() { Limits limits = this.openTelemetryTracingProperties.getLimits(); return SpanLimits.builder() @@ -132,6 +135,7 @@ ContextPropagators otelContextPropagators(ObjectProvider text @Bean @ConditionalOnMissingBean + @ConditionalOnEnabledOpenTelemetry Sampler otelSampler() { return switch (this.openTelemetryTracingProperties.getSampler()) { case ALWAYS_ON -> Sampler.alwaysOn(); @@ -146,12 +150,14 @@ Sampler otelSampler() { @Bean @ConditionalOnMissingBean + @ConditionalOnEnabledOpenTelemetry SpanProcessors spanProcessors(ObjectProvider spanProcessors) { return SpanProcessors.of(spanProcessors.orderedStream().toList()); } @Bean @ConditionalOnMissingBean + @ConditionalOnEnabledOpenTelemetry BatchSpanProcessor otelSpanProcessor(SpanExporters spanExporters, ObjectProvider spanExportingPredicates, ObjectProvider spanReporters, ObjectProvider spanFilters, ObjectProvider meterProvider) { @@ -171,6 +177,7 @@ BatchSpanProcessor otelSpanProcessor(SpanExporters spanExporters, @Bean @ConditionalOnMissingBean + @ConditionalOnEnabledOpenTelemetry SpanExporters spanExporters(ObjectProvider spanExporters) { return SpanExporters.of(spanExporters.orderedStream().toList()); } diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java index 4dfaa732612e..50f47d4286a6 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java @@ -485,6 +485,31 @@ void batchSpanProcessorShouldBeConfiguredWithDefaultProperties() { }); } + @Test + void whenOpenTelemetryIsDisabledDoesNotProvideTracingSignalBeans() { + this.contextRunner.withPropertyValues("management.opentelemetry.enabled=false").run((context) -> { + assertThat(context).doesNotHaveBean(SdkTracerProvider.class); + assertThat(context).doesNotHaveBean(SpanLimits.class); + assertThat(context).doesNotHaveBean(Sampler.class); + assertThat(context).doesNotHaveBean(SpanProcessors.class); + assertThat(context).doesNotHaveBean(SpanExporters.class); + assertThat(context).doesNotHaveBean(BatchSpanProcessor.class); + }); + } + + @Test + void whenOpenTelemetryIsDisabledStillProvidesBridgeAndPropagationBeans() { + this.contextRunner.withPropertyValues("management.opentelemetry.enabled=false").run((context) -> { + assertThat(context).hasSingleBean(OtelTracer.class); + assertThat(context).hasSingleBean(OtelPropagator.class); + assertThat(context).hasSingleBean(OtelCurrentTraceContext.class); + assertThat(context).hasSingleBean(Slf4JEventListener.class); + assertThat(context).hasSingleBean(OtelSpanCustomizer.class); + assertThat(context).hasSingleBean(ContextPropagators.class); + assertThat(context).hasSingleBean(TextMapPropagator.class); + }); + } + @Test // gh-41439 @ForkedClassPath void shouldPublishEventsWhenContextStorageIsInitializedEarly() { diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/ConditionalOnEnabledOpenTelemetry.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/ConditionalOnEnabledOpenTelemetry.java new file mode 100644 index 000000000000..15fbd44352f9 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/ConditionalOnEnabledOpenTelemetry.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.opentelemetry.autoconfigure; + +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.condition.ConditionalOnBooleanProperty; +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that checks whether OpenTelemetry is enabled. It + * matches if the value of the {@code management.opentelemetry.enabled} property is + * {@code true} or if it is not configured. + * + * @author Moritz Halbritter + * @since 4.1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@ConditionalOnBooleanProperty(name = "management.opentelemetry.enabled", matchIfMissing = true) +public @interface ConditionalOnEnabledOpenTelemetry { + +} diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryProperties.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryProperties.java index 228331ff3518..a900b3e373ae 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryProperties.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryProperties.java @@ -30,11 +30,26 @@ @ConfigurationProperties("management.opentelemetry") public class OpenTelemetryProperties { + /** + * Whether OpenTelemetry should be enabled. If OpenTelemetry is disabled, only + * propagators are configured. Metrics, traces, and logging will use no-op + * implementations. + */ + private boolean enabled = true; + /** * Resource attributes. */ private Map resourceAttributes = new HashMap<>(); + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public Map getResourceAttributes() { return this.resourceAttributes; } diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetrySdkAutoConfiguration.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetrySdkAutoConfiguration.java index 465d0d5d98fb..156f0a85f081 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetrySdkAutoConfiguration.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetrySdkAutoConfiguration.java @@ -31,8 +31,10 @@ 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.NoneNestedConditions; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.core.env.Environment; /** @@ -51,6 +53,7 @@ public final class OpenTelemetrySdkAutoConfiguration { @Bean @ConditionalOnMissingBean(OpenTelemetry.class) + @ConditionalOnEnabledOpenTelemetry OpenTelemetrySdk openTelemetrySdk(ObjectProvider openTelemetrySdkTracerProvider, ObjectProvider openTelemetryContextPropagators, ObjectProvider openTelemetrySdkLoggerProvider, @@ -63,6 +66,15 @@ OpenTelemetrySdk openTelemetrySdk(ObjectProvider openTelemetr return builder.build(); } + @Bean + @ConditionalOnMissingBean(OpenTelemetry.class) + @Conditional(OnDisabledOpenTelemetryCondition.class) + OpenTelemetrySdk disabledOpenTelemetrySdk(ObjectProvider openTelemetryContextPropagators) { + OpenTelemetrySdkBuilder builder = OpenTelemetrySdk.builder(); + openTelemetryContextPropagators.ifAvailable(builder::setPropagators); + return builder.build(); + } + @Bean @ConditionalOnMissingBean Resource openTelemetryResource(Environment environment, OpenTelemetryProperties properties) { @@ -75,4 +87,17 @@ private Resource toResource(Environment environment, OpenTelemetryProperties pro return builder.build(); } + static class OnDisabledOpenTelemetryCondition extends NoneNestedConditions { + + OnDisabledOpenTelemetryCondition() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnEnabledOpenTelemetry + static class EnabledCondition { + + } + + } + } diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfiguration.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfiguration.java index 5863e2e1bd5d..80a72028da41 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfiguration.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfiguration.java @@ -31,6 +31,7 @@ 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.opentelemetry.autoconfigure.ConditionalOnEnabledOpenTelemetry; import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration; import org.springframework.boot.opentelemetry.autoconfigure.logging.OpenTelemetryLoggingProperties.Export; import org.springframework.boot.opentelemetry.autoconfigure.logging.OpenTelemetryLoggingProperties.Limits; @@ -44,6 +45,7 @@ */ @AutoConfiguration(after = OpenTelemetrySdkAutoConfiguration.class) @ConditionalOnClass(SdkLoggerProvider.class) +@ConditionalOnEnabledOpenTelemetry @EnableConfigurationProperties(OpenTelemetryLoggingProperties.class) public final class OpenTelemetryLoggingAutoConfiguration { diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetrySdkAutoConfigurationTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetrySdkAutoConfigurationTests.java index f2a407e95dee..d54144d7328f 100644 --- a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetrySdkAutoConfigurationTests.java +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetrySdkAutoConfigurationTests.java @@ -180,6 +180,57 @@ void whenHasSdkMeterProviderProvidesMeterProvider() { }); } + @Test + void whenOpenTelemetryIsDisabledStillProvidesOpenTelemetrySdk() { + this.contextRunner.withPropertyValues("management.opentelemetry.enabled=false").run((context) -> { + assertThat(context).hasSingleBean(OpenTelemetrySdk.class); + assertThat(context).hasBean("disabledOpenTelemetrySdk"); + }); + } + + @Test + void whenOpenTelemetryIsDisabledStillConfiguresPropagators() { + this.contextRunner.withPropertyValues("management.opentelemetry.enabled=false") + .withBean(ContextPropagators.class, ContextPropagators::noop) + .run((context) -> { + OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class); + assertThat(openTelemetry.getPropagators()).isNotNull(); + }); + } + + @Test + void whenOpenTelemetryIsDisabledDoesNotUseTracerProvider() { + SdkTracerProvider tracerProvider = SdkTracerProvider.builder().build(); + this.contextRunner.withPropertyValues("management.opentelemetry.enabled=false") + .withBean(SdkTracerProvider.class, () -> tracerProvider) + .run((context) -> { + OpenTelemetrySdk openTelemetry = context.getBean(OpenTelemetrySdk.class); + assertThat(openTelemetry.getSdkTracerProvider()).isNotSameAs(tracerProvider); + }); + } + + @Test + void whenOpenTelemetryIsDisabledDoesNotUseLoggerProvider() { + SdkLoggerProvider loggerProvider = SdkLoggerProvider.builder().build(); + this.contextRunner.withPropertyValues("management.opentelemetry.enabled=false") + .withBean(SdkLoggerProvider.class, () -> loggerProvider) + .run((context) -> { + OpenTelemetrySdk openTelemetry = context.getBean(OpenTelemetrySdk.class); + assertThat(openTelemetry.getSdkLoggerProvider()).isNotSameAs(loggerProvider); + }); + } + + @Test + void whenOpenTelemetryIsDisabledDoesNotUseMeterProvider() { + SdkMeterProvider meterProvider = SdkMeterProvider.builder().build(); + this.contextRunner.withPropertyValues("management.opentelemetry.enabled=false") + .withBean(SdkMeterProvider.class, () -> meterProvider) + .run((context) -> { + OpenTelemetrySdk openTelemetry = context.getBean(OpenTelemetrySdk.class); + assertThat(openTelemetry.getSdkMeterProvider()).isNotSameAs(meterProvider); + }); + } + @Configuration(proxyBeanMethods = false) static class UserConfiguration { diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java index c964911f8e74..6df0b728035d 100644 --- a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java @@ -70,6 +70,15 @@ void providesBeans() { }); } + @Test + void whenOpenTelemetryIsDisabledDoesNotProvideBeans() { + this.contextRunner.withPropertyValues("management.opentelemetry.enabled=false").run((context) -> { + assertThat(context).doesNotHaveBean(BatchLogRecordProcessor.class); + assertThat(context).doesNotHaveBean(SdkLoggerProvider.class); + assertThat(context).doesNotHaveBean(LogLimits.class); + }); + } + @Test void whenOpenTelemetryLogsIsNotOnClasspathDoesNotProvideBeans() { this.contextRunner.withClassLoader(new FilteredClassLoader("io.opentelemetry.sdk.logs")).run((context) -> { From 2e12df8e4fa954a67db039ff1227b1d3bc2509f2 Mon Sep 17 00:00:00 2001 From: mvirole Date: Tue, 24 Feb 2026 18:28:45 +0530 Subject: [PATCH 323/930] Add more styling support to the Logback and Log4j2 color converters - Add REVERSE (ANSI code 7) to AnsiStyle enum - Expand ColorConverter ELEMENTS map to include all AnsiStyle values (bold, italic, underline, normal, faint, reverse) and all AnsiBackground colors with the bg_ prefix (bg_red, bg_bright_green, etc.) - Update tests for both logback and log4j2 converters See gh-49285 Signed-off-by: mvirole --- .../springframework/boot/ansi/AnsiStyle.java | 4 +- .../boot/logging/log4j2/ColorConverter.java | 56 ++++++++--- .../boot/logging/logback/ColorConverter.java | 48 +++++++-- .../logging/log4j2/ColorConverterTests.java | 97 +++++++++++++++++-- .../logging/logback/ColorConverterTests.java | 78 +++++++++++++++ 5 files changed, 255 insertions(+), 28 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java index c30a88c2bfb5..0d2a734f37b9 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java @@ -32,7 +32,9 @@ public enum AnsiStyle implements AnsiElement { ITALIC("3"), - UNDERLINE("4"); + UNDERLINE("4"), + + REVERSE("7"); private final String code; diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java index af4a4d64921d..0757f3851b00 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java @@ -16,6 +16,7 @@ package org.springframework.boot.logging.log4j2; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -35,6 +36,7 @@ import org.apache.logging.log4j.core.pattern.PatternParser; import org.jspecify.annotations.Nullable; +import org.springframework.boot.ansi.AnsiBackground; import org.springframework.boot.ansi.AnsiColor; import org.springframework.boot.ansi.AnsiElement; import org.springframework.boot.ansi.AnsiOutput; @@ -42,8 +44,11 @@ /** * Log4j2 {@link LogEventPatternConverter} to color output using the {@link AnsiOutput} - * class. A single option 'styling' can be provided to the converter, or if not specified - * color styling will be picked based on the logging level. + * class. One or more styling options can be provided to the converter, or if not + * specified color styling will be picked based on the logging level. Supported options + * include foreground colors (e.g. {@code red}, {@code bright_blue}), background colors + * (e.g. {@code bg_red}, {@code bg_bright_green}), and text styles (e.g. {@code bold}, + * {@code underline}, {@code reverse}). * * @author Vladimir Tsanev * @since 1.3.0 @@ -56,10 +61,17 @@ public final class ColorConverter extends LogEventPatternConverter { static { Map ansiElements = new HashMap<>(); + // Foreground colors (e.g. "red", "bright_blue") Arrays.stream(AnsiColor.values()) .filter((color) -> color != AnsiColor.DEFAULT) .forEach((color) -> ansiElements.put(color.name().toLowerCase(Locale.ROOT), color)); - ansiElements.put("faint", AnsiStyle.FAINT); + // Text styles (e.g. "bold", "italic", "underline", "reverse", "faint", "normal") + Arrays.stream(AnsiStyle.values()) + .forEach((style) -> ansiElements.put(style.name().toLowerCase(Locale.ROOT), style)); + // Background colors with "bg_" prefix (e.g. "bg_red", "bg_bright_blue") + Arrays.stream(AnsiBackground.values()) + .filter((bg) -> bg != AnsiBackground.DEFAULT) + .forEach((bg) -> ansiElements.put("bg_" + bg.name().toLowerCase(Locale.ROOT), bg)); ELEMENTS = Collections.unmodifiableMap(ansiElements); } @@ -75,12 +87,12 @@ public final class ColorConverter extends LogEventPatternConverter { private final List formatters; - private final @Nullable AnsiElement styling; + private final List stylings; - private ColorConverter(List formatters, @Nullable AnsiElement styling) { + private ColorConverter(List formatters, List stylings) { super("style", "style"); this.formatters = formatters; - this.styling = styling; + this.stylings = stylings; } @Override @@ -100,13 +112,15 @@ public void format(LogEvent event, StringBuilder toAppendTo) { formatter.format(event, buf); } if (!buf.isEmpty()) { - AnsiElement element = this.styling; - if (element == null) { + if (this.stylings.isEmpty()) { // Assume highlighting - element = LEVELS.get(event.getLevel().intLevel()); + AnsiElement element = LEVELS.get(event.getLevel().intLevel()); element = (element != null) ? element : AnsiColor.GREEN; + appendAnsiString(toAppendTo, buf.toString(), element); + } + else { + appendAnsiString(toAppendTo, buf.toString(), this.stylings.toArray(new AnsiElement[0])); } - appendAnsiString(toAppendTo, buf.toString(), element); } } @@ -114,6 +128,13 @@ private void appendAnsiString(StringBuilder toAppendTo, String in, AnsiElement e toAppendTo.append(AnsiOutput.toString(element, in)); } + protected void appendAnsiString(StringBuilder toAppendTo, String in, AnsiElement... elements) { + Object[] ansiParams = new Object[elements.length + 1]; + System.arraycopy(elements, 0, ansiParams, 0, elements.length); + ansiParams[elements.length] = in; + toAppendTo.append(AnsiOutput.toString(ansiParams)); + } + /** * Creates a new instance of the class. Required by Log4J2. * @param config the configuration @@ -131,8 +152,19 @@ private void appendAnsiString(StringBuilder toAppendTo, String in, AnsiElement e } PatternParser parser = PatternLayout.createPatternParser(config); List formatters = parser.parse(options[0]); - AnsiElement element = (options.length != 1) ? ELEMENTS.get(options[1]) : null; - return new ColorConverter(formatters, element); + List stylings = new ArrayList<>(); + for (int i = 1; i < options.length; i++) { + if (options[i] != null) { + String[] optionParts = options[i].split(","); + for (String optionPart : optionParts) { + AnsiElement element = ELEMENTS.get(optionPart.trim().toLowerCase(Locale.ROOT)); + if (element != null) { + stylings.add(element); + } + } + } + } + return new ColorConverter(formatters, stylings); } } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java index 5a9bc4152398..12fc43aab067 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java @@ -16,9 +16,11 @@ package org.springframework.boot.logging.logback; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -26,6 +28,7 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.pattern.CompositeConverter; +import org.springframework.boot.ansi.AnsiBackground; import org.springframework.boot.ansi.AnsiColor; import org.springframework.boot.ansi.AnsiElement; import org.springframework.boot.ansi.AnsiOutput; @@ -33,8 +36,11 @@ /** * Logback {@link CompositeConverter} to color output using the {@link AnsiOutput} class. - * A single 'color' option can be provided to the converter, or if not specified color - * will be picked based on the logging level. + * One or more styling options can be provided to the converter, or if not specified color + * will be picked based on the logging level. Supported options include foreground colors + * (e.g. {@code red}, {@code bright_blue}), background colors (e.g. {@code bg_red}, + * {@code bg_bright_green}), and text styles (e.g. {@code bold}, {@code underline}, + * {@code reverse}). * * @author Phillip Webb * @since 1.0.0 @@ -45,10 +51,17 @@ public class ColorConverter extends CompositeConverter { static { Map ansiElements = new HashMap<>(); + // Foreground colors (e.g. "red", "bright_blue") Arrays.stream(AnsiColor.values()) .filter((color) -> color != AnsiColor.DEFAULT) .forEach((color) -> ansiElements.put(color.name().toLowerCase(Locale.ROOT), color)); - ansiElements.put("faint", AnsiStyle.FAINT); + // Text styles (e.g. "bold", "italic", "underline", "reverse", "faint", "normal") + Arrays.stream(AnsiStyle.values()) + .forEach((style) -> ansiElements.put(style.name().toLowerCase(Locale.ROOT), style)); + // Background colors with "bg_" prefix (e.g. "bg_red", "bg_bright_blue") + Arrays.stream(AnsiBackground.values()) + .filter((bg) -> bg != AnsiBackground.DEFAULT) + .forEach((bg) -> ansiElements.put("bg_" + bg.name().toLowerCase(Locale.ROOT), bg)); ELEMENTS = Collections.unmodifiableMap(ansiElements); } @@ -63,19 +76,38 @@ public class ColorConverter extends CompositeConverter { @Override protected String transform(ILoggingEvent event, String in) { - AnsiElement color = ELEMENTS.get(getFirstOption()); - if (color == null) { + List options = getOptionList(); + List elements = new ArrayList<>(); + if (options != null) { + for (String option : options) { + String[] optionParts = option.split(","); + for (String optionPart : optionParts) { + AnsiElement element = ELEMENTS.get(optionPart.trim().toLowerCase(Locale.ROOT)); + if (element != null) { + elements.add(element); + } + } + } + } + if (elements.isEmpty()) { // Assume highlighting - color = LEVELS.get(event.getLevel().toInteger()); - color = (color != null) ? color : AnsiColor.GREEN; + AnsiElement element = LEVELS.get(event.getLevel().toInteger()); + elements.add((element != null) ? element : AnsiColor.GREEN); } - return toAnsiString(in, color); + return toAnsiString(in, elements.toArray(new AnsiElement[0])); } protected String toAnsiString(String in, AnsiElement element) { return AnsiOutput.toString(element, in); } + protected String toAnsiString(String in, AnsiElement... elements) { + Object[] ansiParams = new Object[elements.length + 1]; + System.arraycopy(elements, 0, ansiParams, 0, elements.length); + ansiParams[elements.length] = in; + return AnsiOutput.toString(ansiParams); + } + static String getName(AnsiElement element) { return ELEMENTS.entrySet() .stream() diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java index a699e7aaef1a..ca07f8c56cee 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java @@ -173,11 +173,88 @@ void brightCyan() { assertThat(output).hasToString("\033[96min\033[0;39m"); } + @Test + void bold() { + StringBuilder output = new StringBuilder(); + newConverter("bold").format(this.event, output); + assertThat(output).hasToString("\033[1min\033[0;39m"); + } + + @Test + void italic() { + StringBuilder output = new StringBuilder(); + newConverter("italic").format(this.event, output); + assertThat(output).hasToString("\033[3min\033[0;39m"); + } + + @Test + void underline() { + StringBuilder output = new StringBuilder(); + newConverter("underline").format(this.event, output); + assertThat(output).hasToString("\033[4min\033[0;39m"); + } + + @Test + void reverse() { + StringBuilder output = new StringBuilder(); + newConverter("reverse").format(this.event, output); + assertThat(output).hasToString("\033[7min\033[0;39m"); + } + + @Test + void bgRed() { + StringBuilder output = new StringBuilder(); + newConverter("bg_red").format(this.event, output); + assertThat(output).hasToString("\033[41min\033[0;39m"); + } + + @Test + void bgGreen() { + StringBuilder output = new StringBuilder(); + newConverter("bg_green").format(this.event, output); + assertThat(output).hasToString("\033[42min\033[0;39m"); + } + + @Test + void bgYellow() { + StringBuilder output = new StringBuilder(); + newConverter("bg_yellow").format(this.event, output); + assertThat(output).hasToString("\033[43min\033[0;39m"); + } + + @Test + void bgBlue() { + StringBuilder output = new StringBuilder(); + newConverter("bg_blue").format(this.event, output); + assertThat(output).hasToString("\033[44min\033[0;39m"); + } + + @Test + void bgBrightRed() { + StringBuilder output = new StringBuilder(); + newConverter("bg_bright_red").format(this.event, output); + assertThat(output).hasToString("\033[101min\033[0;39m"); + } + + @Test + void multipleStylesCommaSeparated() { + StringBuilder output = new StringBuilder(); + newConverter("bold, red").format(this.event, output); + assertThat(output).hasToString("\033[1;31min\033[0;39m"); + } + + @Test + void multipleStylesMultipleOptions() { + StringBuilder output = new StringBuilder(); + newConverter("bold", "red").format(this.event, output); + assertThat(output).hasToString("\033[1;31min\033[0;39m"); + } + @Test void highlightFatal() { this.event.setLevel(Level.FATAL); StringBuilder output = new StringBuilder(); - newConverter(null).format(this.event, output); + newConverter((String) null).format(this.event, output); assertThat(output).hasToString("\033[31min\033[0;39m"); } @@ -185,7 +262,7 @@ void highlightFatal() { void highlightError() { this.event.setLevel(Level.ERROR); StringBuilder output = new StringBuilder(); - newConverter(null).format(this.event, output); + newConverter((String) null).format(this.event, output); assertThat(output).hasToString("\033[31min\033[0;39m"); } @@ -193,7 +270,7 @@ void highlightError() { void highlightWarn() { this.event.setLevel(Level.WARN); StringBuilder output = new StringBuilder(); - newConverter(null).format(this.event, output); + newConverter((String) null).format(this.event, output); assertThat(output).hasToString("\033[33min\033[0;39m"); } @@ -201,7 +278,7 @@ void highlightWarn() { void highlightDebug() { this.event.setLevel(Level.DEBUG); StringBuilder output = new StringBuilder(); - newConverter(null).format(this.event, output); + newConverter((String) null).format(this.event, output); assertThat(output).hasToString("\033[32min\033[0;39m"); } @@ -209,12 +286,18 @@ void highlightDebug() { void highlightTrace() { this.event.setLevel(Level.TRACE); StringBuilder output = new StringBuilder(); - newConverter(null).format(this.event, output); + newConverter((String) null).format(this.event, output); assertThat(output).hasToString("\033[32min\033[0;39m"); } - private ColorConverter newConverter(@Nullable String styling) { - ColorConverter converter = ColorConverter.newInstance(null, new String[] { this.in, styling }); + private ColorConverter newConverter(@Nullable String... stylings) { + if (stylings == null) { + stylings = new String[] { null }; + } + String[] options = new String[1 + stylings.length]; + options[0] = this.in; + System.arraycopy(stylings, 0, options, 1, stylings.length); + ColorConverter converter = ColorConverter.newInstance(null, options); assertThat(converter).isNotNull(); return converter; } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/ColorConverterTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/ColorConverterTests.java index 83720c1dc1f5..736595756cab 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/ColorConverterTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/ColorConverterTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.logging.logback; +import java.util.Arrays; import java.util.Collections; import ch.qos.logback.classic.Level; @@ -170,6 +171,83 @@ void brightCyan() { assertThat(out).isEqualTo("\033[96min\033[0;39m"); } + @Test + void bold() { + this.converter.setOptionList(Collections.singletonList("bold")); + String out = this.converter.transform(this.event, this.in); + assertThat(out).isEqualTo("\033[1min\033[0;39m"); + } + + @Test + void italic() { + this.converter.setOptionList(Collections.singletonList("italic")); + String out = this.converter.transform(this.event, this.in); + assertThat(out).isEqualTo("\033[3min\033[0;39m"); + } + + @Test + void underline() { + this.converter.setOptionList(Collections.singletonList("underline")); + String out = this.converter.transform(this.event, this.in); + assertThat(out).isEqualTo("\033[4min\033[0;39m"); + } + + @Test + void reverse() { + this.converter.setOptionList(Collections.singletonList("reverse")); + String out = this.converter.transform(this.event, this.in); + assertThat(out).isEqualTo("\033[7min\033[0;39m"); + } + + @Test + void bgRed() { + this.converter.setOptionList(Collections.singletonList("bg_red")); + String out = this.converter.transform(this.event, this.in); + assertThat(out).isEqualTo("\033[41min\033[0;39m"); + } + + @Test + void bgGreen() { + this.converter.setOptionList(Collections.singletonList("bg_green")); + String out = this.converter.transform(this.event, this.in); + assertThat(out).isEqualTo("\033[42min\033[0;39m"); + } + + @Test + void bgYellow() { + this.converter.setOptionList(Collections.singletonList("bg_yellow")); + String out = this.converter.transform(this.event, this.in); + assertThat(out).isEqualTo("\033[43min\033[0;39m"); + } + + @Test + void bgBlue() { + this.converter.setOptionList(Collections.singletonList("bg_blue")); + String out = this.converter.transform(this.event, this.in); + assertThat(out).isEqualTo("\033[44min\033[0;39m"); + } + + @Test + void bgBrightRed() { + this.converter.setOptionList(Collections.singletonList("bg_bright_red")); + String out = this.converter.transform(this.event, this.in); + assertThat(out).isEqualTo("\033[101min\033[0;39m"); + } + + @Test + void multipleStylesCommaSeparated() { + this.converter.setOptionList(Collections.singletonList("bold, red")); + String out = this.converter.transform(this.event, this.in); + assertThat(out).isEqualTo("\033[1;31min\033[0;39m"); + } + + @Test + void multipleStyles() { + this.converter.setOptionList(Arrays.asList("bold", "red")); + String out = this.converter.transform(this.event, this.in); + assertThat(out).isEqualTo("\033[1;31min\033[0;39m"); + } + @Test void highlightError() { this.event.setLevel(Level.ERROR); From 29e493897178ffd62643787e2b074d02b2ec07e7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 11 Mar 2026 11:26:34 +0000 Subject: [PATCH 324/930] Polish "Add more styling support to the Logback and Log4j2 color converters" See gh-49285 --- .../boot/logging/log4j2/ColorConverter.java | 17 +++------ .../boot/logging/logback/ColorConverter.java | 3 -- .../logging/log4j2/ColorConverterTests.java | 29 +++++---------- .../reference/pages/features/logging.adoc | 37 ++++++++++++++++--- 4 files changed, 47 insertions(+), 39 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java index 0757f3851b00..ba2bc58b35bc 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java @@ -61,14 +61,11 @@ public final class ColorConverter extends LogEventPatternConverter { static { Map ansiElements = new HashMap<>(); - // Foreground colors (e.g. "red", "bright_blue") Arrays.stream(AnsiColor.values()) .filter((color) -> color != AnsiColor.DEFAULT) .forEach((color) -> ansiElements.put(color.name().toLowerCase(Locale.ROOT), color)); - // Text styles (e.g. "bold", "italic", "underline", "reverse", "faint", "normal") Arrays.stream(AnsiStyle.values()) .forEach((style) -> ansiElements.put(style.name().toLowerCase(Locale.ROOT), style)); - // Background colors with "bg_" prefix (e.g. "bg_red", "bg_bright_blue") Arrays.stream(AnsiBackground.values()) .filter((bg) -> bg != AnsiBackground.DEFAULT) .forEach((bg) -> ansiElements.put("bg_" + bg.name().toLowerCase(Locale.ROOT), bg)); @@ -153,14 +150,12 @@ protected void appendAnsiString(StringBuilder toAppendTo, String in, AnsiElement PatternParser parser = PatternLayout.createPatternParser(config); List formatters = parser.parse(options[0]); List stylings = new ArrayList<>(); - for (int i = 1; i < options.length; i++) { - if (options[i] != null) { - String[] optionParts = options[i].split(","); - for (String optionPart : optionParts) { - AnsiElement element = ELEMENTS.get(optionPart.trim().toLowerCase(Locale.ROOT)); - if (element != null) { - stylings.add(element); - } + if (options.length >= 2 && options[1] != null) { + String[] optionParts = options[1].split(","); + for (String optionPart : optionParts) { + AnsiElement element = ELEMENTS.get(optionPart.trim().toLowerCase(Locale.ROOT)); + if (element != null) { + stylings.add(element); } } } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java index 12fc43aab067..4a16f7784b93 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java @@ -51,14 +51,11 @@ public class ColorConverter extends CompositeConverter { static { Map ansiElements = new HashMap<>(); - // Foreground colors (e.g. "red", "bright_blue") Arrays.stream(AnsiColor.values()) .filter((color) -> color != AnsiColor.DEFAULT) .forEach((color) -> ansiElements.put(color.name().toLowerCase(Locale.ROOT), color)); - // Text styles (e.g. "bold", "italic", "underline", "reverse", "faint", "normal") Arrays.stream(AnsiStyle.values()) .forEach((style) -> ansiElements.put(style.name().toLowerCase(Locale.ROOT), style)); - // Background colors with "bg_" prefix (e.g. "bg_red", "bg_bright_blue") Arrays.stream(AnsiBackground.values()) .filter((bg) -> bg != AnsiBackground.DEFAULT) .forEach((bg) -> ansiElements.put("bg_" + bg.name().toLowerCase(Locale.ROOT), bg)); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java index ca07f8c56cee..fe76b5a144e0 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java @@ -237,24 +237,17 @@ void bgBrightRed() { } @Test - void multipleStylesCommaSeparated() { + void multipleStyles() { StringBuilder output = new StringBuilder(); newConverter("bold, red").format(this.event, output); assertThat(output).hasToString("\033[1;31min\033[0;39m"); } - @Test - void multipleStylesMultipleOptions() { - StringBuilder output = new StringBuilder(); - newConverter("bold", "red").format(this.event, output); - assertThat(output).hasToString("\033[1;31min\033[0;39m"); - } - @Test void highlightFatal() { this.event.setLevel(Level.FATAL); StringBuilder output = new StringBuilder(); - newConverter((String) null).format(this.event, output); + newConverter(null).format(this.event, output); assertThat(output).hasToString("\033[31min\033[0;39m"); } @@ -262,7 +255,7 @@ void highlightFatal() { void highlightError() { this.event.setLevel(Level.ERROR); StringBuilder output = new StringBuilder(); - newConverter((String) null).format(this.event, output); + newConverter(null).format(this.event, output); assertThat(output).hasToString("\033[31min\033[0;39m"); } @@ -270,7 +263,7 @@ void highlightError() { void highlightWarn() { this.event.setLevel(Level.WARN); StringBuilder output = new StringBuilder(); - newConverter((String) null).format(this.event, output); + newConverter(null).format(this.event, output); assertThat(output).hasToString("\033[33min\033[0;39m"); } @@ -278,7 +271,7 @@ void highlightWarn() { void highlightDebug() { this.event.setLevel(Level.DEBUG); StringBuilder output = new StringBuilder(); - newConverter((String) null).format(this.event, output); + newConverter(null).format(this.event, output); assertThat(output).hasToString("\033[32min\033[0;39m"); } @@ -286,17 +279,13 @@ void highlightDebug() { void highlightTrace() { this.event.setLevel(Level.TRACE); StringBuilder output = new StringBuilder(); - newConverter((String) null).format(this.event, output); + newConverter(null).format(this.event, output); assertThat(output).hasToString("\033[32min\033[0;39m"); } - private ColorConverter newConverter(@Nullable String... stylings) { - if (stylings == null) { - stylings = new String[] { null }; - } - String[] options = new String[1 + stylings.length]; - options[0] = this.in; - System.arraycopy(stylings, 0, options, 1, stylings.length); + private ColorConverter newConverter(@Nullable String additionalOptions) { + String[] options = (additionalOptions != null) ? new String[] { this.in, additionalOptions } + : new String[] { this.in }; ColorConverter converter = ColorConverter.newInstance(null, options); assertThat(converter).isNotNull(); return converter; diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc index 69ffa2d30cf0..53760e83f542 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc @@ -113,15 +113,15 @@ The following table describes the mapping of log levels to colors: | Green |=== -Alternatively, you can specify the color or style that should be used by providing it as an option to the conversion. -For example, to make the text yellow, use the following setting: +Alternatively, you can specify the color and styles that should be used by providing them as options to the conversion. +For example, to make the text yellow and bold, use the following setting: [source] ---- -%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}){yellow} +%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}){yellow,bold} ---- -The following colors and styles are supported: +The following text colors are supported: * `black` * `blue` @@ -134,13 +134,40 @@ The following colors and styles are supported: * `bright_white` * `bright_yellow` * `cyan` -* `faint` * `green` * `magenta` * `red` * `white` * `yellow` +The following background colors are supported: + +* `bg_black` +* `bg_blue` +* `bg_bright_black` +* `bg_bright_blue` +* `bg_bright_cyan` +* `bg_bright_green` +* `bg_bright_magenta` +* `bg_bright_red` +* `bg_bright_white` +* `bg_bright_yellow` +* `bg_cyan` +* `bg_green` +* `bg_magenta` +* `bg_red` +* `bg_white` +* `bg_yellow` + +The following styles are supported: + +* `bold` +* `faint` +* `italic` +* `normal` +* `reverse` +* `underline` + [[features.logging.file-output]] From 5ca8e0956ae74ae1fda77b7b0bb8eccf39bb4868 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 12 Mar 2026 11:05:26 +0100 Subject: [PATCH 325/930] Add more fine-grained support for selecting metric exemplars Closes gh-49572 --- .../reference/pages/actuator/metrics.adoc | 5 ++ .../OtlpExemplarsAutoConfigurationTests.java | 37 ++++++++++++++ ...etheusExemplarsAutoConfigurationTests.java | 28 +++++++++++ .../autoconfigure/TracingProperties.java | 49 +++++++++++++++++++ .../otlp/OtlpExemplarsAutoConfiguration.java | 27 ++++++++-- .../PrometheusExemplarsAutoConfiguration.java | 31 ++++++++++-- ...zyTracingExemplarContextProviderTests.java | 33 ++++++++++++- .../LazyTracingSpanContextTests.java | 22 ++++++++- 8 files changed, 219 insertions(+), 13 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc index 7b9b7573d4bb..a7172e80c781 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc @@ -530,6 +530,8 @@ If an `OtlpMetricsSender` bean is available, it will be configured on the `OtlpM https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exemplars[OTLP Exemplars] are also supported. To enable this feature, an javadoc:io.micrometer.registry.otlp.ExemplarContextProvider[] bean should be present. If you use xref:actuator/tracing.adoc[Micrometer Tracing], this will be auto-configured for you. +By default, only sampled traces are selected as exemplars. +You can control this behavior using the configprop:management.tracing.exemplars.filter[] property. @@ -556,6 +558,9 @@ https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage[Pr To enable this feature, a javadoc:io.prometheus.metrics.tracer.common.SpanContext[] bean should be present. If you're using the deprecated Prometheus simpleclient support and want to enable that feature, a javadoc:io.prometheus.client.exemplars.tracer.common.SpanContextSupplier[] bean should be present. If you use {url-micrometer-tracing-docs}[Micrometer Tracing], this will be auto-configured for you, but you can always create your own if you want. +By default, only sampled traces are selected as exemplars. +You can control this behavior using the configprop:management.tracing.exemplars.filter[] property. +The value `always-on` is not supported with Prometheus. Please check the https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage[Prometheus Docs], since this feature needs to be explicitly enabled on Prometheus' side, and it is only supported using the https://github.com/OpenObservability/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#exemplars[OpenMetrics] format. For ephemeral or batch jobs that may not exist long enough to be scraped, you can use https://github.com/prometheus/pushgateway[Prometheus Pushgateway] support to expose the metrics to Prometheus. diff --git a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java index ac3f0161a8b2..a963d0c2edeb 100644 --- a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java @@ -94,6 +94,43 @@ void otlpOutputShouldContainExemplars() { }); } + @Test + void otlpOutputShouldContainExemplarsWhenFilterIsAlwaysOnAndSpanIsNotSampled() { + this.contextRunner.withUserConfiguration(TracingConfiguration.class) + .withPropertyValues("management.tracing.sampling.probability=0.0", + "management.tracing.exemplars.filter=always-on") + .run((context) -> { + assertThat(context).hasSingleBean(ExemplarContextProvider.class); + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("test.observation", observationRegistry).stop(); + OtlpMeterRegistry otlpMeterRegistry = context.getBean(OtlpMeterRegistry.class); + TestOtlpMetricsSender metricsSender = context.getBean(TestOtlpMetricsSender.class); + otlpMeterRegistry.close(); + assertThat(metricsSender.getOtlpRequest()).containsOnlyOnce("name: \"test.observation\"") + .containsOnlyOnce("exemplars") + .containsOnlyOnce("span_id") + .containsOnlyOnce("trace_id"); + }); + } + + @Test + void otlpOutputShouldNotContainExemplarsWhenFilterIsAlwaysOff() { + this.contextRunner.withUserConfiguration(TracingConfiguration.class) + .withPropertyValues("management.tracing.exemplars.filter=always-off") + .run((context) -> { + assertThat(context).hasSingleBean(ExemplarContextProvider.class); + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("test.observation", observationRegistry).stop(); + OtlpMeterRegistry otlpMeterRegistry = context.getBean(OtlpMeterRegistry.class); + TestOtlpMetricsSender metricsSender = context.getBean(TestOtlpMetricsSender.class); + otlpMeterRegistry.close(); + assertThat(metricsSender.getOtlpRequest()).containsOnlyOnce("name: \"test.observation\"") + .doesNotContain("exemplars") + .doesNotContain("span_id") + .doesNotContain("trace_id"); + }); + } + @Configuration(proxyBeanMethods = false) private static final class CustomConfiguration { diff --git a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/PrometheusExemplarsAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/PrometheusExemplarsAutoConfigurationTests.java index b78039822845..652ec872e598 100644 --- a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/PrometheusExemplarsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/PrometheusExemplarsAutoConfigurationTests.java @@ -32,6 +32,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; import org.springframework.boot.micrometer.metrics.autoconfigure.export.prometheus.PrometheusMetricsExportAutoConfiguration; import org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration; @@ -148,6 +149,33 @@ void prometheusOpenMetricsOutputShouldContainExemplars() { }); } + @Test + void shouldFailWhenFilterIsAlwaysOn() { + this.contextRunner.withUserConfiguration(TracingConfiguration.class) + .withPropertyValues("management.tracing.exemplars.filter=always-on") + .run((context) -> assertThat(context).hasFailed() + .getFailure() + .rootCause() + .isInstanceOf(InvalidConfigurationPropertyValueException.class) + .hasMessageContaining( + "Property management.tracing.exemplars.filter with value 'always-on' is invalid: Prometheus doesn't support the 'always-on' exemplar filter.")); + } + + @Test + void prometheusOpenMetricsOutputShouldNotContainExemplarsWhenFilterIsAlwaysOff() { + this.contextRunner.withUserConfiguration(TracingConfiguration.class) + .withPropertyValues("management.tracing.exemplars.filter=always-off") + .run((context) -> { + assertThat(context).hasSingleBean(SpanContext.class); + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("test.observation", observationRegistry).stop(); + PrometheusMeterRegistry prometheusMeterRegistry = context.getBean(PrometheusMeterRegistry.class); + String openMetricsOutput = prometheusMeterRegistry.scrape(OpenMetricsTextFormatWriter.CONTENT_TYPE); + assertThat(openMetricsOutput).contains("test_observation_seconds_bucket"); + assertThat(openMetricsOutput).doesNotContain("span_id").doesNotContain("trace_id"); + }); + } + @Configuration(proxyBeanMethods = false) private static final class CustomConfiguration { diff --git a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/TracingProperties.java b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/TracingProperties.java index 7a5378a18b0c..0bda60865f49 100644 --- a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/TracingProperties.java +++ b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/TracingProperties.java @@ -48,6 +48,11 @@ public class TracingProperties { */ private final Propagation propagation = new Propagation(); + /** + * Exemplars configuration. + */ + private final Exemplars exemplars = new Exemplars(); + public Sampling getSampling() { return this.sampling; } @@ -60,6 +65,10 @@ public Propagation getPropagation() { return this.propagation; } + public Exemplars getExemplars() { + return this.exemplars; + } + public static class Sampling { /** @@ -249,4 +258,44 @@ public enum PropagationType { } + /** + * Exemplars configuration. + */ + public static class Exemplars { + + /** + * Filter which exemplars are selected. ALWAYS_ON is not supported when using + * Prometheus. + */ + private Filter filter = Filter.SAMPLED_TRACES; + + public Filter getFilter() { + return this.filter; + } + + public void setFilter(Filter filter) { + this.filter = filter; + } + + public enum Filter { + + /** + * Always select exemplars, regardless of whether the span is sampled. + */ + ALWAYS_ON, + + /** + * Never select exemplars. + */ + ALWAYS_OFF, + + /** + * Only select exemplars from sampled traces. + */ + SAMPLED_TRACES + + } + + } + } diff --git a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java index 848efd2b9577..bc646102d2b0 100644 --- a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java +++ b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java @@ -29,7 +29,10 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.boot.micrometer.tracing.autoconfigure.MicrometerTracingAutoConfiguration; +import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties; +import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Exemplars.Filter; import org.springframework.context.annotation.Bean; import org.springframework.lang.Contract; import org.springframework.util.function.SingletonSupplier; @@ -39,6 +42,7 @@ * Exemplars with Micrometer Tracing. * * @author Jonatan Ivanov + * @author Moritz Halbritter * @since 4.1.0 */ @AutoConfiguration( @@ -46,12 +50,14 @@ after = MicrometerTracingAutoConfiguration.class) @ConditionalOnBean(Tracer.class) @ConditionalOnClass({ Tracer.class, ExemplarContextProvider.class }) +@EnableConfigurationProperties(TracingProperties.class) public final class OtlpExemplarsAutoConfiguration { @Bean @ConditionalOnMissingBean - ExemplarContextProvider exemplarContextProvider(ObjectProvider tracerProvider) { - return new LazyTracingExemplarContextProvider(tracerProvider); + ExemplarContextProvider exemplarContextProvider(ObjectProvider tracerProvider, + TracingProperties properties) { + return new LazyTracingExemplarContextProvider(tracerProvider, properties.getExemplars().getFilter()); } /** @@ -64,14 +70,17 @@ static class LazyTracingExemplarContextProvider implements ExemplarContextProvid private final SingletonSupplier tracer; - LazyTracingExemplarContextProvider(ObjectProvider tracerProvider) { + private final Filter filter; + + LazyTracingExemplarContextProvider(ObjectProvider tracerProvider, Filter filter) { this.tracer = SingletonSupplier.of(tracerProvider::getObject); + this.filter = filter; } @Override public @Nullable OtlpExemplarContext getExemplarContext() { Span span = this.tracer.obtain().currentSpan(); - if (isSampled(span)) { + if (isExemplar(span)) { TraceContext context = span.context(); return new OtlpExemplarContext(context.traceId(), context.spanId()); } @@ -79,10 +88,18 @@ static class LazyTracingExemplarContextProvider implements ExemplarContextProvid } @Contract("null -> false") - private boolean isSampled(@Nullable Span span) { + private boolean isExemplar(@Nullable Span span) { if (span == null) { return false; } + return switch (this.filter) { + case ALWAYS_ON -> true; + case ALWAYS_OFF -> false; + case SAMPLED_TRACES -> isSampled(span); + }; + } + + private boolean isSampled(Span span) { Boolean sampled = span.context().sampled(); return sampled != null && sampled; } diff --git a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfiguration.java b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfiguration.java index a98b081e61b3..8b2164d78809 100644 --- a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfiguration.java +++ b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfiguration.java @@ -27,7 +27,11 @@ 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.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.boot.micrometer.tracing.autoconfigure.MicrometerTracingAutoConfiguration; +import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties; +import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Exemplars.Filter; import org.springframework.context.annotation.Bean; import org.springframework.util.function.SingletonSupplier; @@ -36,6 +40,7 @@ * Micrometer Tracing. * * @author Jonatan Ivanov + * @author Moritz Halbritter * @since 4.0.0 */ @AutoConfiguration( @@ -43,12 +48,13 @@ after = MicrometerTracingAutoConfiguration.class) @ConditionalOnBean(Tracer.class) @ConditionalOnClass({ Tracer.class, SpanContext.class }) +@EnableConfigurationProperties(TracingProperties.class) public final class PrometheusExemplarsAutoConfiguration { @Bean @ConditionalOnMissingBean - SpanContext spanContext(ObjectProvider tracerProvider) { - return new LazyTracingSpanContext(tracerProvider); + SpanContext spanContext(ObjectProvider tracerProvider, TracingProperties properties) { + return new LazyTracingSpanContext(tracerProvider, properties.getExemplars().getFilter()); } /** @@ -60,8 +66,15 @@ static class LazyTracingSpanContext implements SpanContext { private final SingletonSupplier tracer; - LazyTracingSpanContext(ObjectProvider tracerProvider) { + private final Filter filter; + + LazyTracingSpanContext(ObjectProvider tracerProvider, Filter filter) { + if (filter == Filter.ALWAYS_ON) { + throw new InvalidConfigurationPropertyValueException("management.tracing.exemplars.filter", "always-on", + "Prometheus doesn't support the 'always-on' exemplar filter."); + } this.tracer = SingletonSupplier.of(tracerProvider::getObject); + this.filter = filter; } @Override @@ -82,14 +95,22 @@ public boolean isCurrentSpanSampled() { if (currentSpan == null) { return false; } - Boolean sampled = currentSpan.context().sampled(); - return sampled != null && sampled; + return switch (this.filter) { + case ALWAYS_ON -> throw new UnsupportedOperationException("ALWAYS_ON filter is not supported"); + case ALWAYS_OFF -> false; + case SAMPLED_TRACES -> isSampled(currentSpan); + }; } @Override public void markCurrentSpanAsExemplar() { } + private boolean isSampled(Span span) { + Boolean sampled = span.context().sampled(); + return sampled != null && sampled; + } + private @Nullable Span currentSpan() { return this.tracer.obtain().currentSpan(); } diff --git a/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/LazyTracingExemplarContextProviderTests.java b/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/LazyTracingExemplarContextProviderTests.java index 3c7ae921983c..fbeba665762d 100644 --- a/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/LazyTracingExemplarContextProviderTests.java +++ b/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/LazyTracingExemplarContextProviderTests.java @@ -24,6 +24,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Exemplars.Filter; import org.springframework.boot.micrometer.tracing.autoconfigure.otlp.OtlpExemplarsAutoConfiguration.LazyTracingExemplarContextProvider; import static org.assertj.core.api.Assertions.assertThat; @@ -62,8 +63,8 @@ public Tracer getIfUnique() throws BeansException { }; - private final LazyTracingExemplarContextProvider contextProvider = new LazyTracingExemplarContextProvider( - this.objectProvider); + private LazyTracingExemplarContextProvider contextProvider = new LazyTracingExemplarContextProvider( + this.objectProvider, Filter.SAMPLED_TRACES); @Test void whenCurrentSpanIsNullThenExemplarContextIsNull() { @@ -146,4 +147,32 @@ void whenCurrentSpanHasDeferredSamplingThenExemplarContextIsNull() { assertThat(this.contextProvider.getExemplarContext()).isNull(); } + @Test + void whenFilterIsAlwaysOnAndSpanIsNotSampledThenExemplarContextIsNotNull() { + this.contextProvider = new LazyTracingExemplarContextProvider(this.objectProvider, Filter.ALWAYS_ON); + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(false); + given(span.context()).willReturn(traceContext); + assertThat(this.contextProvider.getExemplarContext()).isNotNull(); + } + + @Test + void whenFilterIsAlwaysOnAndCurrentSpanIsNullThenExemplarContextIsNull() { + this.contextProvider = new LazyTracingExemplarContextProvider(this.objectProvider, Filter.ALWAYS_ON); + assertThat(this.contextProvider.getExemplarContext()).isNull(); + } + + @Test + void whenFilterIsAlwaysOffAndSpanIsSampledThenExemplarContextIsNull() { + this.contextProvider = new LazyTracingExemplarContextProvider(this.objectProvider, Filter.ALWAYS_OFF); + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(true); + given(span.context()).willReturn(traceContext); + assertThat(this.contextProvider.getExemplarContext()).isNull(); + } + } diff --git a/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/LazyTracingSpanContextTests.java b/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/LazyTracingSpanContextTests.java index b7a6d7e48ebd..870b8c460313 100644 --- a/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/LazyTracingSpanContextTests.java +++ b/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/LazyTracingSpanContextTests.java @@ -24,9 +24,12 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Exemplars.Filter; import org.springframework.boot.micrometer.tracing.autoconfigure.prometheus.PrometheusExemplarsAutoConfiguration.LazyTracingSpanContext; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -63,7 +66,7 @@ public Tracer getIfUnique() throws BeansException { }; - private final LazyTracingSpanContext spanContext = new LazyTracingSpanContext(this.objectProvider); + private LazyTracingSpanContext spanContext = new LazyTracingSpanContext(this.objectProvider, Filter.SAMPLED_TRACES); @Test void whenCurrentSpanIsNullThenSpanIdIsNull() { @@ -148,4 +151,21 @@ void whenCurrentSpanHasDeferredSamplingThenSampledIsFalse() { assertThat(this.spanContext.isCurrentSpanSampled()).isFalse(); } + @Test + void whenFilterIsAlwaysOnThenConstructorThrows() { + assertThatExceptionOfType(InvalidConfigurationPropertyValueException.class) + .isThrownBy(() -> new LazyTracingSpanContext(this.objectProvider, Filter.ALWAYS_ON)); + } + + @Test + void whenFilterIsAlwaysOffAndSpanIsSampledThenSampledIsFalse() { + this.spanContext = new LazyTracingSpanContext(this.objectProvider, Filter.ALWAYS_OFF); + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(true); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContext.isCurrentSpanSampled()).isFalse(); + } + } From 085d5225700e67855287c627993ad46b41bbfd58 Mon Sep 17 00:00:00 2001 From: Apoorv Darshan Date: Wed, 25 Feb 2026 21:29:34 +0530 Subject: [PATCH 326/930] Update TestRestTemplate's default cookie handling to match RestTemplate's See gh-49261 Signed-off-by: Apoorv Darshan --- .../boot/http/client/HttpClientSettings.java | 47 +++++++++++++++---- .../http/client/HttpComponentsCookieSpec.java | 44 +++++++++++++++++ .../HttpComponentsHttpClientBuilder.java | 8 +++- .../boot/http/client/HttpCookies.java | 42 +++++++++++++++++ .../http/client/JdkHttpClientBuilder.java | 10 ++++ .../http/client/JettyHttpClientBuilder.java | 9 ++++ .../http/client/HttpClientSettingsTests.java | 19 ++++++-- .../HttpClientAutoConfigurationTests.java | 4 +- ...HttpClientSettingsPropertyMapperTests.java | 2 +- .../boot/restclient/RestTemplateBuilder.java | 15 ++++++ .../boot/resttestclient/TestRestTemplate.java | 27 +++++++++-- .../resttestclient/TestRestTemplateTests.java | 16 +++++++ 12 files changed, 222 insertions(+), 21 deletions(-) create mode 100644 module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsCookieSpec.java create mode 100644 module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpCookies.java diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java index 5f35ff04e432..1722ec018840 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java @@ -25,6 +25,8 @@ /** * Settings that can be applied when creating an imperative or reactive HTTP client. * + * @param cookies the cookie handling strategy to use or null to use the underlying + * library's default * @param redirects the follow redirect strategy to use or null to redirect whenever the * underlying library allows it * @param connectTimeout the connect timeout @@ -33,10 +35,36 @@ * @author Phillip Webb * @since 3.5.0 */ -public record HttpClientSettings(@Nullable HttpRedirects redirects, @Nullable Duration connectTimeout, - @Nullable Duration readTimeout, @Nullable SslBundle sslBundle) { +public record HttpClientSettings(@Nullable HttpCookies cookies, @Nullable HttpRedirects redirects, + @Nullable Duration connectTimeout, @Nullable Duration readTimeout, @Nullable SslBundle sslBundle) { - private static final HttpClientSettings defaults = new HttpClientSettings(null, null, null, null); + /** + * Create a new {@link HttpClientSettings} instance. + * @param redirects the follow redirect strategy to use + * @param connectTimeout the connect timeout + * @param readTimeout the read timeout + * @param sslBundle the SSL bundle providing SSL configuration + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link HttpClientSettings#HttpClientSettings(HttpCookies, HttpRedirects, Duration, Duration, SslBundle)} + */ + @Deprecated(since = "4.1.0", forRemoval = true) + public HttpClientSettings(@Nullable HttpRedirects redirects, @Nullable Duration connectTimeout, + @Nullable Duration readTimeout, @Nullable SslBundle sslBundle) { + this(null, redirects, connectTimeout, readTimeout, sslBundle); + } + + private static final HttpClientSettings defaults = new HttpClientSettings(null, null, null, null, null); + + /** + * Return a new {@link HttpClientSettings} instance with an updated cookie handling + * setting. + * @param cookies the new cookie handling setting + * @return a new {@link HttpClientSettings} instance + * @since 4.1.0 + */ + public HttpClientSettings withCookies(@Nullable HttpCookies cookies) { + return new HttpClientSettings(cookies, this.redirects, this.connectTimeout, this.readTimeout, this.sslBundle); + } /** * Return a new {@link HttpClientSettings} instance with an updated connect timeout @@ -46,7 +74,7 @@ public record HttpClientSettings(@Nullable HttpRedirects redirects, @Nullable Du * @since 4.0.0 */ public HttpClientSettings withConnectTimeout(@Nullable Duration connectTimeout) { - return new HttpClientSettings(this.redirects, connectTimeout, this.readTimeout, this.sslBundle); + return new HttpClientSettings(this.cookies, this.redirects, connectTimeout, this.readTimeout, this.sslBundle); } /** @@ -57,7 +85,7 @@ public HttpClientSettings withConnectTimeout(@Nullable Duration connectTimeout) * @since 4.0.0 */ public HttpClientSettings withReadTimeout(@Nullable Duration readTimeout) { - return new HttpClientSettings(this.redirects, this.connectTimeout, readTimeout, this.sslBundle); + return new HttpClientSettings(this.cookies, this.redirects, this.connectTimeout, readTimeout, this.sslBundle); } /** @@ -69,7 +97,7 @@ public HttpClientSettings withReadTimeout(@Nullable Duration readTimeout) { * @since 4.0.0 */ public HttpClientSettings withTimeouts(@Nullable Duration connectTimeout, @Nullable Duration readTimeout) { - return new HttpClientSettings(this.redirects, connectTimeout, readTimeout, this.sslBundle); + return new HttpClientSettings(this.cookies, this.redirects, connectTimeout, readTimeout, this.sslBundle); } /** @@ -80,7 +108,7 @@ public HttpClientSettings withTimeouts(@Nullable Duration connectTimeout, @Nulla * @since 4.0.0 */ public HttpClientSettings withSslBundle(@Nullable SslBundle sslBundle) { - return new HttpClientSettings(this.redirects, this.connectTimeout, this.readTimeout, sslBundle); + return new HttpClientSettings(this.cookies, this.redirects, this.connectTimeout, this.readTimeout, sslBundle); } /** @@ -90,7 +118,7 @@ public HttpClientSettings withSslBundle(@Nullable SslBundle sslBundle) { * @since 4.0.0 */ public HttpClientSettings withRedirects(@Nullable HttpRedirects redirects) { - return new HttpClientSettings(redirects, this.connectTimeout, this.readTimeout, this.sslBundle); + return new HttpClientSettings(this.cookies, redirects, this.connectTimeout, this.readTimeout, this.sslBundle); } /** @@ -104,11 +132,12 @@ public HttpClientSettings orElse(@Nullable HttpClientSettings other) { if (other == null) { return this; } + HttpCookies cookies = (cookies() != null) ? cookies() : other.cookies(); HttpRedirects redirects = (redirects() != null) ? redirects() : other.redirects(); Duration connectTimeout = (connectTimeout() != null) ? connectTimeout() : other.connectTimeout(); Duration readTimeout = (readTimeout() != null) ? readTimeout() : other.readTimeout(); SslBundle sslBundle = (sslBundle() != null) ? sslBundle() : other.sslBundle(); - return new HttpClientSettings(redirects, connectTimeout, readTimeout, sslBundle); + return new HttpClientSettings(cookies, redirects, connectTimeout, readTimeout, sslBundle); } /** diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsCookieSpec.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsCookieSpec.java new file mode 100644 index 000000000000..a597f36c52c4 --- /dev/null +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsCookieSpec.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import org.apache.hc.client5.http.cookie.StandardCookieSpec; +import org.jspecify.annotations.Nullable; + +/** + * Adapts {@link HttpCookies} to an + * Apache HttpComponents + * cookie spec identifier. + * + * @author Apoorv Darshan + */ +final class HttpComponentsCookieSpec { + + private HttpComponentsCookieSpec() { + } + + static @Nullable String get(@Nullable HttpCookies cookies) { + if (cookies == null) { + return null; + } + return switch (cookies) { + case ENABLE_WHEN_POSSIBLE, ENABLE -> StandardCookieSpec.STRICT; + case DISABLE -> StandardCookieSpec.IGNORE; + }; + } + +} diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java index 5e3f8b5c228e..cb1653568cde 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java @@ -180,7 +180,7 @@ public CloseableHttpClient build(@Nullable HttpClientSettings settings) { .useSystemProperties() .setRedirectStrategy(HttpComponentsRedirectStrategy.get(settings.redirects())) .setConnectionManager(createConnectionManager(settings)) - .setDefaultRequestConfig(createDefaultRequestConfig()); + .setDefaultRequestConfig(createDefaultRequestConfig(settings)); this.customizer.accept(builder); return builder.build(); } @@ -218,8 +218,12 @@ private ConnectionConfig createConnectionConfig(HttpClientSettings settings) { return builder.build(); } - private RequestConfig createDefaultRequestConfig() { + private RequestConfig createDefaultRequestConfig(HttpClientSettings settings) { RequestConfig.Builder builder = RequestConfig.custom(); + String cookieSpec = HttpComponentsCookieSpec.get(settings.cookies()); + if (cookieSpec != null) { + builder.setCookieSpec(cookieSpec); + } this.defaultRequestConfigCustomizer.accept(builder); return builder.build(); } diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpCookies.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpCookies.java new file mode 100644 index 000000000000..0f2ba8162dbf --- /dev/null +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpCookies.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +/** + * Cookie handling strategies supported by HTTP clients. + * + * @author Apoorv Darshan + * @since 4.1.0 + */ +public enum HttpCookies { + + /** + * Enable cookies (if the underlying library has support). + */ + ENABLE_WHEN_POSSIBLE, + + /** + * Enable cookies (fail if the underlying library has no support). + */ + ENABLE, + + /** + * Disable cookies (fail if the underlying library has no support). + */ + DISABLE + +} diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkHttpClientBuilder.java index 2eb67a5afc93..58c27fe20926 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkHttpClientBuilder.java @@ -16,6 +16,8 @@ package org.springframework.boot.http.client; +import java.net.CookieHandler; +import java.net.CookieManager; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; import java.util.concurrent.Executor; @@ -83,6 +85,7 @@ public HttpClient build(@Nullable HttpClientSettings settings) { Assert.isTrue(settings.readTimeout() == null, "'settings' must not have a 'readTimeout'"); HttpClient.Builder builder = HttpClient.newBuilder(); PropertyMapper map = PropertyMapper.get(); + map.from(settings::cookies).as(this::asCookieHandler).to(builder::cookieHandler); map.from(settings::redirects).always().as(this::asHttpClientRedirect).to(builder::followRedirects); map.from(settings::connectTimeout).to(builder::connectTimeout); map.from(settings::sslBundle).as(SslBundle::createSslContext).to(builder::sslContext); @@ -99,6 +102,13 @@ private SSLParameters asSslParameters(SslBundle sslBundle) { return parameters; } + private @Nullable CookieHandler asCookieHandler(HttpCookies cookies) { + return switch (cookies) { + case ENABLE_WHEN_POSSIBLE, ENABLE -> new CookieManager(); + case DISABLE -> null; + }; + } + private Redirect asHttpClientRedirect(@Nullable HttpRedirects redirects) { if (redirects == null) { return Redirect.NORMAL; diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java index 157635cd87c3..6ff162655260 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java @@ -28,6 +28,7 @@ import org.eclipse.jetty.client.Request; import org.eclipse.jetty.client.transport.HttpClientTransportDynamic; import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; +import org.eclipse.jetty.http.HttpCookieStore; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.jspecify.annotations.Nullable; @@ -140,6 +141,7 @@ public HttpClient build(@Nullable HttpClientSettings settings) { HttpClient httpClient = createHttpClient(settings.readTimeout(), transport); PropertyMapper map = PropertyMapper.get(); map.from(settings::connectTimeout).as(Duration::toMillis).to(httpClient::setConnectTimeout); + map.from(settings::cookies).as(this::asCookieStore).to(httpClient::setHttpCookieStore); map.from(settings::redirects).always().as(this::followRedirects).to(httpClient::setFollowRedirects); this.customizer.accept(httpClient); return httpClient; @@ -182,6 +184,13 @@ private SslContextFactory.Client createSslContextFactory(SslBundle sslBundle) { return factory; } + private @Nullable HttpCookieStore asCookieStore(HttpCookies cookies) { + return switch (cookies) { + case ENABLE_WHEN_POSSIBLE, ENABLE -> null; + case DISABLE -> new HttpCookieStore.Empty(); + }; + } + private boolean followRedirects(@Nullable HttpRedirects redirects) { if (redirects == null) { return true; diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpClientSettingsTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpClientSettingsTests.java index d37f796e772d..c7b4d1af1709 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpClientSettingsTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpClientSettingsTests.java @@ -47,7 +47,8 @@ void defaults() { @Test void createWithNulls() { - HttpClientSettings settings = new HttpClientSettings(null, null, null, null); + HttpClientSettings settings = new HttpClientSettings(null, null, null, null, null); + assertThat(settings.cookies()).isNull(); assertThat(settings.redirects()).isNull(); assertThat(settings.connectTimeout()).isNull(); assertThat(settings.readTimeout()).isNull(); @@ -82,6 +83,16 @@ void withSslBundleReturnsInstanceWithUpdatedSslBundle() { assertThat(settings.sslBundle()).isSameAs(sslBundle); } + @Test + void withCookiesReturnsInstanceWithUpdatedCookies() { + HttpClientSettings settings = HttpClientSettings.defaults().withCookies(HttpCookies.DISABLE); + assertThat(settings.cookies()).isEqualTo(HttpCookies.DISABLE); + assertThat(settings.redirects()).isNull(); + assertThat(settings.connectTimeout()).isNull(); + assertThat(settings.readTimeout()).isNull(); + assertThat(settings.sslBundle()).isNull(); + } + @Test void withRedirectsReturnsInstanceWithUpdatedRedirect() { HttpClientSettings settings = HttpClientSettings.defaults().withRedirects(HttpRedirects.DONT_FOLLOW); @@ -94,8 +105,10 @@ void withRedirectsReturnsInstanceWithUpdatedRedirect() { @Test void orElseReturnsNewInstanceWithUpdatedValues() { SslBundle sslBundle = mock(SslBundle.class); - HttpClientSettings settings = new HttpClientSettings(null, ONE_SECOND, null, null) - .orElse(new HttpClientSettings(HttpRedirects.FOLLOW_WHEN_POSSIBLE, TWO_SECONDS, TWO_SECONDS, sslBundle)); + HttpClientSettings settings = new HttpClientSettings(null, null, ONE_SECOND, null, null) + .orElse(new HttpClientSettings(HttpCookies.ENABLE, HttpRedirects.FOLLOW_WHEN_POSSIBLE, TWO_SECONDS, + TWO_SECONDS, sslBundle)); + assertThat(settings.cookies()).isEqualTo(HttpCookies.ENABLE); assertThat(settings.redirects()).isEqualTo(HttpRedirects.FOLLOW_WHEN_POSSIBLE); assertThat(settings.connectTimeout()).isEqualTo(ONE_SECOND); assertThat(settings.readTimeout()).isEqualTo(TWO_SECONDS); diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfigurationTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfigurationTests.java index f3ebb359d0eb..9bc5844f677c 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfigurationTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfigurationTests.java @@ -52,14 +52,14 @@ void createsHttpClientSettingsFromProperties() { .withPropertyValues("spring.http.clients.redirects=dont-follow", "spring.http.clients.connect-timeout=1s", "spring.http.clients.read-timeout=2s") .run((context) -> assertThat(context.getBean(HttpClientSettings.class)).isEqualTo(new HttpClientSettings( - HttpRedirects.DONT_FOLLOW, Duration.ofSeconds(1), Duration.ofSeconds(2), null))); + null, HttpRedirects.DONT_FOLLOW, Duration.ofSeconds(1), Duration.ofSeconds(2), null))); } @Test void doesNotReplaceUserProvidedHttpClientSettings() { this.contextRunner.withUserConfiguration(TestHttpClientConfiguration.class) .run((context) -> assertThat(context.getBean(HttpClientSettings.class)) - .isEqualTo(new HttpClientSettings(null, Duration.ofSeconds(1), Duration.ofSeconds(2), null))); + .isEqualTo(new HttpClientSettings(null, null, Duration.ofSeconds(1), Duration.ofSeconds(2), null))); } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientSettingsPropertyMapperTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientSettingsPropertyMapperTests.java index 2a49cce16f4d..e6d0b8b77829 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientSettingsPropertyMapperTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientSettingsPropertyMapperTests.java @@ -93,7 +93,7 @@ void mapMapsSslBundle() { @Test void mapUsesBaseSettingsForMissingProperties() { - HttpClientSettings baseSettings = new HttpClientSettings(HttpRedirects.FOLLOW_WHEN_POSSIBLE, + HttpClientSettings baseSettings = new HttpClientSettings(null, HttpRedirects.FOLLOW_WHEN_POSSIBLE, Duration.ofSeconds(15), Duration.ofSeconds(25), null); HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(null, baseSettings); TestHttpClientSettingsProperties properties = new TestHttpClientSettingsProperties(); diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java index 37848d57e6ff..508e349dca8b 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java @@ -35,6 +35,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.HttpCookies; import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.ssl.SslBundle; import org.springframework.http.client.ClientHttpRequest; @@ -481,6 +482,20 @@ public RestTemplateBuilder readTimeout(Duration readTimeout) { this.customizers, this.requestCustomizers); } + /** + * Sets the cookie handling strategy on the underlying + * {@link ClientHttpRequestFactory}. + * @param cookies the cookie handling strategy + * @return a new builder instance. + * @since 4.1.0 + */ + public RestTemplateBuilder cookies(HttpCookies cookies) { + return new RestTemplateBuilder(this.clientSettings.withCookies(cookies), this.detectRequestFactory, + this.rootUri, this.messageConverters, this.interceptors, this.requestFactoryBuilder, + this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); + } + /** * Sets the redirect strategy on the underlying {@link ClientHttpRequestFactory}. * @param redirects the redirect strategy diff --git a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java index b9988d6d48bb..ef63db42d436 100644 --- a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java +++ b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java @@ -41,6 +41,7 @@ import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpComponentsClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.HttpComponentsHttpClientBuilder.TlsSocketStrategyFactory; +import org.springframework.boot.http.client.HttpCookies; import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; @@ -69,8 +70,7 @@ * status code}. *

    * A {@code TestRestTemplate} can optionally carry Basic authentication headers. If Apache - * Http Client 4.3.2 or better is available (recommended) it will be used as the client, - * and by default configured to ignore cookies. + * Http Client 4.3.2 or better is available (recommended) it will be used as the client. *

    * Note: To prevent injection problems this class intentionally does not extend * {@link RestTemplate}. If you need access to the underlying {@link RestTemplate} use @@ -160,10 +160,13 @@ private static RestTemplateBuilder createInitialBuilder(RestTemplateBuilder buil return builder; } + @SuppressWarnings("deprecation") private static HttpComponentsClientHttpRequestFactoryBuilder applyHttpClientOptions( HttpComponentsClientHttpRequestFactoryBuilder builder, HttpClientOption[] httpClientOptions) { - builder = builder.withDefaultRequestConfigCustomizer( - new CookieSpecCustomizer(HttpClientOption.ENABLE_COOKIES.isPresent(httpClientOptions))); + if (HttpClientOption.ENABLE_COOKIES.isPresent(httpClientOptions)) { + builder = builder.withDefaultRequestConfigCustomizer( + new CookieSpecCustomizer(true)); + } if (HttpClientOption.SSL.isPresent(httpClientOptions)) { builder = builder.withTlsSocketStrategyFactory(new SelfSignedTlsSocketStrategyFactory()); } @@ -974,6 +977,19 @@ public TestRestTemplate withRedirects(HttpRedirects redirects) { return withClientSettings((settings) -> settings.withRedirects(redirects)); } + /** + * Creates a new {@code TestRestTemplate} with the same configuration as this one, + * except that it will apply the given {@link HttpCookies}. The request factory used is + * a new instance of the underlying {@link RestTemplate}'s request factory type (when + * possible). + * @param cookies the new cookie settings + * @return the new template + * @since 4.1.0 + */ + public TestRestTemplate withCookies(HttpCookies cookies) { + return withClientSettings((settings) -> settings.withCookies(cookies)); + } + /** * Creates a new {@code TestRestTemplate} with the same configuration as this one, * except that it will apply the given {@link HttpClientSettings}. The request factory @@ -1036,7 +1052,10 @@ public enum HttpClientOption { /** * Enable cookies. + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link TestRestTemplate#withCookies(HttpCookies)} */ + @Deprecated(since = "4.1.0", forRemoval = true) ENABLE_COOKIES, /** diff --git a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java index 932376a8fcf5..f835ec959511 100644 --- a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java +++ b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java @@ -35,6 +35,7 @@ import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.HttpCookies; import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.boot.resttestclient.TestRestTemplate.HttpClientOption; @@ -140,11 +141,26 @@ void authenticated() { } @Test + @SuppressWarnings("removal") void options() { RequestConfig config = getRequestConfig(new TestRestTemplate(HttpClientOption.ENABLE_COOKIES)); assertThat(config.getCookieSpec()).isEqualTo("strict"); } + @Test + void defaultCookieSpecMatchesRestTemplate() { + RequestConfig config = getRequestConfig(new TestRestTemplate()); + assertThat(config.getCookieSpec()).isNull(); + } + + @Test + void withCookies() { + TestRestTemplate template = new TestRestTemplate(); + assertThat(getRequestConfig(template).getCookieSpec()).isNull(); + assertThat(getRequestConfig(template.withCookies(HttpCookies.ENABLE)).getCookieSpec()).isEqualTo("strict"); + assertThat(getRequestConfig(template.withCookies(HttpCookies.DISABLE)).getCookieSpec()).isEqualTo("ignoreCookies"); + } + @Test void jdkBuilderCanBeSpecifiedWithSpecificRedirects() { RestTemplateBuilder builder = new RestTemplateBuilder() From 5721f57acd007a98aac1a8c321b387b93381c6d0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Mar 2026 12:13:15 +0000 Subject: [PATCH 327/930] Polish "Update TestRestTemplate's default cookie handling to match RestTemplate's" See gh-49261 Signed-off-by: Andy Wilkinson --- .../pages/testing/test-utilities.adoc | 1 - .../boot/http/client/HttpClientSettings.java | 31 +++++++------ .../http/client/HttpComponentsCookieSpec.java | 44 ------------------- .../HttpComponentsHttpClientBuilder.java | 8 +++- ...tpCookies.java => HttpCookieHandling.java} | 12 +++-- .../http/client/JdkHttpClientBuilder.java | 6 +-- .../http/client/JettyHttpClientBuilder.java | 8 ++-- .../http/client/ReactorHttpClientBuilder.java | 3 ++ ...onentsClientHttpRequestFactoryBuilder.java | 4 ++ ...SimpleClientHttpRequestFactoryBuilder.java | 3 ++ .../HttpClientSettingsProperties.java | 14 ++++++ .../HttpClientSettingsPropertyMapper.java | 1 + .../http/client/HttpClientSettingsTests.java | 12 ++--- ...sClientHttpRequestFactoryBuilderTests.java | 25 +++++++++++ ...kClientHttpRequestFactoryBuilderTests.java | 30 +++++++++++++ ...yClientHttpRequestFactoryBuilderTests.java | 25 +++++++++++ ...rClientHttpRequestFactoryBuilderTests.java | 17 +++++++ ...sClientHttpRequestFactoryBuilderTests.java | 14 ++++++ ...eClientHttpRequestFactoryBuilderTests.java | 8 ++++ ...HttpClientSettingsPropertyMapperTests.java | 10 +++++ .../boot/restclient/RestTemplateBuilder.java | 14 +++--- .../boot/resttestclient/TestRestTemplate.java | 19 ++++---- .../resttestclient/TestRestTemplateTests.java | 14 ++++-- 23 files changed, 225 insertions(+), 98 deletions(-) delete mode 100644 module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsCookieSpec.java rename module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/{HttpCookies.java => HttpCookieHandling.java} (63%) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc index 9b60836f8aa2..0143f15733bb 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc @@ -59,7 +59,6 @@ If you are using Spring WebFlux, consider the javadoc:org.springframework.test.w It is recommended, but not mandatory, to use the Apache HTTP Client (version 5.1 or better). If you have that on your classpath, the javadoc:org.springframework.boot.resttestclient.TestRestTemplate[] responds by configuring the client appropriately. -If you do use Apache's HTTP client it is configured to ignore cookies (so the template is stateless). javadoc:org.springframework.boot.resttestclient.TestRestTemplate[] can be instantiated directly in your integration tests, as shown in the following example: diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java index 1722ec018840..b6966fd9bee7 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java @@ -25,7 +25,7 @@ /** * Settings that can be applied when creating an imperative or reactive HTTP client. * - * @param cookies the cookie handling strategy to use or null to use the underlying + * @param cookieHandling the cookie handling strategy to use or null to use the underlying * library's default * @param redirects the follow redirect strategy to use or null to redirect whenever the * underlying library allows it @@ -35,7 +35,7 @@ * @author Phillip Webb * @since 3.5.0 */ -public record HttpClientSettings(@Nullable HttpCookies cookies, @Nullable HttpRedirects redirects, +public record HttpClientSettings(@Nullable HttpCookieHandling cookieHandling, @Nullable HttpRedirects redirects, @Nullable Duration connectTimeout, @Nullable Duration readTimeout, @Nullable SslBundle sslBundle) { /** @@ -45,7 +45,7 @@ public record HttpClientSettings(@Nullable HttpCookies cookies, @Nullable HttpRe * @param readTimeout the read timeout * @param sslBundle the SSL bundle providing SSL configuration * @deprecated since 4.1.0 for removal in 4.3.0 in favor of - * {@link HttpClientSettings#HttpClientSettings(HttpCookies, HttpRedirects, Duration, Duration, SslBundle)} + * {@link HttpClientSettings#HttpClientSettings(HttpCookieHandling, HttpRedirects, Duration, Duration, SslBundle)} */ @Deprecated(since = "4.1.0", forRemoval = true) public HttpClientSettings(@Nullable HttpRedirects redirects, @Nullable Duration connectTimeout, @@ -58,12 +58,13 @@ public HttpClientSettings(@Nullable HttpRedirects redirects, @Nullable Duration /** * Return a new {@link HttpClientSettings} instance with an updated cookie handling * setting. - * @param cookies the new cookie handling setting + * @param cookieHandling the new cookie handling setting * @return a new {@link HttpClientSettings} instance * @since 4.1.0 */ - public HttpClientSettings withCookies(@Nullable HttpCookies cookies) { - return new HttpClientSettings(cookies, this.redirects, this.connectTimeout, this.readTimeout, this.sslBundle); + public HttpClientSettings withCookieHandling(@Nullable HttpCookieHandling cookieHandling) { + return new HttpClientSettings(cookieHandling, this.redirects, this.connectTimeout, this.readTimeout, + this.sslBundle); } /** @@ -74,7 +75,8 @@ public HttpClientSettings withCookies(@Nullable HttpCookies cookies) { * @since 4.0.0 */ public HttpClientSettings withConnectTimeout(@Nullable Duration connectTimeout) { - return new HttpClientSettings(this.cookies, this.redirects, connectTimeout, this.readTimeout, this.sslBundle); + return new HttpClientSettings(this.cookieHandling, this.redirects, connectTimeout, this.readTimeout, + this.sslBundle); } /** @@ -85,7 +87,8 @@ public HttpClientSettings withConnectTimeout(@Nullable Duration connectTimeout) * @since 4.0.0 */ public HttpClientSettings withReadTimeout(@Nullable Duration readTimeout) { - return new HttpClientSettings(this.cookies, this.redirects, this.connectTimeout, readTimeout, this.sslBundle); + return new HttpClientSettings(this.cookieHandling, this.redirects, this.connectTimeout, readTimeout, + this.sslBundle); } /** @@ -97,7 +100,7 @@ public HttpClientSettings withReadTimeout(@Nullable Duration readTimeout) { * @since 4.0.0 */ public HttpClientSettings withTimeouts(@Nullable Duration connectTimeout, @Nullable Duration readTimeout) { - return new HttpClientSettings(this.cookies, this.redirects, connectTimeout, readTimeout, this.sslBundle); + return new HttpClientSettings(this.cookieHandling, this.redirects, connectTimeout, readTimeout, this.sslBundle); } /** @@ -108,7 +111,8 @@ public HttpClientSettings withTimeouts(@Nullable Duration connectTimeout, @Nulla * @since 4.0.0 */ public HttpClientSettings withSslBundle(@Nullable SslBundle sslBundle) { - return new HttpClientSettings(this.cookies, this.redirects, this.connectTimeout, this.readTimeout, sslBundle); + return new HttpClientSettings(this.cookieHandling, this.redirects, this.connectTimeout, this.readTimeout, + sslBundle); } /** @@ -118,7 +122,8 @@ public HttpClientSettings withSslBundle(@Nullable SslBundle sslBundle) { * @since 4.0.0 */ public HttpClientSettings withRedirects(@Nullable HttpRedirects redirects) { - return new HttpClientSettings(this.cookies, redirects, this.connectTimeout, this.readTimeout, this.sslBundle); + return new HttpClientSettings(this.cookieHandling, redirects, this.connectTimeout, this.readTimeout, + this.sslBundle); } /** @@ -132,12 +137,12 @@ public HttpClientSettings orElse(@Nullable HttpClientSettings other) { if (other == null) { return this; } - HttpCookies cookies = (cookies() != null) ? cookies() : other.cookies(); + HttpCookieHandling cookieHandling = (cookieHandling() != null) ? cookieHandling() : other.cookieHandling(); HttpRedirects redirects = (redirects() != null) ? redirects() : other.redirects(); Duration connectTimeout = (connectTimeout() != null) ? connectTimeout() : other.connectTimeout(); Duration readTimeout = (readTimeout() != null) ? readTimeout() : other.readTimeout(); SslBundle sslBundle = (sslBundle() != null) ? sslBundle() : other.sslBundle(); - return new HttpClientSettings(cookies, redirects, connectTimeout, readTimeout, sslBundle); + return new HttpClientSettings(cookieHandling, redirects, connectTimeout, readTimeout, sslBundle); } /** diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsCookieSpec.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsCookieSpec.java deleted file mode 100644 index a597f36c52c4..000000000000 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsCookieSpec.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.http.client; - -import org.apache.hc.client5.http.cookie.StandardCookieSpec; -import org.jspecify.annotations.Nullable; - -/** - * Adapts {@link HttpCookies} to an - * Apache HttpComponents - * cookie spec identifier. - * - * @author Apoorv Darshan - */ -final class HttpComponentsCookieSpec { - - private HttpComponentsCookieSpec() { - } - - static @Nullable String get(@Nullable HttpCookies cookies) { - if (cookies == null) { - return null; - } - return switch (cookies) { - case ENABLE_WHEN_POSSIBLE, ENABLE -> StandardCookieSpec.STRICT; - case DISABLE -> StandardCookieSpec.IGNORE; - }; - } - -} diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java index cb1653568cde..c14ecf3a3d9f 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java @@ -23,6 +23,7 @@ import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.cookie.StandardCookieSpec; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; @@ -220,8 +221,11 @@ private ConnectionConfig createConnectionConfig(HttpClientSettings settings) { private RequestConfig createDefaultRequestConfig(HttpClientSettings settings) { RequestConfig.Builder builder = RequestConfig.custom(); - String cookieSpec = HttpComponentsCookieSpec.get(settings.cookies()); - if (cookieSpec != null) { + if (settings.cookieHandling() != null) { + String cookieSpec = switch (settings.cookieHandling()) { + case ENABLE_WHEN_POSSIBLE, ENABLE -> StandardCookieSpec.STRICT; + case DISABLE -> StandardCookieSpec.IGNORE; + }; builder.setCookieSpec(cookieSpec); } this.defaultRequestConfigCustomizer.accept(builder); diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpCookies.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpCookieHandling.java similarity index 63% rename from module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpCookies.java rename to module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpCookieHandling.java index 0f2ba8162dbf..e43402bd7bd9 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpCookies.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpCookieHandling.java @@ -22,20 +22,24 @@ * @author Apoorv Darshan * @since 4.1.0 */ -public enum HttpCookies { +public enum HttpCookieHandling { /** - * Enable cookies (if the underlying library has support). + * Enable cookie handling (if the underlying library has support). When enabled, + * cookies received in a response are stored and automatically included in subsequent + * matching requests. */ ENABLE_WHEN_POSSIBLE, /** - * Enable cookies (fail if the underlying library has no support). + * Enable cookie handling (fail if the underlying library has no support). When + * enabled, cookies received in a response are stored and automatically included in + * subsequent matching requests. */ ENABLE, /** - * Disable cookies (fail if the underlying library has no support). + * Disable cookie handling (fail if the underlying library has no support). */ DISABLE diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkHttpClientBuilder.java index 58c27fe20926..13e1f6d89702 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkHttpClientBuilder.java @@ -85,7 +85,7 @@ public HttpClient build(@Nullable HttpClientSettings settings) { Assert.isTrue(settings.readTimeout() == null, "'settings' must not have a 'readTimeout'"); HttpClient.Builder builder = HttpClient.newBuilder(); PropertyMapper map = PropertyMapper.get(); - map.from(settings::cookies).as(this::asCookieHandler).to(builder::cookieHandler); + map.from(settings::cookieHandling).as(this::asCookieHandler).to(builder::cookieHandler); map.from(settings::redirects).always().as(this::asHttpClientRedirect).to(builder::followRedirects); map.from(settings::connectTimeout).to(builder::connectTimeout); map.from(settings::sslBundle).as(SslBundle::createSslContext).to(builder::sslContext); @@ -102,8 +102,8 @@ private SSLParameters asSslParameters(SslBundle sslBundle) { return parameters; } - private @Nullable CookieHandler asCookieHandler(HttpCookies cookies) { - return switch (cookies) { + private @Nullable CookieHandler asCookieHandler(HttpCookieHandling cookieHandling) { + return switch (cookieHandling) { case ENABLE_WHEN_POSSIBLE, ENABLE -> new CookieManager(); case DISABLE -> null; }; diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java index 6ff162655260..e01647be3d3e 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java @@ -141,7 +141,7 @@ public HttpClient build(@Nullable HttpClientSettings settings) { HttpClient httpClient = createHttpClient(settings.readTimeout(), transport); PropertyMapper map = PropertyMapper.get(); map.from(settings::connectTimeout).as(Duration::toMillis).to(httpClient::setConnectTimeout); - map.from(settings::cookies).as(this::asCookieStore).to(httpClient::setHttpCookieStore); + map.from(settings::cookieHandling).as(this::asCookieStore).to(httpClient::setHttpCookieStore); map.from(settings::redirects).always().as(this::followRedirects).to(httpClient::setFollowRedirects); this.customizer.accept(httpClient); return httpClient; @@ -184,9 +184,9 @@ private SslContextFactory.Client createSslContextFactory(SslBundle sslBundle) { return factory; } - private @Nullable HttpCookieStore asCookieStore(HttpCookies cookies) { - return switch (cookies) { - case ENABLE_WHEN_POSSIBLE, ENABLE -> null; + private @Nullable HttpCookieStore asCookieStore(HttpCookieHandling cookieHandling) { + return switch (cookieHandling) { + case ENABLE_WHEN_POSSIBLE, ENABLE -> new HttpCookieStore.Default(); case DISABLE -> new HttpCookieStore.Empty(); }; } diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java index e62876134605..c004e316210e 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java @@ -109,6 +109,9 @@ public HttpClient build(@Nullable HttpClientSettings settings) { .orFrom(() -> HttpRedirects.FOLLOW_WHEN_POSSIBLE) .as(this::followRedirects) .to(httpClient, HttpClient::followRedirect); + if (HttpCookieHandling.ENABLE.equals(settings.cookieHandling())) { + throw new IllegalArgumentException("Reactor Netty HTTP client does not support cookie handling"); + } httpClient = map.from(settings::sslBundle).to(httpClient, this::secure); return this.customizer.apply(httpClient); } diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilder.java index c0d579007048..25ae283d228a 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilder.java @@ -78,6 +78,10 @@ private void configure(ClientHttpRequestFactory requestFactory, HttpClientSettin Assert.state(settings.sslBundle() == null, "Unable to set SSL bundle using reflection"); Assert.state(settings.redirects() == null || settings.redirects() == HttpRedirects.FOLLOW_WHEN_POSSIBLE, "Unable to set redirect follow using reflection"); + Assert.state( + settings.cookieHandling() == null + || settings.cookieHandling() == HttpCookieHandling.ENABLE_WHEN_POSSIBLE, + "Unable to set HTTP cookie handling using reflection"); ClientHttpRequestFactory unwrapped = unwrapRequestFactoryIfNecessary(requestFactory); PropertyMapper map = PropertyMapper.get(); map.from(settings::connectTimeout).to((connectTimeout) -> setConnectTimeout(unwrapped, connectTimeout)); diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilder.java index 80764d2ea76c..fb9040a46acb 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilder.java @@ -78,6 +78,9 @@ public SimpleClientHttpRequestFactoryBuilder with(UnaryOperator ClientHttpRequestFactoryBuilder.reactor() + .build(HttpClientSettings.defaults().withCookieHandling(cookieHandling))); + } + + @Test + void throwsWhenCookieHandlingEnabled() { + assertThatIllegalArgumentException().isThrownBy(() -> ClientHttpRequestFactoryBuilder.reactor() + .build(HttpClientSettings.defaults().withCookieHandling(HttpCookieHandling.ENABLE))); + } + @Override protected long connectTimeout(ReactorClientHttpRequestFactory requestFactory) { HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient"); diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java index a90897c57587..f328d0af04dc 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java @@ -67,6 +67,20 @@ void redirectDontFollow(String httpMethod) throws Exception { .withMessage("Unable to set redirect follow using reflection"); } + @Test + void cookieHandlingEnable() { + HttpClientSettings settings = HttpClientSettings.defaults().withCookieHandling(HttpCookieHandling.ENABLE); + assertThatIllegalStateException().isThrownBy(() -> ofTestRequestFactory().build(settings)) + .withMessage("Unable to set HTTP cookie handling using reflection"); + } + + @Test + void cookieHandlingDisable() { + HttpClientSettings settings = HttpClientSettings.defaults().withCookieHandling(HttpCookieHandling.DISABLE); + assertThatIllegalStateException().isThrownBy(() -> ofTestRequestFactory().build(settings)) + .withMessage("Unable to set HTTP cookie handling using reflection"); + } + @Override void connectWithSslBundleAndOptionsMismatch(String httpMethod) throws Exception { assertThatIllegalStateException().isThrownBy(() -> super.connectWithSslBundleAndOptionsMismatch(httpMethod)) diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.java index 1b134981adf7..f23d1f91fcc7 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.http.client; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -25,6 +26,7 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** @@ -81,6 +83,12 @@ void redirectDontFollow(String httpMethod) throws Exception { super.redirectDontFollow(httpMethod); } + @Test + void throwsWhenCookieHandlingEnabled() { + assertThatIllegalArgumentException().isThrownBy(() -> ClientHttpRequestFactoryBuilder.simple() + .build(HttpClientSettings.defaults().withCookieHandling(HttpCookieHandling.ENABLE))); + } + @Override protected HttpStatus getExpectedRedirect(HttpMethod httpMethod) { return (httpMethod != HttpMethod.GET) ? HttpStatus.FOUND : HttpStatus.OK; diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientSettingsPropertyMapperTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientSettingsPropertyMapperTests.java index e6d0b8b77829..54f5f059a2e1 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientSettingsPropertyMapperTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientSettingsPropertyMapperTests.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.HttpCookieHandling; import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; @@ -79,6 +80,15 @@ void mapMapsReadTimeout() { assertThat(result.readTimeout()).isEqualTo(Duration.ofSeconds(30)); } + @Test + void mapMapsCookieHandling() { + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(null, null); + TestHttpClientSettingsProperties properties = new TestHttpClientSettingsProperties(); + properties.setCookieHandling(HttpCookieHandling.DISABLE); + HttpClientSettings result = mapper.map(properties); + assertThat(result.cookieHandling()).isEqualTo(HttpCookieHandling.DISABLE); + } + @Test void mapMapsSslBundle() { SslBundle sslBundle = mock(SslBundle.class); diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java index 508e349dca8b..27abdf5450a3 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java @@ -35,7 +35,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.HttpClientSettings; -import org.springframework.boot.http.client.HttpCookies; +import org.springframework.boot.http.client.HttpCookieHandling; import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.ssl.SslBundle; import org.springframework.http.client.ClientHttpRequest; @@ -485,15 +485,15 @@ public RestTemplateBuilder readTimeout(Duration readTimeout) { /** * Sets the cookie handling strategy on the underlying * {@link ClientHttpRequestFactory}. - * @param cookies the cookie handling strategy + * @param cookieHandling the cookie handling strategy * @return a new builder instance. * @since 4.1.0 */ - public RestTemplateBuilder cookies(HttpCookies cookies) { - return new RestTemplateBuilder(this.clientSettings.withCookies(cookies), this.detectRequestFactory, - this.rootUri, this.messageConverters, this.interceptors, this.requestFactoryBuilder, - this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, - this.customizers, this.requestCustomizers); + public RestTemplateBuilder cookieHandling(HttpCookieHandling cookieHandling) { + return new RestTemplateBuilder(this.clientSettings.withCookieHandling(cookieHandling), + this.detectRequestFactory, this.rootUri, this.messageConverters, this.interceptors, + this.requestFactoryBuilder, this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, + this.defaultHeaders, this.customizers, this.requestCustomizers); } /** diff --git a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java index ef63db42d436..52ebd28f4096 100644 --- a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java +++ b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java @@ -41,7 +41,7 @@ import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpComponentsClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.HttpComponentsHttpClientBuilder.TlsSocketStrategyFactory; -import org.springframework.boot.http.client.HttpCookies; +import org.springframework.boot.http.client.HttpCookieHandling; import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; @@ -164,8 +164,7 @@ private static RestTemplateBuilder createInitialBuilder(RestTemplateBuilder buil private static HttpComponentsClientHttpRequestFactoryBuilder applyHttpClientOptions( HttpComponentsClientHttpRequestFactoryBuilder builder, HttpClientOption[] httpClientOptions) { if (HttpClientOption.ENABLE_COOKIES.isPresent(httpClientOptions)) { - builder = builder.withDefaultRequestConfigCustomizer( - new CookieSpecCustomizer(true)); + builder = builder.withDefaultRequestConfigCustomizer(new CookieSpecCustomizer(true)); } if (HttpClientOption.SSL.isPresent(httpClientOptions)) { builder = builder.withTlsSocketStrategyFactory(new SelfSignedTlsSocketStrategyFactory()); @@ -979,15 +978,15 @@ public TestRestTemplate withRedirects(HttpRedirects redirects) { /** * Creates a new {@code TestRestTemplate} with the same configuration as this one, - * except that it will apply the given {@link HttpCookies}. The request factory used is - * a new instance of the underlying {@link RestTemplate}'s request factory type (when - * possible). - * @param cookies the new cookie settings + * except that it will apply the given {@link HttpCookieHandling}. The request factory + * used is a new instance of the underlying {@link RestTemplate}'s request factory + * type (when possible). + * @param cookieHandling the new cookie handling * @return the new template * @since 4.1.0 */ - public TestRestTemplate withCookies(HttpCookies cookies) { - return withClientSettings((settings) -> settings.withCookies(cookies)); + public TestRestTemplate withCookieHandling(HttpCookieHandling cookieHandling) { + return withClientSettings((settings) -> settings.withCookieHandling(cookieHandling)); } /** @@ -1053,7 +1052,7 @@ public enum HttpClientOption { /** * Enable cookies. * @deprecated since 4.1.0 for removal in 4.3.0 in favor of - * {@link TestRestTemplate#withCookies(HttpCookies)} + * {@link TestRestTemplate#withCookieHandling(HttpCookieHandling)} */ @Deprecated(since = "4.1.0", forRemoval = true) ENABLE_COOKIES, diff --git a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java index f835ec959511..b621b1e018f1 100644 --- a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java +++ b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java @@ -26,6 +26,7 @@ import java.util.stream.Stream; import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.cookie.StandardCookieSpec; import org.apache.hc.client5.http.impl.DefaultRedirectStrategy; import org.apache.hc.client5.http.impl.classic.RedirectExec; import org.apache.hc.client5.http.protocol.RedirectStrategy; @@ -35,7 +36,7 @@ import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.HttpClientSettings; -import org.springframework.boot.http.client.HttpCookies; +import org.springframework.boot.http.client.HttpCookieHandling; import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.boot.resttestclient.TestRestTemplate.HttpClientOption; @@ -154,11 +155,16 @@ void defaultCookieSpecMatchesRestTemplate() { } @Test - void withCookies() { + void withCookieHandling() { TestRestTemplate template = new TestRestTemplate(); assertThat(getRequestConfig(template).getCookieSpec()).isNull(); - assertThat(getRequestConfig(template.withCookies(HttpCookies.ENABLE)).getCookieSpec()).isEqualTo("strict"); - assertThat(getRequestConfig(template.withCookies(HttpCookies.DISABLE)).getCookieSpec()).isEqualTo("ignoreCookies"); + assertThat(getRequestConfig(template.withCookieHandling(HttpCookieHandling.ENABLE)).getCookieSpec()) + .isEqualTo(StandardCookieSpec.STRICT); + assertThat( + getRequestConfig(template.withCookieHandling(HttpCookieHandling.ENABLE_WHEN_POSSIBLE)).getCookieSpec()) + .isEqualTo(StandardCookieSpec.STRICT); + assertThat(getRequestConfig(template.withCookieHandling(HttpCookieHandling.DISABLE)).getCookieSpec()) + .isEqualTo(StandardCookieSpec.IGNORE); } @Test From a00fe98c3657cedfbe982a7e72d18010c41abed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 12 Mar 2026 15:36:08 +0100 Subject: [PATCH 328/930] Upgrade to Infinispan 16.1.1 Closes gh-49576 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 972f5c35b7d4..669f18da0621 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -730,7 +730,7 @@ bom { ] } } - library("Infinispan", "16.1.0") { + library("Infinispan", "16.1.1") { group("org.infinispan") { bom("infinispan-bom") } From 23cfadb8d8e1bed8d0a4ad761698a156b215d2bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 12 Mar 2026 15:36:12 +0100 Subject: [PATCH 329/930] Upgrade to Lombok 1.18.44 Closes gh-49577 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 669f18da0621..34855cb4530c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1357,7 +1357,7 @@ bom { javadoc("https://logback.qos.ch/apidocs/ch.qos.logback.core", "ch.qos.logback") } } - library("Lombok", "1.18.42") { + library("Lombok", "1.18.44") { group("org.projectlombok") { modules = [ "lombok" From 90612f1efec207197ee2ee7297689ea9a6f9e974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 12 Mar 2026 15:36:18 +0100 Subject: [PATCH 330/930] Upgrade to Mockito 5.23.0 Closes gh-49578 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9e909f2ecd52..f1558f732efd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 kotlinVersion=2.3.10 mavenVersion=3.9.13 -mockitoVersion=5.22.0 +mockitoVersion=5.23.0 nativeBuildToolsVersion=0.11.5 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.6 From 166b88b9e197edc300fc6ce4e43993cdd13bfdba Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 12 Mar 2026 14:32:41 +0100 Subject: [PATCH 331/930] Add support for SslBundles to OTLP logging export Closes gh-49583 --- ...rComposeConnectionDetailsFactoryTests.java | 11 ++++ ...rComposeConnectionDetailsFactoryTests.java | 11 ++++ ...ontainerConnectionDetailsFactoryTests.java | 1 + ...ontainerConnectionDetailsFactoryTests.java | 1 + .../boot/opentelemetry/docker/compose/ca.crt | 32 ++++++++++ .../docker/compose/otlp-ssl-compose.yaml | 8 +++ .../otlp/OtlpLoggingConfigurations.java | 55 +++++++++++++++++- .../otlp/OtlpLoggingConnectionDetails.java | 12 ++++ .../logging/otlp/OtlpLoggingProperties.java | 23 ++++++++ ...DockerComposeConnectionDetailsFactory.java | 14 ++++- ...gingContainerConnectionDetailsFactory.java | 10 ++++ ...gingContainerConnectionDetailsFactory.java | 10 +++- .../OtlpLoggingAutoConfigurationTests.java | 55 ++++++++++++++++++ .../autoconfigure/logging/otlp/test.jks | Bin 0 -> 1276 bytes 14 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/ca.crt create mode 100644 module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/otlp-ssl-compose.yaml create mode 100644 module/spring-boot-opentelemetry/src/test/resources/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/test.jks diff --git a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/GrafanaOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/GrafanaOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java index 8f7a08f51a1a..b3a0f2911a83 100644 --- a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/GrafanaOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java +++ b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/GrafanaOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java @@ -19,6 +19,7 @@ import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; @@ -35,6 +36,16 @@ class GrafanaOtlpLoggingDockerComposeConnectionDetailsFactoryTests { void runCreatesConnectionDetails(OtlpLoggingConnectionDetails connectionDetails) { assertThat(connectionDetails.getUrl(Transport.HTTP)).startsWith("http://").endsWith("/v1/logs"); assertThat(connectionDetails.getUrl(Transport.GRPC)).startsWith("http://").endsWith("/v1/logs"); + assertThat(connectionDetails.getSslBundle()).isNull(); + } + + @DockerComposeTest(composeFile = "otlp-ssl-compose.yaml", image = TestImage.GRAFANA_OTEL_LGTM, + additionalResources = "ca.crt") + void runWithSslCreatesConnectionDetails(OtlpLoggingConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUrl(Transport.HTTP)).startsWith("https://").endsWith("/v1/logs"); + assertThat(connectionDetails.getUrl(Transport.GRPC)).startsWith("https://").endsWith("/v1/logs"); + SslBundle sslBundle = connectionDetails.getSslBundle(); + assertThat(sslBundle).isNotNull(); } } diff --git a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/OtelCollectorOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/OtelCollectorOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java index f49096823328..93eedf0bc4a0 100644 --- a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/OtelCollectorOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java +++ b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/OtelCollectorOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java @@ -19,6 +19,7 @@ import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; @@ -34,6 +35,16 @@ class OtelCollectorOtlpLoggingDockerComposeConnectionDetailsFactoryTests { void runCreatesConnectionDetails(OtlpLoggingConnectionDetails connectionDetails) { assertThat(connectionDetails.getUrl(Transport.HTTP)).startsWith("http://").endsWith("/v1/logs"); assertThat(connectionDetails.getUrl(Transport.GRPC)).startsWith("http://").endsWith("/v1/logs"); + assertThat(connectionDetails.getSslBundle()).isNull(); + } + + @DockerComposeTest(composeFile = "otlp-ssl-compose.yaml", image = TestImage.OTEL_COLLECTOR, + additionalResources = "ca.crt") + void runWithSslCreatesConnectionDetails(OtlpLoggingConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUrl(Transport.HTTP)).startsWith("https://").endsWith("/v1/logs"); + assertThat(connectionDetails.getUrl(Transport.GRPC)).startsWith("https://").endsWith("/v1/logs"); + SslBundle sslBundle = connectionDetails.getSslBundle(); + assertThat(sslBundle).isNotNull(); } } diff --git a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactoryTests.java b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactoryTests.java index 563755574a63..2cac2305170c 100644 --- a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactoryTests.java +++ b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactoryTests.java @@ -55,6 +55,7 @@ void connectionCanBeMadeToOpenTelemetryContainer() { .isEqualTo("%s/v1/logs".formatted(container.getOtlpGrpcUrl())); assertThat(this.connectionDetails.getUrl(Transport.HTTP)) .isEqualTo("%s/v1/logs".formatted(container.getOtlpHttpUrl())); + assertThat(this.connectionDetails.getSslBundle()).isNull(); } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactoryTests.java b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactoryTests.java index 69aab41c1971..fc93129bb0e2 100644 --- a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactoryTests.java +++ b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactoryTests.java @@ -56,6 +56,7 @@ void connectionCanBeMadeToOpenTelemetryContainer() { .isEqualTo("http://" + container.getHost() + ":" + container.getMappedPort(4318) + "/v1/logs"); assertThat(this.connectionDetails.getUrl(Transport.GRPC)) .isEqualTo("http://" + container.getHost() + ":" + container.getMappedPort(4317) + "/v1/logs"); + assertThat(this.connectionDetails.getSslBundle()).isNull(); } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/ca.crt b/module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/ca.crt new file mode 100644 index 000000000000..beed250b132b --- /dev/null +++ b/module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/ca.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFhjCCA26gAwIBAgIUfIkk29IT9OpbgfjL8oRIPSLjUcAwDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDUwMTE2NTMyNVoXDTM0MDQyOTE2NTMyNVow +OzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNh +dGUgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAusN2 +KzQQUUxZSiI3ZZuZohFwq2KXSUNPdJ6rgD3/YKNTDSZXKZPO53kYPP0DXf0sm3CH +cyWSWVabyimZYuPWena1MElSL4ZpJ9WwkZoOQ3bPFK1utz6kMOwrgAUcky8H/rIK +j2JEBhkSHUIGr57NjUEwG1ygaSerM8RzWw1PtMq+C8LOu3v94qzE3NDg1QRpyvV9 +OmsLsjISd0ZmAJNi9vmiEH923KnPyiqnQmWKpYicdgQmX1GXylS22jZqAwaOkYGj +X8UdeyvrohkZkM0hn9uaSufQGEW4yKACn3PkjJtzi8drBIyjIi9YcAzBxZB9oVKq +XZMlltgO2fDMmIJi0Ngt0Ci7fCoEMqSocKyDKML6YLr9UWtx4bfsrk+rVO9Q/D/v +8RKgstv7dCf2KWRX3ZJEC0IBHS5gLNq0qqqVcGx3LcSyhdiKJOtSwAnNkHMh+jSQ +xLSlBjcSqTPiGTRK/Rddl+xnU/mBgk7ZBGNrUFaD5McMFjddS7Ih82aHnpQ1gekW +nUGv+Tm/G68h2BvZ5U2q+RfeOCgRW9i/AYW2jgT7IFnfjyUXgBQveauMAchomqFE +VLe95ZgViF6vmH34EKo3w9L5TQiwk/r53YlM7TSOTyDqx66t4zGYDsVMicpKmzi4 +2Rp8EpErARRyREUIKSvWs9O9+uT3+7arNLgHe5ECAwEAAaOBgTB/MB0GA1UdDgQW +BBRVMLDVqPECWaH6GruL9E52VcTrPjAfBgNVHSMEGDAWgBRVMLDVqPECWaH6GruL +9E52VcTrPjAPBgNVHRMBAf8EBTADAQH/MCwGA1UdEQQlMCOCC2V4YW1wbGUuY29t +gglsb2NhbGhvc3SCCTEyNy4wLjAuMTANBgkqhkiG9w0BAQsFAAOCAgEAeSpjCL3j +2GIFBNKr/5amLOYa0kZ6r1dJs+K6xvMsUvsBJ/QQsV5nYDMIoV/NYUd8SyYV4lEj +7LHX5ZbmJrvPk30LGEBG/5Vy2MIATrQrQ14S4nXtEdSnBvTQwPOOaHc+2dTp3YpM +f4ffELKWyispTifx1eqdiUJhURKeQBh+3W7zpyaiN4vJaqEDKGgFQtHA/OyZL2hZ +BpxHB0zpb2iDHV8MeyfOT7HQWUk6p13vdYm6EnyJT8fzWvE+TqYNbqFmB+CLRSXy +R3p1yaeTd4LnVknJ0UBKqEyul3ziHZDhKhBpwdglYOQz4eWjSFhikX9XZ8NaI38Q +QqLZVn0DsH2ztkjrQrUVgK2xn4aUuqoLDk4Hu6h5baUn+f2GLuzx+EXc/i3ikYvw +Y3JyufOgw6nGGFG+/QXEj85XtLPhN7Wm42z2e/BGzi0MLl65sfpEDXvFTA72Yzws +OYaeg/HxeYwUHQgs2fKl/LgV4chntSCvTqfNl6OnQafD/ISJNpx3xWR3HwF+ypFG +UaLE+e1soqEJbzL31U/6pypHLsj8Y8r9hJbZXo2ibnhjFV6fypUAP0rbIzaoWcrJ +T0Sbliz+KQTMzCcubiAi4bI/kZ5FJ4kkaHqUpIWzlx1h2WVJ65ASFDjBWb8eVmB6 +Dyno/RVFR/rUL5091gjGRXhLsi1oUHKdEzU= +-----END CERTIFICATE----- diff --git a/module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/otlp-ssl-compose.yaml b/module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/otlp-ssl-compose.yaml new file mode 100644 index 000000000000..75707fa3e684 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/otlp-ssl-compose.yaml @@ -0,0 +1,8 @@ +services: + otlp: + image: '{imageName}' + ports: + - '4317' + - '4318' + labels: + - 'org.springframework.boot.sslbundle.pem.truststore.certificate=ca.crt' diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java index 4763b7affc66..0722701f2a45 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java @@ -18,11 +18,16 @@ import java.util.Locale; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporterBuilder; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporterBuilder; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -30,9 +35,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.opentelemetry.autoconfigure.logging.ConditionalOnEnabledLoggingExport; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Configurations imported by {@link OtlpLoggingAutoConfiguration}. @@ -48,8 +56,9 @@ static class ConnectionDetails { @Bean @ConditionalOnMissingBean(OtlpLoggingConnectionDetails.class) @ConditionalOnProperty("management.opentelemetry.logging.export.otlp.endpoint") - PropertiesOtlpLoggingConnectionDetails openTelemetryLoggingConnectionDetails(OtlpLoggingProperties properties) { - return new PropertiesOtlpLoggingConnectionDetails(properties); + PropertiesOtlpLoggingConnectionDetails openTelemetryLoggingConnectionDetails(OtlpLoggingProperties properties, + ObjectProvider sslBundles) { + return new PropertiesOtlpLoggingConnectionDetails(properties, sslBundles.getIfAvailable()); } /** @@ -59,8 +68,11 @@ static class PropertiesOtlpLoggingConnectionDetails implements OtlpLoggingConnec private final OtlpLoggingProperties properties; - PropertiesOtlpLoggingConnectionDetails(OtlpLoggingProperties properties) { + private final @Nullable SslBundles sslBundles; + + PropertiesOtlpLoggingConnectionDetails(OtlpLoggingProperties properties, @Nullable SslBundles sslBundles) { this.properties = properties; + this.sslBundles = sslBundles; } @Override @@ -73,6 +85,16 @@ public String getUrl(Transport transport) { return endpoint; } + @Override + public @Nullable SslBundle getSslBundle() { + String bundleName = this.properties.getSsl().getBundle(); + if (StringUtils.hasLength(bundleName)) { + Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context"); + return this.sslBundles.getBundle(bundleName); + } + return null; + } + } } @@ -97,6 +119,7 @@ OtlpHttpLogRecordExporter otlpHttpLogRecordExporter(OtlpLoggingProperties proper .setCompression(properties.getCompression().name().toLowerCase(Locale.US)); properties.getHeaders().forEach(builder::addHeader); meterProvider.ifAvailable(builder::setMeterProvider); + configureSsl(connectionDetails, builder::setSslContext); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } @@ -113,10 +136,36 @@ OtlpGrpcLogRecordExporter otlpGrpcLogRecordExporter(OtlpLoggingProperties proper .setCompression(properties.getCompression().name().toLowerCase(Locale.US)); properties.getHeaders().forEach(builder::addHeader); meterProvider.ifAvailable(builder::setMeterProvider); + configureSsl(connectionDetails, builder::setSslContext); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } + private void configureSsl(OtlpLoggingConnectionDetails connectionDetails, + SslContextConfigurer sslContextConfigurer) { + SslBundle sslBundle = connectionDetails.getSslBundle(); + if (sslBundle != null) { + SSLContext sslContext = sslBundle.createSslContext(); + X509TrustManager trustManager = extractTrustManager(sslBundle); + sslContextConfigurer.configure(sslContext, trustManager); + } + } + + private X509TrustManager extractTrustManager(SslBundle sslBundle) { + for (TrustManager trustManager : sslBundle.getManagers().getTrustManagers()) { + if (trustManager instanceof X509TrustManager x509TrustManager) { + return x509TrustManager; + } + } + throw new IllegalStateException("No X509TrustManager found in the SSL bundle trust managers"); + } + + private interface SslContextConfigurer { + + void configure(SSLContext sslContext, X509TrustManager trustManager); + + } + } } diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConnectionDetails.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConnectionDetails.java index a61350f7a3b6..1ebe588cbc2c 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConnectionDetails.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConnectionDetails.java @@ -16,7 +16,10 @@ package org.springframework.boot.opentelemetry.autoconfigure.logging.otlp; +import org.jspecify.annotations.Nullable; + import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.boot.ssl.SslBundle; /** * Details required to establish a connection to an OpenTelemetry logging service. @@ -33,4 +36,13 @@ public interface OtlpLoggingConnectionDetails extends ConnectionDetails { */ String getUrl(Transport transport); + /** + * SSL bundle to use. + * @return the SSL bundle to use or {@code null} + * @since 4.1.0 + */ + default @Nullable SslBundle getSslBundle() { + return null; + } + } diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java index 1d4ac6e7f777..569935763a66 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java @@ -66,6 +66,8 @@ public class OtlpLoggingProperties { */ private final Map headers = new HashMap<>(); + private final Ssl ssl = new Ssl(); + public @Nullable String getEndpoint() { return this.endpoint; } @@ -110,6 +112,27 @@ public Map getHeaders() { return this.headers; } + public Ssl getSsl() { + return this.ssl; + } + + public static class Ssl { + + /** + * SSL bundle name. + */ + private @Nullable String bundle; + + public @Nullable String getBundle() { + return this.bundle; + } + + public void setBundle(@Nullable String bundle) { + this.bundle = bundle; + } + + } + public enum Compression { /** diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/docker/compose/OtlpLoggingDockerComposeConnectionDetailsFactory.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/docker/compose/OtlpLoggingDockerComposeConnectionDetailsFactory.java index 160fec162baa..d457dc8c8a9e 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/docker/compose/OtlpLoggingDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/docker/compose/OtlpLoggingDockerComposeConnectionDetailsFactory.java @@ -16,11 +16,14 @@ package org.springframework.boot.opentelemetry.docker.compose; +import org.jspecify.annotations.Nullable; + 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; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; /** * {@link DockerComposeConnectionDetailsFactory} to create @@ -57,11 +60,14 @@ private static final class OtlpLoggingDockerComposeConnectionDetails extends Doc private final int httpPort; + private final @Nullable SslBundle sslBundle; + private OtlpLoggingDockerComposeConnectionDetails(RunningService source) { super(source); this.host = source.host(); this.grpcPort = source.ports().get(OTLP_GRPC_PORT); this.httpPort = source.ports().get(OTLP_HTTP_PORT); + this.sslBundle = getSslBundle(source); } @Override @@ -70,7 +76,13 @@ public String getUrl(Transport transport) { case HTTP -> this.httpPort; case GRPC -> this.grpcPort; }; - return "http://%s:%d/v1/logs".formatted(this.host, port); + String scheme = (this.sslBundle != null) ? "https" : "http"; + return "%s://%s:%d/v1/logs".formatted(scheme, this.host, port); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return this.sslBundle; } } diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactory.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactory.java index b50188dbbcbc..b773ac15e52a 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactory.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactory.java @@ -16,10 +16,12 @@ package org.springframework.boot.opentelemetry.testcontainers; +import org.jspecify.annotations.Nullable; import org.testcontainers.grafana.LgtmStackContainer; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -58,9 +60,17 @@ public String getUrl(Transport transport) { case HTTP -> getContainer().getOtlpHttpUrl(); case GRPC -> getContainer().getOtlpGrpcUrl(); }; + if (getSslBundle() != null) { + url = url.replaceFirst("^http://", "https://"); + } return "%s/v1/logs".formatted(url); } + @Override + public @Nullable SslBundle getSslBundle() { + return super.getSslBundle(); + } + } } diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactory.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactory.java index b65c8308acb1..08e71ed6b352 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactory.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactory.java @@ -16,11 +16,13 @@ package org.springframework.boot.opentelemetry.testcontainers; +import org.jspecify.annotations.Nullable; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -64,7 +66,13 @@ public String getUrl(Transport transport) { case HTTP -> OTLP_HTTP_PORT; case GRPC -> OTLP_GRPC_PORT; }; - return "http://%s:%d/v1/logs".formatted(getContainer().getHost(), getContainer().getMappedPort(port)); + String scheme = (getSslBundle() != null) ? "https" : "http"; + return "%s://%s:%d/v1/logs".formatted(scheme, getContainer().getHost(), getContainer().getMappedPort(port)); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return super.getSslBundle(); } } diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java index 41313a95de1e..2839b3c9b9d1 100644 --- a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java @@ -20,6 +20,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; +import javax.net.ssl.X509TrustManager; + import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; @@ -33,13 +35,16 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.context.annotation.ImportCandidates; import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration; import org.springframework.boot.opentelemetry.autoconfigure.logging.OpenTelemetryLoggingAutoConfiguration; import org.springframework.boot.opentelemetry.autoconfigure.logging.SdkLoggerProviderBuilderCustomizer; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConfigurations.ConnectionDetails.PropertiesOtlpLoggingConnectionDetails; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.resources.WithPackageResources; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -266,6 +271,56 @@ void shouldCustomizeGrpcTransportWhenEnabledWithOtlpGrpcLogRecordExporterBuilder }); } + @Test + @WithPackageResources("test.jks") + void whenHasSslBundleConfiguresHttpExporter() { + this.contextRunner.withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.logging.export.otlp.endpoint=https://localhost:4318/v1/logs", + "management.opentelemetry.logging.export.otlp.ssl.bundle=mybundle", + "spring.ssl.bundle.jks.mybundle.truststore.location=classpath:test.jks") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class); + OtlpHttpLogRecordExporter exporter = context.getBean(OtlpHttpLogRecordExporter.class); + assertThat(exporter).extracting("delegate.httpSender.client.sslSocketFactoryOrNull").isNotNull(); + assertThat(exporter).extracting("delegate.httpSender.client.x509TrustManager") + .isInstanceOf(X509TrustManager.class); + }); + } + + @Test + @WithPackageResources("test.jks") + void whenHasSslBundleConfiguresGrpcExporter() { + this.contextRunner.withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.logging.export.otlp.endpoint=https://localhost:4318/v1/logs", + "management.opentelemetry.logging.export.otlp.transport=grpc", + "management.opentelemetry.logging.export.otlp.ssl.bundle=mybundle", + "spring.ssl.bundle.jks.mybundle.truststore.location=classpath:test.jks") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpGrpcLogRecordExporter.class); + OtlpGrpcLogRecordExporter exporter = context.getBean(OtlpGrpcLogRecordExporter.class); + assertThat(exporter).extracting("delegate.grpcSender.client.sslSocketFactoryOrNull").isNotNull(); + assertThat(exporter).extracting("delegate.grpcSender.client.x509TrustManager") + .isInstanceOf(X509TrustManager.class); + }); + } + + @Test + void whenCustomConnectionDetailsProvidesSslBundleConfiguresHttpExporter() { + SslBundle sslBundle = SslBundle.systemDefault(); + OtlpLoggingConnectionDetails connectionDetails = mock(OtlpLoggingConnectionDetails.class); + given(connectionDetails.getUrl(Transport.HTTP)).willReturn("https://localhost:4318/v1/logs"); + given(connectionDetails.getSslBundle()).willReturn(sslBundle); + this.contextRunner + .withBean("customOtlpLoggingConnectionDetails", OtlpLoggingConnectionDetails.class, () -> connectionDetails) + .run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class); + OtlpHttpLogRecordExporter exporter = context.getBean(OtlpHttpLogRecordExporter.class); + assertThat(exporter).extracting("delegate.httpSender.client.sslSocketFactoryOrNull").isNotNull(); + assertThat(exporter).extracting("delegate.httpSender.client.x509TrustManager") + .isInstanceOf(X509TrustManager.class); + }); + } + @Configuration(proxyBeanMethods = false) public static class MultipleSdkLoggerProviderBuilderCustomizersConfig { diff --git a/module/spring-boot-opentelemetry/src/test/resources/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/test.jks b/module/spring-boot-opentelemetry/src/test/resources/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/test.jks new file mode 100644 index 0000000000000000000000000000000000000000..0fc3e802f75461dd074facb9611d350db4d5960f GIT binary patch literal 1276 zcmezO_TO6u1_mZ5W@O+hNi8nXP0YzmEM{O}OjTL=XFE`?-k{cikBv*4jgf^>i%F1? zk(GfZ`?F{4vBFug6<%MmmXJ+)mEgQoslXV6!5_H^yana6V1?kw1w zbE0Bi&GHlLHzfosgjrwL)qKcc5I;l0LF?W2lpQg%-cHp$l(#o)?Jkath1@gQN{hG8 zig@wKv#0R7vd_QC=JG%%Ffy=4=$RT=0v*d`(8R=M(8RcU0W%XL6BCP-)w&Y~JZv0V zZ64=rS(uqv84M~6g$xAPm_u3EggJBalM{0?@{3DgVjNh+*s+LlVG-lTBF2m)W*{fd zYiMC$VQ64zW@K(?5e4L0B5?=MWswHLZ0z7LVq$~_7BeF|vl9agPmO-znfkD()@R+bGrT$(AXmN24#zv*XRX z#$UNu(Lmln78u;Jd@N!tBKmU@J0!OJc3G%!N>OO@P1n+F-CorAVRmOQaA8six!iWP z)M3lXpnJ*TI=kIlH(Yxia-ls?xvctEx&P5B6()tKm`>%bo~@fX9{j%TtMU1G!|pw& zZ6BRjIqQ^`bIxR@OmMno&8^H%tpq36Esh&T(+MJ_la+#pV>+3scS% Date: Fri, 13 Mar 2026 09:10:51 +0100 Subject: [PATCH 332/930] Add support for SslBundles to OTLP traces export Closes gh-49584 --- ...nectionDetailsFactoryIntegrationTests.java | 11 ++++ ...nectionDetailsFactoryIntegrationTests.java | 11 ++++ ...nectionDetailsFactoryIntegrationTests.java | 1 + ...nectionDetailsFactoryIntegrationTests.java | 1 + .../opentelemetry/docker/compose/otlp/ca.crt | 32 ++++++++++ .../docker/compose/otlp/otlp-ssl-compose.yaml | 8 +++ .../otlp/OtlpTracingConfigurations.java | 55 +++++++++++++++- .../otlp/OtlpTracingConnectionDetails.java | 12 ++++ .../otlp/OtlpTracingProperties.java | 23 +++++++ ...DockerComposeConnectionDetailsFactory.java | 14 ++++- ...cingContainerConnectionDetailsFactory.java | 10 +++ ...cingContainerConnectionDetailsFactory.java | 11 +++- .../OtlpTracingAutoConfigurationTests.java | 59 ++++++++++++++++++ .../opentelemetry/autoconfigure/otlp/test.jks | Bin 0 -> 1276 bytes 14 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/resources/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/ca.crt create mode 100644 module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/resources/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/otlp-ssl-compose.yaml create mode 100644 module/spring-boot-micrometer-tracing-opentelemetry/src/test/resources/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/test.jks diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/GrafanaOpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/GrafanaOpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java index 581f9d00e9a1..108d6af4ae20 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/GrafanaOpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/GrafanaOpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -19,6 +19,7 @@ import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingConnectionDetails; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; @@ -35,6 +36,16 @@ class GrafanaOpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegratio void runCreatesConnectionDetails(OtlpTracingConnectionDetails connectionDetails) { assertThat(connectionDetails.getUrl(Transport.HTTP)).startsWith("http://").endsWith("/v1/traces"); assertThat(connectionDetails.getUrl(Transport.GRPC)).startsWith("http://").endsWith("/v1/traces"); + assertThat(connectionDetails.getSslBundle()).isNull(); + } + + @DockerComposeTest(composeFile = "otlp-ssl-compose.yaml", image = TestImage.GRAFANA_OTEL_LGTM, + additionalResources = "ca.crt") + void runWithSslCreatesConnectionDetails(OtlpTracingConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUrl(Transport.HTTP)).startsWith("https://").endsWith("/v1/traces"); + assertThat(connectionDetails.getUrl(Transport.GRPC)).startsWith("https://").endsWith("/v1/traces"); + SslBundle sslBundle = connectionDetails.getSslBundle(); + assertThat(sslBundle).isNotNull(); } } diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java index a68162d0dabc..b6e33e8230e5 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -19,6 +19,7 @@ import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingConnectionDetails; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; @@ -35,6 +36,16 @@ class OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests void runCreatesConnectionDetails(OtlpTracingConnectionDetails connectionDetails) { assertThat(connectionDetails.getUrl(Transport.HTTP)).startsWith("http://").endsWith("/v1/traces"); assertThat(connectionDetails.getUrl(Transport.GRPC)).startsWith("http://").endsWith("/v1/traces"); + assertThat(connectionDetails.getSslBundle()).isNull(); + } + + @DockerComposeTest(composeFile = "otlp-ssl-compose.yaml", image = TestImage.OTEL_COLLECTOR, + additionalResources = "ca.crt") + void runWithSslCreatesConnectionDetails(OtlpTracingConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUrl(Transport.HTTP)).startsWith("https://").endsWith("/v1/traces"); + assertThat(connectionDetails.getUrl(Transport.GRPC)).startsWith("https://").endsWith("/v1/traces"); + SslBundle sslBundle = connectionDetails.getSslBundle(); + assertThat(sslBundle).isNotNull(); } } diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/GrafanaOpenTelemetryTracingContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/GrafanaOpenTelemetryTracingContainerConnectionDetailsFactoryIntegrationTests.java index 441bfba6be76..6b57114b763f 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/GrafanaOpenTelemetryTracingContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/GrafanaOpenTelemetryTracingContainerConnectionDetailsFactoryIntegrationTests.java @@ -55,6 +55,7 @@ void connectionCanBeMadeToOpenTelemetryContainer() { .isEqualTo("%s/v1/traces".formatted(container.getOtlpHttpUrl())); assertThat(this.connectionDetails.getUrl(Transport.GRPC)) .isEqualTo("%s/v1/traces".formatted(container.getOtlpGrpcUrl())); + assertThat(this.connectionDetails.getSslBundle()).isNull(); } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/OpenTelemetryTracingContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/OpenTelemetryTracingContainerConnectionDetailsFactoryIntegrationTests.java index a60f875081a7..aad7c9c7ec3e 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/OpenTelemetryTracingContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/OpenTelemetryTracingContainerConnectionDetailsFactoryIntegrationTests.java @@ -56,6 +56,7 @@ void connectionCanBeMadeToOpenTelemetryContainer() { .isEqualTo("http://" + container.getHost() + ":" + container.getMappedPort(4318) + "/v1/traces"); assertThat(this.connectionDetails.getUrl(Transport.GRPC)) .isEqualTo("http://" + container.getHost() + ":" + container.getMappedPort(4317) + "/v1/traces"); + assertThat(this.connectionDetails.getSslBundle()).isNull(); } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/resources/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/ca.crt b/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/resources/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/ca.crt new file mode 100644 index 000000000000..beed250b132b --- /dev/null +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/resources/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/ca.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFhjCCA26gAwIBAgIUfIkk29IT9OpbgfjL8oRIPSLjUcAwDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDUwMTE2NTMyNVoXDTM0MDQyOTE2NTMyNVow +OzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNh +dGUgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAusN2 +KzQQUUxZSiI3ZZuZohFwq2KXSUNPdJ6rgD3/YKNTDSZXKZPO53kYPP0DXf0sm3CH +cyWSWVabyimZYuPWena1MElSL4ZpJ9WwkZoOQ3bPFK1utz6kMOwrgAUcky8H/rIK +j2JEBhkSHUIGr57NjUEwG1ygaSerM8RzWw1PtMq+C8LOu3v94qzE3NDg1QRpyvV9 +OmsLsjISd0ZmAJNi9vmiEH923KnPyiqnQmWKpYicdgQmX1GXylS22jZqAwaOkYGj +X8UdeyvrohkZkM0hn9uaSufQGEW4yKACn3PkjJtzi8drBIyjIi9YcAzBxZB9oVKq +XZMlltgO2fDMmIJi0Ngt0Ci7fCoEMqSocKyDKML6YLr9UWtx4bfsrk+rVO9Q/D/v +8RKgstv7dCf2KWRX3ZJEC0IBHS5gLNq0qqqVcGx3LcSyhdiKJOtSwAnNkHMh+jSQ +xLSlBjcSqTPiGTRK/Rddl+xnU/mBgk7ZBGNrUFaD5McMFjddS7Ih82aHnpQ1gekW +nUGv+Tm/G68h2BvZ5U2q+RfeOCgRW9i/AYW2jgT7IFnfjyUXgBQveauMAchomqFE +VLe95ZgViF6vmH34EKo3w9L5TQiwk/r53YlM7TSOTyDqx66t4zGYDsVMicpKmzi4 +2Rp8EpErARRyREUIKSvWs9O9+uT3+7arNLgHe5ECAwEAAaOBgTB/MB0GA1UdDgQW +BBRVMLDVqPECWaH6GruL9E52VcTrPjAfBgNVHSMEGDAWgBRVMLDVqPECWaH6GruL +9E52VcTrPjAPBgNVHRMBAf8EBTADAQH/MCwGA1UdEQQlMCOCC2V4YW1wbGUuY29t +gglsb2NhbGhvc3SCCTEyNy4wLjAuMTANBgkqhkiG9w0BAQsFAAOCAgEAeSpjCL3j +2GIFBNKr/5amLOYa0kZ6r1dJs+K6xvMsUvsBJ/QQsV5nYDMIoV/NYUd8SyYV4lEj +7LHX5ZbmJrvPk30LGEBG/5Vy2MIATrQrQ14S4nXtEdSnBvTQwPOOaHc+2dTp3YpM +f4ffELKWyispTifx1eqdiUJhURKeQBh+3W7zpyaiN4vJaqEDKGgFQtHA/OyZL2hZ +BpxHB0zpb2iDHV8MeyfOT7HQWUk6p13vdYm6EnyJT8fzWvE+TqYNbqFmB+CLRSXy +R3p1yaeTd4LnVknJ0UBKqEyul3ziHZDhKhBpwdglYOQz4eWjSFhikX9XZ8NaI38Q +QqLZVn0DsH2ztkjrQrUVgK2xn4aUuqoLDk4Hu6h5baUn+f2GLuzx+EXc/i3ikYvw +Y3JyufOgw6nGGFG+/QXEj85XtLPhN7Wm42z2e/BGzi0MLl65sfpEDXvFTA72Yzws +OYaeg/HxeYwUHQgs2fKl/LgV4chntSCvTqfNl6OnQafD/ISJNpx3xWR3HwF+ypFG +UaLE+e1soqEJbzL31U/6pypHLsj8Y8r9hJbZXo2ibnhjFV6fypUAP0rbIzaoWcrJ +T0Sbliz+KQTMzCcubiAi4bI/kZ5FJ4kkaHqUpIWzlx1h2WVJ65ASFDjBWb8eVmB6 +Dyno/RVFR/rUL5091gjGRXhLsi1oUHKdEzU= +-----END CERTIFICATE----- diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/resources/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/otlp-ssl-compose.yaml b/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/resources/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/otlp-ssl-compose.yaml new file mode 100644 index 000000000000..75707fa3e684 --- /dev/null +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/dockerTest/resources/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/otlp-ssl-compose.yaml @@ -0,0 +1,8 @@ +services: + otlp: + image: '{imageName}' + ports: + - '4317' + - '4318' + labels: + - 'org.springframework.boot.sslbundle.pem.truststore.certificate=ca.crt' diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingConfigurations.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingConfigurations.java index a0495e05bc86..4cb6a7c41d15 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingConfigurations.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingConfigurations.java @@ -18,20 +18,28 @@ import java.util.Locale; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; +import org.jspecify.annotations.Nullable; 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.ConditionalOnProperty; import org.springframework.boot.micrometer.tracing.autoconfigure.ConditionalOnEnabledTracingExport; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Configurations imported by {@link OtlpTracingAutoConfiguration}. @@ -47,8 +55,9 @@ static class ConnectionDetails { @Bean @ConditionalOnMissingBean @ConditionalOnProperty("management.opentelemetry.tracing.export.otlp.endpoint") - OtlpTracingConnectionDetails otlpTracingConnectionDetails(OtlpTracingProperties properties) { - return new PropertiesOtlpTracingConnectionDetails(properties); + OtlpTracingConnectionDetails otlpTracingConnectionDetails(OtlpTracingProperties properties, + ObjectProvider sslBundles) { + return new PropertiesOtlpTracingConnectionDetails(properties, sslBundles.getIfAvailable()); } /** @@ -58,8 +67,11 @@ static class PropertiesOtlpTracingConnectionDetails implements OtlpTracingConnec private final OtlpTracingProperties properties; - PropertiesOtlpTracingConnectionDetails(OtlpTracingProperties properties) { + private final @Nullable SslBundles sslBundles; + + PropertiesOtlpTracingConnectionDetails(OtlpTracingProperties properties, @Nullable SslBundles sslBundles) { this.properties = properties; + this.sslBundles = sslBundles; } @Override @@ -72,6 +84,16 @@ public String getUrl(Transport transport) { return endpoint; } + @Override + public @Nullable SslBundle getSslBundle() { + String bundleName = this.properties.getSsl().getBundle(); + if (StringUtils.hasLength(bundleName)) { + Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context"); + return this.sslBundles.getBundle(bundleName); + } + return null; + } + } } @@ -95,6 +117,7 @@ OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpTracingProperties properties, .setCompression(properties.getCompression().name().toLowerCase(Locale.ROOT)); properties.getHeaders().forEach(builder::addHeader); meterProvider.ifAvailable(builder::setMeterProvider); + configureSsl(connectionDetails, builder::setSslContext); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } @@ -111,10 +134,36 @@ OtlpGrpcSpanExporter otlpGrpcSpanExporter(OtlpTracingProperties properties, .setCompression(properties.getCompression().name().toLowerCase(Locale.ROOT)); properties.getHeaders().forEach(builder::addHeader); meterProvider.ifAvailable(builder::setMeterProvider); + configureSsl(connectionDetails, builder::setSslContext); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } + private void configureSsl(OtlpTracingConnectionDetails connectionDetails, + SslContextConfigurer sslContextConfigurer) { + SslBundle sslBundle = connectionDetails.getSslBundle(); + if (sslBundle != null) { + SSLContext sslContext = sslBundle.createSslContext(); + X509TrustManager trustManager = extractTrustManager(sslBundle); + sslContextConfigurer.configure(sslContext, trustManager); + } + } + + private X509TrustManager extractTrustManager(SslBundle sslBundle) { + for (TrustManager trustManager : sslBundle.getManagers().getTrustManagers()) { + if (trustManager instanceof X509TrustManager x509TrustManager) { + return x509TrustManager; + } + } + throw new IllegalStateException("No X509TrustManager found in the SSL bundle trust managers"); + } + + private interface SslContextConfigurer { + + void configure(SSLContext sslContext, X509TrustManager trustManager); + + } + } } diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingConnectionDetails.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingConnectionDetails.java index dacf9b17d4ba..c5e0336059c2 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingConnectionDetails.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingConnectionDetails.java @@ -16,7 +16,10 @@ package org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp; +import org.jspecify.annotations.Nullable; + import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.boot.ssl.SslBundle; /** * Details required to establish a connection to an OpenTelemetry service. @@ -34,4 +37,13 @@ public interface OtlpTracingConnectionDetails extends ConnectionDetails { */ String getUrl(Transport transport); + /** + * SSL bundle to use. + * @return the SSL bundle to use or {@code null} + * @since 4.1.0 + */ + default @Nullable SslBundle getSslBundle() { + return null; + } + } diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingProperties.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingProperties.java index e74c5fc27b43..edf073fa3494 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingProperties.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingProperties.java @@ -66,6 +66,8 @@ public class OtlpTracingProperties { */ private Map headers = new HashMap<>(); + private final Ssl ssl = new Ssl(); + public @Nullable String getEndpoint() { return this.endpoint; } @@ -114,6 +116,27 @@ public void setHeaders(Map headers) { this.headers = headers; } + public Ssl getSsl() { + return this.ssl; + } + + public static class Ssl { + + /** + * SSL bundle name. + */ + private @Nullable String bundle; + + public @Nullable String getBundle() { + return this.bundle; + } + + public void setBundle(@Nullable String bundle) { + this.bundle = bundle; + } + + } + public enum Compression { /** diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java index 1252cfbfbe49..16786f08bc2b 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/docker/compose/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java @@ -16,11 +16,14 @@ package org.springframework.boot.micrometer.tracing.opentelemetry.docker.compose.otlp; +import org.jspecify.annotations.Nullable; + 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; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingConnectionDetails; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; /** * {@link DockerComposeConnectionDetailsFactory} to create @@ -58,11 +61,14 @@ private static final class OpenTelemetryTracingDockerComposeConnectionDetails ex private final int httpPort; + private final @Nullable SslBundle sslBundle; + private OpenTelemetryTracingDockerComposeConnectionDetails(RunningService source) { super(source); this.host = source.host(); this.grpcPort = source.ports().get(OTLP_GRPC_PORT); this.httpPort = source.ports().get(OTLP_HTTP_PORT); + this.sslBundle = getSslBundle(source); } @Override @@ -71,7 +77,13 @@ public String getUrl(Transport transport) { case HTTP -> this.httpPort; case GRPC -> this.grpcPort; }; - return "http://%s:%d/v1/traces".formatted(this.host, port); + String scheme = (this.sslBundle != null) ? "https" : "http"; + return "%s://%s:%d/v1/traces".formatted(scheme, this.host, port); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return this.sslBundle; } } diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/GrafanaOpenTelemetryTracingContainerConnectionDetailsFactory.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/GrafanaOpenTelemetryTracingContainerConnectionDetailsFactory.java index b9c836fa0c7b..070957bd5a86 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/GrafanaOpenTelemetryTracingContainerConnectionDetailsFactory.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/GrafanaOpenTelemetryTracingContainerConnectionDetailsFactory.java @@ -16,10 +16,12 @@ package org.springframework.boot.micrometer.tracing.opentelemetry.testcontainers.otlp; +import org.jspecify.annotations.Nullable; import org.testcontainers.grafana.LgtmStackContainer; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingConnectionDetails; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -59,9 +61,17 @@ public String getUrl(Transport transport) { case HTTP -> getContainer().getOtlpHttpUrl(); case GRPC -> getContainer().getOtlpGrpcUrl(); }; + if (getSslBundle() != null) { + url = url.replaceFirst("^http://", "https://"); + } return "%s/v1/traces".formatted(url); } + @Override + public @Nullable SslBundle getSslBundle() { + return super.getSslBundle(); + } + } } diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/OpenTelemetryTracingContainerConnectionDetailsFactory.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/OpenTelemetryTracingContainerConnectionDetailsFactory.java index 47abe066588d..080060acebb6 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/OpenTelemetryTracingContainerConnectionDetailsFactory.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/testcontainers/otlp/OpenTelemetryTracingContainerConnectionDetailsFactory.java @@ -16,11 +16,13 @@ package org.springframework.boot.micrometer.tracing.opentelemetry.testcontainers.otlp; +import org.jspecify.annotations.Nullable; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingConnectionDetails; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -64,7 +66,14 @@ public String getUrl(Transport transport) { case HTTP -> OTLP_HTTP_PORT; case GRPC -> OTLP_GRPC_PORT; }; - return "http://%s:%d/v1/traces".formatted(getContainer().getHost(), getContainer().getMappedPort(port)); + String scheme = (getSslBundle() != null) ? "https" : "http"; + return "%s://%s:%d/v1/traces".formatted(scheme, getContainer().getHost(), + getContainer().getMappedPort(port)); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return super.getSslBundle(); } } diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfigurationTests.java index 9990ef98ab7e..a4379ac7b5e0 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfigurationTests.java @@ -20,6 +20,8 @@ import java.util.List; import java.util.function.Supplier; +import javax.net.ssl.X509TrustManager; + import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.exporter.internal.compression.GzipCompressor; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; @@ -30,13 +32,18 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingConfigurations.ConnectionDetails.PropertiesOtlpTracingConnectionDetails; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.resources.WithPackageResources; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Tests for {@link OtlpTracingAutoConfiguration}. @@ -269,6 +276,58 @@ void shouldCustomizeGrpcTransportWhenEnabledWithOtlpGrpcSpanExporterBuilderCusto }); } + @Test + @WithPackageResources("test.jks") + void whenHasSslBundleConfiguresHttpExporter() { + this.contextRunner.withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class)) + .withPropertyValues( + "management.opentelemetry.tracing.export.otlp.endpoint=https://localhost:4318/v1/traces", + "management.opentelemetry.tracing.export.otlp.ssl.bundle=mybundle", + "spring.ssl.bundle.jks.mybundle.truststore.location=classpath:test.jks") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class); + OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class); + assertThat(exporter).extracting("delegate.httpSender.client.sslSocketFactoryOrNull").isNotNull(); + assertThat(exporter).extracting("delegate.httpSender.client.x509TrustManager") + .isInstanceOf(X509TrustManager.class); + }); + } + + @Test + @WithPackageResources("test.jks") + void whenHasSslBundleConfiguresGrpcExporter() { + this.contextRunner.withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class)) + .withPropertyValues( + "management.opentelemetry.tracing.export.otlp.endpoint=https://localhost:4318/v1/traces", + "management.opentelemetry.tracing.export.otlp.transport=grpc", + "management.opentelemetry.tracing.export.otlp.ssl.bundle=mybundle", + "spring.ssl.bundle.jks.mybundle.truststore.location=classpath:test.jks") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpGrpcSpanExporter.class); + OtlpGrpcSpanExporter exporter = context.getBean(OtlpGrpcSpanExporter.class); + assertThat(exporter).extracting("delegate.grpcSender.client.sslSocketFactoryOrNull").isNotNull(); + assertThat(exporter).extracting("delegate.grpcSender.client.x509TrustManager") + .isInstanceOf(X509TrustManager.class); + }); + } + + @Test + void whenCustomConnectionDetailsProvidesSslBundleConfiguresHttpExporter() { + SslBundle sslBundle = SslBundle.systemDefault(); + OtlpTracingConnectionDetails connectionDetails = mock(OtlpTracingConnectionDetails.class); + given(connectionDetails.getUrl(Transport.HTTP)).willReturn("https://localhost:4318/v1/traces"); + given(connectionDetails.getSslBundle()).willReturn(sslBundle); + this.contextRunner + .withBean("customOtlpTracingConnectionDetails", OtlpTracingConnectionDetails.class, () -> connectionDetails) + .run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class); + OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class); + assertThat(exporter).extracting("delegate.httpSender.client.sslSocketFactoryOrNull").isNotNull(); + assertThat(exporter).extracting("delegate.httpSender.client.x509TrustManager") + .isInstanceOf(X509TrustManager.class); + }); + } + @Configuration(proxyBeanMethods = false) private static final class MeterProviderConfiguration { diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/resources/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/test.jks b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/resources/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/test.jks new file mode 100644 index 0000000000000000000000000000000000000000..0fc3e802f75461dd074facb9611d350db4d5960f GIT binary patch literal 1276 zcmezO_TO6u1_mZ5W@O+hNi8nXP0YzmEM{O}OjTL=XFE`?-k{cikBv*4jgf^>i%F1? zk(GfZ`?F{4vBFug6<%MmmXJ+)mEgQoslXV6!5_H^yana6V1?kw1w zbE0Bi&GHlLHzfosgjrwL)qKcc5I;l0LF?W2lpQg%-cHp$l(#o)?Jkath1@gQN{hG8 zig@wKv#0R7vd_QC=JG%%Ffy=4=$RT=0v*d`(8R=M(8RcU0W%XL6BCP-)w&Y~JZv0V zZ64=rS(uqv84M~6g$xAPm_u3EggJBalM{0?@{3DgVjNh+*s+LlVG-lTBF2m)W*{fd zYiMC$VQ64zW@K(?5e4L0B5?=MWswHLZ0z7LVq$~_7BeF|vl9agPmO-znfkD()@R+bGrT$(AXmN24#zv*XRX z#$UNu(Lmln78u;Jd@N!tBKmU@J0!OJc3G%!N>OO@P1n+F-CorAVRmOQaA8six!iWP z)M3lXpnJ*TI=kIlH(Yxia-ls?xvctEx&P5B6()tKm`>%bo~@fX9{j%TtMU1G!|pw& zZ6BRjIqQ^`bIxR@OmMno&8^H%tpq36Esh&T(+MJ_la+#pV>+3scS% Date: Fri, 13 Mar 2026 09:51:57 +0100 Subject: [PATCH 333/930] Add support for SslBundles to OTLP metrics export Closes gh-49590 --- .../build.gradle | 1 + ...nectionDetailsFactoryIntegrationTests.java | 10 ++ ...nectionDetailsFactoryIntegrationTests.java | 10 ++ ...nectionDetailsFactoryIntegrationTests.java | 7 + ...nectionDetailsFactoryIntegrationTests.java | 5 + .../metrics/docker/compose/otlp/ca.crt | 32 +++++ .../docker/compose/otlp/otlp-ssl-compose.yaml | 8 ++ .../export/otlp/JdkClientHttpSender.java | 92 +++++++++++++ .../otlp/OtlpMetricsConnectionDetails.java | 11 ++ .../OtlpMetricsExportAutoConfiguration.java | 50 +++++-- .../export/otlp/OtlpMetricsProperties.java | 23 ++++ ...DockerComposeConnectionDetailsFactory.java | 14 +- ...ricsContainerConnectionDetailsFactory.java | 13 +- ...ricsContainerConnectionDetailsFactory.java | 11 +- .../export/otlp/JdkClientHttpSenderTests.java | 127 ++++++++++++++++++ ...lpMetricsExportAutoConfigurationTests.java | 92 ++++++++++++- ...lpMetricsPropertiesConfigAdapterTests.java | 2 +- .../autoconfigure/export/otlp/test.jks | Bin 0 -> 1276 bytes 18 files changed, 492 insertions(+), 16 deletions(-) create mode 100644 module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/ca.crt create mode 100644 module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/otlp-ssl-compose.yaml create mode 100644 module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSender.java create mode 100644 module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSenderTests.java create mode 100644 module/spring-boot-micrometer-metrics/src/test/resources/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/test.jks diff --git a/module/spring-boot-micrometer-metrics/build.gradle b/module/spring-boot-micrometer-metrics/build.gradle index cfaa0830b333..bfa9a9c73d48 100644 --- a/module/spring-boot-micrometer-metrics/build.gradle +++ b/module/spring-boot-micrometer-metrics/build.gradle @@ -74,6 +74,7 @@ dependencies { testImplementation(project(":core:spring-boot-test")) testImplementation(project(":test-support:spring-boot-test-support")) + testImplementation("com.squareup.okhttp3:mockwebserver") testImplementation("io.micrometer:micrometer-registry-atlas") testImplementation("io.micrometer:micrometer-registry-new-relic") testImplementation("io.micrometer:micrometer-registry-prometheus") diff --git a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/GrafanaOpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/GrafanaOpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java index 1d2853613106..f044a4c5a4d5 100644 --- a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/GrafanaOpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/GrafanaOpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -18,6 +18,7 @@ import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsConnectionDetails; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; @@ -33,6 +34,15 @@ class GrafanaOpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegratio @DockerComposeTest(composeFile = "otlp-compose.yaml", image = TestImage.GRAFANA_OTEL_LGTM) void runCreatesConnectionDetails(OtlpMetricsConnectionDetails connectionDetails) { assertThat(connectionDetails.getUrl()).startsWith("http://").endsWith("/v1/metrics"); + assertThat(connectionDetails.getSslBundle()).isNull(); + } + + @DockerComposeTest(composeFile = "otlp-ssl-compose.yaml", image = TestImage.GRAFANA_OTEL_LGTM, + additionalResources = "ca.crt") + void runWithSslCreatesConnectionDetails(OtlpMetricsConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUrl()).startsWith("https://").endsWith("/v1/metrics"); + SslBundle sslBundle = connectionDetails.getSslBundle(); + assertThat(sslBundle).isNotNull(); } } diff --git a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java index df854e5470fd..84fb8203b5eb 100644 --- a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -18,6 +18,7 @@ import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsConnectionDetails; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; @@ -33,6 +34,15 @@ class OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests @DockerComposeTest(composeFile = "otlp-compose.yaml", image = TestImage.OTEL_COLLECTOR) void runCreatesConnectionDetails(OtlpMetricsConnectionDetails connectionDetails) { assertThat(connectionDetails.getUrl()).startsWith("http://").endsWith("/v1/metrics"); + assertThat(connectionDetails.getSslBundle()).isNull(); + } + + @DockerComposeTest(composeFile = "otlp-ssl-compose.yaml", image = TestImage.OTEL_COLLECTOR, + additionalResources = "ca.crt") + void runWithSslCreatesConnectionDetails(OtlpMetricsConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUrl()).startsWith("https://").endsWith("/v1/metrics"); + SslBundle sslBundle = connectionDetails.getSslBundle(); + assertThat(sslBundle).isNotNull(); } } diff --git a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java index b60cf69ea4e2..1486793828bb 100644 --- a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java @@ -33,6 +33,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsConnectionDetails; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsExportAutoConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; @@ -42,6 +43,8 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.web.servlet.client.RestTestClient; +import static org.assertj.core.api.Assertions.assertThat; + /** * Tests for {@link GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory}. * @@ -60,8 +63,12 @@ class GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTes @Autowired private MeterRegistry meterRegistry; + @Autowired + private OtlpMetricsConnectionDetails connectionDetails; + @Test void connectionCanBeMadeToOpenTelemetryCollectorContainer() { + assertThat(this.connectionDetails.getSslBundle()).isNull(); 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)); diff --git a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java index 927165fdb7b5..223414c66baa 100644 --- a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java @@ -35,6 +35,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsConnectionDetails; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsExportAutoConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; @@ -75,8 +76,12 @@ class OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests { @Autowired private MeterRegistry meterRegistry; + @Autowired + private OtlpMetricsConnectionDetails connectionDetails; + @Test void connectionCanBeMadeToOpenTelemetryCollectorContainer() { + assertThat(this.connectionDetails.getSslBundle()).isNull(); 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)); diff --git a/module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/ca.crt b/module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/ca.crt new file mode 100644 index 000000000000..beed250b132b --- /dev/null +++ b/module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/ca.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFhjCCA26gAwIBAgIUfIkk29IT9OpbgfjL8oRIPSLjUcAwDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDUwMTE2NTMyNVoXDTM0MDQyOTE2NTMyNVow +OzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNh +dGUgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAusN2 +KzQQUUxZSiI3ZZuZohFwq2KXSUNPdJ6rgD3/YKNTDSZXKZPO53kYPP0DXf0sm3CH +cyWSWVabyimZYuPWena1MElSL4ZpJ9WwkZoOQ3bPFK1utz6kMOwrgAUcky8H/rIK +j2JEBhkSHUIGr57NjUEwG1ygaSerM8RzWw1PtMq+C8LOu3v94qzE3NDg1QRpyvV9 +OmsLsjISd0ZmAJNi9vmiEH923KnPyiqnQmWKpYicdgQmX1GXylS22jZqAwaOkYGj +X8UdeyvrohkZkM0hn9uaSufQGEW4yKACn3PkjJtzi8drBIyjIi9YcAzBxZB9oVKq +XZMlltgO2fDMmIJi0Ngt0Ci7fCoEMqSocKyDKML6YLr9UWtx4bfsrk+rVO9Q/D/v +8RKgstv7dCf2KWRX3ZJEC0IBHS5gLNq0qqqVcGx3LcSyhdiKJOtSwAnNkHMh+jSQ +xLSlBjcSqTPiGTRK/Rddl+xnU/mBgk7ZBGNrUFaD5McMFjddS7Ih82aHnpQ1gekW +nUGv+Tm/G68h2BvZ5U2q+RfeOCgRW9i/AYW2jgT7IFnfjyUXgBQveauMAchomqFE +VLe95ZgViF6vmH34EKo3w9L5TQiwk/r53YlM7TSOTyDqx66t4zGYDsVMicpKmzi4 +2Rp8EpErARRyREUIKSvWs9O9+uT3+7arNLgHe5ECAwEAAaOBgTB/MB0GA1UdDgQW +BBRVMLDVqPECWaH6GruL9E52VcTrPjAfBgNVHSMEGDAWgBRVMLDVqPECWaH6GruL +9E52VcTrPjAPBgNVHRMBAf8EBTADAQH/MCwGA1UdEQQlMCOCC2V4YW1wbGUuY29t +gglsb2NhbGhvc3SCCTEyNy4wLjAuMTANBgkqhkiG9w0BAQsFAAOCAgEAeSpjCL3j +2GIFBNKr/5amLOYa0kZ6r1dJs+K6xvMsUvsBJ/QQsV5nYDMIoV/NYUd8SyYV4lEj +7LHX5ZbmJrvPk30LGEBG/5Vy2MIATrQrQ14S4nXtEdSnBvTQwPOOaHc+2dTp3YpM +f4ffELKWyispTifx1eqdiUJhURKeQBh+3W7zpyaiN4vJaqEDKGgFQtHA/OyZL2hZ +BpxHB0zpb2iDHV8MeyfOT7HQWUk6p13vdYm6EnyJT8fzWvE+TqYNbqFmB+CLRSXy +R3p1yaeTd4LnVknJ0UBKqEyul3ziHZDhKhBpwdglYOQz4eWjSFhikX9XZ8NaI38Q +QqLZVn0DsH2ztkjrQrUVgK2xn4aUuqoLDk4Hu6h5baUn+f2GLuzx+EXc/i3ikYvw +Y3JyufOgw6nGGFG+/QXEj85XtLPhN7Wm42z2e/BGzi0MLl65sfpEDXvFTA72Yzws +OYaeg/HxeYwUHQgs2fKl/LgV4chntSCvTqfNl6OnQafD/ISJNpx3xWR3HwF+ypFG +UaLE+e1soqEJbzL31U/6pypHLsj8Y8r9hJbZXo2ibnhjFV6fypUAP0rbIzaoWcrJ +T0Sbliz+KQTMzCcubiAi4bI/kZ5FJ4kkaHqUpIWzlx1h2WVJ65ASFDjBWb8eVmB6 +Dyno/RVFR/rUL5091gjGRXhLsi1oUHKdEzU= +-----END CERTIFICATE----- diff --git a/module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/otlp-ssl-compose.yaml b/module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/otlp-ssl-compose.yaml new file mode 100644 index 000000000000..75707fa3e684 --- /dev/null +++ b/module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/otlp-ssl-compose.yaml @@ -0,0 +1,8 @@ +services: + otlp: + image: '{imageName}' + ports: + - '4317' + - '4318' + labels: + - 'org.springframework.boot.sslbundle.pem.truststore.certificate=ca.crt' diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSender.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSender.java new file mode 100644 index 000000000000..f4fba0b4b74a --- /dev/null +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSender.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; + +import javax.net.ssl.SSLParameters; + +import io.micrometer.core.ipc.http.HttpSender; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslOptions; + +/** + * {@link HttpSender} implementation using the JDK {@link HttpClient}. + * + * @author Moritz Halbritter + */ +class JdkClientHttpSender implements HttpSender { + + private final HttpClient httpClient; + + private final Duration timeout; + + /** + * Creates a new {@link JdkClientHttpSender}. + * @param connectTimeout the connect timeout + * @param timeout the request timeout + * @param sslBundle the SSL bundle to use for TLS configuration, or {@code null} + */ + JdkClientHttpSender(Duration connectTimeout, Duration timeout, @Nullable SslBundle sslBundle) { + this.httpClient = buildHttpClient(connectTimeout, sslBundle); + this.timeout = timeout; + } + + private static HttpClient buildHttpClient(Duration connectTimeout, @Nullable SslBundle sslBundle) { + HttpClient.Builder builder = HttpClient.newBuilder().connectTimeout(connectTimeout); + if (sslBundle != null) { + builder.sslContext(sslBundle.createSslContext()); + builder.sslParameters(asSslParameters(sslBundle)); + } + return builder.build(); + } + + private static SSLParameters asSslParameters(SslBundle sslBundle) { + SslOptions options = sslBundle.getOptions(); + SSLParameters parameters = new SSLParameters(); + parameters.setCipherSuites(options.getCiphers()); + parameters.setProtocols(options.getEnabledProtocols()); + return parameters; + } + + @Override + public Response send(Request request) throws IOException { + HttpRequest.Builder httpRequest = HttpRequest.newBuilder() + .uri(URI.create(request.getUrl().toString())) + .timeout(this.timeout); + request.getRequestHeaders().forEach(httpRequest::header); + httpRequest.method(request.getMethod().name(), BodyPublishers.ofByteArray(request.getEntity())); + try { + HttpResponse response = this.httpClient.send(httpRequest.build(), BodyHandlers.ofString()); + return new Response(response.statusCode(), response.body()); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IOException("HTTP request interrupted", ex); + } + } + +} diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsConnectionDetails.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsConnectionDetails.java index f5bc6644539d..8be74ca7968a 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsConnectionDetails.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsConnectionDetails.java @@ -19,11 +19,13 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.boot.ssl.SslBundle; /** * Details required to establish a connection to an OpenTelemetry Collector service. * * @author Eddú Meléndez + * @author Moritz Halbritter * @since 4.0.0 */ public interface OtlpMetricsConnectionDetails extends ConnectionDetails { @@ -34,4 +36,13 @@ public interface OtlpMetricsConnectionDetails extends ConnectionDetails { */ @Nullable String getUrl(); + /** + * SSL bundle to use. + * @return the SSL bundle to use or {@code null} + * @since 4.1.0 + */ + default @Nullable SslBundle getSslBundle() { + return null; + } + } diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfiguration.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfiguration.java index 832715f6bbff..5c218497e65c 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfiguration.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfiguration.java @@ -16,9 +16,12 @@ package org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp; +import java.time.Duration; + import io.micrometer.core.instrument.Clock; import io.micrometer.registry.otlp.ExemplarContextProvider; import io.micrometer.registry.otlp.OtlpConfig; +import io.micrometer.registry.otlp.OtlpHttpMetricsSender; import io.micrometer.registry.otlp.OtlpMeterRegistry; import io.micrometer.registry.otlp.OtlpMetricsSender; import org.jspecify.annotations.Nullable; @@ -36,10 +39,14 @@ import org.springframework.boot.micrometer.metrics.autoconfigure.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.micrometer.metrics.autoconfigure.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryProperties; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.thread.Threading; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; import org.springframework.core.task.VirtualThreadTaskExecutor; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for exporting metrics to OTLP. @@ -65,8 +72,8 @@ public final class OtlpMetricsExportAutoConfiguration { @Bean @ConditionalOnMissingBean - OtlpMetricsConnectionDetails otlpMetricsConnectionDetails() { - return new PropertiesOtlpMetricsConnectionDetails(this.properties); + OtlpMetricsConnectionDetails otlpMetricsConnectionDetails(ObjectProvider sslBundles) { + return new PropertiesOtlpMetricsConnectionDetails(this.properties, sslBundles.getIfAvailable()); } @Bean @@ -77,11 +84,20 @@ OtlpConfig otlpConfig(OpenTelemetryProperties openTelemetryProperties, environment); } + @Bean + @ConditionalOnMissingBean(OtlpMetricsSender.class) + OtlpHttpMetricsSender otlpMetricsSender(OtlpMetricsConnectionDetails connectionDetails) { + Duration connectTimeout = this.properties.getConnectTimeout(); + Duration timeout = connectTimeout.plus(this.properties.getReadTimeout()); + JdkClientHttpSender httpSender = new JdkClientHttpSender(connectTimeout, timeout, + connectionDetails.getSslBundle()); + return new OtlpHttpMetricsSender(httpSender); + } + @Bean @ConditionalOnMissingBean @ConditionalOnThreading(Threading.PLATFORM) - OtlpMeterRegistry otlpMeterRegistry(OtlpConfig otlpConfig, Clock clock, - ObjectProvider metricsSender, + OtlpMeterRegistry otlpMeterRegistry(OtlpConfig otlpConfig, Clock clock, OtlpMetricsSender metricsSender, ObjectProvider exemplarContextProvider) { return builder(otlpConfig, clock, metricsSender, exemplarContextProvider).build(); } @@ -90,19 +106,18 @@ OtlpMeterRegistry otlpMeterRegistry(OtlpConfig otlpConfig, Clock clock, @ConditionalOnMissingBean @ConditionalOnThreading(Threading.VIRTUAL) OtlpMeterRegistry otlpMeterRegistryVirtualThreads(OtlpConfig otlpConfig, Clock clock, - ObjectProvider metricsSender, - ObjectProvider exemplarContextProvider) { + OtlpMetricsSender metricsSender, ObjectProvider exemplarContextProvider) { VirtualThreadTaskExecutor executor = new VirtualThreadTaskExecutor("otlp-meter-registry-"); return builder(otlpConfig, clock, metricsSender, exemplarContextProvider) .threadFactory(executor.getVirtualThreadFactory()) .build(); } - private OtlpMeterRegistry.Builder builder(OtlpConfig otlpConfig, Clock clock, - ObjectProvider metricsSender, + private OtlpMeterRegistry.Builder builder(OtlpConfig otlpConfig, Clock clock, OtlpMetricsSender metricsSender, ObjectProvider exemplarContextProvider) { - OtlpMeterRegistry.Builder builder = OtlpMeterRegistry.builder(otlpConfig).clock(clock); - metricsSender.ifAvailable(builder::metricsSender); + OtlpMeterRegistry.Builder builder = OtlpMeterRegistry.builder(otlpConfig) + .clock(clock) + .metricsSender(metricsSender); exemplarContextProvider.ifAvailable(builder::exemplarContextProvider); return builder; } @@ -114,8 +129,11 @@ static class PropertiesOtlpMetricsConnectionDetails implements OtlpMetricsConnec private final OtlpMetricsProperties properties; - PropertiesOtlpMetricsConnectionDetails(OtlpMetricsProperties properties) { + private final @Nullable SslBundles sslBundles; + + PropertiesOtlpMetricsConnectionDetails(OtlpMetricsProperties properties, @Nullable SslBundles sslBundles) { this.properties = properties; + this.sslBundles = sslBundles; } @Override @@ -123,6 +141,16 @@ static class PropertiesOtlpMetricsConnectionDetails implements OtlpMetricsConnec return this.properties.getUrl(); } + @Override + public @Nullable SslBundle getSslBundle() { + String bundleName = this.properties.getSsl().getBundle(); + if (StringUtils.hasLength(bundleName)) { + Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context"); + return this.sslBundles.getBundle(bundleName); + } + return null; + } + } } diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java index 0f4687adbdf4..99c11d898dad 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java @@ -91,6 +91,8 @@ public class OtlpMetricsProperties extends StepRegistryProperties { */ private final Map meter = new LinkedHashMap<>(); + private final Ssl ssl = new Ssl(); + public @Nullable String getUrl() { return this.url; } @@ -167,6 +169,27 @@ public Map getMeter() { return this.meter; } + public Ssl getSsl() { + return this.ssl; + } + + public static class Ssl { + + /** + * SSL bundle name. + */ + private @Nullable String bundle; + + public @Nullable String getBundle() { + return this.bundle; + } + + public void setBundle(@Nullable String bundle) { + this.bundle = bundle; + } + + } + /** * Per-meter settings. */ diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java index c9bc4351be19..64ad38ac7695 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java @@ -16,10 +16,13 @@ package org.springframework.boot.micrometer.metrics.docker.compose.otlp; +import org.jspecify.annotations.Nullable; + 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; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsConnectionDetails; +import org.springframework.boot.ssl.SslBundle; /** * {@link DockerComposeConnectionDetailsFactory} to create @@ -52,15 +55,24 @@ private static final class OpenTelemetryMetricsDockerComposeConnectionDetails ex private final int port; + private final @Nullable SslBundle sslBundle; + private OpenTelemetryMetricsDockerComposeConnectionDetails(RunningService source) { super(source); this.host = source.host(); this.port = source.ports().get(OTLP_PORT); + this.sslBundle = getSslBundle(source); } @Override public String getUrl() { - return "http://%s:%d/v1/metrics".formatted(this.host, this.port); + String scheme = (this.sslBundle != null) ? "https" : "http"; + return "%s://%s:%d/v1/metrics".formatted(scheme, this.host, this.port); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return this.sslBundle; } } diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory.java index 8111122fbab8..2e956722cbaa 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory.java @@ -16,9 +16,11 @@ package org.springframework.boot.micrometer.metrics.testcontainers.otlp; +import org.jspecify.annotations.Nullable; import org.testcontainers.grafana.LgtmStackContainer; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsConnectionDetails; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -54,7 +56,16 @@ private OpenTelemetryMetricsContainerConnectionDetails(ContainerConnectionSource @Override public String getUrl() { - return "%s/v1/metrics".formatted(getContainer().getOtlpHttpUrl()); + String url = getContainer().getOtlpHttpUrl(); + if (getSslBundle() != null) { + url = url.replaceFirst("^http://", "https://"); + } + return "%s/v1/metrics".formatted(url); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return super.getSslBundle(); } } diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactory.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactory.java index 98e5ed282d46..a2944ae02363 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactory.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactory.java @@ -16,10 +16,12 @@ package org.springframework.boot.micrometer.metrics.testcontainers.otlp; +import org.jspecify.annotations.Nullable; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsConnectionDetails; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -55,7 +57,14 @@ private OpenTelemetryMetricsContainerConnectionDetails(ContainerConnectionSource @Override public String getUrl() { - return "http://%s:%d/v1/metrics".formatted(getContainer().getHost(), getContainer().getMappedPort(4318)); + String scheme = (getSslBundle() != null) ? "https" : "http"; + return "%s://%s:%d/v1/metrics".formatted(scheme, getContainer().getHost(), + getContainer().getMappedPort(4318)); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return super.getSslBundle(); } } diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSenderTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSenderTests.java new file mode 100644 index 000000000000..60c2e911f4e3 --- /dev/null +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSenderTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp; + +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.ipc.http.HttpSender.Response; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +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; +import static org.assertj.core.api.Assertions.assertThatIOException; + +/** + * Tests for {@link JdkClientHttpSender}. + * + * @author Moritz Halbritter + */ +class JdkClientHttpSenderTests { + + private MockWebServer mockWebServer; + + private JdkClientHttpSender sender; + + @BeforeEach + void setUp() throws IOException { + this.mockWebServer = new MockWebServer(); + this.mockWebServer.start(); + this.sender = new JdkClientHttpSender(Duration.ofSeconds(5), Duration.ofSeconds(5), null); + } + + @AfterEach + void tearDown() throws IOException { + this.mockWebServer.shutdown(); + } + + @Test + void sendShouldSendGetRequest() throws Throwable { + this.mockWebServer.enqueue(new MockResponse().setBody("response-body").setResponseCode(200)); + String url = this.mockWebServer.url("/test").toString(); + Response response = this.sender.get(url).send(); + assertThat(response.code()).isEqualTo(200); + assertThat(response.body()).isEqualTo("response-body"); + RecordedRequest request = this.mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()).isEqualTo("/test"); + } + + @Test + void sendShouldSendPostRequest() throws Throwable { + this.mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + String url = this.mockWebServer.url("/test").toString(); + Response response = this.sender.post(url).withJsonContent("{\"key\":\"value\"}").send(); + assertThat(response.code()).isEqualTo(200); + RecordedRequest request = this.mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getHeader("Content-Type")).isEqualTo("application/json"); + assertThat(request.getBody().readUtf8()).isEqualTo("{\"key\":\"value\"}"); + } + + @Test + void sendShouldIncludeHeaders() throws Throwable { + this.mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + String url = this.mockWebServer.url("/test").toString(); + this.sender.post(url).withHeader("X-Custom", "custom-value").send(); + RecordedRequest request = this.mockWebServer.takeRequest(); + assertThat(request.getHeader("X-Custom")).isEqualTo("custom-value"); + } + + @Test + void sendShouldHandleServerError() throws Throwable { + this.mockWebServer.enqueue(new MockResponse().setResponseCode(500).setBody("error")); + String url = this.mockWebServer.url("/test").toString(); + Response response = this.sender.get(url).send(); + assertThat(response.code()).isEqualTo(500); + assertThat(response.body()).isEqualTo("error"); + } + + @Test + void sendShouldTimeoutOnSlowResponse() { + JdkClientHttpSender sender = new JdkClientHttpSender(Duration.ofSeconds(5), Duration.ofMillis(10), null); + this.mockWebServer.enqueue(new MockResponse().setResponseCode(200).setHeadersDelay(500, TimeUnit.MILLISECONDS)); + String url = this.mockWebServer.url("/test").toString(); + assertThatIOException().isThrownBy(() -> sender.get(url).send()).withMessageContaining("timed out"); + } + + @Test + void sendShouldSendPutRequest() throws Throwable { + this.mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + String url = this.mockWebServer.url("/test").toString(); + this.sender.put(url).withPlainText("data").send(); + RecordedRequest request = this.mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("PUT"); + assertThat(request.getBody().readUtf8()).isEqualTo("data"); + } + + @Test + void sendShouldSendDeleteRequest() throws Throwable { + this.mockWebServer.enqueue(new MockResponse().setResponseCode(204)); + String url = this.mockWebServer.url("/test").toString(); + Response response = this.sender.delete(url).send(); + assertThat(response.code()).isEqualTo(204); + RecordedRequest request = this.mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("DELETE"); + } + +} diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java index 95f5e002660a..d4b011fadf04 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java @@ -16,11 +16,16 @@ package org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp; +import java.net.http.HttpClient; +import java.security.NoSuchAlgorithmException; import java.util.concurrent.ScheduledExecutorService; +import javax.net.ssl.SSLContext; + import io.micrometer.core.instrument.Clock; import io.micrometer.registry.otlp.ExemplarContextProvider; import io.micrometer.registry.otlp.OtlpConfig; +import io.micrometer.registry.otlp.OtlpHttpMetricsSender; import io.micrometer.registry.otlp.OtlpMeterRegistry; import io.micrometer.registry.otlp.OtlpMetricsSender; import org.junit.jupiter.api.Test; @@ -28,16 +33,23 @@ import org.junit.jupiter.api.condition.JRE; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsExportAutoConfiguration.PropertiesOtlpMetricsConnectionDetails; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslOptions; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.testsupport.assertj.ScheduledExecutorServiceAssert; +import org.springframework.boot.testsupport.classpath.resources.WithPackageResources; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Tests for {@link OtlpMetricsExportAutoConfiguration}. @@ -140,7 +152,10 @@ void testConnectionFactoryWithOverridesWhenUsingCustomConnectionDetails() { @Test void allowsCustomMetricsSenderToBeUsed() { this.contextRunner.withUserConfiguration(BaseConfiguration.class, CustomMetricsSenderConfiguration.class) - .run(this::assertHasCustomMetricsSender); + .run((context) -> { + assertHasCustomMetricsSender(context); + assertThat(context).doesNotHaveBean(OtlpHttpMetricsSender.class); + }); } @Test @@ -184,6 +199,81 @@ void shouldBackOffIfSpringBootOpenTelemetryIsMissing() { .run((context) -> assertThat(context).doesNotHaveBean(OtlpMetricsExportAutoConfiguration.class)); } + @Test + void autoConfiguresOtlpHttpMetricsSender() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(OtlpHttpMetricsSender.class)); + } + + @Test + void whenNoSslBundleConfiguredConnectionDetailsReturnsNull() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(OtlpMetricsConnectionDetails.class); + OtlpMetricsConnectionDetails connectionDetails = context.getBean(OtlpMetricsConnectionDetails.class); + assertThat(connectionDetails.getSslBundle()).isNull(); + }); + } + + @Test + void whenNoSslBundleDefaultHttpSenderHasDefaultSslContext() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpMetricsSender.class); + OtlpHttpMetricsSender metricsSender = context.getBean(OtlpHttpMetricsSender.class); + HttpClient httpClient = extractHttpClient(metricsSender); + assertThat(httpClient.sslContext()).isSameAs(SSLContext.getDefault()); + }); + } + + @Test + @WithPackageResources("test.jks") + void whenHasSslBundleConfiguresSslOnHttpSender() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class)) + .withPropertyValues("management.otlp.metrics.export.ssl.bundle=mybundle", + "spring.ssl.bundle.jks.mybundle.truststore.location=classpath:test.jks") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpMetricsSender.class); + OtlpHttpMetricsSender metricsSender = context.getBean(OtlpHttpMetricsSender.class); + HttpClient httpClient = extractHttpClient(metricsSender); + assertThat(httpClient.sslContext()).isNotSameAs(SSLContext.getDefault()); + }); + } + + @Test + void whenCustomConnectionDetailsProvidesSslBundleConfiguresSslOnHttpSender() throws NoSuchAlgorithmException { + SSLContext customSslContext = SSLContext.getInstance("TLS"); + SslBundle sslBundle = mock(SslBundle.class); + given(sslBundle.createSslContext()).willReturn(customSslContext); + given(sslBundle.getOptions()).willReturn(SslOptions.NONE); + OtlpMetricsConnectionDetails connectionDetails = new OtlpMetricsConnectionDetails() { + @Override + public String getUrl() { + return "https://localhost:4318/v1/metrics"; + } + + @Override + public SslBundle getSslBundle() { + return sslBundle; + } + }; + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withBean("customOtlpMetricsConnectionDetails", OtlpMetricsConnectionDetails.class, () -> connectionDetails) + .run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpMetricsSender.class); + OtlpHttpMetricsSender metricsSender = context.getBean(OtlpHttpMetricsSender.class); + HttpClient httpClient = extractHttpClient(metricsSender); + assertThat(httpClient.sslContext()).isSameAs(customSslContext); + }); + } + + private HttpClient extractHttpClient(OtlpHttpMetricsSender metricsSender) { + Object field = ReflectionTestUtils.getField(metricsSender, "httpSender"); + assertThat(field).isNotNull(); + Object httpClient = ReflectionTestUtils.getField(field, "httpClient"); + assertThat(httpClient).isNotNull(); + return (HttpClient) httpClient; + } + private void assertHasCustomMetricsSender(AssertableApplicationContext context) { assertThat(context).hasSingleBean(OtlpMeterRegistry.class); OtlpMeterRegistry registry = context.getBean(OtlpMeterRegistry.class); diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java index 1ab506cb9e3e..e78e7ddbd45b 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java @@ -55,7 +55,7 @@ void setUp() { this.properties = new OtlpMetricsProperties(); this.openTelemetryProperties = new OpenTelemetryProperties(); this.environment = new MockEnvironment(); - this.connectionDetails = new PropertiesOtlpMetricsConnectionDetails(this.properties); + this.connectionDetails = new PropertiesOtlpMetricsConnectionDetails(this.properties, null); } @Test diff --git a/module/spring-boot-micrometer-metrics/src/test/resources/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/test.jks b/module/spring-boot-micrometer-metrics/src/test/resources/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/test.jks new file mode 100644 index 0000000000000000000000000000000000000000..0fc3e802f75461dd074facb9611d350db4d5960f GIT binary patch literal 1276 zcmezO_TO6u1_mZ5W@O+hNi8nXP0YzmEM{O}OjTL=XFE`?-k{cikBv*4jgf^>i%F1? zk(GfZ`?F{4vBFug6<%MmmXJ+)mEgQoslXV6!5_H^yana6V1?kw1w zbE0Bi&GHlLHzfosgjrwL)qKcc5I;l0LF?W2lpQg%-cHp$l(#o)?Jkath1@gQN{hG8 zig@wKv#0R7vd_QC=JG%%Ffy=4=$RT=0v*d`(8R=M(8RcU0W%XL6BCP-)w&Y~JZv0V zZ64=rS(uqv84M~6g$xAPm_u3EggJBalM{0?@{3DgVjNh+*s+LlVG-lTBF2m)W*{fd zYiMC$VQ64zW@K(?5e4L0B5?=MWswHLZ0z7LVq$~_7BeF|vl9agPmO-znfkD()@R+bGrT$(AXmN24#zv*XRX z#$UNu(Lmln78u;Jd@N!tBKmU@J0!OJc3G%!N>OO@P1n+F-CorAVRmOQaA8six!iWP z)M3lXpnJ*TI=kIlH(Yxia-ls?xvctEx&P5B6()tKm`>%bo~@fX9{j%TtMU1G!|pw& zZ6BRjIqQ^`bIxR@OmMno&8^H%tpq36Esh&T(+MJ_la+#pV>+3scS% Date: Fri, 13 Mar 2026 15:07:11 +0100 Subject: [PATCH 334/930] Rename management.tracing.exemplars.filter to management.tracing.exemplars.include See gh-49572 --- .../reference/pages/actuator/metrics.adoc | 10 ++++---- .../OtlpExemplarsAutoConfigurationTests.java | 8 +++--- ...etheusExemplarsAutoConfigurationTests.java | 10 ++++---- .../autoconfigure/TracingProperties.java | 25 +++++++++---------- .../otlp/OtlpExemplarsAutoConfiguration.java | 16 ++++++------ .../PrometheusExemplarsAutoConfiguration.java | 23 +++++++++-------- ...zyTracingExemplarContextProviderTests.java | 16 ++++++------ .../LazyTracingSpanContextTests.java | 13 +++++----- 8 files changed, 61 insertions(+), 60 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc index a7172e80c781..563acd583377 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc @@ -530,8 +530,8 @@ If an `OtlpMetricsSender` bean is available, it will be configured on the `OtlpM https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exemplars[OTLP Exemplars] are also supported. To enable this feature, an javadoc:io.micrometer.registry.otlp.ExemplarContextProvider[] bean should be present. If you use xref:actuator/tracing.adoc[Micrometer Tracing], this will be auto-configured for you. -By default, only sampled traces are selected as exemplars. -You can control this behavior using the configprop:management.tracing.exemplars.filter[] property. +By default, only sampled traces are included as exemplars. +You can control this behavior using the configprop:management.tracing.exemplars.include[] property. @@ -558,9 +558,9 @@ https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage[Pr To enable this feature, a javadoc:io.prometheus.metrics.tracer.common.SpanContext[] bean should be present. If you're using the deprecated Prometheus simpleclient support and want to enable that feature, a javadoc:io.prometheus.client.exemplars.tracer.common.SpanContextSupplier[] bean should be present. If you use {url-micrometer-tracing-docs}[Micrometer Tracing], this will be auto-configured for you, but you can always create your own if you want. -By default, only sampled traces are selected as exemplars. -You can control this behavior using the configprop:management.tracing.exemplars.filter[] property. -The value `always-on` is not supported with Prometheus. +By default, only sampled traces are included as exemplars. +You can control this behavior using the configprop:management.tracing.exemplars.include[] property. +The value `all` is not supported with Prometheus. Please check the https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage[Prometheus Docs], since this feature needs to be explicitly enabled on Prometheus' side, and it is only supported using the https://github.com/OpenObservability/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#exemplars[OpenMetrics] format. For ephemeral or batch jobs that may not exist long enough to be scraped, you can use https://github.com/prometheus/pushgateway[Prometheus Pushgateway] support to expose the metrics to Prometheus. diff --git a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java index a963d0c2edeb..45ae6f424807 100644 --- a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java @@ -95,10 +95,10 @@ void otlpOutputShouldContainExemplars() { } @Test - void otlpOutputShouldContainExemplarsWhenFilterIsAlwaysOnAndSpanIsNotSampled() { + void otlpOutputShouldContainExemplarsWhenIncludeIsAllAndSpanIsNotSampled() { this.contextRunner.withUserConfiguration(TracingConfiguration.class) .withPropertyValues("management.tracing.sampling.probability=0.0", - "management.tracing.exemplars.filter=always-on") + "management.tracing.exemplars.include=all") .run((context) -> { assertThat(context).hasSingleBean(ExemplarContextProvider.class); ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); @@ -114,9 +114,9 @@ void otlpOutputShouldContainExemplarsWhenFilterIsAlwaysOnAndSpanIsNotSampled() { } @Test - void otlpOutputShouldNotContainExemplarsWhenFilterIsAlwaysOff() { + void otlpOutputShouldNotContainExemplarsWhenIncludeIsNone() { this.contextRunner.withUserConfiguration(TracingConfiguration.class) - .withPropertyValues("management.tracing.exemplars.filter=always-off") + .withPropertyValues("management.tracing.exemplars.include=none") .run((context) -> { assertThat(context).hasSingleBean(ExemplarContextProvider.class); ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); diff --git a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/PrometheusExemplarsAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/PrometheusExemplarsAutoConfigurationTests.java index 652ec872e598..b04e55cfeca6 100644 --- a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/PrometheusExemplarsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/PrometheusExemplarsAutoConfigurationTests.java @@ -150,21 +150,21 @@ void prometheusOpenMetricsOutputShouldContainExemplars() { } @Test - void shouldFailWhenFilterIsAlwaysOn() { + void shouldFailWhenIncludeIsAll() { this.contextRunner.withUserConfiguration(TracingConfiguration.class) - .withPropertyValues("management.tracing.exemplars.filter=always-on") + .withPropertyValues("management.tracing.exemplars.include=all") .run((context) -> assertThat(context).hasFailed() .getFailure() .rootCause() .isInstanceOf(InvalidConfigurationPropertyValueException.class) .hasMessageContaining( - "Property management.tracing.exemplars.filter with value 'always-on' is invalid: Prometheus doesn't support the 'always-on' exemplar filter.")); + "Property management.tracing.exemplars.include with value 'all' is invalid: Prometheus doesn't support including 'all' traces as exemplars.")); } @Test - void prometheusOpenMetricsOutputShouldNotContainExemplarsWhenFilterIsAlwaysOff() { + void prometheusOpenMetricsOutputShouldNotContainExemplarsWhenIncludeIsNone() { this.contextRunner.withUserConfiguration(TracingConfiguration.class) - .withPropertyValues("management.tracing.exemplars.filter=always-off") + .withPropertyValues("management.tracing.exemplars.include=none") .run((context) -> { assertThat(context).hasSingleBean(SpanContext.class); ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); diff --git a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/TracingProperties.java b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/TracingProperties.java index 0bda60865f49..b1588f9f054e 100644 --- a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/TracingProperties.java +++ b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/TracingProperties.java @@ -264,33 +264,32 @@ public enum PropagationType { public static class Exemplars { /** - * Filter which exemplars are selected. ALWAYS_ON is not supported when using - * Prometheus. + * Traces to include as exemplars. 'all' is not supported when using Prometheus. */ - private Filter filter = Filter.SAMPLED_TRACES; + private Include include = Include.SAMPLED_TRACES; - public Filter getFilter() { - return this.filter; + public Include getInclude() { + return this.include; } - public void setFilter(Filter filter) { - this.filter = filter; + public void setInclude(Include include) { + this.include = include; } - public enum Filter { + public enum Include { /** - * Always select exemplars, regardless of whether the span is sampled. + * Include all traces as exemplars, regardless of whether the span is sampled. */ - ALWAYS_ON, + ALL, /** - * Never select exemplars. + * Don't include any traces as exemplars. */ - ALWAYS_OFF, + NONE, /** - * Only select exemplars from sampled traces. + * Only include sampled traces as exemplars. */ SAMPLED_TRACES diff --git a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java index bc646102d2b0..2225819a1c8c 100644 --- a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java +++ b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java @@ -32,7 +32,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.micrometer.tracing.autoconfigure.MicrometerTracingAutoConfiguration; import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties; -import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Exemplars.Filter; +import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Exemplars.Include; import org.springframework.context.annotation.Bean; import org.springframework.lang.Contract; import org.springframework.util.function.SingletonSupplier; @@ -57,7 +57,7 @@ public final class OtlpExemplarsAutoConfiguration { @ConditionalOnMissingBean ExemplarContextProvider exemplarContextProvider(ObjectProvider tracerProvider, TracingProperties properties) { - return new LazyTracingExemplarContextProvider(tracerProvider, properties.getExemplars().getFilter()); + return new LazyTracingExemplarContextProvider(tracerProvider, properties.getExemplars().getInclude()); } /** @@ -70,11 +70,11 @@ static class LazyTracingExemplarContextProvider implements ExemplarContextProvid private final SingletonSupplier tracer; - private final Filter filter; + private final Include include; - LazyTracingExemplarContextProvider(ObjectProvider tracerProvider, Filter filter) { + LazyTracingExemplarContextProvider(ObjectProvider tracerProvider, Include include) { this.tracer = SingletonSupplier.of(tracerProvider::getObject); - this.filter = filter; + this.include = include; } @Override @@ -92,9 +92,9 @@ private boolean isExemplar(@Nullable Span span) { if (span == null) { return false; } - return switch (this.filter) { - case ALWAYS_ON -> true; - case ALWAYS_OFF -> false; + return switch (this.include) { + case ALL -> true; + case NONE -> false; case SAMPLED_TRACES -> isSampled(span); }; } diff --git a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfiguration.java b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfiguration.java index 8b2164d78809..7181f68b9ce4 100644 --- a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfiguration.java +++ b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfiguration.java @@ -31,7 +31,7 @@ import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.boot.micrometer.tracing.autoconfigure.MicrometerTracingAutoConfiguration; import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties; -import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Exemplars.Filter; +import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Exemplars.Include; import org.springframework.context.annotation.Bean; import org.springframework.util.function.SingletonSupplier; @@ -54,7 +54,7 @@ public final class PrometheusExemplarsAutoConfiguration { @Bean @ConditionalOnMissingBean SpanContext spanContext(ObjectProvider tracerProvider, TracingProperties properties) { - return new LazyTracingSpanContext(tracerProvider, properties.getExemplars().getFilter()); + return new LazyTracingSpanContext(tracerProvider, properties.getExemplars().getInclude()); } /** @@ -66,15 +66,15 @@ static class LazyTracingSpanContext implements SpanContext { private final SingletonSupplier tracer; - private final Filter filter; + private final Include include; - LazyTracingSpanContext(ObjectProvider tracerProvider, Filter filter) { - if (filter == Filter.ALWAYS_ON) { - throw new InvalidConfigurationPropertyValueException("management.tracing.exemplars.filter", "always-on", - "Prometheus doesn't support the 'always-on' exemplar filter."); + LazyTracingSpanContext(ObjectProvider tracerProvider, Include include) { + if (include == Include.ALL) { + throw new InvalidConfigurationPropertyValueException("management.tracing.exemplars.include", "all", + "Prometheus doesn't support including 'all' traces as exemplars."); } this.tracer = SingletonSupplier.of(tracerProvider::getObject); - this.filter = filter; + this.include = include; } @Override @@ -95,9 +95,10 @@ public boolean isCurrentSpanSampled() { if (currentSpan == null) { return false; } - return switch (this.filter) { - case ALWAYS_ON -> throw new UnsupportedOperationException("ALWAYS_ON filter is not supported"); - case ALWAYS_OFF -> false; + return switch (this.include) { + case ALL -> + throw new UnsupportedOperationException("Including 'all' traces as exemplars is not supported"); + case NONE -> false; case SAMPLED_TRACES -> isSampled(currentSpan); }; } diff --git a/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/LazyTracingExemplarContextProviderTests.java b/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/LazyTracingExemplarContextProviderTests.java index fbeba665762d..84ddfd32ee04 100644 --- a/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/LazyTracingExemplarContextProviderTests.java +++ b/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/LazyTracingExemplarContextProviderTests.java @@ -24,7 +24,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Exemplars.Filter; +import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Exemplars.Include; import org.springframework.boot.micrometer.tracing.autoconfigure.otlp.OtlpExemplarsAutoConfiguration.LazyTracingExemplarContextProvider; import static org.assertj.core.api.Assertions.assertThat; @@ -64,7 +64,7 @@ public Tracer getIfUnique() throws BeansException { }; private LazyTracingExemplarContextProvider contextProvider = new LazyTracingExemplarContextProvider( - this.objectProvider, Filter.SAMPLED_TRACES); + this.objectProvider, Include.SAMPLED_TRACES); @Test void whenCurrentSpanIsNullThenExemplarContextIsNull() { @@ -148,8 +148,8 @@ void whenCurrentSpanHasDeferredSamplingThenExemplarContextIsNull() { } @Test - void whenFilterIsAlwaysOnAndSpanIsNotSampledThenExemplarContextIsNotNull() { - this.contextProvider = new LazyTracingExemplarContextProvider(this.objectProvider, Filter.ALWAYS_ON); + void whenIncludeIsAllAndSpanIsNotSampledThenExemplarContextIsNotNull() { + this.contextProvider = new LazyTracingExemplarContextProvider(this.objectProvider, Include.ALL); Span span = mock(Span.class); given(this.tracer.currentSpan()).willReturn(span); TraceContext traceContext = mock(TraceContext.class); @@ -159,14 +159,14 @@ void whenFilterIsAlwaysOnAndSpanIsNotSampledThenExemplarContextIsNotNull() { } @Test - void whenFilterIsAlwaysOnAndCurrentSpanIsNullThenExemplarContextIsNull() { - this.contextProvider = new LazyTracingExemplarContextProvider(this.objectProvider, Filter.ALWAYS_ON); + void whenIncludeIsAllAndCurrentSpanIsNullThenExemplarContextIsNull() { + this.contextProvider = new LazyTracingExemplarContextProvider(this.objectProvider, Include.ALL); assertThat(this.contextProvider.getExemplarContext()).isNull(); } @Test - void whenFilterIsAlwaysOffAndSpanIsSampledThenExemplarContextIsNull() { - this.contextProvider = new LazyTracingExemplarContextProvider(this.objectProvider, Filter.ALWAYS_OFF); + void whenIncludeIsNoneAndSpanIsSampledThenExemplarContextIsNull() { + this.contextProvider = new LazyTracingExemplarContextProvider(this.objectProvider, Include.NONE); Span span = mock(Span.class); given(this.tracer.currentSpan()).willReturn(span); TraceContext traceContext = mock(TraceContext.class); diff --git a/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/LazyTracingSpanContextTests.java b/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/LazyTracingSpanContextTests.java index 870b8c460313..d8628ff2e00e 100644 --- a/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/LazyTracingSpanContextTests.java +++ b/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/LazyTracingSpanContextTests.java @@ -25,7 +25,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; -import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Exemplars.Filter; +import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Exemplars.Include; import org.springframework.boot.micrometer.tracing.autoconfigure.prometheus.PrometheusExemplarsAutoConfiguration.LazyTracingSpanContext; import static org.assertj.core.api.Assertions.assertThat; @@ -66,7 +66,8 @@ public Tracer getIfUnique() throws BeansException { }; - private LazyTracingSpanContext spanContext = new LazyTracingSpanContext(this.objectProvider, Filter.SAMPLED_TRACES); + private LazyTracingSpanContext spanContext = new LazyTracingSpanContext(this.objectProvider, + Include.SAMPLED_TRACES); @Test void whenCurrentSpanIsNullThenSpanIdIsNull() { @@ -152,14 +153,14 @@ void whenCurrentSpanHasDeferredSamplingThenSampledIsFalse() { } @Test - void whenFilterIsAlwaysOnThenConstructorThrows() { + void whenIncludeIsAllThenConstructorThrows() { assertThatExceptionOfType(InvalidConfigurationPropertyValueException.class) - .isThrownBy(() -> new LazyTracingSpanContext(this.objectProvider, Filter.ALWAYS_ON)); + .isThrownBy(() -> new LazyTracingSpanContext(this.objectProvider, Include.ALL)); } @Test - void whenFilterIsAlwaysOffAndSpanIsSampledThenSampledIsFalse() { - this.spanContext = new LazyTracingSpanContext(this.objectProvider, Filter.ALWAYS_OFF); + void whenIncludeIsNoneAndSpanIsSampledThenSampledIsFalse() { + this.spanContext = new LazyTracingSpanContext(this.objectProvider, Include.NONE); Span span = mock(Span.class); given(this.tracer.currentSpan()).willReturn(span); TraceContext traceContext = mock(TraceContext.class); From 9f872bdde95f41204c6bf6f277f13269e9e7b309 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 13 Mar 2026 15:34:13 +0100 Subject: [PATCH 335/930] Fix OtlpMetricsExportAutoConfigurationTests on JDK 26 --- .../export/otlp/OtlpMetricsExportAutoConfigurationTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java index d4b011fadf04..f4c267b8f19e 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java @@ -17,7 +17,6 @@ package org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp; import java.net.http.HttpClient; -import java.security.NoSuchAlgorithmException; import java.util.concurrent.ScheduledExecutorService; import javax.net.ssl.SSLContext; @@ -240,8 +239,9 @@ void whenHasSslBundleConfiguresSslOnHttpSender() { } @Test - void whenCustomConnectionDetailsProvidesSslBundleConfiguresSslOnHttpSender() throws NoSuchAlgorithmException { + void whenCustomConnectionDetailsProvidesSslBundleConfiguresSslOnHttpSender() throws Exception { SSLContext customSslContext = SSLContext.getInstance("TLS"); + customSslContext.init(null, null, null); SslBundle sslBundle = mock(SslBundle.class); given(sslBundle.createSslContext()).willReturn(customSslContext); given(sslBundle.getOptions()).willReturn(SslOptions.NONE); From 43e0aac11d54650127c9115221634b95fd6e6c6a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Mar 2026 13:13:41 +0000 Subject: [PATCH 336/930] Upgrade to MSSQL JDBC 13.4.0.jre11 Closes gh-49588 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 34855cb4530c..6714cf19b1f8 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1649,7 +1649,7 @@ bom { releaseNotes("https://github.com/mongodb/mongo-java-driver/releases/tag/r{version}") } } - library("MSSQL JDBC", "13.2.1.jre11") { + library("MSSQL JDBC", "13.4.0.jre11") { prohibit { endsWith(".jre8") because "we want to use the jre11 version" From ce19630ab9c0e4ab0d1a18b4dc0f5e1e48497aa7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Mar 2026 13:13:41 +0000 Subject: [PATCH 337/930] Upgrade to Spring Framework 7.0.6 Closes gh-49441 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f1558f732efd..b9e7437f1f59 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ mockitoVersion=5.23.0 nativeBuildToolsVersion=0.11.5 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.6 -springFrameworkVersion=7.0.6-SNAPSHOT +springFrameworkVersion=7.0.6 springFramework60xVersion=6.0.23 tomcatVersion=11.0.18 From 91edda999e301a34982fc6b176f170edee9c4b0b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Mar 2026 13:13:46 +0000 Subject: [PATCH 338/930] Upgrade to Spring HATEOAS 3.1.0-M1 Closes gh-49589 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6714cf19b1f8..8f467f1cb5af 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2505,7 +2505,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-graphql/releases/tag/v{version}") } } - library("Spring HATEOAS", "3.0.2") { + library("Spring HATEOAS", "3.1.0-M1") { considerSnapshots() group("org.springframework.hateoas") { modules = [ From 8e5ae847962955c9ccf5c6d84c3270eafa56b350 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Mar 2026 17:20:11 +0000 Subject: [PATCH 339/930] Adapt to nullability changes in spring-security-oauth2-jose See gh-49446 --- ...th2AuthorizationServerPropertiesMapper.java | 6 +++++- ...izationServerJwtAutoConfigurationTests.java | 3 ++- .../autoconfigure/JwtDecoderConfiguration.java | 18 ++++++++++++++---- .../ReactiveJwtDecoderConfiguration.java | 17 ++++++++++++----- ...nverterCustomizationsArgumentsProvider.java | 4 ++-- ...h2ResourceServerAutoConfigurationTests.java | 4 +++- 6 files changed, 38 insertions(+), 14 deletions(-) diff --git a/module/spring-boot-security-oauth2-authorization-server/src/main/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerPropertiesMapper.java b/module/spring-boot-security-oauth2-authorization-server/src/main/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerPropertiesMapper.java index c52de010a068..df5b2a12b8e1 100644 --- a/module/spring-boot-security-oauth2-authorization-server/src/main/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerPropertiesMapper.java +++ b/module/spring-boot-security-oauth2-authorization-server/src/main/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerPropertiesMapper.java @@ -33,6 +33,7 @@ import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; +import org.springframework.util.Assert; /** * Maps {@link OAuth2AuthorizationServerProperties} to Authorization Server types. @@ -132,11 +133,14 @@ private JwsAlgorithm jwsAlgorithm(String signingAlgorithm) { if (jwsAlgorithm == null) { jwsAlgorithm = MacAlgorithm.from(name); } + Assert.notNull(jwsAlgorithm, "JWS algorithm " + name + " is unknown"); return jwsAlgorithm; } private SignatureAlgorithm signatureAlgorithm(String signatureAlgorithm) { - return SignatureAlgorithm.from(signatureAlgorithm.toUpperCase(Locale.ROOT)); + SignatureAlgorithm algorithm = SignatureAlgorithm.from(signatureAlgorithm.toUpperCase(Locale.ROOT)); + Assert.notNull(algorithm, "Signature algorithm " + signatureAlgorithm + " is unknown"); + return algorithm; } } diff --git a/module/spring-boot-security-oauth2-authorization-server/src/test/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerJwtAutoConfigurationTests.java b/module/spring-boot-security-oauth2-authorization-server/src/test/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerJwtAutoConfigurationTests.java index d871ba119266..3f7960d9734b 100644 --- a/module/spring-boot-security-oauth2-authorization-server/src/test/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerJwtAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-authorization-server/src/test/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerJwtAutoConfigurationTests.java @@ -31,6 +31,7 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link OAuth2AuthorizationServerJwtAutoConfiguration}. @@ -96,7 +97,7 @@ static class TestJwtDecoderConfiguration { @Bean JwtDecoder jwtDecoder() { - return (token) -> null; + return mock(JwtDecoder.class); } } diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtDecoderConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtDecoderConfiguration.java index 0571e3ef4312..4e2d3fee621e 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtDecoderConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtDecoderConfiguration.java @@ -31,6 +31,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.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.core.OAuth2TokenValidator; @@ -47,6 +48,7 @@ import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; /** * {@link Configuration @Configuration} for JWT decoder beans. @@ -82,7 +84,7 @@ class JwtDecoderConfiguration { @ConditionalOnPublicKeyJwtDecoder JwtDecoder jwtDecoderByPublicKeyValue() throws Exception { PublicKeyJwtDecoderBuilder builder = NimbusJwtDecoder.withPublicKey(getReadPublicKey()); - builder.signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())); + builder.signatureAlgorithm(exactlyOneAlgorithm()); NimbusJwtDecoder decoder = builder.build(); decoder.setJwtValidator(getValidator()); return decoder; @@ -98,18 +100,26 @@ private byte[] decodeKeyProperty(String value) { .decode(value.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "")); } - private String exactlyOneAlgorithm() { + private SignatureAlgorithm exactlyOneAlgorithm() { List algorithms = this.properties.getJwsAlgorithms(); Assert.state(algorithms != null && algorithms.size() == 1, () -> "Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + algorithms.size() + " were configured"); - return algorithms.get(0); + SignatureAlgorithm algorithm = SignatureAlgorithm.from(algorithms.get(0)); + if (algorithm == null) { + throw new InvalidConfigurationPropertyValueException( + "spring.security.oauth2.resourceserver.jwt.jws-algorithms", + StringUtils.collectionToCommaDelimitedString(algorithms), "Unknown algorithm"); + } + return algorithm; } @Bean @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") JwtDecoder jwtDecoderByJwkKeySetUri() { - JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri()); + String jwkSetUri = this.properties.getJwkSetUri(); + Assert.state(jwkSetUri != null, "No JWK Set URI property specified"); + JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri); builder.jwsAlgorithms(this::jwsAlgorithms); return buildJwkSetUriJwtDecoder(builder); } diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtDecoderConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtDecoderConfiguration.java index 50110a4b954c..28e0eca5f930 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtDecoderConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtDecoderConfiguration.java @@ -33,6 +33,7 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.boot.security.oauth2.server.resource.autoconfigure.ConditionalOnIssuerLocationJwtDecoder; import org.springframework.boot.security.oauth2.server.resource.autoconfigure.ConditionalOnPublicKeyJwtDecoder; import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties; @@ -88,7 +89,7 @@ class ReactiveJwtDecoderConfiguration { NimbusReactiveJwtDecoder reactiveJwtDecoderByPublicKeyValue() throws Exception { RSAPublicKey publicKey = getReadPublicKey(); PublicKeyReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder.withPublicKey(publicKey); - builder.signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())); + builder.signatureAlgorithm(exactlyOneAlgorithm()); NimbusReactiveJwtDecoder decoder = builder.build(); decoder.setJwtValidator(getValidator()); return decoder; @@ -104,19 +105,25 @@ private byte[] decodeKeyProperty(String value) { .decode(value.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "")); } - private String exactlyOneAlgorithm() { + private SignatureAlgorithm exactlyOneAlgorithm() { List algorithms = this.properties.getJwsAlgorithms(); Assert.state(algorithms != null && algorithms.size() == 1, () -> "Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + algorithms.size() + " were configured"); - return algorithms.get(0); + SignatureAlgorithm algorithm = SignatureAlgorithm.from(algorithms.get(0)); + if (algorithm == null) { + throw new InvalidConfigurationPropertyValueException( + "spring.security.oauth2.resourceserver.jwt.jws-algorithms", algorithms.get(0), "Unknown algorithm"); + } + return algorithm; } @Bean @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") ReactiveJwtDecoder reactiveJwtDecoderByJwkKeySetUri() { - JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder - .withJwkSetUri(this.properties.getJwkSetUri()); + String jwkSetUri = this.properties.getJwkSetUri(); + Assert.notNull(jwkSetUri, "No JWK Set URI specified"); + JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri); builder.jwsAlgorithms(this::jwsAlgorithms); return buildJwkSetUriJwtDecoder(builder); } diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterCustomizationsArgumentsProvider.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterCustomizationsArgumentsProvider.java index 1c7c39a362ba..23f56974a46c 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterCustomizationsArgumentsProvider.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterCustomizationsArgumentsProvider.java @@ -72,10 +72,10 @@ public Stream provideArguments(ParameterDeclarations parame .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) + Jwt customAuthoritiesClaimJwt = jwtBuilder.claim("scp", "value") .claim(customAuthoritiesClaim, jwtScopes[0] + " " + jwtScopes[1]) .build(); - Jwt customAuthoritiesClaimAndDelimiterJwt = jwtBuilder.claim("scp", null) + Jwt customAuthoritiesClaimAndDelimiterJwt = jwtBuilder.claim("scp", "value") .claim(customAuthoritiesClaim, jwtScopes[0] + "~" + jwtScopes[1]) .build(); String[] customPrefixAuthorities = { customPrefix + jwtScopes[0], customPrefix + jwtScopes[1] }; diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java index 442fe082524c..0a9f063ab9ee 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java @@ -46,6 +46,7 @@ import tools.jackson.databind.json.JsonMapper; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; @@ -314,7 +315,8 @@ void autoConfigurationShouldFailIfAlgorithmIsInvalid() { "spring.security.oauth2.resourceserver.jwt.jws-algorithms=NOT_VALID") .run((context) -> assertThat(context).hasFailed() .getFailure() - .hasMessageContaining("signatureAlgorithm cannot be null")); + .hasMessageContaining("Unknown algorithm") + .hasRootCauseExactlyInstanceOf(InvalidConfigurationPropertyValueException.class)); } @Test From 5d54a0a145c1fa411598619e42c9f6dc47bd7d6b Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 15 Mar 2026 20:53:05 -0700 Subject: [PATCH 340/930] Upgrade to Elasticsearch Client 9.3.2 Closes gh-49603 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8f467f1cb5af..fdf24a110483 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -401,7 +401,7 @@ bom { releaseNotes("https://github.com/ehcache/ehcache3/releases/tag/v{version}") } } - library("Elasticsearch Client", "9.3.1") { + library("Elasticsearch Client", "9.3.2") { prohibit { contains "-alpha" contains "-beta" From 538e02b5ef6e04749d3f7ae857e8731f75173169 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 16 Mar 2026 09:19:50 +0100 Subject: [PATCH 341/930] Don't register ExemplarContextProvider if exemplars include is 'none' See gh-49572 --- .../OtlpExemplarsAutoConfigurationTests.java | 15 +-- .../otlp/OtlpExemplarsAutoConfiguration.java | 21 ++- .../OtlpExemplarsAutoConfigurationTests.java | 121 ++++++++++++++++++ 3 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfigurationTests.java diff --git a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java index 45ae6f424807..2bd4bb96d49a 100644 --- a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java @@ -114,21 +114,10 @@ void otlpOutputShouldContainExemplarsWhenIncludeIsAllAndSpanIsNotSampled() { } @Test - void otlpOutputShouldNotContainExemplarsWhenIncludeIsNone() { + void shouldNotSupplyExemplarContextProviderWhenIncludeIsNone() { this.contextRunner.withUserConfiguration(TracingConfiguration.class) .withPropertyValues("management.tracing.exemplars.include=none") - .run((context) -> { - assertThat(context).hasSingleBean(ExemplarContextProvider.class); - ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); - Observation.start("test.observation", observationRegistry).stop(); - OtlpMeterRegistry otlpMeterRegistry = context.getBean(OtlpMeterRegistry.class); - TestOtlpMetricsSender metricsSender = context.getBean(TestOtlpMetricsSender.class); - otlpMeterRegistry.close(); - assertThat(metricsSender.getOtlpRequest()).containsOnlyOnce("name: \"test.observation\"") - .doesNotContain("exemplars") - .doesNotContain("span_id") - .doesNotContain("trace_id"); - }); + .run((context) -> assertThat(context).doesNotHaveBean(ExemplarContextProvider.class)); } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java index 2225819a1c8c..3dcaa6c1f1fe 100644 --- a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java +++ b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfiguration.java @@ -29,11 +29,14 @@ 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.NoneNestedConditions; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.micrometer.tracing.autoconfigure.MicrometerTracingAutoConfiguration; import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties; import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Exemplars.Include; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.lang.Contract; import org.springframework.util.function.SingletonSupplier; @@ -54,12 +57,26 @@ public final class OtlpExemplarsAutoConfiguration { @Bean - @ConditionalOnMissingBean - ExemplarContextProvider exemplarContextProvider(ObjectProvider tracerProvider, + @ConditionalOnMissingBean(ExemplarContextProvider.class) + @Conditional(ExemplarContextProviderNotDisabledCondition.class) + LazyTracingExemplarContextProvider exemplarContextProvider(ObjectProvider tracerProvider, TracingProperties properties) { return new LazyTracingExemplarContextProvider(tracerProvider, properties.getExemplars().getInclude()); } + static class ExemplarContextProviderNotDisabledCondition extends NoneNestedConditions { + + ExemplarContextProviderNotDisabledCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(value = "management.tracing.exemplars.include", havingValue = "none") + static class NoneIncluded { + + } + + } + /** * Since the MeterRegistry can depend on the {@link Tracer} (Exemplars) and the * {@link Tracer} can depend on the MeterRegistry (recording metrics), this diff --git a/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfigurationTests.java new file mode 100644 index 000000000000..62d2086a8299 --- /dev/null +++ b/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/otlp/OtlpExemplarsAutoConfigurationTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.micrometer.tracing.autoconfigure.otlp; + +import io.micrometer.registry.otlp.ExemplarContextProvider; +import io.micrometer.tracing.Tracer; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.micrometer.tracing.autoconfigure.otlp.OtlpExemplarsAutoConfiguration.LazyTracingExemplarContextProvider; +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 OtlpExemplarsAutoConfiguration}. + * + * @author Moritz Halbritter + */ +class OtlpExemplarsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OtlpExemplarsAutoConfiguration.class)); + + @Test + void shouldSupplyExemplarContextProvider() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ExemplarContextProvider.class); + assertThat(context).hasSingleBean(LazyTracingExemplarContextProvider.class); + }); + } + + @Test + void shouldBackOffOnCustomExemplarContextProvider() { + this.contextRunner + .withUserConfiguration(TracerConfiguration.class, CustomExemplarContextProviderConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(ExemplarContextProvider.class); + assertThat(context).doesNotHaveBean(LazyTracingExemplarContextProvider.class); + assertThat(context).hasBean("customExemplarContextProvider"); + }); + } + + @Test + void shouldNotSupplyExemplarContextProviderWhenTracerBeanIsMissing() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ExemplarContextProvider.class)); + } + + @Test + void shouldNotSupplyExemplarContextProviderWhenTracerClassIsMissing() { + this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class)) + .run((context) -> assertThat(context).doesNotHaveBean(ExemplarContextProvider.class)); + } + + @Test + void shouldNotSupplyExemplarContextProviderWhenExemplarContextProviderClassIsMissing() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class) + .withClassLoader(new FilteredClassLoader(ExemplarContextProvider.class)) + .run((context) -> assertThat(context).doesNotHaveBean(ExemplarContextProvider.class)); + } + + @Test + void shouldNotSupplyExemplarContextProviderWhenIncludeIsNone() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class) + .withPropertyValues("management.tracing.exemplars.include=none") + .run((context) -> assertThat(context).doesNotHaveBean(ExemplarContextProvider.class)); + } + + @Test + void shouldSupplyExemplarContextProviderWhenIncludeIsSampledTraces() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class) + .withPropertyValues("management.tracing.exemplars.include=sampled-traces") + .run((context) -> assertThat(context).hasSingleBean(ExemplarContextProvider.class)); + } + + @Test + void shouldSupplyExemplarContextProviderWhenIncludeIsAll() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class) + .withPropertyValues("management.tracing.exemplars.include=all") + .run((context) -> assertThat(context).hasSingleBean(ExemplarContextProvider.class)); + } + + @Configuration(proxyBeanMethods = false) + private static final class TracerConfiguration { + + @Bean + Tracer tracer() { + return mock(Tracer.class); + } + + } + + @Configuration(proxyBeanMethods = false) + private static final class CustomExemplarContextProviderConfiguration { + + @Bean + ExemplarContextProvider customExemplarContextProvider() { + return mock(ExemplarContextProvider.class); + } + + } + +} From da63bf608520f1b952c7bd600ba5e3723a9c5234 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 16 Mar 2026 10:26:42 +0100 Subject: [PATCH 342/930] Don't register SpanContext if exemplars include is 'none' See gh-49572 --- ...etheusExemplarsAutoConfigurationTests.java | 12 +- .../PrometheusExemplarsAutoConfiguration.java | 21 ++- ...etheusExemplarsAutoConfigurationTests.java | 126 ++++++++++++++++++ 3 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfigurationTests.java diff --git a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/PrometheusExemplarsAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/PrometheusExemplarsAutoConfigurationTests.java index b04e55cfeca6..6d16bcfe1dff 100644 --- a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/PrometheusExemplarsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/PrometheusExemplarsAutoConfigurationTests.java @@ -162,18 +162,10 @@ void shouldFailWhenIncludeIsAll() { } @Test - void prometheusOpenMetricsOutputShouldNotContainExemplarsWhenIncludeIsNone() { + void shouldNotSupplySpanContextWhenIncludeIsNone() { this.contextRunner.withUserConfiguration(TracingConfiguration.class) .withPropertyValues("management.tracing.exemplars.include=none") - .run((context) -> { - assertThat(context).hasSingleBean(SpanContext.class); - ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); - Observation.start("test.observation", observationRegistry).stop(); - PrometheusMeterRegistry prometheusMeterRegistry = context.getBean(PrometheusMeterRegistry.class); - String openMetricsOutput = prometheusMeterRegistry.scrape(OpenMetricsTextFormatWriter.CONTENT_TYPE); - assertThat(openMetricsOutput).contains("test_observation_seconds_bucket"); - assertThat(openMetricsOutput).doesNotContain("span_id").doesNotContain("trace_id"); - }); + .run((context) -> assertThat(context).doesNotHaveBean(SpanContext.class)); } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfiguration.java b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfiguration.java index 7181f68b9ce4..696564eecdc8 100644 --- a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfiguration.java +++ b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfiguration.java @@ -27,12 +27,15 @@ 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.NoneNestedConditions; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.boot.micrometer.tracing.autoconfigure.MicrometerTracingAutoConfiguration; import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties; import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Exemplars.Include; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.util.function.SingletonSupplier; /** @@ -52,11 +55,25 @@ public final class PrometheusExemplarsAutoConfiguration { @Bean - @ConditionalOnMissingBean - SpanContext spanContext(ObjectProvider tracerProvider, TracingProperties properties) { + @ConditionalOnMissingBean(SpanContext.class) + @Conditional(SpanContextNotDisabledCondition.class) + LazyTracingSpanContext spanContext(ObjectProvider tracerProvider, TracingProperties properties) { return new LazyTracingSpanContext(tracerProvider, properties.getExemplars().getInclude()); } + static class SpanContextNotDisabledCondition extends NoneNestedConditions { + + SpanContextNotDisabledCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(value = "management.tracing.exemplars.include", havingValue = "none") + static class NoneIncluded { + + } + + } + /** * Since the MeterRegistry can depend on the {@link Tracer} (Exemplars) and the * {@link Tracer} can depend on the MeterRegistry (recording metrics), this diff --git a/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfigurationTests.java new file mode 100644 index 000000000000..862e96bb5772 --- /dev/null +++ b/module/spring-boot-micrometer-tracing/src/test/java/org/springframework/boot/micrometer/tracing/autoconfigure/prometheus/PrometheusExemplarsAutoConfigurationTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.micrometer.tracing.autoconfigure.prometheus; + +import io.micrometer.tracing.Tracer; +import io.prometheus.metrics.tracer.common.SpanContext; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.boot.micrometer.tracing.autoconfigure.prometheus.PrometheusExemplarsAutoConfiguration.LazyTracingSpanContext; +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 PrometheusExemplarsAutoConfiguration}. + * + * @author Moritz Halbritter + */ +class PrometheusExemplarsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(PrometheusExemplarsAutoConfiguration.class)); + + @Test + void shouldSupplySpanContext() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(SpanContext.class); + assertThat(context).hasSingleBean(LazyTracingSpanContext.class); + }); + } + + @Test + void shouldBackOffOnCustomSpanContext() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class, CustomSpanContextConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(SpanContext.class); + assertThat(context).doesNotHaveBean(LazyTracingSpanContext.class); + assertThat(context).hasBean("customSpanContext"); + }); + } + + @Test + void shouldNotSupplySpanContextWhenTracerBeanIsMissing() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(SpanContext.class)); + } + + @Test + void shouldNotSupplySpanContextWhenTracerClassIsMissing() { + this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class)) + .run((context) -> assertThat(context).doesNotHaveBean(SpanContext.class)); + } + + @Test + void shouldNotSupplySpanContextWhenSpanContextClassIsMissing() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class) + .withClassLoader(new FilteredClassLoader(SpanContext.class)) + .run((context) -> assertThat(context).doesNotHaveBean(SpanContext.class)); + } + + @Test + void shouldNotSupplySpanContextWhenIncludeIsNone() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class) + .withPropertyValues("management.tracing.exemplars.include=none") + .run((context) -> assertThat(context).doesNotHaveBean(SpanContext.class)); + } + + @Test + void shouldFailWhenIncludeIsAll() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class) + .withPropertyValues("management.tracing.exemplars.include=all") + .run((context) -> assertThat(context).hasFailed() + .getFailure() + .rootCause() + .isInstanceOf(InvalidConfigurationPropertyValueException.class) + .hasMessageContaining( + "Property management.tracing.exemplars.include with value 'all' is invalid: Prometheus doesn't support including 'all' traces as exemplars.")); + } + + @Test + void shouldSupplySpanContextWhenIncludeIsSampledTraces() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class) + .withPropertyValues("management.tracing.exemplars.include=sampled-traces") + .run((context) -> assertThat(context).hasSingleBean(SpanContext.class)); + } + + @Configuration(proxyBeanMethods = false) + private static final class TracerConfiguration { + + @Bean + Tracer tracer() { + return mock(Tracer.class); + } + + } + + @Configuration(proxyBeanMethods = false) + private static final class CustomSpanContextConfiguration { + + @Bean + SpanContext customSpanContext() { + return mock(SpanContext.class); + } + + } + +} From 8fff41b3ec56533ff69e5f1c4617aa0bcc44acbc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Mar 2026 18:22:29 +0000 Subject: [PATCH 343/930] Auto-configure ExpressionJwtGrantedAuthoritiesConverters Closes gh-48490 --- .../JwtConverterConfiguration.java | 86 +++++++++++++-- .../OAuth2ResourceServerProperties.java | 16 +++ .../ReactiveJwtConverterConfiguration.java | 104 +++++++++++++++--- ...2ResourceServerAutoConfigurationTests.java | 68 ++++++++++++ ...2ResourceServerAutoConfigurationTests.java | 73 ++++++++++++ 5 files changed, 327 insertions(+), 20 deletions(-) diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterConfiguration.java index 655284b76aec..22f3aca92dba 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterConfiguration.java @@ -16,17 +16,31 @@ package org.springframework.boot.security.oauth2.server.resource.autoconfigure; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionMessage; 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.OnPropertyListCondition; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.DelegatingJwtGrantedAuthoritiesConverter; +import org.springframework.security.oauth2.server.resource.authentication.ExpressionJwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.util.CollectionUtils; /** * {@link Configuration @Configuration} for JWT converter beans. @@ -47,18 +61,24 @@ class JwtConverterConfiguration { @Bean JwtAuthenticationConverter jwtAuthenticationConverter(OAuth2ResourceServerProperties properties) { - return jwtAuthenticationConverter(properties.getJwt()); - } - - private JwtAuthenticationConverter jwtAuthenticationConverter(OAuth2ResourceServerProperties.Jwt properties) { PropertyMapper map = PropertyMapper.get(); + OAuth2ResourceServerProperties.Jwt jwtProperties = properties.getJwt(); JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); - map.from(properties::getPrincipalClaimName).to(converter::setPrincipalClaimName); - map.from(properties).as(this::getGrantedAuthoritiesConverter).to(converter::setJwtGrantedAuthoritiesConverter); + map.from(jwtProperties::getPrincipalClaimName).to(converter::setPrincipalClaimName); + map.from(jwtProperties).as(this::grantedAuthoritiesConverter).to(converter::setJwtGrantedAuthoritiesConverter); return converter; } - private JwtGrantedAuthoritiesConverter getGrantedAuthoritiesConverter( + private Converter> grantedAuthoritiesConverter( + OAuth2ResourceServerProperties.Jwt properties) { + List authoritiesExpressions = properties.getAuthoritiesExpressions(); + if (CollectionUtils.isEmpty(authoritiesExpressions)) { + return createJwtGrantedAuthoritiesConverter(properties); + } + return createExpressionJwtGrantedAuthoritiesConverters(properties, authoritiesExpressions); + } + + private Converter> createJwtGrantedAuthoritiesConverter( OAuth2ResourceServerProperties.Jwt properties) { PropertyMapper map = PropertyMapper.get(); JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter(); @@ -68,6 +88,44 @@ private JwtGrantedAuthoritiesConverter getGrantedAuthoritiesConverter( return converter; } + private Converter> createExpressionJwtGrantedAuthoritiesConverters( + OAuth2ResourceServerProperties.Jwt properties, List authoritiesExpressions) { + checkMutualExclusivity(properties); + List>> converters = new ArrayList<>(); + SpelExpressionParser parser = new SpelExpressionParser(); + for (String authoritiesExpression : authoritiesExpressions) { + ExpressionJwtGrantedAuthoritiesConverter converter = new ExpressionJwtGrantedAuthoritiesConverter( + parser.parseExpression(authoritiesExpression)); + if (properties.getAuthorityPrefix() != null) { + converter.setAuthorityPrefix(properties.getAuthorityPrefix()); + } + converters.add(converter); + } + return (converters.size() == 1) ? converters.get(0) : new DelegatingJwtGrantedAuthoritiesConverter(converters); + } + + private void checkMutualExclusivity(OAuth2ResourceServerProperties.Jwt properties) { + MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleMatchingValuesIn((entries) -> { + entries.put("spring.security.oauth2.resourceserver.jwt.authorities-expressions", + properties.getAuthoritiesExpressions()); + entries.put("spring.security.oauth2.resourceserver.jwt.authorities-claim-name", + properties.getAuthoritiesClaimName()); + }, (value) -> !nullOrEmptyList(value)); + MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleMatchingValuesIn((entries) -> { + entries.put("spring.security.oauth2.resourceserver.jwt.authorities-expressions", + properties.getAuthoritiesExpressions()); + entries.put("spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter", + properties.getAuthoritiesClaimDelimiter()); + }, (value) -> !nullOrEmptyList(value)); + } + + private boolean nullOrEmptyList(Object value) { + if (value == null) { + return true; + } + return (value instanceof List list) ? list.isEmpty() : false; + } + static class PropertiesCondition extends AnyNestedCondition { PropertiesCondition() { @@ -89,6 +147,20 @@ static class OnAuthoritiesClaimName { } + @Conditional(OnAuthoritiesExpressionsCondition.class) + static class OnAuthoritiesExpressions { + + } + + static class OnAuthoritiesExpressionsCondition extends OnPropertyListCondition { + + OnAuthoritiesExpressionsCondition() { + super("spring.security.oauth2.resourceserver.jwt.authorities-expressions", + () -> ConditionMessage.forCondition("Authorities expressions")); + } + + } + } } diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerProperties.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerProperties.java index 594cfbc860fc..d025da819da8 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerProperties.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerProperties.java @@ -98,6 +98,14 @@ public static class Jwt { */ private @Nullable String authoritiesClaimName; + /** + * List of expressions to use to extract authorities from a JWT. Mutually + * exclusive with + * 'spring.security.oauth2.resourceserver.jwt.authorities-claim-name' and + * 'spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter'. + */ + private List authoritiesExpressions = new ArrayList<>(); + /** * JWT principal claim name. */ @@ -167,6 +175,14 @@ public void setAuthoritiesClaimName(@Nullable String authoritiesClaimName) { this.authoritiesClaimName = authoritiesClaimName; } + public List getAuthoritiesExpressions() { + return this.authoritiesExpressions; + } + + public void setAuthoritiesExpressions(List authoritiesExpressions) { + this.authoritiesExpressions = authoritiesExpressions; + } + public @Nullable String getPrincipalClaimName() { return this.principalClaimName; } diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtConverterConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtConverterConfiguration.java index b287d04fbb21..f2d4a951bfbb 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtConverterConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtConverterConfiguration.java @@ -16,19 +16,33 @@ package org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionMessage; 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.OnPropertyListCondition; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.DelegatingJwtGrantedAuthoritiesConverter; +import org.springframework.security.oauth2.server.resource.authentication.ExpressionJwtGrantedAuthoritiesConverter; 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.util.CollectionUtils; /** * {@link Configuration @Configuration} for JWT converter beans. @@ -50,24 +64,74 @@ class ReactiveJwtConverterConfiguration { @Bean ReactiveJwtAuthenticationConverter reactiveJwtAuthenticationConverter(OAuth2ResourceServerProperties properties) { - return reactiveJwtAuthenticationConverter(properties.getJwt()); - } - - private ReactiveJwtAuthenticationConverter reactiveJwtAuthenticationConverter( - OAuth2ResourceServerProperties.Jwt properties1) { PropertyMapper map = PropertyMapper.get(); - JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); - map.from(properties1.getAuthorityPrefix()).to(grantedAuthoritiesConverter::setAuthorityPrefix); - map.from(properties1.getAuthoritiesClaimDelimiter()) - .to(grantedAuthoritiesConverter::setAuthoritiesClaimDelimiter); - map.from(properties1.getAuthoritiesClaimName()).to(grantedAuthoritiesConverter::setAuthoritiesClaimName); + OAuth2ResourceServerProperties.Jwt jwtProperties = properties.getJwt(); ReactiveJwtAuthenticationConverter jwtAuthenticationConverter = new ReactiveJwtAuthenticationConverter(); - map.from(properties1.getPrincipalClaimName()).to(jwtAuthenticationConverter::setPrincipalClaimName); - jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter( - new ReactiveJwtGrantedAuthoritiesConverterAdapter(grantedAuthoritiesConverter)); + map.from(jwtProperties::getPrincipalClaimName).to(jwtAuthenticationConverter::setPrincipalClaimName); + map.from(jwtProperties) + .as(this::grantedAuthoritiesConverter) + .as(ReactiveJwtGrantedAuthoritiesConverterAdapter::new) + .to(jwtAuthenticationConverter::setJwtGrantedAuthoritiesConverter); return jwtAuthenticationConverter; } + private Converter> grantedAuthoritiesConverter( + OAuth2ResourceServerProperties.Jwt properties) { + List authoritiesExpressions = properties.getAuthoritiesExpressions(); + if (CollectionUtils.isEmpty(authoritiesExpressions)) { + return createJwtGrantedAuthoritiesConverter(properties); + } + return createExpressionJwtGrantedAuthoritiesConverters(properties, authoritiesExpressions); + } + + private Converter> createJwtGrantedAuthoritiesConverter( + OAuth2ResourceServerProperties.Jwt properties) { + PropertyMapper map = PropertyMapper.get(); + JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter(); + map.from(properties::getAuthorityPrefix).to(converter::setAuthorityPrefix); + map.from(properties::getAuthoritiesClaimDelimiter).to(converter::setAuthoritiesClaimDelimiter); + map.from(properties::getAuthoritiesClaimName).to(converter::setAuthoritiesClaimName); + return converter; + } + + private Converter> createExpressionJwtGrantedAuthoritiesConverters( + OAuth2ResourceServerProperties.Jwt properties, List authoritiesExpressions) { + checkMutualExclusivity(properties); + List>> converters = new ArrayList<>(); + SpelExpressionParser parser = new SpelExpressionParser(); + for (String authoritiesExpression : authoritiesExpressions) { + ExpressionJwtGrantedAuthoritiesConverter converter = new ExpressionJwtGrantedAuthoritiesConverter( + parser.parseExpression(authoritiesExpression)); + if (properties.getAuthorityPrefix() != null) { + converter.setAuthorityPrefix(properties.getAuthorityPrefix()); + } + converters.add(converter); + } + return (converters.size() == 1) ? converters.get(0) : new DelegatingJwtGrantedAuthoritiesConverter(converters); + } + + private void checkMutualExclusivity(OAuth2ResourceServerProperties.Jwt properties) { + MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleMatchingValuesIn((entries) -> { + entries.put("spring.security.oauth2.resourceserver.jwt.authorities-expressions", + properties.getAuthoritiesExpressions()); + entries.put("spring.security.oauth2.resourceserver.jwt.authorities-claim-name", + properties.getAuthoritiesClaimName()); + }, (value) -> !nullOrEmptyList(value)); + MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleMatchingValuesIn((entries) -> { + entries.put("spring.security.oauth2.resourceserver.jwt.authorities-expressions", + properties.getAuthoritiesExpressions()); + entries.put("spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter", + properties.getAuthoritiesClaimDelimiter()); + }, (value) -> !nullOrEmptyList(value)); + } + + private boolean nullOrEmptyList(Object value) { + if (value == null) { + return true; + } + return (value instanceof List list) ? list.isEmpty() : false; + } + static class PropertiesCondition extends AnyNestedCondition { PropertiesCondition() { @@ -89,6 +153,20 @@ static class OnAuthoritiesClaimName { } + @Conditional(OnAuthoritiesExpressionsCondition.class) + static class OnAuthoritiesExpressions { + + } + + static class OnAuthoritiesExpressionsCondition extends OnPropertyListCondition { + + OnAuthoritiesExpressionsCondition() { + super("spring.security.oauth2.resourceserver.jwt.authorities-expressions", + () -> ConditionMessage.forCondition("Authorities expressions")); + } + + } + } } diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java index 0a9f063ab9ee..a4047ecb3339 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java @@ -47,6 +47,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; @@ -74,6 +75,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.ExpressionJwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; import org.springframework.security.web.SecurityFilterChain; @@ -689,6 +691,72 @@ void shouldConfigureJwtConverterIfAuthorityClaimsNameIsSet() { .run((context) -> assertThat(context).hasSingleBean(JwtAuthenticationConverter.class)); } + @Test + void shouldConfigureJwtConverterIfAuthoritiesExpressionIsSet() { + this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions=zero") + .run((context) -> { + assertThat(context).hasSingleBean(JwtAuthenticationConverter.class); + JwtAuthenticationConverter converter = context.getBean(JwtAuthenticationConverter.class); + assertThat(converter).extracting("jwtGrantedAuthoritiesConverter") + .isInstanceOf(ExpressionJwtGrantedAuthoritiesConverter.class) + .extracting("authorityPrefix") + .isEqualTo("SCOPE_"); + }); + } + + @Test + void shouldConfigureJwtConverterIfAuthoritiesExpressionsAreSet() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", + "spring.security.oauth2.resourceserver.jwt.authorities-expressions[1]=one") + .run((context) -> { + assertThat(context).hasSingleBean(JwtAuthenticationConverter.class); + JwtAuthenticationConverter converter = context.getBean(JwtAuthenticationConverter.class); + assertThat(converter) + .extracting("jwtGrantedAuthoritiesConverter.authoritiesConverters", InstanceOfAssertFactories.LIST) + .hasSize(2) + .extracting("authorityPrefix") + .containsOnly("SCOPE_"); + }); + } + + @Test + void shouldApplyCustomAuthorityPrefixIfAuthoritiesExpressionsAreSet() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", + "spring.security.oauth2.resourceserver.jwt.authorities-expressions[1]=one", + "spring.security.oauth2.resourceserver.jwt.authority-prefix=CUSTOM_") + .run((context) -> { + assertThat(context).hasSingleBean(JwtAuthenticationConverter.class); + JwtAuthenticationConverter converter = context.getBean(JwtAuthenticationConverter.class); + assertThat(converter) + .extracting("jwtGrantedAuthoritiesConverter.authoritiesConverters", InstanceOfAssertFactories.LIST) + .hasSize(2) + .extracting("authorityPrefix") + .containsOnly("CUSTOM_"); + }); + } + + @Test + void shouldFailIfBothAuthoritiesExpressionsAndAuthoritiesClaimDelimiterAreSet() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", + "spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter=delimiter") + .run((context) -> assertThat(context).getFailure() + .rootCause() + .isInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class)); + } + + @Test + void shouldFailIfBothAuthoritiesExpressionsAndAuthoritiesClaimNameAreSet() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", + "spring.security.oauth2.resourceserver.jwt.authorities-claim-name=name") + .run((context) -> assertThat(context).getFailure() + .rootCause() + .isInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class)); + } + @Test void jwtAuthenticationConverterByJwtConfigIsConditionalOnMissingBean() { String propertiesPrincipalClaim = "principal_from_properties"; diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index e9b2e3bb26fa..c1fcf8c0d8be 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -48,6 +48,7 @@ import tools.jackson.databind.json.JsonMapper; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; import org.springframework.boot.security.oauth2.server.resource.autoconfigure.JwtConverterCustomizationsArgumentsProvider; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; @@ -77,6 +78,7 @@ import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.ExpressionJwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; import org.springframework.security.web.server.SecurityWebFilterChain; @@ -678,6 +680,77 @@ void shouldConfigureJwtConverterIfAuthorityClaimsNameIsSet() { .run((context) -> assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class)); } + @Test + void shouldConfigureJwtConverterIfAuthoritiesExpressionIsSet() { + this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions=zero") + .run((context) -> { + assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class); + ReactiveJwtAuthenticationConverter converter = context + .getBean(ReactiveJwtAuthenticationConverter.class); + assertThat(converter).extracting("jwtGrantedAuthoritiesConverter.grantedAuthoritiesConverter") + .isInstanceOf(ExpressionJwtGrantedAuthoritiesConverter.class) + .extracting("authorityPrefix") + .isEqualTo("SCOPE_"); + }); + } + + @Test + void shouldConfigureJwtConverterIfAuthoritiesExpressionsAreSet() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", + "spring.security.oauth2.resourceserver.jwt.authorities-expressions[1]=one") + .run((context) -> { + assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class); + ReactiveJwtAuthenticationConverter converter = context + .getBean(ReactiveJwtAuthenticationConverter.class); + assertThat(converter) + .extracting("jwtGrantedAuthoritiesConverter.grantedAuthoritiesConverter.authoritiesConverters", + InstanceOfAssertFactories.LIST) + .hasSize(2) + .extracting("authorityPrefix") + .containsOnly("SCOPE_"); + }); + } + + @Test + void shouldApplyCustomAuthorityPrefixIfAuthoritiesExpressionsAreSet() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", + "spring.security.oauth2.resourceserver.jwt.authorities-expressions[1]=one", + "spring.security.oauth2.resourceserver.jwt.authority-prefix=CUSTOM_") + .run((context) -> { + assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class); + ReactiveJwtAuthenticationConverter converter = context + .getBean(ReactiveJwtAuthenticationConverter.class); + assertThat(converter) + .extracting("jwtGrantedAuthoritiesConverter.grantedAuthoritiesConverter.authoritiesConverters", + InstanceOfAssertFactories.LIST) + .hasSize(2) + .extracting("authorityPrefix") + .containsOnly("CUSTOM_"); + }); + } + + @Test + void shouldFailIfBothAuthoritiesExpressionsAndAuthoritiesClaimDelimiterAreSet() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", + "spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter=delimiter") + .run((context) -> assertThat(context).getFailure() + .rootCause() + .isInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class)); + } + + @Test + void shouldFailIfBothAuthoritiesExpressionsAndAuthoritiesClaimNameAreSet() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", + "spring.security.oauth2.resourceserver.jwt.authorities-claim-name=name") + .run((context) -> assertThat(context).getFailure() + .rootCause() + .isInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class)); + } + @ParameterizedTest(name = "{0}") @ArgumentsSource(JwtConverterCustomizationsArgumentsProvider.class) void autoConfigurationShouldConfigureResourceServerWithJwtConverterCustomizations(String[] properties, Jwt jwt, From f2981d1bdc29c049a8646e3306e322bcfb6d2d1e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Mar 2026 10:35:57 +0000 Subject: [PATCH 344/930] Rename property to authorities-claim-expressions See gh-48490 --- .../JwtConverterConfiguration.java | 24 +++++++++---------- .../OAuth2ResourceServerProperties.java | 8 +++---- .../ReactiveJwtConverterConfiguration.java | 24 +++++++++---------- ...2ResourceServerAutoConfigurationTests.java | 15 ++++++------ ...2ResourceServerAutoConfigurationTests.java | 15 ++++++------ 5 files changed, 44 insertions(+), 42 deletions(-) diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterConfiguration.java index 22f3aca92dba..3ffb6e0e9e5a 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/JwtConverterConfiguration.java @@ -71,11 +71,11 @@ JwtAuthenticationConverter jwtAuthenticationConverter(OAuth2ResourceServerProper private Converter> grantedAuthoritiesConverter( OAuth2ResourceServerProperties.Jwt properties) { - List authoritiesExpressions = properties.getAuthoritiesExpressions(); - if (CollectionUtils.isEmpty(authoritiesExpressions)) { + List authoritiesClaimExpressions = properties.getAuthoritiesClaimExpressions(); + if (CollectionUtils.isEmpty(authoritiesClaimExpressions)) { return createJwtGrantedAuthoritiesConverter(properties); } - return createExpressionJwtGrantedAuthoritiesConverters(properties, authoritiesExpressions); + return createExpressionJwtGrantedAuthoritiesConverters(properties, authoritiesClaimExpressions); } private Converter> createJwtGrantedAuthoritiesConverter( @@ -89,13 +89,13 @@ private Converter> createJwtGrantedAuthorities } private Converter> createExpressionJwtGrantedAuthoritiesConverters( - OAuth2ResourceServerProperties.Jwt properties, List authoritiesExpressions) { + OAuth2ResourceServerProperties.Jwt properties, List claimExpressions) { checkMutualExclusivity(properties); List>> converters = new ArrayList<>(); SpelExpressionParser parser = new SpelExpressionParser(); - for (String authoritiesExpression : authoritiesExpressions) { + for (String claimExpression : claimExpressions) { ExpressionJwtGrantedAuthoritiesConverter converter = new ExpressionJwtGrantedAuthoritiesConverter( - parser.parseExpression(authoritiesExpression)); + parser.parseExpression(claimExpression)); if (properties.getAuthorityPrefix() != null) { converter.setAuthorityPrefix(properties.getAuthorityPrefix()); } @@ -106,14 +106,14 @@ private Converter> createExpressionJwtGrantedA private void checkMutualExclusivity(OAuth2ResourceServerProperties.Jwt properties) { MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleMatchingValuesIn((entries) -> { - entries.put("spring.security.oauth2.resourceserver.jwt.authorities-expressions", - properties.getAuthoritiesExpressions()); + entries.put("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions", + properties.getAuthoritiesClaimExpressions()); entries.put("spring.security.oauth2.resourceserver.jwt.authorities-claim-name", properties.getAuthoritiesClaimName()); }, (value) -> !nullOrEmptyList(value)); MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleMatchingValuesIn((entries) -> { - entries.put("spring.security.oauth2.resourceserver.jwt.authorities-expressions", - properties.getAuthoritiesExpressions()); + entries.put("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions", + properties.getAuthoritiesClaimExpressions()); entries.put("spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter", properties.getAuthoritiesClaimDelimiter()); }, (value) -> !nullOrEmptyList(value)); @@ -155,8 +155,8 @@ static class OnAuthoritiesExpressions { static class OnAuthoritiesExpressionsCondition extends OnPropertyListCondition { OnAuthoritiesExpressionsCondition() { - super("spring.security.oauth2.resourceserver.jwt.authorities-expressions", - () -> ConditionMessage.forCondition("Authorities expressions")); + super("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions", + () -> ConditionMessage.forCondition("Authorities claim expressions")); } } diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerProperties.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerProperties.java index d025da819da8..b5bcb914d75d 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerProperties.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerProperties.java @@ -104,7 +104,7 @@ public static class Jwt { * 'spring.security.oauth2.resourceserver.jwt.authorities-claim-name' and * 'spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter'. */ - private List authoritiesExpressions = new ArrayList<>(); + private List authoritiesClaimExpressions = new ArrayList<>(); /** * JWT principal claim name. @@ -175,12 +175,12 @@ public void setAuthoritiesClaimName(@Nullable String authoritiesClaimName) { this.authoritiesClaimName = authoritiesClaimName; } - public List getAuthoritiesExpressions() { - return this.authoritiesExpressions; + public List getAuthoritiesClaimExpressions() { + return this.authoritiesClaimExpressions; } public void setAuthoritiesExpressions(List authoritiesExpressions) { - this.authoritiesExpressions = authoritiesExpressions; + this.authoritiesClaimExpressions = authoritiesExpressions; } public @Nullable String getPrincipalClaimName() { diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtConverterConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtConverterConfiguration.java index f2d4a951bfbb..9ff24500f90d 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtConverterConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtConverterConfiguration.java @@ -77,11 +77,11 @@ ReactiveJwtAuthenticationConverter reactiveJwtAuthenticationConverter(OAuth2Reso private Converter> grantedAuthoritiesConverter( OAuth2ResourceServerProperties.Jwt properties) { - List authoritiesExpressions = properties.getAuthoritiesExpressions(); - if (CollectionUtils.isEmpty(authoritiesExpressions)) { + List authoritiesClaimExpressions = properties.getAuthoritiesClaimExpressions(); + if (CollectionUtils.isEmpty(authoritiesClaimExpressions)) { return createJwtGrantedAuthoritiesConverter(properties); } - return createExpressionJwtGrantedAuthoritiesConverters(properties, authoritiesExpressions); + return createExpressionJwtGrantedAuthoritiesConverters(properties, authoritiesClaimExpressions); } private Converter> createJwtGrantedAuthoritiesConverter( @@ -95,13 +95,13 @@ private Converter> createJwtGrantedAuthorities } private Converter> createExpressionJwtGrantedAuthoritiesConverters( - OAuth2ResourceServerProperties.Jwt properties, List authoritiesExpressions) { + OAuth2ResourceServerProperties.Jwt properties, List claimExpressions) { checkMutualExclusivity(properties); List>> converters = new ArrayList<>(); SpelExpressionParser parser = new SpelExpressionParser(); - for (String authoritiesExpression : authoritiesExpressions) { + for (String claimExpression : claimExpressions) { ExpressionJwtGrantedAuthoritiesConverter converter = new ExpressionJwtGrantedAuthoritiesConverter( - parser.parseExpression(authoritiesExpression)); + parser.parseExpression(claimExpression)); if (properties.getAuthorityPrefix() != null) { converter.setAuthorityPrefix(properties.getAuthorityPrefix()); } @@ -112,14 +112,14 @@ private Converter> createExpressionJwtGrantedA private void checkMutualExclusivity(OAuth2ResourceServerProperties.Jwt properties) { MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleMatchingValuesIn((entries) -> { - entries.put("spring.security.oauth2.resourceserver.jwt.authorities-expressions", - properties.getAuthoritiesExpressions()); + entries.put("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions", + properties.getAuthoritiesClaimExpressions()); entries.put("spring.security.oauth2.resourceserver.jwt.authorities-claim-name", properties.getAuthoritiesClaimName()); }, (value) -> !nullOrEmptyList(value)); MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleMatchingValuesIn((entries) -> { - entries.put("spring.security.oauth2.resourceserver.jwt.authorities-expressions", - properties.getAuthoritiesExpressions()); + entries.put("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions", + properties.getAuthoritiesClaimExpressions()); entries.put("spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter", properties.getAuthoritiesClaimDelimiter()); }, (value) -> !nullOrEmptyList(value)); @@ -161,8 +161,8 @@ static class OnAuthoritiesExpressions { static class OnAuthoritiesExpressionsCondition extends OnPropertyListCondition { OnAuthoritiesExpressionsCondition() { - super("spring.security.oauth2.resourceserver.jwt.authorities-expressions", - () -> ConditionMessage.forCondition("Authorities expressions")); + super("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions", + () -> ConditionMessage.forCondition("Authorities claim expressions")); } } diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java index a4047ecb3339..e9cd6cb36af5 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OAuth2ResourceServerAutoConfigurationTests.java @@ -693,7 +693,8 @@ void shouldConfigureJwtConverterIfAuthorityClaimsNameIsSet() { @Test void shouldConfigureJwtConverterIfAuthoritiesExpressionIsSet() { - this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions=zero") + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions=zero") .run((context) -> { assertThat(context).hasSingleBean(JwtAuthenticationConverter.class); JwtAuthenticationConverter converter = context.getBean(JwtAuthenticationConverter.class); @@ -707,8 +708,8 @@ void shouldConfigureJwtConverterIfAuthoritiesExpressionIsSet() { @Test void shouldConfigureJwtConverterIfAuthoritiesExpressionsAreSet() { this.contextRunner - .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", - "spring.security.oauth2.resourceserver.jwt.authorities-expressions[1]=one") + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions[0]=zero", + "spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions[1]=one") .run((context) -> { assertThat(context).hasSingleBean(JwtAuthenticationConverter.class); JwtAuthenticationConverter converter = context.getBean(JwtAuthenticationConverter.class); @@ -723,8 +724,8 @@ void shouldConfigureJwtConverterIfAuthoritiesExpressionsAreSet() { @Test void shouldApplyCustomAuthorityPrefixIfAuthoritiesExpressionsAreSet() { this.contextRunner - .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", - "spring.security.oauth2.resourceserver.jwt.authorities-expressions[1]=one", + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions[0]=zero", + "spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions[1]=one", "spring.security.oauth2.resourceserver.jwt.authority-prefix=CUSTOM_") .run((context) -> { assertThat(context).hasSingleBean(JwtAuthenticationConverter.class); @@ -740,7 +741,7 @@ void shouldApplyCustomAuthorityPrefixIfAuthoritiesExpressionsAreSet() { @Test void shouldFailIfBothAuthoritiesExpressionsAndAuthoritiesClaimDelimiterAreSet() { this.contextRunner - .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions[0]=zero", "spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter=delimiter") .run((context) -> assertThat(context).getFailure() .rootCause() @@ -750,7 +751,7 @@ void shouldFailIfBothAuthoritiesExpressionsAndAuthoritiesClaimDelimiterAreSet() @Test void shouldFailIfBothAuthoritiesExpressionsAndAuthoritiesClaimNameAreSet() { this.contextRunner - .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions[0]=zero", "spring.security.oauth2.resourceserver.jwt.authorities-claim-name=name") .run((context) -> assertThat(context).getFailure() .rootCause() diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index c1fcf8c0d8be..0e4822986536 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -682,7 +682,8 @@ void shouldConfigureJwtConverterIfAuthorityClaimsNameIsSet() { @Test void shouldConfigureJwtConverterIfAuthoritiesExpressionIsSet() { - this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions=zero") + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions=zero") .run((context) -> { assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class); ReactiveJwtAuthenticationConverter converter = context @@ -697,8 +698,8 @@ void shouldConfigureJwtConverterIfAuthoritiesExpressionIsSet() { @Test void shouldConfigureJwtConverterIfAuthoritiesExpressionsAreSet() { this.contextRunner - .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", - "spring.security.oauth2.resourceserver.jwt.authorities-expressions[1]=one") + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions[0]=zero", + "spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions[1]=one") .run((context) -> { assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class); ReactiveJwtAuthenticationConverter converter = context @@ -715,8 +716,8 @@ void shouldConfigureJwtConverterIfAuthoritiesExpressionsAreSet() { @Test void shouldApplyCustomAuthorityPrefixIfAuthoritiesExpressionsAreSet() { this.contextRunner - .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", - "spring.security.oauth2.resourceserver.jwt.authorities-expressions[1]=one", + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions[0]=zero", + "spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions[1]=one", "spring.security.oauth2.resourceserver.jwt.authority-prefix=CUSTOM_") .run((context) -> { assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class); @@ -734,7 +735,7 @@ void shouldApplyCustomAuthorityPrefixIfAuthoritiesExpressionsAreSet() { @Test void shouldFailIfBothAuthoritiesExpressionsAndAuthoritiesClaimDelimiterAreSet() { this.contextRunner - .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions[0]=zero", "spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter=delimiter") .run((context) -> assertThat(context).getFailure() .rootCause() @@ -744,7 +745,7 @@ void shouldFailIfBothAuthoritiesExpressionsAndAuthoritiesClaimDelimiterAreSet() @Test void shouldFailIfBothAuthoritiesExpressionsAndAuthoritiesClaimNameAreSet() { this.contextRunner - .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-expressions[0]=zero", + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-expressions[0]=zero", "spring.security.oauth2.resourceserver.jwt.authorities-claim-name=name") .run((context) -> assertThat(context).getFailure() .rootCause() From b12d40e8e5c3ba85fc9fda9fce430313a8c23d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 12 Sep 2025 17:13:17 +0200 Subject: [PATCH 345/930] Add support for Spring Batch MongoDB This commit adds support for running Spring Batch jobs with a MongoDB store. It aligns as much as possible to the JDBC counterpart, with spring-boot-starter-batch-data-mongodb and spring-boot-starter-batch-data-mongodb-test. As we do not have a way to initialize a MongoDB store at the moment, this commit adds a conservative approach of executing commands defined by the standard Spring Batch schema script. Closes gh-43236 --- .../antora-asciidoc-attributes.properties | 1 + documentation/spring-boot-docs/build.gradle | 1 + .../reference/pages/io/spring-batch.adoc | 17 +- .../build.gradle | 45 ++++ ...ongoAutoConfigurationIntegrationTests.java | 98 ++++++++ .../BatchDataMongoAutoConfiguration.java | 149 +++++++++++++ .../BatchDataMongoProperties.java | 96 ++++++++ ...tchMongoSchemaInitializationException.java | 35 +++ .../BatchMongoSchemaInitializer.java | 69 ++++++ ...DependsOnSchemaInitializationDetector.java | 39 ++++ .../mongodb/autoconfigure/package-info.java | 23 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../BatchDataMongoAutoConfigurationTests.java | 210 ++++++++++++++++++ .../BatchMongoSchemaInitializerTests.java | 84 +++++++ settings.gradle | 4 + .../build.gradle | 34 +++ .../batch/SampleBatchApplication.java | 46 ++++ .../java/smoketest/batch/package-info.java | 20 ++ .../batch/SampleBatchApplicationTests.java | 53 +++++ .../build.gradle | 26 +++ .../build.gradle | 28 +++ 21 files changed, 1077 insertions(+), 2 deletions(-) create mode 100644 module/spring-boot-batch-data-mongodb/build.gradle create mode 100644 module/spring-boot-batch-data-mongodb/src/dockerTest/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationIntegrationTests.java create mode 100644 module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfiguration.java create mode 100644 module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoProperties.java create mode 100644 module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializationException.java create mode 100644 module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializer.java create mode 100644 module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/JobRepositoryDependsOnSchemaInitializationDetector.java create mode 100644 module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/package-info.java create mode 100644 module/spring-boot-batch-data-mongodb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationTests.java create mode 100644 module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializerTests.java create mode 100644 smoke-test/spring-boot-smoke-test-batch-data-mongodb/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/SampleBatchApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/test/java/smoketest/batch/SampleBatchApplicationTests.java create mode 100644 starter/spring-boot-starter-batch-data-mongodb-test/build.gradle create mode 100644 starter/spring-boot-starter-batch-data-mongodb/build.gradle diff --git a/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties b/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties index 23061f7cf278..e1fef5f204ff 100644 --- a/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties +++ b/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties @@ -117,6 +117,7 @@ apiref-openjdk=https://docs.oracle.com/en/java/javase/17/docs/api code-spring-boot=https://github.com/{github-repo}/tree/{github-ref} code-spring-boot-autoconfigure-src={code-spring-boot}/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure +code-spring-boot-batch-data-mongodb-src={code-spring-boot}/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb code-spring-boot-batch-jdbc-src={code-spring-boot}/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc code-spring-boot-batch-src={code-spring-boot}/module/spring-boot-batch/src/main/java/org/springframework/boot/batch code-spring-boot-freemarker-src={code-spring-boot}/module/spring-boot-freemarker/src/main/java/org/springframework/boot/freemarker diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index b4a3d44067d9..3781363d0bf3 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -235,6 +235,7 @@ dependencies { javadocMacros(project(":module:spring-boot-activemq")) javadocMacros(project(":module:spring-boot-artemis")) javadocMacros(project(":module:spring-boot-batch")) + javadocMacros(project(":module:spring-boot-batch-data-mongodb")) javadocMacros(project(":module:spring-boot-batch-jdbc")) javadocMacros(project(":module:spring-boot-cache-test")) javadocMacros(project(":module:spring-boot-data-r2dbc-test")) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/spring-batch.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/spring-batch.adoc index bc6c8ff443f6..843fc0642f0b 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/spring-batch.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/spring-batch.adoc @@ -7,6 +7,7 @@ When building a batch application, the following stores can be auto-configured: * In-memory * JDBC +* MongoDB Each store has specific additional settings. For instance, it is possible to customize the tables prefix for the JDBC store, as shown in the following example: @@ -19,8 +20,20 @@ spring: table-prefix: "CUSTOM_" ---- +When using the MongoDB store, you can enable initialization of the Spring Batch job repository schema (collections and indexes): + +[configprops,yaml] +---- +spring: + batch: + data: + mongodb: + schema: + initialize: true +---- + To disable Spring Boot's auto-configuration and take complete control of Spring Batch's configuration, add javadoc:org.springframework.batch.core.configuration.annotation.EnableBatchProcessing[format=annotation] to one of your javadoc:org.springframework.context.annotation.Configuration[format=annotation] classes or extend javadoc:org.springframework.batch.core.configuration.support.DefaultBatchConfiguration[]. -This will cause the auto-configuration to back off, including initialization of Spring Batch's database schema if you're using the JDBC-based store. +This will cause the auto-configuration to back off, including initialization of Spring Batch's database schema (JDBC or MongoDB). Spring Batch can then be configured using the `@Enable*JobRepository` annotation's attributes rather than the previously described configuration properties. To learn more about manually configuring Spring Batch, see the API documentation of: @@ -50,4 +63,4 @@ spring: enabled: false ---- -See {code-spring-boot-batch-src}/autoconfigure/BatchAutoConfiguration.java[`BatchAutoConfiguration`] and {code-spring-boot-batch-jdbc-src}/autoconfigure/BatchJdbcAutoConfiguration.java[`BatchJdbcAutoConfiguration`] for more details. +See {code-spring-boot-batch-src}/autoconfigure/BatchAutoConfiguration.java[`BatchAutoConfiguration`], {code-spring-boot-batch-jdbc-src}/autoconfigure/BatchJdbcAutoConfiguration.java[`BatchJdbcAutoConfiguration`], and {code-spring-boot-batch-data-mongodb-src}/autoconfigure/BatchDataMongoAutoConfiguration.java[`BatchDataMongoAutoConfiguration`] for more details. diff --git a/module/spring-boot-batch-data-mongodb/build.gradle b/module/spring-boot-batch-data-mongodb/build.gradle new file mode 100644 index 000000000000..a49a40e515f2 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/build.gradle @@ -0,0 +1,45 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java-library" + id "org.springframework.boot.auto-configuration" + id "org.springframework.boot.configuration-properties" + id "org.springframework.boot.deployed" + id "org.springframework.boot.docker-test" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot Batch Data MongoDB" + +dependencies { + api(project(":module:spring-boot-batch")) + api(project(":module:spring-boot-data-mongodb")) + + optional(project(":core:spring-boot-autoconfigure")) + optional("org.mongodb:mongodb-driver-sync") + + dockerTestImplementation(project(":core:spring-boot-test")) + dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) + dockerTestImplementation("org.mongodb:mongodb-driver-sync") + dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") + dockerTestImplementation("org.testcontainers:testcontainers-mongodb") + + testImplementation(project(":core:spring-boot-test")) + testImplementation(project(":test-support:spring-boot-test-support")) + + testRuntimeOnly("ch.qos.logback:logback-classic") +} diff --git a/module/spring-boot-batch-data-mongodb/src/dockerTest/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationIntegrationTests.java b/module/spring-boot-batch-data-mongodb/src/dockerTest/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationIntegrationTests.java new file mode 100644 index 000000000000..55baa70cba34 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/dockerTest/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationIntegrationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.batch.mongodb.autoconfigure; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.mongodb.MongoDBContainer; + +import org.springframework.batch.core.job.Job; +import org.springframework.batch.core.job.JobExecution; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.job.parameters.JobParameters; +import org.springframework.batch.core.job.parameters.JobParametersBuilder; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.infrastructure.repeat.RepeatStatus; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.data.mongodb.autoconfigure.DataMongoAutoConfiguration; +import org.springframework.boot.mongodb.autoconfigure.MongoAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.container.TestImage; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.core.MongoOperations; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link BatchDataMongoAutoConfiguration}. + * + * @author Stephane Nicoll + */ +@Testcontainers(disabledWithoutDocker = true) +class BatchDataMongoAutoConfigurationIntegrationTests { + + @Container + static final MongoDBContainer mongoDb = TestImage.container(MongoDBContainer.class).withReplicaSet(); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(JobConfiguration.class) + .withPropertyValues("spring.batch.data.mongodb.schema.initialize=true", + "spring.mongodb.uri=" + mongoDb.getReplicaSetUrl()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, DataMongoAutoConfiguration.class, + BatchDataMongoAutoConfiguration.class)); + + @Test + void runJob() { + this.contextRunner.withUserConfiguration(JobConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class) + .hasSingleBean(JobRepository.class) + .hasSingleBean(Job.class); + + JobParameters jobParameters = new JobParametersBuilder().addString("name", "foo") + .addLocalDateTime("runtime", LocalDateTime.now()) + .toJobParameters(); + JobExecution jobExecution = context.getBean(JobOperator.class) + .start(context.getBean(Job.class), jobParameters); + + assertThat(jobExecution).isNotNull(); + assertThat(context.getBean(JobRepository.class).getLastJobExecution("job", jobParameters)).isNotNull(); + assertThat(context.getBean(MongoOperations.class).getCollection("BATCH_JOB_EXECUTION").countDocuments()) + .isPositive(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class JobConfiguration { + + @Bean + Job job(JobRepository jobRepository) { + return new JobBuilder("job", jobRepository) + .start(new StepBuilder("step1", jobRepository) + .tasklet((contribution, chunkContext) -> RepeatStatus.FINISHED) + .build()) + .build(); + } + + } + +} diff --git a/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfiguration.java b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfiguration.java new file mode 100644 index 000000000000..9638437abb40 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfiguration.java @@ -0,0 +1,149 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.batch.mongodb.autoconfigure; + +import com.mongodb.client.MongoClient; +import org.jspecify.annotations.Nullable; + +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration; +import org.springframework.batch.core.configuration.support.MongoDefaultBatchConfiguration; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.batch.autoconfigure.BatchAutoConfiguration; +import org.springframework.boot.batch.autoconfigure.BatchJobLauncherAutoConfiguration; +import org.springframework.boot.batch.autoconfigure.BatchTaskExecutor; +import org.springframework.boot.batch.autoconfigure.BatchTransactionManager; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.boot.data.mongodb.autoconfigure.DataMongoAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.task.TaskExecutor; +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.MongoTransactionManager; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.transaction.annotation.Isolation; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Spring Batch using Data MongoDB. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +@AutoConfiguration(before = { BatchAutoConfiguration.class, BatchJobLauncherAutoConfiguration.class }, + after = DataMongoAutoConfiguration.class) +@ConditionalOnClass({ JobOperator.class, MongoClient.class, MongoOperations.class }) +@ConditionalOnBean(MongoDatabaseFactory.class) +@ConditionalOnMissingBean(value = DefaultBatchConfiguration.class, annotation = EnableBatchProcessing.class) +@EnableConfigurationProperties(BatchDataMongoProperties.class) +public final class BatchDataMongoAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty("spring.batch.data.mongodb.schema.initialize") + @ConditionalOnBean(MongoOperations.class) + @Import(JobRepositoryDependsOnSchemaInitializationDetector.class) + static class BatchMongoDatabaseInitializerConfiguration { + + @Bean(name = JobRepositoryDependsOnSchemaInitializationDetector.SCHEMA_INITIALIZATION_BEAN_NAME) + InitializingBean batchMongoDataInitializingBean(MongoOperations mongoOperations, + BatchDataMongoProperties properties) { + return () -> { + BatchMongoSchemaInitializer initializer = new BatchMongoSchemaInitializer(mongoOperations); + String schemaLocation = properties.getSchema().getLocation(); + Resource resource = new ClassPathResource(schemaLocation); + if (!resource.exists()) { + throw new InvalidConfigurationPropertyValueException("spring.batch.data.mongodb.schema.location", + schemaLocation, "resource does not exist"); + } + initializer.initialize(resource); + }; + } + + } + + @Configuration(proxyBeanMethods = false) + static class SpringBootBatchMongoConfiguration extends MongoDefaultBatchConfiguration { + + private final MongoOperations mongoOperations; + + private final MongoTransactionManager transactionManager; + + private final @Nullable TaskExecutor taskExecutor; + + private final BatchDataMongoProperties properties; + + SpringBootBatchMongoConfiguration(MongoDatabaseFactory mongoDatabaseFactory, + ObjectProvider transactionManager, + @BatchTransactionManager ObjectProvider batchTransactionManager, + @BatchTaskExecutor ObjectProvider batchTaskExecutor, + BatchDataMongoProperties properties) { + this.mongoOperations = createMongoOperations(mongoDatabaseFactory); + this.transactionManager = batchTransactionManager.getIfAvailable( + () -> transactionManager.getIfAvailable(() -> new MongoTransactionManager(mongoDatabaseFactory))); + this.taskExecutor = batchTaskExecutor.getIfAvailable(); + this.properties = properties; + } + + private static MongoTemplate createMongoOperations(MongoDatabaseFactory mongoDatabaseFactory) { + MongoTemplate template = new MongoTemplate(mongoDatabaseFactory); + MappingMongoConverter converter = (MappingMongoConverter) template.getConverter(); + converter.setMapKeyDotReplacement("."); + return template; + } + + @Override + protected MongoOperations getMongoOperations() { + return this.mongoOperations; + } + + @Override + protected MongoTransactionManager getTransactionManager() { + return this.transactionManager; + } + + @Override + protected boolean getValidateTransactionState() { + return this.properties.isValidateTransactionState(); + } + + @Override + protected Isolation getIsolationLevelForCreate() { + Isolation isolation = this.properties.getIsolationLevelForCreate(); + return (isolation != null) ? isolation : super.getIsolationLevelForCreate(); + } + + @Override + protected TaskExecutor getTaskExecutor() { + return (this.taskExecutor != null) ? this.taskExecutor : super.getTaskExecutor(); + } + + } + +} diff --git a/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoProperties.java b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoProperties.java new file mode 100644 index 000000000000..47d742b02c21 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoProperties.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.batch.mongodb.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.transaction.annotation.Isolation; + +/** + * Configuration properties for Spring Batch using Data MongoDB. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +@ConfigurationProperties("spring.batch.data.mongodb") +public class BatchDataMongoProperties { + + /** + * Whether to validate the transaction state. + */ + private boolean validateTransactionState = true; + + /** + * Transaction isolation level to use when creating job metadata for new jobs. + */ + private @Nullable Isolation isolationLevelForCreate; + + private final Schema schema = new Schema(); + + public boolean isValidateTransactionState() { + return this.validateTransactionState; + } + + public void setValidateTransactionState(boolean validateTransactionState) { + this.validateTransactionState = validateTransactionState; + } + + public @Nullable Isolation getIsolationLevelForCreate() { + return this.isolationLevelForCreate; + } + + public void setIsolationLevelForCreate(@Nullable Isolation isolationLevelForCreate) { + this.isolationLevelForCreate = isolationLevelForCreate; + } + + public Schema getSchema() { + return this.schema; + } + + public static class Schema { + + /** + * Path to the newline-delimited JSON script used to create the Spring Batch job + * repository collections and indexes in MongoDB. + */ + private String location = "org/springframework/batch/core/schema-mongodb.jsonl"; + + /** + * Whether to initialize the Spring Batch job repository schema in MongoDB. + */ + private boolean initialize; + + public String getLocation() { + return this.location; + } + + public void setLocation(String location) { + this.location = location; + } + + public boolean isInitialize() { + return this.initialize; + } + + public void setInitialize(boolean initialize) { + this.initialize = initialize; + } + + } + +} diff --git a/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializationException.java b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializationException.java new file mode 100644 index 000000000000..ff8e375d5b9b --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializationException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.batch.mongodb.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.dao.DataAccessException; + +/** + * Thrown when the Spring Batch repository schema cannot be initialized. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +public class BatchMongoSchemaInitializationException extends DataAccessException { + + public BatchMongoSchemaInitializationException(String msg, @Nullable Throwable cause) { + super(msg, cause); + } + +} diff --git a/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializer.java b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializer.java new file mode 100644 index 000000000000..70d2df1ae51a --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializer.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.batch.mongodb.autoconfigure; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.Resource; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.util.StreamUtils; + +/** + * Initializes the Spring Batch job repository schema (collections and indexes) in MongoDB + * by executing commands from a newline-delimited JSON script. + * + * @author Stephane Nicoll + */ +class BatchMongoSchemaInitializer { + + private static final Log logger = LogFactory.getLog(BatchMongoSchemaInitializer.class); + + private final MongoOperations mongoOperations; + + BatchMongoSchemaInitializer(MongoOperations mongoOperations) { + this.mongoOperations = mongoOperations; + } + + void initialize(Resource schema) throws IOException { + StreamUtils.copyToString(schema.getInputStream(), StandardCharsets.UTF_8) + .lines() + .filter((line) -> !line.isBlank()) + .forEach((command) -> { + try { + executeCommand(command); + } + catch (Exception ex) { + throw new BatchMongoSchemaInitializationException( + "Failed to initialize Batch repository schema using '%s', command failed: %s" + .formatted(schema, command), + ex); + } + }); + } + + private void executeCommand(String command) { + if (logger.isTraceEnabled()) { + logger.trace("Executing command: " + command); + } + this.mongoOperations.executeCommand(command); + } + +} diff --git a/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/JobRepositoryDependsOnSchemaInitializationDetector.java b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/JobRepositoryDependsOnSchemaInitializationDetector.java new file mode 100644 index 000000000000..6d9eb02fd1ee --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/JobRepositoryDependsOnSchemaInitializationDetector.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.batch.mongodb.autoconfigure; + +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.boot.autoconfigure.AbstractDependsOnBeanFactoryPostProcessor; + +/** + * {@link AbstractDependsOnBeanFactoryPostProcessor} implementation that makes sure Spring + * Batch's {@link JobRepository} depends on schema initialization. + * + * @author Stephane Nicoll + */ +class JobRepositoryDependsOnSchemaInitializationDetector extends AbstractDependsOnBeanFactoryPostProcessor { + + /** + * Bean name responsible for schema initialization. + */ + static final String SCHEMA_INITIALIZATION_BEAN_NAME = "batchMongoDataInitializingBean"; + + JobRepositoryDependsOnSchemaInitializationDetector() { + super(JobRepository.class, SCHEMA_INITIALIZATION_BEAN_NAME); + } + +} diff --git a/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/package-info.java b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/package-info.java new file mode 100644 index 000000000000..b4c9a93c093d --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 Batch with Data MongoDB. + */ +@NullMarked +package org.springframework.boot.batch.mongodb.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-batch-data-mongodb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-batch-data-mongodb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000000..86c6e8af1b38 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springframework.boot.batch.mongodb.autoconfigure.BatchDataMongoAutoConfiguration diff --git a/module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationTests.java b/module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationTests.java new file mode 100644 index 000000000000..a16e6ac90037 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.batch.mongodb.autoconfigure; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.batch.autoconfigure.BatchTransactionManager; +import org.springframework.boot.batch.mongodb.autoconfigure.BatchDataMongoAutoConfiguration.SpringBootBatchMongoConfiguration; +import org.springframework.boot.data.mongodb.autoconfigure.DataMongoAutoConfiguration; +import org.springframework.boot.mongodb.autoconfigure.MongoAutoConfiguration; +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.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.MongoTransactionManager; +import org.springframework.data.mongodb.core.MongoExceptionTranslator; +import org.springframework.data.mongodb.core.MongoOperations; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; + +/** + * Tests for {@link BatchDataMongoAutoConfiguration}. + * + * @author Stephane Nicoll + */ +class BatchDataMongoAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(BatchDataMongoAutoConfiguration.class)); + + @Test + void autoConfigurationWithSpringDataMongoDb() { + this.contextRunner + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, DataMongoAutoConfiguration.class)) + .run((context) -> assertThat(context).hasSingleBean(JobRepository.class).hasSingleBean(JobOperator.class)); + } + + @Test + void autoConfigurationOnlyRequiresMongoDatabaseFactory() { + this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory) + .run((context) -> assertThat(context).hasSingleBean(JobRepository.class).hasSingleBean(JobOperator.class)); + } + + @Test + void autConfigurationUsesMainTransactionManager() { + MongoTransactionManager transactionManager = mock(MongoTransactionManager.class); + this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory) + .withBean(MongoTransactionManager.class, () -> transactionManager) + .run((context) -> assertThat( + context.getBean(SpringBootBatchMongoConfiguration.class).getTransactionManager()) + .isSameAs(transactionManager)); + } + + @Test + void autConfigurationFavorsBatchTransactionManager() { + MongoTransactionManager transactionManager = mock(MongoTransactionManager.class); + this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory) + .withBean(MongoTransactionManager.class, () -> transactionManager) + .withUserConfiguration(BatchTransactionManagerConfiguration.class) + .run((context) -> { + assertThat(context.getBeansOfType(MongoTransactionManager.class)).hasSize(2); + assertThat(context.getBean(SpringBootBatchMongoConfiguration.class).getTransactionManager()) + .isSameAs(context.getBean("customTransactionManager")); + }); + } + + @Test + void autConfigurationCreatesBatchTransactionManagerIfNecessary() { + MongoDatabaseFactory mongoDatabaseFactory = mockMongoDatabaseFactory(); + this.contextRunner.withBean(MongoDatabaseFactory.class, () -> mongoDatabaseFactory).run((context) -> { + assertThat(context).doesNotHaveBean(MongoTransactionManager.class); + assertThat(context.getBean(SpringBootBatchMongoConfiguration.class).getTransactionManager()) + .satisfies((mongoTransactionManager) -> assertThat(mongoTransactionManager.getDatabaseFactory()) + .isSameAs(mongoDatabaseFactory)); + }); + } + + @Test + void autoconfigurationBacksOffEntirelyIfSpringMongoDbAbsent() { + this.contextRunner + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, DataMongoAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(MongoOperations.class)) + .run((context) -> assertThat(context).doesNotHaveBean(JobRepository.class) + .doesNotHaveBean(JobOperator.class)); + } + + @Test + void autoConfigurationBacksOfEntirelyIfSpringMongoDbIsNotConfigured() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(JobRepository.class) + .doesNotHaveBean(JobOperator.class)); + } + + @Test + void autoConfigurationBacksOffWhenUserEnablesBatchProcessing() { + this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory) + .withUserConfiguration(EnableBatchProcessingConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(SpringBootBatchMongoConfiguration.class)); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesBatchConfiguration() { + this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory) + .withUserConfiguration(CustomBatchConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(SpringBootBatchMongoConfiguration.class)); + } + + @Test + void schemaInitializerBeanNotCreatedByDefault() { + this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory) + .run((context) -> assertThat(context) + .doesNotHaveBean(JobRepositoryDependsOnSchemaInitializationDetector.SCHEMA_INITIALIZATION_BEAN_NAME)); + } + + @Test + void schemaInitializerBeanCreatedWhenSchemaInitializeEnabled() { + MongoOperations mongoOperations = mock(MongoOperations.class); + this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory) + .withBean(MongoOperations.class, () -> mongoOperations) + .withPropertyValues("spring.batch.data.mongodb.schema.initialize=true") + .run((context) -> { + assertThat(context) + .hasBean(JobRepositoryDependsOnSchemaInitializationDetector.SCHEMA_INITIALIZATION_BEAN_NAME); + // see org/springframework/batch/core/schema-mongodb.jsonl + then(mongoOperations).should(times(7)).executeCommand(anyString()); + }); + } + + @Test + void jobRepositoryDependsOnSchemaInitializerWhenSchemaInitializationEnabled() { + this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory) + .withBean(MongoOperations.class, Mockito::mock) + .withPropertyValues("spring.batch.data.mongodb.schema.initialize=true") + .run((context) -> { + assertThat(context).hasSingleBean(JobRepository.class); + BeanDefinition jobRepositoryDefinition = context.getBeanFactory().getBeanDefinition("jobRepository"); + assertThat(jobRepositoryDefinition.getDependsOn()) + .contains(JobRepositoryDependsOnSchemaInitializationDetector.SCHEMA_INITIALIZATION_BEAN_NAME); + }); + } + + @Test + void schemaInitializationFailsWhenSchemaLocationDoesNotExist() { + this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory) + .withBean(MongoOperations.class, Mockito::mock) + .withPropertyValues("spring.batch.data.mongodb.schema.initialize=true", + "spring.batch.data.mongodb.schema.location=classpath:does/not/exist.jsonl") + .run((context) -> assertThat(context).getFailure() + .hasRootCauseInstanceOf( + org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException.class) + .rootCause() + .hasMessageContaining("spring.batch.data.mongodb.schema.location") + .hasMessageContaining("resource does not exist")); + } + + private MongoDatabaseFactory mockMongoDatabaseFactory() { + MongoDatabaseFactory factory = mock(MongoDatabaseFactory.class); + given(factory.getExceptionTranslator()).willReturn(MongoExceptionTranslator.DEFAULT_EXCEPTION_TRANSLATOR); + return factory; + } + + @EnableBatchProcessing + @Configuration(proxyBeanMethods = false) + static class EnableBatchProcessingConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + static class CustomBatchConfiguration extends DefaultBatchConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + static class BatchTransactionManagerConfiguration { + + @Bean + @BatchTransactionManager + MongoTransactionManager customTransactionManager() { + return mock(MongoTransactionManager.class); + } + + } + +} diff --git a/module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializerTests.java b/module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializerTests.java new file mode 100644 index 000000000000..1e3fc3f8881a --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializerTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.batch.mongodb.autoconfigure; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.data.mongodb.core.MongoOperations; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BatchMongoSchemaInitializer}. + * + * @author Stephane Nicoll + */ +class BatchMongoSchemaInitializerTests { + + @Test + void initializeExecutesEachNonBlankLine() throws IOException { + MongoOperations mongoOperations = mock(MongoOperations.class); + BatchMongoSchemaInitializer initializer = new BatchMongoSchemaInitializer(mongoOperations); + Resource schema = resource("{\"create\": \"batch_job_instance\"}\n{\"create\": \"batch_job_execution\"}"); + + initializer.initialize(schema); + + then(mongoOperations).should().executeCommand("{\"create\": \"batch_job_instance\"}"); + then(mongoOperations).should().executeCommand("{\"create\": \"batch_job_execution\"}"); + } + + @Test + void initializeSkipsBlankLines() throws IOException { + MongoOperations mongoOperations = mock(MongoOperations.class); + BatchMongoSchemaInitializer initializer = new BatchMongoSchemaInitializer(mongoOperations); + Resource schema = resource("{\"create\": \"col1\"}\n\n \n{\"create\": \"col2\"}"); + + initializer.initialize(schema); + + then(mongoOperations).should().executeCommand("{\"create\": \"col1\"}"); + then(mongoOperations).should().executeCommand("{\"create\": \"col2\"}"); + } + + @Test + void initializeThrowsWhenCommandFails() { + MongoOperations mongoOperations = mock(MongoOperations.class); + given(mongoOperations.executeCommand(anyString())).willThrow(new RuntimeException("Command failed")); + BatchMongoSchemaInitializer initializer = new BatchMongoSchemaInitializer(mongoOperations); + Resource schema = resource("{\"create\": \"batch_job_instance\"}"); + + assertThatExceptionOfType(BatchMongoSchemaInitializationException.class) + .isThrownBy(() -> initializer.initialize(schema)) + .withMessageContaining("Failed to initialize Batch repository schema") + .withMessageContaining("{\"create\": \"batch_job_instance\"}") + .withCauseInstanceOf(RuntimeException.class); + } + + private static Resource resource(String content) { + return new InputStreamResource(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))); + } + +} diff --git a/settings.gradle b/settings.gradle index 99feb9ce1617..82506ce30b77 100644 --- a/settings.gradle +++ b/settings.gradle @@ -85,6 +85,7 @@ include "module:spring-boot-artemis" include "module:spring-boot-autoconfigure-classic" include "module:spring-boot-autoconfigure-classic-modules" include "module:spring-boot-batch" +include "module:spring-boot-batch-data-mongodb" include "module:spring-boot-batch-jdbc" include "module:spring-boot-cache" include "module:spring-boot-cache-test" @@ -214,6 +215,8 @@ include "starter:spring-boot-starter-artemis-test" include "starter:spring-boot-starter-aspectj" include "starter:spring-boot-starter-aspectj-test" include "starter:spring-boot-starter-batch" +include "starter:spring-boot-starter-batch-data-mongodb" +include "starter:spring-boot-starter-batch-data-mongodb-test" include "starter:spring-boot-starter-batch-jdbc" include "starter:spring-boot-starter-batch-jdbc-test" include "starter:spring-boot-starter-batch-test" @@ -396,6 +399,7 @@ include ":smoke-test:spring-boot-smoke-test-artemis" include ":smoke-test:spring-boot-smoke-test-aspectj" include ":smoke-test:spring-boot-smoke-test-autoconfigure-classic" include ":smoke-test:spring-boot-smoke-test-batch" +include ":smoke-test:spring-boot-smoke-test-batch-data-mongodb" include ":smoke-test:spring-boot-smoke-test-batch-jdbc" include ":smoke-test:spring-boot-smoke-test-bootstrap-registry" include ":smoke-test:spring-boot-smoke-test-cache" diff --git a/smoke-test/spring-boot-smoke-test-batch-data-mongodb/build.gradle b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/build.gradle new file mode 100644 index 000000000000..47eaf9992f08 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/build.gradle @@ -0,0 +1,34 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java" +} + +description = "Spring Boot Batch with Data MongoDB smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-batch-data-mongodb")) + + testImplementation(project(":starter:spring-boot-starter-batch-data-mongodb-test")) + testImplementation(project(":test-support:spring-boot-docker-test-support")) + testImplementation("org.testcontainers:testcontainers-junit-jupiter") + testImplementation("org.testcontainers:testcontainers-mongodb") +} + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} diff --git a/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/SampleBatchApplication.java b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/SampleBatchApplication.java new file mode 100644 index 000000000000..93a4190821c1 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/SampleBatchApplication.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.batch; + +import org.springframework.batch.core.job.Job; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.infrastructure.repeat.RepeatStatus; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class SampleBatchApplication { + + @Bean + Job job(JobRepository jobRepository) { + return new JobBuilder("job", jobRepository) + .start(new StepBuilder("step1", jobRepository) + .tasklet((contribution, chunkContext) -> RepeatStatus.FINISHED) + .build()) + .build(); + } + + public static void main(String[] args) { + // System.exit is common for Batch applications since the exit code can be used to + // drive a workflow + System.exit(SpringApplication.exit(SpringApplication.run(SampleBatchApplication.class, args))); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/package-info.java b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/package-info.java new file mode 100644 index 000000000000..fd87f1b1cbd1 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NullMarked +package smoketest.batch; + +import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/test/java/smoketest/batch/SampleBatchApplicationTests.java b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/test/java/smoketest/batch/SampleBatchApplicationTests.java new file mode 100644 index 000000000000..5dd7b1d3bcb4 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/test/java/smoketest/batch/SampleBatchApplicationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.batch; + +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.testcontainers.mongodb.MongoDBContainer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.testsupport.container.TestImage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Smoke tests for Spring Batch with MongoDB. + * + * @author Stephane Nicoll + */ +@ExtendWith(OutputCaptureExtension.class) +@Testcontainers(disabledWithoutDocker = true) +class SampleBatchApplicationTests { + + @Container + static final MongoDBContainer mongoDb = TestImage.container(MongoDBContainer.class).withReplicaSet(); + + @Test + void testDefaultSettings(CapturedOutput output) { + int exitCode = SpringApplication.exit(SpringApplication.run(SampleBatchApplication.class, + "--spring.mongodb.uri=" + mongoDb.getReplicaSetUrl(), + "--spring.batch.data.mongodb.schema.initialize=true")); + assertThat(exitCode).isZero(); + assertThat(output).contains("completed with the following parameters"); + } + +} diff --git a/starter/spring-boot-starter-batch-data-mongodb-test/build.gradle b/starter/spring-boot-starter-batch-data-mongodb-test/build.gradle new file mode 100644 index 000000000000..3d147580a4a1 --- /dev/null +++ b/starter/spring-boot-starter-batch-data-mongodb-test/build.gradle @@ -0,0 +1,26 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for testing using Spring Batch with Data MongoDB" + +dependencies { + api(project(":starter:spring-boot-starter-batch-test")) + api(project(":starter:spring-boot-starter-data-mongodb-test")) +} diff --git a/starter/spring-boot-starter-batch-data-mongodb/build.gradle b/starter/spring-boot-starter-batch-data-mongodb/build.gradle new file mode 100644 index 000000000000..41f6f95c98be --- /dev/null +++ b/starter/spring-boot-starter-batch-data-mongodb/build.gradle @@ -0,0 +1,28 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring Batch with Data MongoDB" + +dependencies { + api(project(":starter:spring-boot-starter-batch")) + api(project(":starter:spring-boot-starter-data-mongodb")) + + api(project(":module:spring-boot-batch-data-mongodb")) +} From 980f97b6e0a8f31cfc6db39cbd613f07fffe4b6d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Mar 2026 11:40:14 +0000 Subject: [PATCH 346/930] Upgrade to Glassfish JAXB 4.0.7 Closes gh-49610 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index fdf24a110483..ffbb46db9f89 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -484,7 +484,7 @@ bom { releaseNotes("https://github.com/git-commit-id/git-commit-id-maven-plugin/releases/tag/v{version}") } } - library("Glassfish JAXB", "4.0.6") { + library("Glassfish JAXB", "4.0.7") { alignWith { dependencyManagementDeclaredIn("org.glassfish.jaxb:jaxb-bom") { excluding { candidate -> candidate.classifier().equals("sources") } From 6da3b1b41bd2a944d6059f85e2ed8177409e63bf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Mar 2026 11:40:19 +0000 Subject: [PATCH 347/930] Upgrade to Hibernate 7.2.7.Final Closes gh-49611 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ffbb46db9f89..e24f489621f1 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -611,7 +611,7 @@ bom { releaseNotes("https://github.com/hazelcast/hazelcast/releases/tag/v{version}") } } - library("Hibernate", "7.2.6.Final") { + library("Hibernate", "7.2.7.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From 1b2af2796488101d8612a7adb1c70ed0f939c0f6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Mar 2026 11:40:23 +0000 Subject: [PATCH 348/930] Upgrade to Jakarta XML Bind 4.0.5 Closes gh-49612 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e24f489621f1..4e5c2cf1ae87 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -948,7 +948,7 @@ bom { .formatted(version.major(), version.minor()), "jakarta.ws.rs") } } - library("Jakarta XML Bind", "4.0.4") { + library("Jakarta XML Bind", "4.0.5") { alignWith { version { from "org.glassfish.jaxb:jaxb-core" From 85c8432baf0609394ba3246e719254470609e612 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Mar 2026 11:40:28 +0000 Subject: [PATCH 349/930] Upgrade to Kotlin 2.3.20 Closes gh-49613 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b9e7437f1f59..11e584a9b01a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ jackson2Version=2.21.1 jacksonVersion=3.1.0 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 -kotlinVersion=2.3.10 +kotlinVersion=2.3.20 mavenVersion=3.9.13 mockitoVersion=5.23.0 nativeBuildToolsVersion=0.11.5 From a904ce939a68bee3537cf20a3e72a9e2dcedfb78 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Mar 2026 11:40:32 +0000 Subject: [PATCH 350/930] Upgrade to SQLite JDBC 3.51.3.0 Closes gh-49614 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4e5c2cf1ae87..cb78a9f20ff0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2658,7 +2658,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ws/releases/tag/v{version}") } } - library("SQLite JDBC", "3.51.2.0") { + library("SQLite JDBC", "3.51.3.0") { group("org.xerial") { modules = [ "sqlite-jdbc" From 23e671f1cdd579abb0e19f69508979349c864cd5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Mar 2026 13:19:47 +0000 Subject: [PATCH 351/930] Revert "Upgrade to Kotlin 2.3.20" This reverts commit 85c8432baf0609394ba3246e719254470609e612. The upgrade breaks various tests in spring-boot-gradle-plugin that use the Kotlin DSL. See gh-49613 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 11e584a9b01a..b9e7437f1f59 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ jackson2Version=2.21.1 jacksonVersion=3.1.0 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 -kotlinVersion=2.3.20 +kotlinVersion=2.3.10 mavenVersion=3.9.13 mockitoVersion=5.23.0 nativeBuildToolsVersion=0.11.5 From 255626f29ed119bb34023909043e0a66d7a5ddb5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Mar 2026 14:08:30 +0000 Subject: [PATCH 352/930] Upgrade to Kotlin 2.3.20 Closes gh-49613 --- .../boot/gradle/testkit/PluginClasspathGradleBuild.java | 3 +-- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java index a9f617a72647..1e7785c35650 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java @@ -86,10 +86,9 @@ private List pluginClasspath() { if (this.kotlin) { classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.cli.common.PropertiesKt"))); classpath.add(new File(pathOfJarContaining(KotlinToolingVersion.class))); - classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.build.report.metrics.BuildTime"))); + classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.build.report.metrics.BuildTimes"))); classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.buildtools.api.CompilationService"))); classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.daemon.client.KotlinCompilerClient"))); - classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.konan.library.KonanLibrary"))); classpath.add(new File(pathOfJarContaining(KotlinCompilerPluginSupportPlugin.class))); classpath.add(new File(pathOfJarContaining(LanguageSettings.class))); classpath.add(new File(pathOfJarContaining(BuildUidService.class))); diff --git a/gradle.properties b/gradle.properties index b9e7437f1f59..11e584a9b01a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ jackson2Version=2.21.1 jacksonVersion=3.1.0 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 -kotlinVersion=2.3.10 +kotlinVersion=2.3.20 mavenVersion=3.9.13 mockitoVersion=5.23.0 nativeBuildToolsVersion=0.11.5 From db9130b2a660c790a7b2bfc8ad0fa00738986d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 16 Mar 2026 21:10:29 +0100 Subject: [PATCH 353/930] Upgrade to Spring AMQP 4.1.0-M3 Closes gh-49438 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index cb78a9f20ff0..02a1b96dbac2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2427,7 +2427,7 @@ bom { ] } } - library("Spring AMQP", "4.1.0-SNAPSHOT") { + library("Spring AMQP", "4.1.0-M3") { considerSnapshots() group("org.springframework.amqp") { bom("spring-amqp-bom") From 9f51b398fa782a834ff7948b1059e4e504d2b64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 16 Mar 2026 21:10:29 +0100 Subject: [PATCH 354/930] Upgrade to Spring Data Bom 2026.0.0-M2 Closes gh-49440 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 02a1b96dbac2..c7a35fa1b58f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2457,7 +2457,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-batch/releases/tag/v{version}") } } - library("Spring Data Bom", "2026.0.0-SNAPSHOT") { + library("Spring Data Bom", "2026.0.0-M2") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From b6adeeb37ddd7d83d09910298d70d96d2e78cf15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 16 Mar 2026 21:10:29 +0100 Subject: [PATCH 355/930] Upgrade to Spring LDAP 4.1.0-M2 Closes gh-49444 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c7a35fa1b58f..08c390af7267 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2555,7 +2555,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-kafka/releases/tag/v{version}") } } - library("Spring LDAP", "4.1.0-SNAPSHOT") { + library("Spring LDAP", "4.1.0-M2") { considerSnapshots() group("org.springframework.ldap") { modules = [ From 9c23da79f037266a1db78b365445d62e754b3e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 16 Mar 2026 21:10:30 +0100 Subject: [PATCH 356/930] Upgrade to Spring Security 7.1.0-M3 Closes gh-49446 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 08c390af7267..45eb53f48c60 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2605,7 +2605,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-restdocs/releases/tag/v{version}") } } - library("Spring Security", "7.1.0-SNAPSHOT") { + library("Spring Security", "7.1.0-M3") { considerSnapshots() group("org.springframework.security") { bom("spring-security-bom") From 040e19eeeefbb19747d39b28facf2f1b73b09ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 16 Mar 2026 21:13:02 +0100 Subject: [PATCH 357/930] Revert "Upgrade to Elasticsearch Client 9.3.2" This reverts commit 5d54a0a145c1fa411598619e42c9f6dc47bd7d6b. See gh-49603 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 45eb53f48c60..83b35055516b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -401,7 +401,7 @@ bom { releaseNotes("https://github.com/ehcache/ehcache3/releases/tag/v{version}") } } - library("Elasticsearch Client", "9.3.2") { + library("Elasticsearch Client", "9.3.1") { prohibit { contains "-alpha" contains "-beta" From 67b25457f6d83fefb4d727e474b4d7ffb3a03913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 17 Mar 2026 09:28:26 +0100 Subject: [PATCH 358/930] Upgrade to Infinispan 16.1.2 Closes gh-49626 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 83b35055516b..2945c69392de 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -730,7 +730,7 @@ bom { ] } } - library("Infinispan", "16.1.1") { + library("Infinispan", "16.1.2") { group("org.infinispan") { bom("infinispan-bom") } From 065ebe5e0eed3e12626ff7400dcd34948407a7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 17 Mar 2026 09:28:26 +0100 Subject: [PATCH 359/930] Upgrade to Spring Kafka 4.1.0-M2 Closes gh-49443 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 2945c69392de..c6be1164c25d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2537,7 +2537,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-integration/releases/tag/v{version}") } } - library("Spring Kafka", "4.1.0-SNAPSHOT") { + library("Spring Kafka", "4.1.0-M2") { considerSnapshots() group("org.springframework.kafka") { modules = [ From 18cd0a277b5a4b62bf7339a5fd01c1cb6ac3cad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 17 Mar 2026 09:28:27 +0100 Subject: [PATCH 360/930] Upgrade to Spring Pulsar 2.0.4 Closes gh-49445 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c6be1164c25d..5f4a7da03743 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2575,7 +2575,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ldap/releases/tag/{version}") } } - library("Spring Pulsar", "2.0.4-SNAPSHOT") { + library("Spring Pulsar", "2.0.4") { considerSnapshots() group("org.springframework.pulsar") { bom("spring-pulsar-bom") From f4c595dad813423c2b2f718ca0f32fa9dd7f45b0 Mon Sep 17 00:00:00 2001 From: Jay Choi Date: Thu, 12 Feb 2026 11:54:32 +0900 Subject: [PATCH 361/930] Add SSL support to auto-configuration for Rabbit Streams See gh-48960 Signed-off-by: Jay Choi --- .../amqp/autoconfigure/RabbitProperties.java | 45 +++++++ .../RabbitStreamConfiguration.java | 57 +++++++- .../RabbitStreamConnectionDetails.java | 10 ++ ...DockerComposeConnectionDetailsFactory.java | 10 ++ ...reamContainerConnectionDetailsFactory.java | 8 ++ .../autoconfigure/RabbitPropertiesTests.java | 31 +++++ .../RabbitStreamConfigurationTests.java | 123 ++++++++++++++++-- 7 files changed, 270 insertions(+), 14 deletions(-) diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java index 419b6da1c6a0..480bb3b1788f 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java @@ -52,6 +52,7 @@ * @author Scott Frederick * @author Lasse Wulff * @author Yanming Zhou + * @author Jay Choi * @since 4.0.0 */ @ConfigurationProperties("spring.rabbitmq") @@ -1311,6 +1312,11 @@ public static final class Stream { */ private @Nullable String name; + /** + * SSL configuration for RabbitMQ instance with the Stream plugin enabled. + */ + private final StreamSsl ssl = new StreamSsl(); + public String getHost() { return this.host; } @@ -1359,6 +1365,45 @@ public void setName(@Nullable String name) { this.name = name; } + public StreamSsl getSsl() { + return this.ssl; + } + + public static class StreamSsl { + + /** + * Whether to enable SSL support. Enabled automatically if "bundle" is + * provided. + */ + private @Nullable Boolean enabled; + + /** + * SSL bundle name. + */ + private @Nullable String bundle; + + public @Nullable Boolean getEnabled() { + return this.enabled; + } + + public boolean determineEnabled() { + return Boolean.TRUE.equals(getEnabled()) || this.bundle != null; + } + + public void setEnabled(@Nullable Boolean enabled) { + this.enabled = enabled; + } + + public @Nullable String getBundle() { + return this.bundle; + } + + public void setBundle(@Nullable String bundle) { + this.bundle = bundle; + } + + } + } } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java index 0b396dd01f7e..e49039a54f76 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java @@ -16,19 +16,28 @@ package org.springframework.boot.amqp.autoconfigure; +import javax.net.ssl.SSLException; + import com.rabbitmq.stream.Environment; import com.rabbitmq.stream.EnvironmentBuilder; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.config.ContainerCustomizer; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Stream; +import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Stream.StreamSsl; import org.springframework.boot.amqp.autoconfigure.RabbitProperties.StreamContainer; 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.PropertyMapper; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.ssl.SslManagerBundle; +import org.springframework.boot.ssl.SslOptions; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory; @@ -41,12 +50,14 @@ import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Configuration for Spring RabbitMQ Stream plugin support. * * @author Gary Russell * @author Eddú Meléndez + * @author Jay Choi */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(StreamRabbitListenerContainerFactory.class) @@ -54,8 +65,9 @@ class RabbitStreamConfiguration { @Bean @ConditionalOnMissingBean - RabbitStreamConnectionDetails rabbitStreamConnectionDetails(RabbitProperties rabbitProperties) { - return new PropertiesRabbitStreamConnectionDetails(rabbitProperties.getStream()); + RabbitStreamConnectionDetails rabbitStreamConnectionDetails(RabbitProperties rabbitProperties, + @Nullable SslBundles sslBundles) { + return new PropertiesRabbitStreamConnectionDetails(rabbitProperties.getStream(), sslBundles); } @Bean(name = "rabbitListenerContainerFactory") @@ -131,15 +143,41 @@ private static EnvironmentBuilder configure(EnvironmentBuilder builder, RabbitPr .to(builder::virtualHost); map.from(streamConnectionDetails.getUsername()).orFrom(connectionDetails::getUsername).to(builder::username); map.from(streamConnectionDetails.getPassword()).orFrom(connectionDetails::getPassword).to(builder::password); + SslBundle sslBundle = streamConnectionDetails.getSslBundle(); + if (sslBundle != null) { + builder.tls().sslContext(createSslContext(sslBundle)); + } + else if (stream.getSsl().determineEnabled()) { + builder.tls(); + } return builder; } + private static SslContext createSslContext(SslBundle sslBundle) { + SslOptions options = sslBundle.getOptions(); + SslManagerBundle managers = sslBundle.getManagers(); + try { + return SslContextBuilder.forClient() + .keyManager(managers.getKeyManagerFactory()) + .trustManager(managers.getTrustManagerFactory()) + .ciphers(SslOptions.asSet(options.getCiphers())) + .protocols(options.getEnabledProtocols()) + .build(); + } + catch (SSLException ex) { + throw new IllegalStateException("Failed to create SSL context for RabbitMQ Stream", ex); + } + } + static class PropertiesRabbitStreamConnectionDetails implements RabbitStreamConnectionDetails { private final Stream streamProperties; - PropertiesRabbitStreamConnectionDetails(Stream streamProperties) { + private final @Nullable SslBundles sslBundles; + + PropertiesRabbitStreamConnectionDetails(Stream streamProperties, @Nullable SslBundles sslBundles) { this.streamProperties = streamProperties; + this.sslBundles = sslBundles; } @Override @@ -167,6 +205,19 @@ public int getPort() { return this.streamProperties.getPassword(); } + @Override + public @Nullable SslBundle getSslBundle() { + StreamSsl ssl = this.streamProperties.getSsl(); + if (!ssl.determineEnabled()) { + return null; + } + if (StringUtils.hasLength(ssl.getBundle())) { + Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context"); + return this.sslBundles.getBundle(ssl.getBundle()); + } + return null; + } + } } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java index 8b08349167d8..5de8fc8301cd 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java @@ -19,11 +19,13 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.boot.ssl.SslBundle; /** * Details required to establish a connection to a RabbitMQ Stream service. * * @author Eddú Meléndez + * @author Jay Choi * @since 4.1.0 */ public interface RabbitStreamConnectionDetails extends ConnectionDetails { @@ -64,4 +66,12 @@ public interface RabbitStreamConnectionDetails extends ConnectionDetails { return null; } + /** + * SSL bundle to use. + * @return the SSL bundle to use or {@code null} + */ + default @Nullable SslBundle getSslBundle() { + return null; + } + } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java index e482c04c0220..186db12035c7 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java @@ -23,6 +23,7 @@ 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; +import org.springframework.boot.ssl.SslBundle; /** * {@link DockerComposeConnectionDetailsFactory} to create {@link RabbitConnectionDetails} @@ -32,6 +33,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @author Scott Frederick + * @author Jay Choi */ class RabbitStreamDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { @@ -66,11 +68,14 @@ static class RabbitStreamDockerComposeConnectionDetails extends DockerComposeCon private final int port; + private final @Nullable SslBundle sslBundle; + protected RabbitStreamDockerComposeConnectionDetails(RunningService service) { super(service); this.environment = new RabbitEnvironment(service.env()); this.host = service.host(); this.port = service.ports().get(RABBITMQ_STREAMS_PORT); + this.sslBundle = getSslBundle(service); } @Override @@ -98,6 +103,11 @@ public int getPort() { return this.port; } + @Override + public @Nullable SslBundle getSslBundle() { + return this.sslBundle; + } + } } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java index 8d2bf2bc9dc4..6f321c774604 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java @@ -16,9 +16,11 @@ package org.springframework.boot.amqp.testcontainers; +import org.jspecify.annotations.Nullable; import org.testcontainers.rabbitmq.RabbitMQContainer; import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -29,6 +31,7 @@ * {@link ServiceConnection @ServiceConnection}-annotated {@link RabbitMQContainer}. * * @author Eddú Meléndez + * @author Jay Choi */ class RabbitStreamContainerConnectionDetailsFactory extends ContainerConnectionDetailsFactory { @@ -81,6 +84,11 @@ public String getPassword() { return getContainer().getAdminPassword(); } + @Override + public @Nullable SslBundle getSslBundle() { + return super.getSslBundle(); + } + } } diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java index cc3f17a5a802..93548c283a41 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java @@ -38,6 +38,7 @@ * @author Stephane Nicoll * @author Rafael Carvalho * @author Scott Frederick + * @author Jay Choi */ class RabbitPropertiesTests { @@ -381,4 +382,34 @@ void hostPropertyMustBeSingleHost() { .withMessageContaining("spring.rabbitmq.host"); } + @Test + void streamSslIsDisabledByDefault() { + assertThat(this.properties.getStream().getSsl().determineEnabled()).isFalse(); + } + + @Test + void streamSslIsEnabledWhenEnabledIsTrue() { + this.properties.getStream().getSsl().setEnabled(true); + assertThat(this.properties.getStream().getSsl().determineEnabled()).isTrue(); + } + + @Test + void streamSslIsEnabledWhenBundleIsSet() { + this.properties.getStream().getSsl().setBundle("test-bundle"); + assertThat(this.properties.getStream().getSsl().determineEnabled()).isTrue(); + } + + @Test + void streamSslIsDisabledWhenEnabledIsFalseAndBundleIsNotSet() { + this.properties.getStream().getSsl().setEnabled(false); + assertThat(this.properties.getStream().getSsl().determineEnabled()).isFalse(); + } + + @Test + void streamSslIsEnabledWhenBundleIsSetButEnabledIsFalse() { + this.properties.getStream().getSsl().setBundle("test-bundle"); + this.properties.getStream().getSsl().setEnabled(false); + assertThat(this.properties.getStream().getSsl().determineEnabled()).isTrue(); + } + } diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java index 088c31f6ccdb..87230b0618c9 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java @@ -25,6 +25,7 @@ import com.rabbitmq.stream.Environment; import com.rabbitmq.stream.EnvironmentBuilder; import org.assertj.core.api.InstanceOfAssertFactories; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.MessageListenerContainer; @@ -35,6 +36,10 @@ import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.boot.amqp.autoconfigure.RabbitStreamConfiguration.PropertiesRabbitStreamConnectionDetails; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.ssl.NoSuchSslBundleException; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -53,8 +58,11 @@ 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.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; /** * Tests for {@link RabbitStreamConfiguration}. @@ -63,11 +71,12 @@ * @author Andy Wilkinson * @author Eddú Meléndez * @author Moritz Halbritter + * @author Jay Choi */ class RabbitStreamConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(RabbitAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(RabbitAutoConfiguration.class, SslAutoConfiguration.class)); @Test @SuppressWarnings("unchecked") @@ -150,7 +159,7 @@ void environmentUsesConnectionDetailsByDefault() { RabbitProperties properties = new RabbitProperties(); RabbitStreamConfiguration.configure(builder, properties, new TestRabbitConnectionDetails("guest", "guest", "vhost"), - getRabbitStreamConnectionDetails(properties)); + getRabbitStreamConnectionDetails(properties, null)); then(builder).should().port(5552); then(builder).should().host("localhost"); then(builder).should().virtualHost("vhost"); @@ -167,7 +176,7 @@ void whenStreamPortIsSetThenEnvironmentUsesCustomPort() { properties.getStream().setPort(5553); RabbitStreamConfiguration.configure(builder, properties, new TestRabbitConnectionDetails("guest", "guest", "vhost"), - getRabbitStreamConnectionDetails(properties)); + getRabbitStreamConnectionDetails(properties, null)); then(builder).should().port(5553); } @@ -178,7 +187,7 @@ void whenStreamHostIsSetThenEnvironmentUsesCustomHost() { properties.getStream().setHost("stream.rabbit.example.com"); RabbitStreamConfiguration.configure(builder, properties, new TestRabbitConnectionDetails("guest", "guest", "vhost"), - getRabbitStreamConnectionDetails(properties)); + getRabbitStreamConnectionDetails(properties, null)); then(builder).should().host("stream.rabbit.example.com"); } @@ -189,7 +198,7 @@ void whenStreamVirtualHostIsSetThenEnvironmentUsesCustomVirtualHost() { properties.getStream().setVirtualHost("stream-virtual-host"); RabbitStreamConfiguration.configure(builder, properties, new TestRabbitConnectionDetails("guest", "guest", "vhost"), - getRabbitStreamConnectionDetails(properties)); + getRabbitStreamConnectionDetails(properties, null)); then(builder).should().virtualHost("stream-virtual-host"); } @@ -200,7 +209,7 @@ void whenStreamVirtualHostIsNotSetButDefaultVirtualHostIsSetThenEnvironmentUsesD properties.setVirtualHost("default-virtual-host"); RabbitStreamConfiguration.configure(builder, properties, new TestRabbitConnectionDetails("guest", "guest", "default-virtual-host"), - getRabbitStreamConnectionDetails(properties)); + getRabbitStreamConnectionDetails(properties, null)); then(builder).should().virtualHost("default-virtual-host"); } @@ -212,7 +221,7 @@ void whenStreamCredentialsAreNotSetThenEnvironmentUsesConnectionDetailsCredentia properties.setPassword("secret"); RabbitStreamConfiguration.configure(builder, properties, new TestRabbitConnectionDetails("bob", "password", "vhost"), - getRabbitStreamConnectionDetails(properties)); + getRabbitStreamConnectionDetails(properties, null)); then(builder).should().username("bob"); then(builder).should().password("password"); } @@ -227,7 +236,7 @@ void whenStreamCredentialsAreSetThenEnvironmentUsesStreamCredentials() { properties.getStream().setPassword("confidential"); RabbitStreamConfiguration.configure(builder, properties, new TestRabbitConnectionDetails("guest", "guest", "vhost"), - getRabbitStreamConnectionDetails(properties)); + getRabbitStreamConnectionDetails(properties, null)); then(builder).should().username("bob"); then(builder).should().password("confidential"); } @@ -319,8 +328,92 @@ void connectionDetailsAreApplied() { .containsExactly("rabbitmq", 5555)); } - private RabbitStreamConnectionDetails getRabbitStreamConnectionDetails(RabbitProperties properties) { - return new PropertiesRabbitStreamConnectionDetails(properties.getStream()); + @Test + void whenStreamSslIsNotConfiguredThenTlsIsNotUsed() { + EnvironmentBuilder builder = mock(EnvironmentBuilder.class); + RabbitProperties properties = new RabbitProperties(); + RabbitStreamConfiguration.configure(builder, properties, + new TestRabbitConnectionDetails("guest", "guest", "vhost"), + getRabbitStreamConnectionDetails(properties, null)); + then(builder).should(never()).tls(); + } + + @Test + void whenStreamSslIsEnabledThenTlsIsUsed() { + EnvironmentBuilder builder = mock(EnvironmentBuilder.class); + RabbitProperties properties = new RabbitProperties(); + properties.getStream().getSsl().setEnabled(true); + RabbitStreamConfiguration.configure(builder, properties, + new TestRabbitConnectionDetails("guest", "guest", "vhost"), + getRabbitStreamConnectionDetails(properties, null)); + then(builder).should().tls(); + } + + @Test + void whenStreamSslBundleIsConfiguredThenTlsIsUsed() { + this.contextRunner.withPropertyValues("spring.rabbitmq.stream.ssl.bundle=test-bundle", + "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.password=secret") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(Environment.class); + }); + } + + @Test + void whenStreamSslIsDisabledThenTlsIsNotUsed() { + EnvironmentBuilder builder = mock(EnvironmentBuilder.class); + RabbitProperties properties = new RabbitProperties(); + properties.getStream().getSsl().setEnabled(false); + RabbitStreamConfiguration.configure(builder, properties, + new TestRabbitConnectionDetails("guest", "guest", "vhost"), + getRabbitStreamConnectionDetails(properties, null)); + then(builder).should(never()).tls(); + } + + @Test + void whenStreamSslIsDisabledWithBundleThenTlsIsStillUsed() { + EnvironmentBuilder builder = mock(EnvironmentBuilder.class); + SslBundles sslBundles = mock(SslBundles.class); + RabbitProperties properties = new RabbitProperties(); + properties.getStream().getSsl().setEnabled(false); + properties.getStream().getSsl().setBundle("some-bundle"); + RabbitStreamConfiguration.configure(builder, properties, + new TestRabbitConnectionDetails("guest", "guest", "vhost"), + getRabbitStreamConnectionDetails(properties, sslBundles)); + then(builder).should().tls(); + } + + @Test + void whenConnectionDetailsSslBundleIsProvidedThenTlsIsUsedWithoutProperties() { + this.contextRunner.withUserConfiguration(CustomConnectionDetails.class) + .withPropertyValues( + "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.password=secret") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(Environment.class); + }); + } + + @Test + void whenStreamSslBundleIsInvalidThenFails() { + EnvironmentBuilder builder = mock(EnvironmentBuilder.class); + SslBundles sslBundles = mock(SslBundles.class); + given(sslBundles.getBundle("invalid-bundle")).willThrow( + new NoSuchSslBundleException("invalid-bundle", "SSL bundle name 'invalid-bundle' cannot be found")); + RabbitProperties properties = new RabbitProperties(); + properties.getStream().getSsl().setBundle("invalid-bundle"); + assertThatExceptionOfType(NoSuchSslBundleException.class) + .isThrownBy(() -> RabbitStreamConfiguration.configure(builder, properties, + new TestRabbitConnectionDetails("guest", "guest", "vhost"), + getRabbitStreamConnectionDetails(properties, sslBundles))) + .withMessageContaining("invalid-bundle"); + } + + private RabbitStreamConnectionDetails getRabbitStreamConnectionDetails(RabbitProperties properties, + @Nullable SslBundles sslBundles) { + return new PropertiesRabbitStreamConnectionDetails(properties.getStream(), sslBundles); } @Configuration(proxyBeanMethods = false) @@ -448,7 +541,7 @@ public List

    getAddresses() { static class CustomConnectionDetails { @Bean - RabbitStreamConnectionDetails customRabbitMqStreamConnectionDetails() { + RabbitStreamConnectionDetails customRabbitMqStreamConnectionDetails(SslBundles sslBundles) { return new RabbitStreamConnectionDetails() { @Override public String getHost() { @@ -459,6 +552,14 @@ public String getHost() { public int getPort() { return 5555; } + + @Override + public @Nullable SslBundle getSslBundle() { + if (sslBundles.getBundleNames().contains("test-bundle")) { + return sslBundles.getBundle("test-bundle"); + } + return null; + } }; } From 391c898b49300c8cae5bcab78d942413b1e7fb16 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Mar 2026 11:25:21 +0000 Subject: [PATCH 362/930] Polish "Add SSL support to auto-configuration for Rabbit Streams" See gh-48960 Signed-off-by: Andy Wilkinson --- .../pages/features/dev-services.adoc | 3 +- .../reference/pages/messaging/amqp.adoc | 6 + .../pages/testing/testcontainers.adoc | 3 +- ...nectionDetailsFactoryIntegrationTests.java | 3 +- ...nectionDetailsFactoryIntegrationTests.java | 10 ++ ...nectionDetailsFactoryIntegrationTests.java | 151 ++++++++++++++++++ .../boot/amqp/{docker/compose => }/ca.crt | 0 .../boot/amqp/{docker/compose => }/client.crt | 0 .../boot/amqp/{docker/compose => }/client.key | 0 .../docker/compose/rabbit-ssl-compose.yaml | 2 +- .../compose/rabbit-stream-ssl-compose.yaml | 32 ++++ .../{rabbitmq.conf => rabbitmq-ssl.conf} | 0 .../docker/compose/rabbitmq-stream-ssl.conf | 8 + .../boot/amqp/{docker/compose => }/server.crt | 0 .../boot/amqp/{docker/compose => }/server.key | 0 .../testcontainers/rabbitmq-stream-ssl.conf | 8 + .../amqp/autoconfigure/RabbitProperties.java | 6 +- .../RabbitStreamConfiguration.java | 4 +- ...DockerComposeConnectionDetailsFactory.java | 8 +- ...reamContainerConnectionDetailsFactory.java | 6 +- 20 files changed, 237 insertions(+), 13 deletions(-) create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java rename module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/{docker/compose => }/ca.crt (100%) rename module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/{docker/compose => }/client.crt (100%) rename module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/{docker/compose => }/client.key (100%) create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml rename module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/{rabbitmq.conf => rabbitmq-ssl.conf} (100%) create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf rename module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/{docker/compose => }/server.crt (100%) rename module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/{docker/compose => }/server.key (100%) create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc index fbaab365f52f..dac48926bed9 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc @@ -152,7 +152,8 @@ SSL is supported for the following service connections: * Cassandra * Elasticsearch * MongoDB -* RabbitMQ (excluding streams) +* RabbitMQ +* RabbitMQ Streams * Redis To enable SSL support for a service, you can use https://docs.docker.com/reference/compose-file/services/#labels[service labels]. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index 2f928840c73e..aaa67268da6f 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc @@ -101,6 +101,12 @@ If you need to create more javadoc:org.springframework.rabbit.stream.producer.Ra +[[messaging.amqp.sending-stream.ssl]] +=== SSL +To use SSL with RabbitMQ Streams, set configprop:spring.rabbitmq.stream.ssl.enabled[] to `true` or set configprop:spring.rabbitmq.stream.ssl.bundle[] to configure the xref:features/ssl.adoc#features.ssl.bundles[SSL bundle] to use. + + + [[messaging.amqp.receiving]] == Receiving a Message diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc index 4a71a35fc804..d54476a6a809 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc @@ -235,7 +235,8 @@ The SSL annotations are supported for the following service connections: * Elasticsearch * Kafka * MongoDB -* RabbitMQ (excluding streams) +* RabbitMQ +* RabbitMQ Streams * Redis The `ElasticsearchContainer` additionally supports automatic detection of server side SSL. diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java index cab917ea4c59..4d644b742e48 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -41,7 +41,8 @@ void runCreatesConnectionDetails(RabbitConnectionDetails connectionDetails) { } @DockerComposeTest(composeFile = "rabbit-ssl-compose.yaml", image = TestImage.RABBITMQ, - additionalResources = { "ca.crt", "server.crt", "server.key", "client.crt", "client.key", "rabbitmq.conf" }) + additionalResources = { "../../ca.crt", "../../server.crt", "../../server.key", "../../client.crt", + "../../client.key", "rabbitmq-ssl.conf" }) void runWithSslCreatesConnectionDetails(RabbitConnectionDetails connectionDetails) { assertConnectionDetails(connectionDetails); SslBundle sslBundle = connectionDetails.getSslBundle(); diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index 1547fbf825c4..67f8fad358e6 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -18,6 +18,7 @@ import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; @@ -34,6 +35,15 @@ void runCreatesConnectionDetails(RabbitStreamConnectionDetails connectionDetails assertConnectionDetails(connectionDetails); } + @DockerComposeTest(composeFile = "rabbit-stream-ssl-compose.yaml", image = TestImage.RABBITMQ, + additionalResources = { "../../ca.crt", "../../server.crt", "../../server.key", "../../client.crt", + "../../client.key", "rabbitmq-stream-ssl.conf" }) + void runWithSslCreatesConnectionDetails(RabbitStreamConnectionDetails connectionDetails) { + assertConnectionDetails(connectionDetails); + SslBundle sslBundle = connectionDetails.getSslBundle(); + assertThat(sslBundle).isNotNull(); + } + private void assertConnectionDetails(RabbitStreamConnectionDetails connectionDetails) { assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); assertThat(connectionDetails.getPassword()).isEqualTo("secret"); diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..d439b0fdbf48 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import com.rabbitmq.stream.Address; +import com.rabbitmq.stream.Environment; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.rabbitmq.RabbitMQContainer; +import org.testcontainers.utility.MountableFile; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.PemKeyStore; +import org.springframework.boot.testcontainers.service.connection.PemTrustStore; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.TestImage; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; +import org.springframework.rabbit.stream.support.StreamAdmin; +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 RabbitStreamContainerConnectionDetailsFactory} with SSL. + * + * @author Eddú Meléndez + * @author Andy Wilkinson + */ +@SpringJUnitConfig +@TestPropertySource( + properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) +@Testcontainers(disabledWithoutDocker = true) +class RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests { + + private static final int RABBITMQ_STREAMS_TLS_PORT = 5551; + + @Container + @ServiceConnection(type = RabbitStreamConnectionDetails.class) + @PemTrustStore(certificate = "classpath:org/springframework/boot/amqp/ca.crt") + @PemKeyStore(certificate = "classpath:org/springframework/boot/amqp/client.crt", + privateKey = "classpath:org/springframework/boot/amqp/client.key") + static final RabbitMQContainer rabbit = getRabbitMqStreamContainer(); + + private static RabbitMQContainer getRabbitMqStreamContainer() { + RabbitMQContainer container = TestImage.container(RabbitMQContainer.class); + container.addExposedPorts(RABBITMQ_STREAMS_TLS_PORT); + String enabledPlugins = "[rabbitmq_stream,rabbitmq_prometheus]."; + container.withCopyToContainer(Transferable.of(enabledPlugins), "/etc/rabbitmq/enabled_plugins"); + container.withCopyFileToContainer( + MountableFile + .forClasspathResource("org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf"), + "/etc/rabbitmq/rabbitmq.conf"); + container.withCopyFileToContainer(MountableFile.forClasspathResource("org/springframework/boot/amqp/ca.crt"), + "/etc/rabbitmq/ca.crt"); + container.withCopyFileToContainer( + MountableFile.forClasspathResource("org/springframework/boot/amqp/server.key"), + "/etc/rabbitmq/server.key"); + container.withCopyFileToContainer( + MountableFile.forClasspathResource("org/springframework/boot/amqp/server.crt"), + "/etc/rabbitmq/server.crt"); + return container; + } + + @Autowired(required = false) + private RabbitConnectionDetails connectionDetails; + + @Autowired(required = false) + private RabbitStreamConnectionDetails streamConnectionDetails; + + @Autowired + private RabbitStreamTemplate rabbitStreamTemplate; + + @Autowired + private TestListener listener; + + @Test + void connectionCanBeMadeToRabbitContainer() { + assertThat(this.connectionDetails).isNotInstanceOf(RabbitMqContainerConnectionDetails.class); + assertThat(this.streamConnectionDetails).isInstanceOf(RabbitMqStreamContainerConnectionDetails.class); + assertThat(this.streamConnectionDetails.getSslBundle()).isNotNull(); + this.rabbitStreamTemplate.convertAndSend("message"); + Awaitility.waitAtMost(Duration.ofMinutes(4)) + .untilAsserted(() -> assertThat(this.listener.messages).containsExactly("message")); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(RabbitAutoConfiguration.class) + static class TestConfiguration { + + @Bean + StreamAdmin streamAdmin(Environment env) { + return new StreamAdmin(env, (sc) -> sc.stream("stream.queue1").create()); + } + + @Bean + EnvironmentBuilderCustomizer environmentBuilderCustomizer() { + return (env) -> env.addressResolver( + (address) -> new Address(rabbit.getHost(), rabbit.getMappedPort(RABBITMQ_STREAMS_TLS_PORT))); + } + + @Bean + TestListener testListener() { + return new TestListener(); + } + + } + + static class TestListener { + + private final List messages = new ArrayList<>(); + + @RabbitListener(queues = "stream.queue1") + void processMessage(String message) { + this.messages.add(message); + } + + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/ca.crt b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/ca.crt similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/ca.crt rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/ca.crt diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/client.crt b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.crt similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/client.crt rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.crt diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/client.key b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.key similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/client.key rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.key diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml index 3f39d29ddbb9..b72f1e22ed17 100644 --- a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml +++ b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml @@ -12,7 +12,7 @@ services: - ssl-key - ssl-cert volumes: - - ./rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro + - ./rabbitmq-ssl.conf:/etc/rabbitmq/rabbitmq.conf:ro labels: - 'org.springframework.boot.sslbundle.pem.keystore.certificate=client.crt' - 'org.springframework.boot.sslbundle.pem.keystore.private-key=client.key' diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml new file mode 100644 index 000000000000..f8deafc0f032 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml @@ -0,0 +1,32 @@ +services: + rabbitmq: + image: '{imageName}' + environment: + - 'RABBITMQ_DEFAULT_USER=myuser' + - 'RABBITMQ_DEFAULT_PASS=secret' + configs: + - source: plugins + target: /etc/rabbitmq/enabled_plugins + ports: + - '5551' + - '5552' + secrets: + - ssl-ca + - ssl-key + - ssl-cert + volumes: + - ./rabbitmq-stream-ssl.conf:/etc/rabbitmq/rabbitmq.conf:ro + labels: + - 'org.springframework.boot.sslbundle.pem.keystore.certificate=client.crt' + - 'org.springframework.boot.sslbundle.pem.keystore.private-key=client.key' + - 'org.springframework.boot.sslbundle.pem.truststore.certificate=ca.crt' +secrets: + ssl-ca: + file: 'ca.crt' + ssl-key: + file: 'server.key' + ssl-cert: + file: 'server.crt' +configs: + plugins: + content: "[rabbitmq_stream]." diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq.conf b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-ssl.conf similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq.conf rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-ssl.conf diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf new file mode 100644 index 000000000000..6b7da370cf3f --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf @@ -0,0 +1,8 @@ +stream.listeners.ssl.1 = 5551 + +ssl_options.cacertfile=/run/secrets/ssl-ca +ssl_options.certfile=/run/secrets/ssl-cert +ssl_options.keyfile=/run/secrets/ssl-key + +ssl_options.verify=verify_peer +ssl_options.fail_if_no_peer_cert=true diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/server.crt b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.crt similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/server.crt rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.crt diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/server.key b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.key similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/server.key rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.key diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf new file mode 100644 index 000000000000..92a5e5319874 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf @@ -0,0 +1,8 @@ +stream.listeners.ssl.1 = 5551 + +ssl_options.cacertfile=/etc/rabbitmq/ca.crt +ssl_options.certfile=/etc/rabbitmq/server.crt +ssl_options.keyfile=/etc/rabbitmq/server.key + +ssl_options.verify=verify_peer +ssl_options.fail_if_no_peer_cert=true diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java index 480bb3b1788f..e9fdff70acfa 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java @@ -1315,7 +1315,7 @@ public static final class Stream { /** * SSL configuration for RabbitMQ instance with the Stream plugin enabled. */ - private final StreamSsl ssl = new StreamSsl(); + private final Ssl ssl = new Ssl(); public String getHost() { return this.host; @@ -1365,11 +1365,11 @@ public void setName(@Nullable String name) { this.name = name; } - public StreamSsl getSsl() { + public Ssl getSsl() { return this.ssl; } - public static class StreamSsl { + public static class Ssl { /** * Whether to enable SSL support. Enabled automatically if "bundle" is diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java index e49039a54f76..416a23d8196f 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java @@ -28,7 +28,7 @@ import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Stream; -import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Stream.StreamSsl; +import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Stream.Ssl; import org.springframework.boot.amqp.autoconfigure.RabbitProperties.StreamContainer; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -207,7 +207,7 @@ public int getPort() { @Override public @Nullable SslBundle getSslBundle() { - StreamSsl ssl = this.streamProperties.getSsl(); + Ssl ssl = this.streamProperties.getSsl(); if (!ssl.determineEnabled()) { return null; } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java index 186db12035c7..6b19483ff4a9 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java @@ -38,8 +38,6 @@ class RabbitStreamDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { - private static final int RABBITMQ_STREAMS_PORT = 5552; - protected RabbitStreamDockerComposeConnectionDetailsFactory() { super("rabbitmq"); } @@ -62,6 +60,10 @@ protected RabbitStreamDockerComposeConnectionDetailsFactory() { static class RabbitStreamDockerComposeConnectionDetails extends DockerComposeConnectionDetails implements RabbitStreamConnectionDetails { + private static final int STREAMS_PORT = 5552; + + private static final int STREAMS_TLS_PORT = 5551; + private final RabbitEnvironment environment; private final String host; @@ -74,8 +76,8 @@ protected RabbitStreamDockerComposeConnectionDetails(RunningService service) { super(service); this.environment = new RabbitEnvironment(service.env()); this.host = service.host(); - this.port = service.ports().get(RABBITMQ_STREAMS_PORT); this.sslBundle = getSslBundle(service); + this.port = service.ports().get((this.sslBundle != null) ? STREAMS_TLS_PORT : STREAMS_PORT); } @Override diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java index 6f321c774604..5fc1321b7427 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java @@ -60,6 +60,10 @@ protected RabbitStreamConnectionDetails getContainerConnectionDetails( static final class RabbitMqStreamContainerConnectionDetails extends ContainerConnectionDetails implements RabbitStreamConnectionDetails { + private static final int STREAMS_PORT = 5552; + + private static final int STREAMS_TLS_PORT = 5551; + private RabbitMqStreamContainerConnectionDetails(ContainerConnectionSource source) { super(source); } @@ -71,7 +75,7 @@ public String getHost() { @Override public int getPort() { - return getContainer().getMappedPort(5552); + return getContainer().getMappedPort((getSslBundle() != null) ? STREAMS_TLS_PORT : STREAMS_PORT); } @Override From 55eba139ec1d07daccc66bcd8f98e93d5c8558d9 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 23 Jan 2026 19:18:09 -0800 Subject: [PATCH 363/930] Support binding of default properties when an empty property is defined Update the `Binder` so that empty properties are treated as an indicator that default value binding should be attempted. This update allow bound objects to differentiate between a completely missing property vs one that is present but doesn't have any values. For example, given the following value object: @ConfigurationProperties("my") public record My(Name name, int age) { public record Name(String first, String last) { } } The following binding scenarios are supported: 1) Full binding my.name.first=Spring my.name.last=Boot my.age=4 Binds to `new My(new Name("Spring", "Boot"), 4)` (works the same as Spring Boot 4.0) 2) Missing Properties my.age=4 Binds to `new My(null, 4)` (works the same as Spring Boot 4.0) 3) Default Properties my.name= my.age=4 Binds to `new My(new Name(null, null), 4)` (previously would throw a converter exception) Closes gh-48920 --- .../boot/context/properties/bind/Binder.java | 12 ++- .../properties/bind/DataObjectBinder.java | 4 +- .../properties/bind/JavaBeanBinder.java | 2 +- .../properties/bind/ValueObjectBinder.java | 10 ++- .../bind/ValueObjectBinderTests.java | 45 +++++++++++ .../MockConfigurationPropertySource.java | 6 +- .../PropertiesPropertySourceLoaderTests.java | 16 ++++ .../pages/features/external-config.adoc | 76 +++++++++++++++++-- .../nonnull/MyProperties.java | 2 +- .../nonnull/MyProperties.kt | 2 +- 10 files changed, 154 insertions(+), 21 deletions(-) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/{ => defaultvalues}/nonnull/MyProperties.java (96%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/{ => defaultvalues}/nonnull/MyProperties.kt (94%) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java index ac0298e1c9af..e42afcb502fc 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java @@ -49,6 +49,7 @@ import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ObjectUtils; /** * A container object which Binds objects from one or more @@ -435,14 +436,16 @@ public T bindOrCreate(ConfigurationPropertyName name, Bindable target, @N } catch (ConverterNotFoundException ex) { // We might still be able to bind it using the recursive binders - Object instance = bindDataObject(name, target, handler, context, allowRecursiveBinding); + boolean fallbackToDefaultValue = ObjectUtils.isEmpty(property.getValue()); + Object instance = bindDataObject(name, target, handler, context, allowRecursiveBinding, + fallbackToDefaultValue); if (instance != null) { return instance; } throw ex; } } - return bindDataObject(name, target, handler, context, allowRecursiveBinding); + return bindDataObject(name, target, handler, context, allowRecursiveBinding, false); } private @Nullable AggregateBinder getAggregateBinder(Bindable target, Context context) { @@ -493,7 +496,7 @@ public T bindOrCreate(ConfigurationPropertyName name, Bindable target, @N } private @Nullable Object bindDataObject(ConfigurationPropertyName name, Bindable target, BindHandler handler, - Context context, boolean allowRecursiveBinding) { + Context context, boolean allowRecursiveBinding, boolean fallbackToDefaultValue) { if (isUnbindableBean(name, target, context)) { return null; } @@ -505,7 +508,8 @@ public T bindOrCreate(ConfigurationPropertyName name, Bindable target, @N DataObjectPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName), propertyTarget, handler, context, false, false); Supplier<@Nullable Object> supplier = () -> fromDataObjectBinders(bindMethod, - (dataObjectBinder) -> dataObjectBinder.bind(name, target, context, propertyBinder)); + (dataObjectBinder) -> dataObjectBinder.bind(name, target, context, propertyBinder, + fallbackToDefaultValue)); return context.withDataObject(type, supplier); } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java index dfd41d8616dc..6fb0d26f6bfd 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java @@ -40,10 +40,12 @@ interface DataObjectBinder { * @param target the bindable to bind * @param context the bind context * @param propertyBinder property binder + * @param fallbackToDefaultValue if an attempt should be made to return a new default + * value when no values are bound * @return a bound instance or {@code null} */ @Nullable T bind(ConfigurationPropertyName name, Bindable target, Context context, - DataObjectPropertyBinder propertyBinder); + DataObjectPropertyBinder propertyBinder, boolean fallbackToDefaultValue); /** * Return a newly created instance or {@code null} if the {@link DataObjectBinder} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java index 92cdbced79d8..33532663571e 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java @@ -61,7 +61,7 @@ class JavaBeanBinder implements DataObjectBinder { @Override public @Nullable T bind(ConfigurationPropertyName name, Bindable target, Context context, - DataObjectPropertyBinder propertyBinder) { + DataObjectPropertyBinder propertyBinder, boolean fallbackToDefaultValue) { boolean hasKnownBindableProperties = target.getValue() != null && hasKnownBindableProperties(name, context); Bean bean = Bean.get(target, context, hasKnownBindableProperties); if (bean == null) { diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java index 919c6fc4fd20..200e722e7af4 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java @@ -75,7 +75,7 @@ class ValueObjectBinder implements DataObjectBinder { @Override public @Nullable T bind(ConfigurationPropertyName name, Bindable target, Binder.Context context, - DataObjectPropertyBinder propertyBinder) { + DataObjectPropertyBinder propertyBinder, boolean fallbackToDefaultValue) { ValueObject valueObject = ValueObject.get(target, context, this.constructorProvider, Discoverer.LENIENT); if (valueObject == null) { return null; @@ -94,7 +94,13 @@ class ValueObjectBinder implements DataObjectBinder { } context.clearConfigurationProperty(); context.popConstructorBoundTypes(); - return bound ? valueObject.instantiate(args) : null; + if (bound) { + return valueObject.instantiate(args); + } + if (fallbackToDefaultValue) { + return getNewDefaultValueInstanceIfPossible(context, target.getType()); + } + return null; } @Override diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java index 35acae7bd453..74ec2981f752 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java @@ -161,6 +161,51 @@ void bindToClassShouldBindNested() { assertThat(bean.getValueBean().getEnumValue()).isNull(); } + @Test + void bindToClassWhenNoValuesShouldNotBind() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + this.sources.add(source); + assertThat(this.binder.bind("foo", Bindable.of(ExampleNestedBean.class)).isBound()).isFalse(); + } + + @Test + void bindToClassWhenParentOfEmptyStringAndSubValuesShouldFail() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value-bean", ""); + source.put("foo.value-bean.int-value", "123"); + source.put("foo.value-bean.long-value", "34"); + source.put("foo.value-bean.string-value", "foo"); + this.sources.add(source); + ExampleNestedBean bean = this.binder.bind("foo", Bindable.of(ExampleNestedBean.class)).get(); + assertThat(bean.getValueBean().getIntValue()).isEqualTo(123); + assertThat(bean.getValueBean().getLongValue()).isEqualTo(34); + assertThat(bean.getValueBean().isBooleanValue()).isFalse(); + assertThat(bean.getValueBean().getStringValue()).isEqualTo("foo"); + assertThat(bean.getValueBean().getEnumValue()).isNull(); + } + + @Test + void bindToClassWhenParentOfEmptyStringAndNoSubValuesShouldFail() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value-bean", ""); + this.sources.add(source); + ExampleNestedBean bean = this.binder.bind("foo", Bindable.of(ExampleNestedBean.class)).get(); + assertThat(bean.getValueBean().getIntValue()).isEqualTo(0); + assertThat(bean.getValueBean().getLongValue()).isEqualTo(0); + assertThat(bean.getValueBean().isBooleanValue()).isFalse(); + assertThat(bean.getValueBean().getStringValue()).isNull(); + assertThat(bean.getValueBean().getEnumValue()).isNull(); + } + + @Test + void bindToClassWhenParentOfNullAndNoSubValuesShouldNotBind() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value-bean", null); + this.sources.add(source); + BindResult bindResult = this.binder.bind("foo", Bindable.of(ExampleNestedBean.class)); + assertThat(bindResult.isBound()).isFalse(); + } + @Test void bindToClassWithNoValueForPrimitiveShouldUseDefault() { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MockConfigurationPropertySource.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MockConfigurationPropertySource.java index b1c807746c55..79a2217a80f8 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MockConfigurationPropertySource.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/source/MockConfigurationPropertySource.java @@ -52,15 +52,15 @@ public MockConfigurationPropertySource(Map configs) { configs.forEach(this::put); } - public void put(String name, String value) { + public void put(String name, @Nullable String value) { put(ConfigurationPropertyName.of(name), value); } - public void put(ConfigurationPropertyName name, String value) { + public void put(ConfigurationPropertyName name, @Nullable String value) { put(name, OriginTrackedValue.of(value)); } - private void put(ConfigurationPropertyName name, OriginTrackedValue value) { + private void put(ConfigurationPropertyName name, @Nullable OriginTrackedValue value) { this.map.put(name, value); } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/env/PropertiesPropertySourceLoaderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/env/PropertiesPropertySourceLoaderTests.java index f97ce1a5e825..7d0241129ef2 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/env/PropertiesPropertySourceLoaderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/env/PropertiesPropertySourceLoaderTests.java @@ -131,4 +131,20 @@ void loadXml() throws Exception { assertThat(source.getProperty("test")).isEqualTo("xml"); } + @Test + @WithResource(name = "test.properties", content = """ + one=1 + none + zero= + two=2 + """) + void loadWithEmptyValues() throws Exception { + List> loaded = this.loader.load("test.properties", new ClassPathResource("test.properties")); + PropertySource source = loaded.get(0); + assertThat(source.getProperty("one")).isEqualTo("1"); + assertThat(source.getProperty("none")).isEqualTo(""); + assertThat(source.getProperty("zero")).isEqualTo(""); + assertThat(source.getProperty("two")).isEqualTo("2"); + } + } diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc index 55574a126530..5915325fd6b0 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc @@ -866,14 +866,6 @@ Unless your record has multiple constructors, there is no need to use javadoc:or Nested members of a constructor bound class (such as `Security` in the example above) will also be bound through their constructor. -Default values can be specified using javadoc:org.springframework.boot.context.properties.bind.DefaultValue[format=annotation] on constructor parameters and record components. -The conversion service will be applied to coerce the annotation's javadoc:java.lang.String[] value to the target type of a missing property. - -Referring to the previous example, if no properties are bound to `Security`, the `MyProperties` instance will contain a `null` value for `security`. -To make it contain a non-null instance of `Security` even when no properties are bound to it (when using Kotlin, this will require the `username` and `password` parameters of `Security` to be declared as nullable as they do not have default values), use an empty javadoc:org.springframework.boot.context.properties.bind.DefaultValue[format=annotation] annotation: - -include-code::nonnull/MyProperties[tag=*] - NOTE: To use constructor binding the class must be enabled using javadoc:org.springframework.boot.context.properties.EnableConfigurationProperties[format=annotation] or configuration property scanning. You cannot use constructor binding with beans that are created by the regular Spring mechanisms (for example javadoc:org.springframework.stereotype.Component[format=annotation] beans, beans created by using javadoc:org.springframework.context.annotation.Bean[format=annotation] methods or beans loaded by using javadoc:org.springframework.context.annotation.Import[format=annotation]) @@ -888,6 +880,74 @@ TIP: To use a reserved keyword in the name of a property, such as `my.service.im +[[features.external-config.typesafe-configuration-properties.constructor-binding.default-values]] +==== Default Values + +Default values can be specified using javadoc:org.springframework.boot.context.properties.bind.DefaultValue[format=annotation] on constructor parameters and record components. +The conversion service will be applied to coerce the annotation's javadoc:java.lang.String[] value to the target type of a missing property. + +In the `MyProperties` example above, you can see the nested `Security` class uses `@DefaultValue("USER")` for the `roles` parameter. +This means that if `security` properties are defined, but `roles` is not, the default of `"USER"` will be bound. + +For example, the following properties: + +[configprops%novalidate,yaml] +---- +my: + service: + enabled: true + security: + username: admin +---- + +Will be bound as `new MyProperties(true, null, new Security("admin", null, List.of("USER")))` + +If the `security` property is not present at all, then the `Security` instance will be `null`. + +For example, the following properties: + +[configprops%novalidate,yaml] +---- +my: + service: + enabled: true +---- + +Will be bound as `new MyProperties(true, null, null)` + +[NOTE] +==== +You can define an empty `security` property if you want to trigger binding with fully default `Security` instance. + +For YAML, you can use the following syntax: + +[,yaml] +---- +my: + service: + enabled: true + security: {} +---- + +With `Properties` you can use: + +[,properties] +---- +my.service.enabled=true +my.service.security= +---- + +Will be bound as `new MyProperties(true, null, new Security(null, null, List.of("USER")))` +==== + +If you want to always bind a a non-null instance of `Security`, even when properties are missing, you can use use an empty javadoc:org.springframework.boot.context.properties.bind.DefaultValue[format=annotation] annotation: + +include-code::nonnull/MyProperties[tag=*] + +TIP: When using Kotlin, you will need to declare the `username` and `password` parameters as nullable since they do not have default values + + + [[features.external-config.typesafe-configuration-properties.enabling-annotated-types]] === Enabling @ConfigurationProperties-annotated Types diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/nonnull/MyProperties.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/defaultvalues/nonnull/MyProperties.java similarity index 96% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/nonnull/MyProperties.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/defaultvalues/nonnull/MyProperties.java index c1714b675119..29aef9a6aed7 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/nonnull/MyProperties.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/defaultvalues/nonnull/MyProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.constructorbinding.nonnull; +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.constructorbinding.defaultvalues.nonnull; import java.net.InetAddress; import java.util.List; diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/nonnull/MyProperties.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/defaultvalues/nonnull/MyProperties.kt similarity index 94% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/nonnull/MyProperties.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/defaultvalues/nonnull/MyProperties.kt index 64a0deb59f09..a9e9d4c25b3c 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/nonnull/MyProperties.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/defaultvalues/nonnull/MyProperties.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.constructorbinding.nonnull +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.constructorbinding.defaultvalues.nonnull import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.bind.DefaultValue From 79261acd307133a100229b2ef61979961104c5b5 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 17 Mar 2026 18:56:35 -0700 Subject: [PATCH 364/930] Support empty property defaults binding without parameters Refine empty property default binding support to include no-arg constructors. See gh-48920 --- .../properties/bind/ValueObjectBinder.java | 37 +++++++++---------- .../bind/ValueObjectBinderTests.java | 13 +++++++ 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java index 200e722e7af4..d1f2a473655f 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java @@ -77,25 +77,24 @@ class ValueObjectBinder implements DataObjectBinder { public @Nullable T bind(ConfigurationPropertyName name, Bindable target, Binder.Context context, DataObjectPropertyBinder propertyBinder, boolean fallbackToDefaultValue) { ValueObject valueObject = ValueObject.get(target, context, this.constructorProvider, Discoverer.LENIENT); - if (valueObject == null) { - return null; - } - Class targetType = target.getType().resolve(); - Assert.state(targetType != null, "'targetType' must not be null"); - context.pushConstructorBoundTypes(targetType); - List parameters = valueObject.getConstructorParameters(); - List<@Nullable Object> args = new ArrayList<>(parameters.size()); - boolean bound = false; - for (ConstructorParameter parameter : parameters) { - Object arg = parameter.bind(propertyBinder); - bound = bound || arg != null; - arg = (arg != null) ? arg : getDefaultValue(context, parameter); - args.add(arg); - } - context.clearConfigurationProperty(); - context.popConstructorBoundTypes(); - if (bound) { - return valueObject.instantiate(args); + if (valueObject != null) { + Class targetType = target.getType().resolve(); + Assert.state(targetType != null, "'targetType' must not be null"); + context.pushConstructorBoundTypes(targetType); + List parameters = valueObject.getConstructorParameters(); + List<@Nullable Object> args = new ArrayList<>(parameters.size()); + boolean bound = false; + for (ConstructorParameter parameter : parameters) { + Object arg = parameter.bind(propertyBinder); + bound = bound || arg != null; + arg = (arg != null) ? arg : getDefaultValue(context, parameter); + args.add(arg); + } + context.clearConfigurationProperty(); + context.popConstructorBoundTypes(); + if (bound) { + return valueObject.instantiate(args); + } } if (fallbackToDefaultValue) { return getNewDefaultValueInstanceIfPossible(context, target.getType()); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java index 74ec2981f752..08c2b2cd5d7c 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java @@ -206,6 +206,15 @@ void bindToClassWhenParentOfNullAndNoSubValuesShouldNotBind() { assertThat(bindResult.isBound()).isFalse(); } + @Test + void bindToEmptyRecordWhenParentOfEmptyString() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo", ""); + this.sources.add(source); + EmptyRecod emptyRecod = this.binder.bind("foo", Bindable.of(EmptyRecod.class)).get(); + assertThat(emptyRecod).isNotNull(); + } + @Test void bindToClassWithNoValueForPrimitiveShouldUseDefault() { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); @@ -999,4 +1008,8 @@ void setJsonPath(@Nullable JsonPath jsonPath) { } + record EmptyRecod() { + + } + } From 18f875befbfc0c43560a7dbb1d9d5f420a19d8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Mar 2026 07:51:03 +0100 Subject: [PATCH 365/930] Upgrade to Flyway 12.1.1 Closes gh-49633 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 30914ef87912..711a9cc06952 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -425,7 +425,7 @@ bom { javadoc("elasticsearch-java", version -> "https://javadoc.io/doc/co.elastic.clients/elasticsearch-java/%s/index.html".formatted(version), "co.elastic.clients.elasticsearch", "co.elastic.clients.transport") } } - library("Flyway", "12.1.0") { + library("Flyway", "12.1.1") { group("org.flywaydb") { modules = [ "flyway-command-test-connection", From ba9315004d90d3a9af0102ec0a11dd1c667bf93f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Mar 2026 07:51:07 +0100 Subject: [PATCH 366/930] Upgrade to JBoss Logging 3.6.3.Final Closes gh-49634 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 711a9cc06952..ec71096725ab 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1040,7 +1040,7 @@ bom { .formatted(version.toString().replace(".java11", ""))) } } - library("JBoss Logging", "3.6.2.Final") { + library("JBoss Logging", "3.6.3.Final") { group("org.jboss.logging") { modules = [ "jboss-logging" From 7bf1e65fcc0eacfe18c0d30accc4231b4281813c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Mar 2026 07:51:07 +0100 Subject: [PATCH 367/930] Upgrade to Spring Integration 7.1.0-M3 Closes gh-49442 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ec71096725ab..696eac813851 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2526,7 +2526,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-hateoas/releases/tag/{version}") } } - library("Spring Integration", "7.1.0-SNAPSHOT") { + library("Spring Integration", "7.1.0-M3") { considerSnapshots() group("org.springframework.integration") { bom("spring-integration-bom") From 025ea78b01f432d62e6b59d0dba1086de1952a9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Mar 2026 07:51:07 +0100 Subject: [PATCH 368/930] Upgrade to Spring WS 5.0.1 Closes gh-49532 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 696eac813851..911b51285a6c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2647,7 +2647,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-session/releases/tag/{version}") } } - library("Spring WS", "5.0.1-SNAPSHOT") { + library("Spring WS", "5.0.1") { considerSnapshots() group("org.springframework.ws") { bom("spring-ws-bom") From 4418c451f31b828b9eaeeed5ebfc5646e95423c1 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Wed, 18 Mar 2026 15:06:03 +0800 Subject: [PATCH 369/930] Fix typo See gh-49635 Signed-off-by: Yanming Zhou --- .../context/properties/bind/ValueObjectBinderTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java index 08c2b2cd5d7c..dc0a21acb609 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java @@ -211,8 +211,8 @@ void bindToEmptyRecordWhenParentOfEmptyString() { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo", ""); this.sources.add(source); - EmptyRecod emptyRecod = this.binder.bind("foo", Bindable.of(EmptyRecod.class)).get(); - assertThat(emptyRecod).isNotNull(); + EmptyRecord emptyRecord = this.binder.bind("foo", Bindable.of(EmptyRecord.class)).get(); + assertThat(emptyRecord).isNotNull(); } @Test @@ -1008,7 +1008,7 @@ void setJsonPath(@Nullable JsonPath jsonPath) { } - record EmptyRecod() { + record EmptyRecord() { } From 2ccfde80cc28b1bc625d1d1369db2a6d62609ce6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 18 Mar 2026 12:01:14 +0000 Subject: [PATCH 370/930] Auto-configure Jackson mappers with bean-based handler instantiator Closes gh-48711 --- .../JacksonAutoConfiguration.java | 37 ++- .../SpringBeanHandlerInstantiator.java | 120 ++++++++ .../JacksonAutoConfigurationTests.java | 11 + .../SpringBeanHandlerInstantiatorTests.java | 273 ++++++++++++++++++ 4 files changed, 427 insertions(+), 14 deletions(-) create mode 100644 module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/SpringBeanHandlerInstantiator.java create mode 100644 module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/SpringBeanHandlerInstantiatorTests.java diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java index 7a0c90417656..1643c29a09f3 100644 --- a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java @@ -43,6 +43,7 @@ import tools.jackson.databind.PropertyNamingStrategy; import tools.jackson.databind.cfg.ConstructorDetector; import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.cfg.HandlerInstantiator; import tools.jackson.databind.cfg.MapperBuilder; import tools.jackson.databind.json.JsonMapper; import tools.jackson.dataformat.cbor.CBORFactory; @@ -57,6 +58,7 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurationPackages; @@ -174,8 +176,10 @@ StandardJsonFactoryBuilderCustomizer standardJsonFactoryBuilderCustomizer() { } @Bean - StandardJsonMapperBuilderCustomizer standardJsonMapperBuilderCustomizer(ObjectProvider modules) { - return new StandardJsonMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList()); + StandardJsonMapperBuilderCustomizer standardJsonMapperBuilderCustomizer(ObjectProvider modules, + AutowireCapableBeanFactory beanFactory) { + return new StandardJsonMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList(), + beanFactory); } static final class StandardJsonFactoryBuilderCustomizer @@ -195,9 +199,9 @@ public void customize(JsonFactoryBuilder jsonFactoryBuilder) { static final class StandardJsonMapperBuilderCustomizer extends AbstractMapperBuilderCustomizer implements JsonMapperBuilderCustomizer { - StandardJsonMapperBuilderCustomizer(JacksonProperties jacksonProperties, - Collection modules) { - super(jacksonProperties, modules); + StandardJsonMapperBuilderCustomizer(JacksonProperties jacksonProperties, Collection modules, + AutowireCapableBeanFactory beanFactory) { + super(jacksonProperties, modules, beanFactory); } @Override @@ -280,9 +284,9 @@ StandardCborFactoryBuilderCustomizer standardCborFactoryBuilderCustomizer() { @Bean StandardCborMapperBuilderCustomizer standardCborMapperBuilderCustomizer(ObjectProvider modules, - JacksonCborProperties cborProperties) { + JacksonCborProperties cborProperties, AutowireCapableBeanFactory beanFactory) { return new StandardCborMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList(), - cborProperties); + cborProperties, beanFactory); } static final class StandardCborFactoryBuilderCustomizer @@ -305,8 +309,8 @@ static class StandardCborMapperBuilderCustomizer extends AbstractMapperBuilderCu private final JacksonCborProperties cborProperties; StandardCborMapperBuilderCustomizer(JacksonProperties jacksonProperties, Collection modules, - JacksonCborProperties cborProperties) { - super(jacksonProperties, modules); + JacksonCborProperties cborProperties, AutowireCapableBeanFactory beanFactory) { + super(jacksonProperties, modules, beanFactory); this.cborProperties = cborProperties; } @@ -370,9 +374,9 @@ StandardXmlFactoryBuilderCustomizer standardXmlFactoryBuilderCustomizer() { @Bean StandardXmlMapperBuilderCustomizer standardXmlMapperBuilderCustomizer(ObjectProvider modules, - JacksonXmlProperties xmlProperties) { + JacksonXmlProperties xmlProperties, AutowireCapableBeanFactory beanFactory) { return new StandardXmlMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList(), - xmlProperties); + xmlProperties, beanFactory); } @Configuration(proxyBeanMethods = false) @@ -415,8 +419,8 @@ static class StandardXmlMapperBuilderCustomizer extends AbstractMapperBuilderCus private final JacksonXmlProperties xmlProperties; StandardXmlMapperBuilderCustomizer(JacksonProperties jacksonProperties, Collection modules, - JacksonXmlProperties xmlProperties) { - super(jacksonProperties, modules); + JacksonXmlProperties xmlProperties, AutowireCapableBeanFactory beanFactory) { + super(jacksonProperties, modules, beanFactory); this.xmlProperties = xmlProperties; } @@ -508,9 +512,13 @@ abstract static class AbstractMapperBuilderCustomizer modules; - AbstractMapperBuilderCustomizer(JacksonProperties jacksonProperties, Collection modules) { + private final HandlerInstantiator handlerInstantiator; + + AbstractMapperBuilderCustomizer(JacksonProperties jacksonProperties, Collection modules, + AutowireCapableBeanFactory beanFactory) { this.jacksonProperties = jacksonProperties; this.modules = modules; + this.handlerInstantiator = new SpringBeanHandlerInstantiator(beanFactory); } @Override @@ -538,6 +546,7 @@ protected void customize(B builder) { if (this.jacksonProperties.getTimeZone() != null) { builder.defaultTimeZone(this.jacksonProperties.getTimeZone()); } + builder.handlerInstantiator(this.handlerInstantiator); configureVisibility(builder, this.jacksonProperties.getVisibility()); configureFeatures(builder, this.jacksonProperties.getDeserialization(), builder::configure); configureFeatures(builder, this.jacksonProperties.getSerialization(), builder::configure); diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/SpringBeanHandlerInstantiator.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/SpringBeanHandlerInstantiator.java new file mode 100644 index 000000000000..ec413399d555 --- /dev/null +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/SpringBeanHandlerInstantiator.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jackson.autoconfigure; + +import com.fasterxml.jackson.annotation.ObjectIdGenerator; +import com.fasterxml.jackson.annotation.ObjectIdResolver; +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.DeserializationConfig; +import tools.jackson.databind.KeyDeserializer; +import tools.jackson.databind.PropertyNamingStrategy; +import tools.jackson.databind.SerializationConfig; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.cfg.HandlerInstantiator; +import tools.jackson.databind.cfg.MapperConfig; +import tools.jackson.databind.deser.ValueInstantiator; +import tools.jackson.databind.introspect.Annotated; +import tools.jackson.databind.jsontype.TypeIdResolver; +import tools.jackson.databind.jsontype.TypeResolverBuilder; +import tools.jackson.databind.ser.VirtualBeanPropertyWriter; +import tools.jackson.databind.util.Converter; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.util.Assert; + +/** + * Package-private adaptation of Spring Framework's + * {@link org.springframework.http.support.JacksonHandlerInstantiator}. Allows Spring Boot + * to offer bean-based handler instantiation without requiring {@code spring-web}. + * + * @author Sebastien Deleuze + * @author Andy Wilkinson + * @see org.springframework.http.support.JacksonHandlerInstantiator + */ +class SpringBeanHandlerInstantiator extends HandlerInstantiator { + + private final AutowireCapableBeanFactory beanFactory; + + SpringBeanHandlerInstantiator(AutowireCapableBeanFactory beanFactory) { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + this.beanFactory = beanFactory; + } + + @Override + @Nullable public ValueDeserializer deserializerInstance(DeserializationConfig config, Annotated annotated, + Class deserClass) { + return (ValueDeserializer) this.beanFactory.createBean(deserClass); + } + + @Override + public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, + Class keyDeserClass) { + return (KeyDeserializer) this.beanFactory.createBean(keyDeserClass); + } + + @Override + public ValueSerializer serializerInstance(SerializationConfig config, Annotated annotated, Class serClass) { + return (ValueSerializer) this.beanFactory.createBean(serClass); + } + + @Override + public TypeResolverBuilder typeResolverBuilderInstance(MapperConfig config, Annotated annotated, + Class builderClass) { + return (TypeResolverBuilder) this.beanFactory.createBean(builderClass); + } + + @Override + public TypeIdResolver typeIdResolverInstance(MapperConfig config, Annotated annotated, Class resolverClass) { + return (TypeIdResolver) this.beanFactory.createBean(resolverClass); + } + + @Override + public ValueInstantiator valueInstantiatorInstance(MapperConfig config, Annotated annotated, + Class implClass) { + return (ValueInstantiator) this.beanFactory.createBean(implClass); + } + + @Override + public ObjectIdGenerator objectIdGeneratorInstance(MapperConfig config, Annotated annotated, + Class implClass) { + return (ObjectIdGenerator) this.beanFactory.createBean(implClass); + } + + @Override + public ObjectIdResolver resolverIdGeneratorInstance(MapperConfig config, Annotated annotated, + Class implClass) { + return (ObjectIdResolver) this.beanFactory.createBean(implClass); + } + + @Override + public PropertyNamingStrategy namingStrategyInstance(MapperConfig config, Annotated annotated, + Class implClass) { + return (PropertyNamingStrategy) this.beanFactory.createBean(implClass); + } + + @Override + public Converter converterInstance(MapperConfig config, Annotated annotated, Class implClass) { + return (Converter) this.beanFactory.createBean(implClass); + } + + @Override + public VirtualBeanPropertyWriter virtualPropertyWriterInstance(MapperConfig config, Class implClass) { + return (VirtualBeanPropertyWriter) this.beanFactory.createBean(implClass); + } + +} diff --git a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java index 96ecbab97481..49da0a450645 100644 --- a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java +++ b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java @@ -913,6 +913,17 @@ void defaultStreamWriteConstraintsMatchJacksonDefaults(MapperType mapperType) { }); } + @EnumSource + @ParameterizedTest + void mapperHasASpringBeanHandlerInstantiator(MapperType mapperType) { + this.contextRunner.run((context) -> { + assertThat(mapperType.getMapper(context).deserializationConfig().getHandlerInstantiator()) + .isInstanceOf(SpringBeanHandlerInstantiator.class); + assertThat(mapperType.getMapper(context).serializationConfig().getHandlerInstantiator()) + .isInstanceOf(SpringBeanHandlerInstantiator.class); + }); + } + static class MyDateFormat extends SimpleDateFormat { MyDateFormat() { diff --git a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/SpringBeanHandlerInstantiatorTests.java b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/SpringBeanHandlerInstantiatorTests.java new file mode 100644 index 000000000000..131a1b49a152 --- /dev/null +++ b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/SpringBeanHandlerInstantiatorTests.java @@ -0,0 +1,273 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jackson.autoconfigure; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DatabindContext; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.KeyDeserializer; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.annotation.JsonTypeIdResolver; +import tools.jackson.databind.annotation.JsonTypeResolver; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.jsontype.NamedType; +import tools.jackson.databind.jsontype.TypeDeserializer; +import tools.jackson.databind.jsontype.TypeIdResolver; +import tools.jackson.databind.jsontype.TypeSerializer; +import tools.jackson.databind.jsontype.impl.StdTypeResolverBuilder; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SpringBeanHandlerInstantiator}. Adapted from Spring Framework's + * JacksonHandlerInstantiatorTests. + * + * @author Sebastien Deleuze + * @author Andy Wilkinson + */ +class SpringBeanHandlerInstantiatorTests { + + @Test + void autowiredSerializer() { + withHandlerInstantiator((jsonMapper) -> { + User user = new User("bob"); + String json = jsonMapper.writeValueAsString(user); + assertThat(json).isEqualTo("{\"username\":\"BOB\"}"); + }); + } + + @Test + void autowiredDeserializer() { + withHandlerInstantiator((jsonMapper) -> { + String json = "{\"username\":\"bob\"}"; + User user = jsonMapper.readValue(json, User.class); + assertThat(user.getUsername()).isEqualTo("BOB"); + }); + } + + @Test + void autowiredKeyDeserializer() { + withHandlerInstantiator((jsonMapper) -> { + String json = "{\"credentials\":{\"bob\":\"admin\"}}"; + SecurityRegistry registry = jsonMapper.readValue(json, SecurityRegistry.class); + assertThat(registry.getCredentials()).containsKey("BOB"); + assertThat(registry.getCredentials()).doesNotContainKey("bob"); + }); + } + + @Test + void applicationContextAwaretypeResolverBuilder() { + withHandlerInstantiator((jsonMapper) -> { + jsonMapper.writeValueAsString(new Group()); + assertThat(CustomTypeResolverBuilder.autowiredFieldInitialized).isTrue(); + }); + } + + @Test + void applicationContextAwareTypeIdResolver() { + withHandlerInstantiator((jsonMapper) -> { + jsonMapper.writeValueAsString(new Group()); + assertThat(CustomTypeIdResolver.autowiredFieldInitialized).isTrue(); + }); + } + + private void withHandlerInstantiator(Consumer consumer) { + new ApplicationContextRunner().withBean(Capitalizer.class) + .run((context) -> consumer.accept(JsonMapper.builder() + .handlerInstantiator(new SpringBeanHandlerInstantiator(context.getBeanFactory())) + .build())); + } + + static class UserDeserializer extends ValueDeserializer { + + @Autowired + private Capitalizer capitalizer; + + @Override + public User deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) { + JsonNode node = jsonParser.readValueAsTree(); + return new User(this.capitalizer.capitalize(node.get("username").asString())); + } + + } + + static class UserSerializer extends ValueSerializer { + + @Autowired + private Capitalizer capitalizer; + + @Override + public void serialize(User user, JsonGenerator jsonGenerator, SerializationContext serializationContext) { + + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringProperty("username", this.capitalizer.capitalize(user.getUsername())); + jsonGenerator.writeEndObject(); + } + + } + + static class UpperCaseKeyDeserializer extends KeyDeserializer { + + @Autowired + private Capitalizer capitalizer; + + @Override + public Object deserializeKey(String key, DeserializationContext context) { + return this.capitalizer.capitalize(key); + } + + } + + static class CustomTypeResolverBuilder extends StdTypeResolverBuilder { + + @Autowired + private Capitalizer capitalizer; + + static boolean autowiredFieldInitialized; + + @Override + public TypeSerializer buildTypeSerializer(SerializationContext serializationContext, JavaType baseType, + Collection subtypes) { + + autowiredFieldInitialized = (this.capitalizer != null); + return super.buildTypeSerializer(serializationContext, baseType, subtypes); + } + + @Override + public TypeDeserializer buildTypeDeserializer(DeserializationContext deserializationContext, JavaType baseType, + Collection subtypes) { + + return super.buildTypeDeserializer(deserializationContext, baseType, subtypes); + } + + } + + static class CustomTypeIdResolver implements TypeIdResolver { + + @Autowired + private Capitalizer capitalizer; + + static boolean autowiredFieldInitialized; + + @Override + public String idFromValueAndType(DatabindContext ctxt, Object o, Class type) { + return type.getClass().getName(); + } + + @Override + public JsonTypeInfo.Id getMechanism() { + return JsonTypeInfo.Id.CUSTOM; + } + + @Override + public String idFromValue(DatabindContext databindContext, Object value) { + autowiredFieldInitialized = (this.capitalizer != null); + return value.getClass().getName(); + } + + @Override + public void init(JavaType type) { + } + + @Override + public @Nullable String idFromBaseType(DatabindContext ctxt) { + return null; + } + + @Override + public @Nullable JavaType typeFromId(DatabindContext context, String id) { + return null; + } + + @Override + public @Nullable String getDescForKnownTypeIds() { + return null; + } + + } + + @JsonDeserialize(using = UserDeserializer.class) + @JsonSerialize(using = UserSerializer.class) + public static class User { + + private final String username; + + User(String username) { + this.username = username; + } + + public String getUsername() { + return this.username; + } + + } + + public static class SecurityRegistry { + + @JsonDeserialize(keyUsing = UpperCaseKeyDeserializer.class) + private Map credentials = new HashMap<>(); + + public void addCredential(String username, String credential) { + this.credentials.put(username, credential); + } + + public Map getCredentials() { + return this.credentials; + } + + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, property = "type") + @JsonTypeResolver(CustomTypeResolverBuilder.class) + @JsonTypeIdResolver(CustomTypeIdResolver.class) + public static class Group { + + public String getType() { + return Group.class.getName(); + } + + } + + static class Capitalizer { + + String capitalize(String text) { + return text.toUpperCase(Locale.ROOT); + } + + } + +} From 3d16823bee9f7e9ba911779be27cacd9bdb2893c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Mar 2026 18:16:53 +0100 Subject: [PATCH 371/930] Upgrade to Spring Batch 6.0.3 Closes gh-49439 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 911b51285a6c..9ff7bd915f9a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2446,7 +2446,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-amqp/releases/tag/v{version}") } } - library("Spring Batch", "6.0.3-SNAPSHOT") { + library("Spring Batch", "6.0.3") { considerSnapshots() group("org.springframework.batch") { bom("spring-batch-bom") From 72b8a6ca057ea9bfb9260f46f59e08e560d99600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 17 Mar 2026 11:08:20 +0100 Subject: [PATCH 372/930] Migrate Spring RabbitMQ support to spring-boot-rabbitmq MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See gh-49620 Co-authored-by: Eddú Meléndez --- documentation/spring-boot-docs/build.gradle | 2 +- .../build.gradle | 6 +++--- .../build.gradle | 0 ...oseConnectionDetailsFactoryIntegrationTests.java | 0 ...oseConnectionDetailsFactoryIntegrationTests.java | 0 ...oseConnectionDetailsFactoryIntegrationTests.java | 0 ...oseConnectionDetailsFactoryIntegrationTests.java | 0 ...nerConnectionDetailsFactoryIntegrationTests.java | 0 ...nerConnectionDetailsFactoryIntegrationTests.java | 0 ...nerConnectionDetailsFactoryIntegrationTests.java | 0 ...nerConnectionDetailsFactoryIntegrationTests.java | 0 ...nerConnectionDetailsFactoryIntegrationTests.java | 0 ...nerConnectionDetailsFactoryIntegrationTests.java | 0 .../src/dockerTest/resources/logback-test.xml | 0 .../resources/org/springframework/boot/amqp/ca.crt | 0 .../org/springframework/boot/amqp/client.crt | 0 .../org/springframework/boot/amqp/client.key | 0 ...and-rabbit-stream-separate-services-compose.yaml | 0 ...it-and-rabbit-stream-single-service-compose.yaml | 0 .../boot/amqp/docker/compose/rabbit-compose.yaml | 0 .../amqp/docker/compose/rabbit-ssl-compose.yaml | 0 .../amqp/docker/compose/rabbit-stream-compose.yaml | 0 .../docker/compose/rabbit-stream-ssl-compose.yaml | 0 .../boot/amqp/docker/compose/rabbitmq-ssl.conf | 0 .../amqp/docker/compose/rabbitmq-stream-ssl.conf | 0 .../org/springframework/boot/amqp/server.crt | 0 .../org/springframework/boot/amqp/server.key | 0 .../amqp/testcontainers/rabbitmq-stream-ssl.conf | 0 .../src/dockerTest/resources/spring.properties | 0 .../AbstractConnectionFactoryConfigurer.java | 0 ...actRabbitListenerContainerFactoryConfigurer.java | 0 .../CachingConnectionFactoryConfigurer.java | 0 .../autoconfigure/ConnectionFactoryCustomizer.java | 0 ...ectRabbitListenerContainerFactoryConfigurer.java | 0 .../autoconfigure/EnvironmentBuilderCustomizer.java | 0 .../PropertiesRabbitConnectionDetails.java | 0 .../RabbitAnnotationDrivenConfiguration.java | 0 .../amqp/autoconfigure/RabbitAutoConfiguration.java | 0 .../amqp/autoconfigure/RabbitConnectionDetails.java | 0 .../RabbitConnectionFactoryBeanConfigurer.java | 0 .../RabbitListenerRetrySettingsCustomizer.java | 0 .../boot/amqp/autoconfigure/RabbitProperties.java | 0 .../autoconfigure/RabbitRetryTemplateSettings.java | 0 .../autoconfigure/RabbitStreamConfiguration.java | 0 .../RabbitStreamConnectionDetails.java | 0 .../RabbitStreamTemplateConfigurer.java | 0 .../autoconfigure/RabbitTemplateConfigurer.java | 0 .../autoconfigure/RabbitTemplateCustomizer.java | 0 .../RabbitTemplateRetrySettingsCustomizer.java | 0 ...pleRabbitListenerContainerFactoryConfigurer.java | 0 .../SslBundleRabbitConnectionFactoryBean.java | 0 .../RabbitHealthContributorAutoConfiguration.java | 0 .../amqp/autoconfigure/health/package-info.java | 0 ...RabbitConnectionFactoryMetricsPostProcessor.java | 0 .../metrics/RabbitMetricsAutoConfiguration.java | 0 .../amqp/autoconfigure/metrics/package-info.java | 0 .../boot/amqp/autoconfigure/package-info.java | 0 ...RabbitDockerComposeConnectionDetailsFactory.java | 0 .../boot/amqp/docker/compose/RabbitEnvironment.java | 0 ...StreamDockerComposeConnectionDetailsFactory.java | 0 .../boot/amqp/docker/compose/package-info.java | 0 .../boot/amqp/health/RabbitHealthIndicator.java | 0 .../boot/amqp/health/package-info.java | 0 .../boot/amqp/metrics/RabbitMetrics.java | 0 .../boot/amqp/metrics/package-info.java | 0 ...atedRabbitContainerConnectionDetailsFactory.java | 0 .../RabbitContainerConnectionDetailsFactory.java | 0 ...bbitStreamContainerConnectionDetailsFactory.java | 0 .../boot/amqp/testcontainers/package-info.java | 0 .../additional-spring-configuration-metadata.json | 0 .../src/main/resources/META-INF/spring.factories | 0 ...ork.boot.autoconfigure.AutoConfiguration.imports | 0 .../PropertiesRabbitConnectionDetailsTests.java | 0 .../autoconfigure/RabbitAutoConfigurationTests.java | 0 .../amqp/autoconfigure/RabbitPropertiesTests.java | 0 .../RabbitStreamConfigurationTests.java | 0 ...bbitHealthContributorAutoConfigurationTests.java | 0 ...nfigurationMeterBinderCycleIntegrationTests.java | 0 .../RabbitMetricsAutoConfigurationTests.java | 0 .../amqp/docker/compose/RabbitEnvironmentTests.java | 0 .../amqp/health/RabbitHealthIndicatorTests.java | 0 .../boot/amqp/metrics/RabbitMetricsTests.java | 0 .../src/test/resources/logback-test.xml | 0 .../boot/amqp/autoconfigure/test.jks | Bin platform/spring-boot-dependencies/build.gradle | 6 +++--- settings.gradle | 8 ++++---- .../build.gradle | 4 ++-- .../amqp/SampleAmqpSimpleApplicationSslTests.java | 0 .../amqp/SampleAmqpSimpleApplicationTests.java | 0 .../smoketest/amqp/SecureRabbitMqContainer.java | 0 .../smoketest/amqp/SampleAmqpSimpleApplication.java | 0 .../src/main/java/smoketest/amqp/Sender.java | 0 .../src/main/java/smoketest/amqp/package-info.java | 0 .../src/test/resources/ssl/rabbitmq.conf | 0 .../src/test/resources/ssl/test-ca.crt | 0 .../src/test/resources/ssl/test-ca.key | 0 .../src/test/resources/ssl/test-client.crt | 0 .../src/test/resources/ssl/test-client.key | 0 .../src/test/resources/ssl/test-server.crt | 0 .../src/test/resources/ssl/test-server.key | 0 .../build.gradle | 2 +- .../build.gradle | 2 +- 102 files changed, 15 insertions(+), 15 deletions(-) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/build.gradle (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/logback-test.xml (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/org/springframework/boot/amqp/ca.crt (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/org/springframework/boot/amqp/client.crt (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/org/springframework/boot/amqp/client.key (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-ssl.conf (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/org/springframework/boot/amqp/server.crt (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/org/springframework/boot/amqp/server.key (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/spring.properties (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractConnectionFactoryConfigurer.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/CachingConnectionFactoryConfigurer.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionFactoryCustomizer.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/EnvironmentBuilderCustomizer.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetails.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionDetails.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitListenerRetrySettingsCustomizer.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitRetryTemplateSettings.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateCustomizer.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/SslBundleRabbitConnectionFactoryBean.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/health/package-info.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/package-info.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/health/RabbitHealthIndicator.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/health/package-info.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/metrics/RabbitMetrics.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/metrics/package-info.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/resources/META-INF/additional-spring-configuration-metadata.json (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/resources/META-INF/spring.factories (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/test/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetailsTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/test/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/test/java/org/springframework/boot/amqp/health/RabbitHealthIndicatorTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/test/java/org/springframework/boot/amqp/metrics/RabbitMetricsTests.java (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/test/resources/logback-test.xml (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/test/resources/org/springframework/boot/amqp/autoconfigure/test.jks (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/build.gradle (94%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/dockerTest/java/smoketest/amqp/SecureRabbitMqContainer.java (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/main/java/smoketest/amqp/Sender.java (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/main/java/smoketest/amqp/package-info.java (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/test/resources/ssl/rabbitmq.conf (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/test/resources/ssl/test-ca.crt (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/test/resources/ssl/test-ca.key (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/test/resources/ssl/test-client.crt (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/test/resources/ssl/test-client.key (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/test/resources/ssl/test-server.crt (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/test/resources/ssl/test-server.key (100%) rename starter/{spring-boot-starter-amqp-test => spring-boot-starter-rabbitmq-test}/build.gradle (93%) rename starter/{spring-boot-starter-amqp => spring-boot-starter-rabbitmq}/build.gradle (94%) diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index 3781363d0bf3..7f20bc5ed26b 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -94,7 +94,6 @@ dependencies { implementation(project(path: ":loader:spring-boot-loader-tools")) implementation(project(path: ":module:spring-boot-actuator")) implementation(project(path: ":module:spring-boot-actuator-autoconfigure")) - implementation(project(path: ":module:spring-boot-amqp")) implementation(project(path: ":module:spring-boot-cache")) implementation(project(path: ":module:spring-boot-cache-test")) implementation(project(path: ":module:spring-boot-data-cassandra")) @@ -126,6 +125,7 @@ dependencies { implementation(project(path: ":module:spring-boot-micrometer-metrics")) implementation(project(path: ":module:spring-boot-persistence")) implementation(project(path: ":module:spring-boot-r2dbc")) + implementation(project(path: ":module:spring-boot-rabbitmq")) implementation(project(path: ":module:spring-boot-restdocs")) implementation(project(path: ":module:spring-boot-reactor-netty")) implementation(project(path: ":module:spring-boot-restclient")) diff --git a/module/spring-boot-autoconfigure-classic-modules/build.gradle b/module/spring-boot-autoconfigure-classic-modules/build.gradle index 5f5b5099674e..07ab22f09847 100644 --- a/module/spring-boot-autoconfigure-classic-modules/build.gradle +++ b/module/spring-boot-autoconfigure-classic-modules/build.gradle @@ -28,9 +28,6 @@ dependencies { api(project(":module:spring-boot-activemq")) { transitive = false } - api(project(":module:spring-boot-amqp")) { - transitive = false - } api(project(":module:spring-boot-artemis")) { transitive = false } @@ -208,6 +205,9 @@ dependencies { api(project(":module:spring-boot-r2dbc")) { transitive = false } + api(project(":module:spring-boot-rabbitmq")) { + transitive = false + } api(project(":module:spring-boot-reactor")) { transitive = false } diff --git a/module/spring-boot-amqp/build.gradle b/module/spring-boot-rabbitmq/build.gradle similarity index 100% rename from module/spring-boot-amqp/build.gradle rename to module/spring-boot-rabbitmq/build.gradle diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java diff --git a/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml b/module/spring-boot-rabbitmq/src/dockerTest/resources/logback-test.xml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/logback-test.xml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/ca.crt b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/ca.crt similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/ca.crt rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/ca.crt diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.crt b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/client.crt similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.crt rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/client.crt diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.key b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/client.key similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.key rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/client.key diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-ssl.conf b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-ssl.conf similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-ssl.conf rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-ssl.conf diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.crt b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/server.crt similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.crt rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/server.crt diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.key b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/server.key similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.key rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/server.key diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf diff --git a/module/spring-boot-amqp/src/dockerTest/resources/spring.properties b/module/spring-boot-rabbitmq/src/dockerTest/resources/spring.properties similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/spring.properties rename to module/spring-boot-rabbitmq/src/dockerTest/resources/spring.properties diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractConnectionFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractConnectionFactoryConfigurer.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractConnectionFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractConnectionFactoryConfigurer.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/CachingConnectionFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/CachingConnectionFactoryConfigurer.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/CachingConnectionFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/CachingConnectionFactoryConfigurer.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionFactoryCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionFactoryCustomizer.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionFactoryCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionFactoryCustomizer.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/EnvironmentBuilderCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/EnvironmentBuilderCustomizer.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/EnvironmentBuilderCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/EnvironmentBuilderCustomizer.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetails.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetails.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetails.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetails.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionDetails.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionDetails.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionDetails.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionDetails.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitListenerRetrySettingsCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitListenerRetrySettingsCustomizer.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitListenerRetrySettingsCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitListenerRetrySettingsCustomizer.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitRetryTemplateSettings.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitRetryTemplateSettings.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitRetryTemplateSettings.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitRetryTemplateSettings.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateCustomizer.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateCustomizer.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SslBundleRabbitConnectionFactoryBean.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/SslBundleRabbitConnectionFactoryBean.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SslBundleRabbitConnectionFactoryBean.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/SslBundleRabbitConnectionFactoryBean.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/health/package-info.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/health/package-info.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/package-info.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/package-info.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/RabbitHealthIndicator.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/health/RabbitHealthIndicator.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/RabbitHealthIndicator.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/health/RabbitHealthIndicator.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/health/package-info.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/health/package-info.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/RabbitMetrics.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/metrics/RabbitMetrics.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/RabbitMetrics.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/metrics/RabbitMetrics.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/metrics/package-info.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/metrics/package-info.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java similarity index 100% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-rabbitmq/src/main/resources/META-INF/additional-spring-configuration-metadata.json similarity index 100% rename from module/spring-boot-amqp/src/main/resources/META-INF/additional-spring-configuration-metadata.json rename to module/spring-boot-rabbitmq/src/main/resources/META-INF/additional-spring-configuration-metadata.json diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories similarity index 100% rename from module/spring-boot-amqp/src/main/resources/META-INF/spring.factories rename to module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports similarity index 100% rename from module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename to module/spring-boot-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetailsTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetailsTests.java similarity index 100% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetailsTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetailsTests.java diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java similarity index 100% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java similarity index 100% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java similarity index 100% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java similarity index 100% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java similarity index 100% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java similarity index 100% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java similarity index 100% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/health/RabbitHealthIndicatorTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/health/RabbitHealthIndicatorTests.java similarity index 100% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/health/RabbitHealthIndicatorTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/health/RabbitHealthIndicatorTests.java diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/metrics/RabbitMetricsTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/metrics/RabbitMetricsTests.java similarity index 100% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/metrics/RabbitMetricsTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/metrics/RabbitMetricsTests.java diff --git a/module/spring-boot-amqp/src/test/resources/logback-test.xml b/module/spring-boot-rabbitmq/src/test/resources/logback-test.xml similarity index 100% rename from module/spring-boot-amqp/src/test/resources/logback-test.xml rename to module/spring-boot-rabbitmq/src/test/resources/logback-test.xml diff --git a/module/spring-boot-amqp/src/test/resources/org/springframework/boot/amqp/autoconfigure/test.jks b/module/spring-boot-rabbitmq/src/test/resources/org/springframework/boot/amqp/autoconfigure/test.jks similarity index 100% rename from module/spring-boot-amqp/src/test/resources/org/springframework/boot/amqp/autoconfigure/test.jks rename to module/spring-boot-rabbitmq/src/test/resources/org/springframework/boot/amqp/autoconfigure/test.jks diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9ff7bd915f9a..20f1f17dbc16 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2049,7 +2049,6 @@ bom { "spring-boot-activemq", "spring-boot-actuator", "spring-boot-actuator-autoconfigure", - "spring-boot-amqp", "spring-boot-artemis", "spring-boot-autoconfigure", "spring-boot-autoconfigure-classic", @@ -2141,6 +2140,7 @@ bom { "spring-boot-pulsar", "spring-boot-quartz", "spring-boot-r2dbc", + "spring-boot-rabbitmq", "spring-boot-reactor", "spring-boot-reactor-netty", "spring-boot-restclient", @@ -2166,8 +2166,6 @@ bom { "spring-boot-starter-activemq-test", "spring-boot-starter-actuator", "spring-boot-starter-actuator-test", - "spring-boot-starter-amqp", - "spring-boot-starter-amqp-test", "spring-boot-starter-artemis", "spring-boot-starter-artemis-test", "spring-boot-starter-aspectj", @@ -2280,6 +2278,8 @@ bom { "spring-boot-starter-quartz-test", "spring-boot-starter-r2dbc", "spring-boot-starter-r2dbc-test", + "spring-boot-starter-rabbitmqmq", + "spring-boot-starter-rabbitmqmq-test", "spring-boot-starter-reactor-netty", "spring-boot-starter-restclient", "spring-boot-starter-restclient-test", diff --git a/settings.gradle b/settings.gradle index 82506ce30b77..5fc768cf5728 100644 --- a/settings.gradle +++ b/settings.gradle @@ -80,7 +80,6 @@ include "core:spring-boot-testcontainers" include "module:spring-boot-activemq" include "module:spring-boot-actuator" include "module:spring-boot-actuator-autoconfigure" -include "module:spring-boot-amqp" include "module:spring-boot-artemis" include "module:spring-boot-autoconfigure-classic" include "module:spring-boot-autoconfigure-classic-modules" @@ -164,6 +163,7 @@ include "module:spring-boot-persistence" include "module:spring-boot-pulsar" include "module:spring-boot-quartz" include "module:spring-boot-r2dbc" +include "module:spring-boot-rabbitmq" include "module:spring-boot-reactor" include "module:spring-boot-reactor-netty" include "module:spring-boot-restclient" @@ -208,8 +208,6 @@ include "starter:spring-boot-starter-activemq" include "starter:spring-boot-starter-activemq-test" include "starter:spring-boot-starter-actuator" include "starter:spring-boot-starter-actuator-test" -include "starter:spring-boot-starter-amqp" -include "starter:spring-boot-starter-amqp-test" include "starter:spring-boot-starter-artemis" include "starter:spring-boot-starter-artemis-test" include "starter:spring-boot-starter-aspectj" @@ -325,6 +323,8 @@ include "starter:spring-boot-starter-quartz" include "starter:spring-boot-starter-quartz-test" include "starter:spring-boot-starter-r2dbc" include "starter:spring-boot-starter-r2dbc-test" +include "starter:spring-boot-starter-rabbitmq" +include "starter:spring-boot-starter-rabbitmq-test" include "starter:spring-boot-starter-reactor-netty" include "starter:spring-boot-starter-restclient" include "starter:spring-boot-starter-restclient-test" @@ -393,7 +393,6 @@ include ":smoke-test:spring-boot-smoke-test-actuator-extension" include ":smoke-test:spring-boot-smoke-test-actuator-log4j2" include ":smoke-test:spring-boot-smoke-test-actuator-noweb" include ":smoke-test:spring-boot-smoke-test-actuator-ui" -include ":smoke-test:spring-boot-smoke-test-amqp" include ":smoke-test:spring-boot-smoke-test-ant" include ":smoke-test:spring-boot-smoke-test-artemis" include ":smoke-test:spring-boot-smoke-test-aspectj" @@ -441,6 +440,7 @@ include ":smoke-test:spring-boot-smoke-test-prometheus" include ":smoke-test:spring-boot-smoke-test-property-validation" include ":smoke-test:spring-boot-smoke-test-pulsar" include ":smoke-test:spring-boot-smoke-test-quartz" +include ":smoke-test:spring-boot-smoke-test-rabbitmq" include ":smoke-test:spring-boot-smoke-test-reactive-oauth2-client" include ":smoke-test:spring-boot-smoke-test-reactive-oauth2-resource-server" include ":smoke-test:spring-boot-smoke-test-restclient" diff --git a/smoke-test/spring-boot-smoke-test-amqp/build.gradle b/smoke-test/spring-boot-smoke-test-rabbitmq/build.gradle similarity index 94% rename from smoke-test/spring-boot-smoke-test-amqp/build.gradle rename to smoke-test/spring-boot-smoke-test-rabbitmq/build.gradle index cadb8379f2ef..6da49cf2ee9a 100644 --- a/smoke-test/spring-boot-smoke-test-amqp/build.gradle +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/build.gradle @@ -22,10 +22,10 @@ plugins { description = "Spring Boot AMQP smoke test" dependencies { - implementation(project(":starter:spring-boot-starter-amqp")) + implementation(project(":starter:spring-boot-starter-rabbitmq")) dockerTestImplementation(project(":core:spring-boot-testcontainers")) - dockerTestImplementation(project(":starter:spring-boot-starter-amqp-test")) + dockerTestImplementation(project(":starter:spring-boot-starter-rabbitmq-test")) dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) dockerTestImplementation("org.awaitility:awaitility") dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SecureRabbitMqContainer.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SecureRabbitMqContainer.java similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SecureRabbitMqContainer.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SecureRabbitMqContainer.java diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Sender.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/Sender.java similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Sender.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/Sender.java diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/package-info.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/package-info.java similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/package-info.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/package-info.java diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/rabbitmq.conf b/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/rabbitmq.conf similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/rabbitmq.conf rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/rabbitmq.conf diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.crt b/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-ca.crt similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.crt rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-ca.crt diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.key b/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-ca.key similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.key rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-ca.key diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.crt b/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-client.crt similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.crt rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-client.crt diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.key b/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-client.key similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.key rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-client.key diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.crt b/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-server.crt similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.crt rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-server.crt diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.key b/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-server.key similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.key rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-server.key diff --git a/starter/spring-boot-starter-amqp-test/build.gradle b/starter/spring-boot-starter-rabbitmq-test/build.gradle similarity index 93% rename from starter/spring-boot-starter-amqp-test/build.gradle rename to starter/spring-boot-starter-rabbitmq-test/build.gradle index efe35c54451b..1ceffd67e6a4 100644 --- a/starter/spring-boot-starter-amqp-test/build.gradle +++ b/starter/spring-boot-starter-rabbitmq-test/build.gradle @@ -21,7 +21,7 @@ plugins { description = "Starter for testing Spring AMQP and Rabbit MQ" dependencies { - api(project(":starter:spring-boot-starter-amqp")) + api(project(":starter:spring-boot-starter-rabbitmq")) api(project(":starter:spring-boot-starter-test")) api("org.springframework.amqp:spring-rabbit-test") diff --git a/starter/spring-boot-starter-amqp/build.gradle b/starter/spring-boot-starter-rabbitmq/build.gradle similarity index 94% rename from starter/spring-boot-starter-amqp/build.gradle rename to starter/spring-boot-starter-rabbitmq/build.gradle index bd8d7c906e19..ff4e3da9c008 100644 --- a/starter/spring-boot-starter-amqp/build.gradle +++ b/starter/spring-boot-starter-rabbitmq/build.gradle @@ -23,5 +23,5 @@ description = "Starter for using Spring AMQP and Rabbit MQ" dependencies { api(project(":starter:spring-boot-starter")) - api(project(":module:spring-boot-amqp")) + api(project(":module:spring-boot-rabbitmq")) } From b820d9feac55cc4dcdb623210c9737cb7d4c4c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 17 Mar 2026 11:34:39 +0100 Subject: [PATCH 373/930] Harmonize support to refer to RabbitMQ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adapts description to refer to RabbitMQ rather than AMQP. See gh-49620 Co-authored-by: Eddú Meléndez --- .../src/docs/antora/modules/reference/pages/messaging/amqp.adoc | 2 +- module/spring-boot-rabbitmq/build.gradle | 2 +- smoke-test/spring-boot-smoke-test-rabbitmq/build.gradle | 2 +- starter/spring-boot-starter-rabbitmq-test/build.gradle | 2 +- starter/spring-boot-starter-rabbitmq/build.gradle | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index aaa67268da6f..a7b5befce73b 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc @@ -3,7 +3,7 @@ The Advanced Message Queuing Protocol (AMQP) is a platform-neutral, wire-level protocol for message-oriented middleware. The Spring AMQP project applies core Spring concepts to the development of AMQP-based messaging solutions. -Spring Boot offers several conveniences for working with AMQP through RabbitMQ, including the `spring-boot-starter-amqp` starter. +Spring Boot offers several conveniences for working with AMQP through RabbitMQ, including the `spring-boot-starter-rabbitmq` starter. diff --git a/module/spring-boot-rabbitmq/build.gradle b/module/spring-boot-rabbitmq/build.gradle index 0152cb160b86..4942083bfa4f 100644 --- a/module/spring-boot-rabbitmq/build.gradle +++ b/module/spring-boot-rabbitmq/build.gradle @@ -23,7 +23,7 @@ plugins { id "org.springframework.boot.optional-dependencies" } -description = "Spring Boot AMQP" +description = "Spring Boot RabbitMQ" dependencies { api(project(":core:spring-boot")) diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/build.gradle b/smoke-test/spring-boot-smoke-test-rabbitmq/build.gradle index 6da49cf2ee9a..48fe3815c2bd 100644 --- a/smoke-test/spring-boot-smoke-test-rabbitmq/build.gradle +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/build.gradle @@ -19,7 +19,7 @@ plugins { id "org.springframework.boot.docker-test" } -description = "Spring Boot AMQP smoke test" +description = "Spring Boot RabbitMQ smoke test" dependencies { implementation(project(":starter:spring-boot-starter-rabbitmq")) diff --git a/starter/spring-boot-starter-rabbitmq-test/build.gradle b/starter/spring-boot-starter-rabbitmq-test/build.gradle index 1ceffd67e6a4..217f6ee71891 100644 --- a/starter/spring-boot-starter-rabbitmq-test/build.gradle +++ b/starter/spring-boot-starter-rabbitmq-test/build.gradle @@ -18,7 +18,7 @@ plugins { id "org.springframework.boot.starter" } -description = "Starter for testing Spring AMQP and Rabbit MQ" +description = "Starter for testing RabbitMQ messaging and streaming broker" dependencies { api(project(":starter:spring-boot-starter-rabbitmq")) diff --git a/starter/spring-boot-starter-rabbitmq/build.gradle b/starter/spring-boot-starter-rabbitmq/build.gradle index ff4e3da9c008..64879f96866f 100644 --- a/starter/spring-boot-starter-rabbitmq/build.gradle +++ b/starter/spring-boot-starter-rabbitmq/build.gradle @@ -18,7 +18,7 @@ plugins { id "org.springframework.boot.starter" } -description = "Starter for using Spring AMQP and Rabbit MQ" +description = "Starter for using RabbitMQ messaging and streaming broker" dependencies { api(project(":starter:spring-boot-starter")) From edadf816abcda532048324696336014778e75d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 17 Mar 2026 12:40:06 +0100 Subject: [PATCH 374/930] Rename package for RabbitMQ support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit renames the RabbitMQ root packages from amqp to rabbitmq. See gh-49620 Co-authored-by: Eddú Meléndez --- .../reference/pages/actuator/endpoints.adoc | 2 +- .../pages/features/dev-services.adoc | 4 +-- .../reference/pages/messaging/amqp.adoc | 16 +++++----- .../pages/testing/testcontainers.adoc | 10 +++--- .../custom/MyRabbitConfiguration.java | 2 +- .../receiving/custom/MyRabbitConfiguration.kt | 2 +- ...nectionDetailsFactoryIntegrationTests.java | 8 ++--- ...nectionDetailsFactoryIntegrationTests.java | 6 ++-- ...nectionDetailsFactoryIntegrationTests.java | 4 +-- ...nectionDetailsFactoryIntegrationTests.java | 8 ++--- ...nectionDetailsFactoryIntegrationTests.java | 6 ++-- ...nectionDetailsFactoryIntegrationTests.java | 6 ++-- ...nectionDetailsFactoryIntegrationTests.java | 14 ++++---- ...nectionDetailsFactoryIntegrationTests.java | 30 +++++++++--------- ...nectionDetailsFactoryIntegrationTests.java | 14 ++++---- ...nectionDetailsFactoryIntegrationTests.java | 14 ++++---- .../boot/{amqp => rabbitmq}/ca.crt | 0 .../boot/{amqp => rabbitmq}/client.crt | 0 .../boot/{amqp => rabbitmq}/client.key | 0 ...bbit-stream-separate-services-compose.yaml | 0 ...-rabbit-stream-single-service-compose.yaml | 0 .../docker/compose/rabbit-compose.yaml | 0 .../docker/compose/rabbit-ssl-compose.yaml | 0 .../docker/compose/rabbit-stream-compose.yaml | 0 .../compose/rabbit-stream-ssl-compose.yaml | 0 .../docker/compose/rabbitmq-ssl.conf | 0 .../docker/compose/rabbitmq-stream-ssl.conf | 0 .../boot/{amqp => rabbitmq}/server.crt | 0 .../boot/{amqp => rabbitmq}/server.key | 0 .../testcontainers/rabbitmq-stream-ssl.conf | 0 .../AbstractConnectionFactoryConfigurer.java | 4 +-- ...bitListenerContainerFactoryConfigurer.java | 8 ++--- .../CachingConnectionFactoryConfigurer.java | 4 +-- .../ConnectionFactoryCustomizer.java | 4 +-- ...bitListenerContainerFactoryConfigurer.java | 4 +-- .../EnvironmentBuilderCustomizer.java | 4 +-- .../PropertiesRabbitConnectionDetails.java | 4 +-- .../RabbitAnnotationDrivenConfiguration.java | 2 +- .../RabbitAutoConfiguration.java | 4 +-- .../RabbitConnectionDetails.java | 4 +-- ...RabbitConnectionFactoryBeanConfigurer.java | 6 ++-- ...RabbitListenerRetrySettingsCustomizer.java | 4 +-- .../autoconfigure/RabbitProperties.java | 4 +-- .../RabbitRetryTemplateSettings.java | 4 +-- .../RabbitStreamConfiguration.java | 8 ++--- .../RabbitStreamConnectionDetails.java | 2 +- .../RabbitStreamTemplateConfigurer.java | 4 +-- .../RabbitTemplateConfigurer.java | 4 +-- .../RabbitTemplateCustomizer.java | 4 +-- ...RabbitTemplateRetrySettingsCustomizer.java | 4 +-- ...bitListenerContainerFactoryConfigurer.java | 4 +-- .../SslBundleRabbitConnectionFactoryBean.java | 2 +- ...bitHealthContributorAutoConfiguration.java | 8 ++--- .../autoconfigure/health/package-info.java | 2 +- ...ConnectionFactoryMetricsPostProcessor.java | 4 +-- .../RabbitMetricsAutoConfiguration.java | 6 ++-- .../autoconfigure/metrics/package-info.java | 2 +- .../autoconfigure/package-info.java | 2 +- ...DockerComposeConnectionDetailsFactory.java | 6 ++-- .../docker/compose/RabbitEnvironment.java | 2 +- ...DockerComposeConnectionDetailsFactory.java | 6 ++-- .../docker/compose/package-info.java | 2 +- .../health/RabbitHealthIndicator.java | 4 +-- .../health/package-info.java | 2 +- .../metrics/RabbitMetrics.java | 4 +-- .../metrics/package-info.java | 2 +- ...bbitContainerConnectionDetailsFactory.java | 4 +-- ...bbitContainerConnectionDetailsFactory.java | 4 +-- ...reamContainerConnectionDetailsFactory.java | 4 +-- .../testcontainers/package-info.java | 2 +- .../main/resources/META-INF/spring.factories | 10 +++--- ...ot.autoconfigure.AutoConfiguration.imports | 6 ++-- ...ropertiesRabbitConnectionDetailsTests.java | 4 +-- .../RabbitAutoConfigurationTests.java | 16 +++++----- .../autoconfigure/RabbitPropertiesTests.java | 2 +- .../RabbitStreamConfigurationTests.java | 8 ++--- ...althContributorAutoConfigurationTests.java | 6 ++-- ...ationMeterBinderCycleIntegrationTests.java | 4 +-- .../RabbitMetricsAutoConfigurationTests.java | 4 +-- .../compose/RabbitEnvironmentTests.java | 2 +- .../health/RabbitHealthIndicatorTests.java | 2 +- .../metrics/RabbitMetricsTests.java | 2 +- .../{amqp => rabbitmq}/autoconfigure/test.jks | Bin .../SampleRabbitApplicationSslTests.java} | 4 +-- .../SampleRabbitApplicationTests.java} | 4 +-- .../SecureRabbitMqContainer.java | 2 +- .../SampleRabbitApplication.java} | 8 ++--- .../smoketest/{amqp => rabbitmq}/Sender.java | 2 +- .../{amqp => rabbitmq}/package-info.java | 2 +- 89 files changed, 196 insertions(+), 196 deletions(-) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/{amqp => rabbitmq}/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java (89%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/{amqp => rabbitmq}/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java (91%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/{amqp => rabbitmq}/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java (93%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/{amqp => rabbitmq}/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java (88%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/{amqp => rabbitmq}/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java (93%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/{amqp => rabbitmq}/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java (93%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/{amqp => rabbitmq}/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java (86%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/{amqp => rabbitmq}/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java (82%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/{amqp => rabbitmq}/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java (89%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/{amqp => rabbitmq}/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java (88%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/{amqp => rabbitmq}/ca.crt (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/{amqp => rabbitmq}/client.crt (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/{amqp => rabbitmq}/client.key (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/{amqp => rabbitmq}/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/{amqp => rabbitmq}/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/{amqp => rabbitmq}/docker/compose/rabbit-compose.yaml (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/{amqp => rabbitmq}/docker/compose/rabbit-ssl-compose.yaml (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/{amqp => rabbitmq}/docker/compose/rabbit-stream-compose.yaml (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/{amqp => rabbitmq}/docker/compose/rabbit-stream-ssl-compose.yaml (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/{amqp => rabbitmq}/docker/compose/rabbitmq-ssl.conf (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/{amqp => rabbitmq}/docker/compose/rabbitmq-stream-ssl.conf (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/{amqp => rabbitmq}/server.crt (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/{amqp => rabbitmq}/server.key (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/{amqp => rabbitmq}/testcontainers/rabbitmq-stream-ssl.conf (100%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/AbstractConnectionFactoryConfigurer.java (98%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java (96%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/CachingConnectionFactoryConfigurer.java (97%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/ConnectionFactoryCustomizer.java (93%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java (96%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/EnvironmentBuilderCustomizer.java (94%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/PropertiesRabbitConnectionDetails.java (94%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitAnnotationDrivenConfiguration.java (99%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitAutoConfiguration.java (99%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitConnectionDetails.java (97%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java (97%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitListenerRetrySettingsCustomizer.java (94%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitProperties.java (99%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitRetryTemplateSettings.java (95%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitStreamConfiguration.java (96%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitStreamConnectionDetails.java (97%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitStreamTemplateConfigurer.java (98%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitTemplateConfigurer.java (98%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitTemplateCustomizer.java (93%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java (94%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java (96%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/SslBundleRabbitConnectionFactoryBean.java (96%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java (91%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/health/package-info.java (91%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java (96%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java (92%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/metrics/package-info.java (91%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/package-info.java (92%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java (92%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/docker/compose/RabbitEnvironment.java (95%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java (93%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/docker/compose/package-info.java (92%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/health/RabbitHealthIndicator.java (96%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/health/package-info.java (93%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/metrics/RabbitMetrics.java (96%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/metrics/package-info.java (93%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java (95%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/testcontainers/RabbitContainerConnectionDetailsFactory.java (94%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java (95%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/{amqp => rabbitmq}/testcontainers/package-info.java (92%) rename module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/PropertiesRabbitConnectionDetailsTests.java (95%) rename module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitAutoConfigurationTests.java (99%) rename module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitPropertiesTests.java (99%) rename module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/RabbitStreamConfigurationTests.java (98%) rename module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java (89%) rename module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java (94%) rename module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java (95%) rename module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/{amqp => rabbitmq}/docker/compose/RabbitEnvironmentTests.java (97%) rename module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/{amqp => rabbitmq}/health/RabbitHealthIndicatorTests.java (98%) rename module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/{amqp => rabbitmq}/metrics/RabbitMetricsTests.java (97%) rename module/spring-boot-rabbitmq/src/test/resources/org/springframework/boot/{amqp => rabbitmq}/autoconfigure/test.jks (100%) rename smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/{amqp/SampleAmqpSimpleApplicationSslTests.java => rabbitmq/SampleRabbitApplicationSslTests.java} (97%) rename smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/{amqp/SampleAmqpSimpleApplicationTests.java => rabbitmq/SampleRabbitApplicationTests.java} (96%) rename smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/{amqp => rabbitmq}/SecureRabbitMqContainer.java (98%) rename smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/{amqp/SampleAmqpSimpleApplication.java => rabbitmq/SampleRabbitApplication.java} (87%) rename smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/{amqp => rabbitmq}/Sender.java (97%) rename smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/{amqp => rabbitmq}/package-info.java (96%) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc index c231b03da80f..dbc310ac5564 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc @@ -673,7 +673,7 @@ with the `key` listed in the following table: | Always responds with `UP`. | `rabbit` -| javadoc:org.springframework.boot.amqp.health.RabbitHealthIndicator[] +| javadoc:org.springframework.boot.rabbitmq.health.RabbitHealthIndicator[] | Checks that a Rabbit server is up. | `redis` diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc index dac48926bed9..36e57ff478db 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc @@ -125,10 +125,10 @@ The following service connections are currently supported: | javadoc:org.springframework.boot.r2dbc.autoconfigure.R2dbcConnectionDetails[] | Containers named "clickhouse/clickhouse-server", "gvenzl/oracle-free", "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres" -| javadoc:org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails[] +| javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails[] | Containers named "rabbitmq" with container port 5672 mapped -| javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] +| javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[] | Containers named "rabbitmq" with container port 5552 mapped | javadoc:org.springframework.boot.data.redis.autoconfigure.DataRedisConnectionDetails[] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index a7b5befce73b..3864f7ebe9ea 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc @@ -38,12 +38,12 @@ spring: NOTE: When specifying addresses that way, the `host` and `port` properties are ignored. If the address uses the `amqps` protocol, SSL support is enabled automatically. -See javadoc:org.springframework.boot.amqp.autoconfigure.RabbitProperties[] for more of the supported property-based configuration options. -To configure lower-level details of the RabbitMQ javadoc:com.rabbitmq.client.ConnectionFactory[] that is used by Spring AMQP, define a javadoc:org.springframework.boot.amqp.autoconfigure.ConnectionFactoryCustomizer[] bean. +See javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties[] for more of the supported property-based configuration options. +To configure lower-level details of the RabbitMQ javadoc:com.rabbitmq.client.ConnectionFactory[] that is used by Spring AMQP, define a javadoc:org.springframework.boot.rabbitmq.autoconfigure.ConnectionFactoryCustomizer[] bean. If a javadoc:org.springframework.amqp.rabbit.connection.ConnectionNameStrategy[] bean exists in the context, it will be automatically used to name connections created by the auto-configured javadoc:org.springframework.amqp.rabbit.connection.CachingConnectionFactory[]. -To make an application-wide, additive customization to the javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[], use a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitTemplateCustomizer[] bean. +To make an application-wide, additive customization to the javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[], use a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitTemplateCustomizer[] bean. TIP: See https://spring.io/blog/2010/06/14/understanding-amqp-the-protocol-used-by-rabbitmq/[Understanding AMQP, the protocol used by RabbitMQ] for more details. @@ -74,9 +74,9 @@ spring: ---- Retries are disabled by default. -You can also customize the javadoc:org.springframework.core.retry.RetryTemplate[] programmatically by declaring a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitTemplateRetrySettingsCustomizer[] bean. +You can also customize the javadoc:org.springframework.core.retry.RetryTemplate[] programmatically by declaring a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitTemplateRetrySettingsCustomizer[] bean. -If you need to create more javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] with the same settings as the factories used by the auto-configuration. +If you need to create more javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] with the same settings as the factories used by the auto-configuration. If there's a bean of type javadoc:org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservationConvention[] in the context, it will automatically be configured on the javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[]. @@ -97,7 +97,7 @@ spring: If a javadoc:org.springframework.amqp.support.converter.MessageConverter[], javadoc:org.springframework.rabbit.stream.support.converter.StreamMessageConverter[], javadoc:org.springframework.rabbit.stream.producer.ProducerCustomizer[] or javadoc:org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservationConvention[] bean is defined, it is associated automatically to the auto-configured javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[]. -If you need to create more javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[] with the same settings as the factories used by the auto-configuration. +If you need to create more javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[] with the same settings as the factories used by the auto-configuration. @@ -120,7 +120,7 @@ include-code::MyBean[] TIP: See javadoc:org.springframework.amqp.rabbit.annotation.EnableRabbit[format=annotation] for more details. -If you need to create more javadoc:org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.amqp.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer[] and a javadoc:org.springframework.boot.amqp.autoconfigure.DirectRabbitListenerContainerFactoryConfigurer[] that you can use to initialize a javadoc:org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory[] and a javadoc:org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory[] with the same settings as the factories used by the auto-configuration. +If you need to create more javadoc:org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.rabbitmq.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer[] and a javadoc:org.springframework.boot.rabbitmq.autoconfigure.DirectRabbitListenerContainerFactoryConfigurer[] that you can use to initialize a javadoc:org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory[] and a javadoc:org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory[] with the same settings as the factories used by the auto-configuration. TIP: It does not matter which container type you chose. Those two beans are exposed by the auto-configuration. @@ -137,7 +137,7 @@ You can enable retries to handle situations where your listener throws an except By default, javadoc:org.springframework.amqp.rabbit.retry.RejectAndDontRequeueRecoverer[] is used, but you can define a javadoc:org.springframework.amqp.rabbit.retry.MessageRecoverer[] of your own. When retries are exhausted, the message is rejected and either dropped or routed to a dead-letter exchange if the broker is configured to do so. By default, retries are disabled. -You can also customize the javadoc:org.springframework.core.retry.RetryPolicy[] programmatically by declaring a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitListenerRetrySettingsCustomizer[] bean. +You can also customize the javadoc:org.springframework.core.retry.RetryPolicy[] programmatically by declaring a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitListenerRetrySettingsCustomizer[] bean. IMPORTANT: By default, if retries are disabled and the listener throws an exception, the delivery is retried indefinitely. You can modify this behavior in two ways: Set the `defaultRequeueRejected` property to `false` so that zero re-deliveries are attempted or throw an javadoc:org.springframework.amqp.AmqpRejectAndDontRequeueException[] to signal the message should be rejected. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc index d54476a6a809..72240022f6ad 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc @@ -173,11 +173,11 @@ javadoc:org.testcontainers.clickhouse.ClickHouseContainer[], javadoc:{url-testcontainers-mariadb-javadoc}/org.testcontainers.mariadb.MariaDBContainer[], javadoc:{url-testcontainers-mssqlserver-javadoc}/org.testcontainers.mssqlserver.MSSQLServerContainer[], javadoc:{url-testcontainers-mysql-javadoc}/org.testcontainers.mysql.MySQLContainer[], javadoc:org.testcontainers.oracle.OracleContainer[OracleContainer (free)], javadoc:{url-testcontainers-oracle-xe-javadoc}/org.testcontainers.containers.OracleContainer[OracleContainer (XE)] or javadoc:{url-testcontainers-postgresql-javadoc}/org.testcontainers.postgresql.PostgreSQLContainer[] -| javadoc:org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails[] +| javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails[] | Containers of type javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] -| javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] -| Containers of type javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] when the `@ServiceConnection` `type` attribute includes javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] +| javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[] +| Containers of type javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] when the `@ServiceConnection` `type` attribute includes javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[] | javadoc:org.springframework.boot.data.redis.autoconfigure.DataRedisConnectionDetails[] | Containers of type javadoc:com.redis.testcontainers.RedisContainer[] or javadoc:com.redis.testcontainers.RedisStackContainer[], or containers named "redis", "redis/redis-stack" or "redis/redis-stack-server" @@ -188,12 +188,12 @@ javadoc:org.testcontainers.oracle.OracleContainer[OracleContainer (free)], javad [TIP] ==== -By default, with the exception of javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[], all applicable connection details beans will be created for a given javadoc:org.testcontainers.containers.Container[]. +By default, with the exception of javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[], all applicable connection details beans will be created for a given javadoc:org.testcontainers.containers.Container[]. For example, a javadoc:{url-testcontainers-postgresql-javadoc}/org.testcontainers.postgresql.PostgreSQLContainer[] will create both javadoc:org.springframework.boot.jdbc.autoconfigure.JdbcConnectionDetails[] and javadoc:org.springframework.boot.r2dbc.autoconfigure.R2dbcConnectionDetails[]. If you want to create only a subset of the applicable types, you can use the `type` attribute of javadoc:org.springframework.boot.testcontainers.service.connection.ServiceConnection[format=annotation]. -To create a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] bean from a javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[], you must opt in using the `type` attribute of javadoc:org.springframework.boot.testcontainers.service.connection.ServiceConnection[format=annotation]. +To create a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[] bean from a javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[], you must opt in using the `type` attribute of javadoc:org.springframework.boot.testcontainers.service.connection.ServiceConnection[format=annotation]. The container must also expose port 5552, the RabbitMQ streams port. ==== diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java index e109d5de244a..172c9cbba0d1 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java @@ -18,7 +18,7 @@ import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.boot.amqp.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer; +import org.springframework.boot.rabbitmq.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt index 4f1c80f38666..95ca6f610039 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt @@ -19,7 +19,7 @@ package org.springframework.boot.docs.messaging.amqp.receiving.custom import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory import org.springframework.amqp.rabbit.connection.CachingConnectionFactory import org.springframework.amqp.rabbit.connection.ConnectionFactory -import org.springframework.boot.amqp.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer +import org.springframework.boot.rabbitmq.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 89% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index 5b1c7797bbac..dcef4737265a 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 91% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java index 4d644b742e48..fb1e627f60e4 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 93% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index 67f8fad358e6..b54ab7270fb4 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 88% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index d9f077bc21cd..ae9d61597f63 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java similarity index 93% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java index 51e32711184d..91aed53b74b5 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.time.Duration; import java.util.ArrayList; @@ -30,9 +30,9 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java similarity index 93% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java index ea80da8d9bf3..f371b8562284 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.time.Duration; import java.util.ArrayList; @@ -30,9 +30,9 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 86% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java index 1e65d213f2ac..8820db4606a9 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.time.Duration; import java.util.ArrayList; @@ -31,13 +31,13 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java similarity index 82% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java index d439b0fdbf48..6eab916aef27 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.time.Duration; import java.util.ArrayList; @@ -32,13 +32,13 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.PemKeyStore; import org.springframework.boot.testcontainers.service.connection.PemTrustStore; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -68,9 +68,9 @@ class RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests { @Container @ServiceConnection(type = RabbitStreamConnectionDetails.class) - @PemTrustStore(certificate = "classpath:org/springframework/boot/amqp/ca.crt") - @PemKeyStore(certificate = "classpath:org/springframework/boot/amqp/client.crt", - privateKey = "classpath:org/springframework/boot/amqp/client.key") + @PemTrustStore(certificate = "classpath:org/springframework/boot/rabbitmq/ca.crt") + @PemKeyStore(certificate = "classpath:org/springframework/boot/rabbitmq/client.crt", + privateKey = "classpath:org/springframework/boot/rabbitmq/client.key") static final RabbitMQContainer rabbit = getRabbitMqStreamContainer(); private static RabbitMQContainer getRabbitMqStreamContainer() { @@ -80,15 +80,15 @@ private static RabbitMQContainer getRabbitMqStreamContainer() { container.withCopyToContainer(Transferable.of(enabledPlugins), "/etc/rabbitmq/enabled_plugins"); container.withCopyFileToContainer( MountableFile - .forClasspathResource("org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf"), + .forClasspathResource("org/springframework/boot/rabbitmq/testcontainers/rabbitmq-stream-ssl.conf"), "/etc/rabbitmq/rabbitmq.conf"); - container.withCopyFileToContainer(MountableFile.forClasspathResource("org/springframework/boot/amqp/ca.crt"), - "/etc/rabbitmq/ca.crt"); container.withCopyFileToContainer( - MountableFile.forClasspathResource("org/springframework/boot/amqp/server.key"), + MountableFile.forClasspathResource("org/springframework/boot/rabbitmq/ca.crt"), "/etc/rabbitmq/ca.crt"); + container.withCopyFileToContainer( + MountableFile.forClasspathResource("org/springframework/boot/rabbitmq/server.key"), "/etc/rabbitmq/server.key"); container.withCopyFileToContainer( - MountableFile.forClasspathResource("org/springframework/boot/amqp/server.crt"), + MountableFile.forClasspathResource("org/springframework/boot/rabbitmq/server.crt"), "/etc/rabbitmq/server.crt"); return container; } diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 89% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java index 778f6536bd57..e28c2657a7c8 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -35,13 +35,13 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 88% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java index 6dc8b12291c3..3d306284fa4f 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -35,13 +35,13 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/ca.crt b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/ca.crt similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/ca.crt rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/ca.crt diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/client.crt b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/client.crt similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/client.crt rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/client.crt diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/client.key b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/client.key similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/client.key rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/client.key diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-ssl-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-ssl-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-ssl-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-ssl-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-ssl.conf b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-ssl.conf similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-ssl.conf rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-ssl.conf diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-ssl.conf similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-ssl.conf diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/server.crt b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/server.crt similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/server.crt rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/server.crt diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/server.key b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/server.key similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/server.key rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/server.key diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/testcontainers/rabbitmq-stream-ssl.conf similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/testcontainers/rabbitmq-stream-ssl.conf diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractConnectionFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractConnectionFactoryConfigurer.java similarity index 98% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractConnectionFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractConnectionFactoryConfigurer.java index 07b17ce5a9a1..f1667d8130ba 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractConnectionFactoryConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractConnectionFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.util.stream.Collectors; @@ -33,7 +33,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb - * @since 4.0.0 + * @since 4.1.0 */ public abstract class AbstractConnectionFactoryConfigurer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java index ffed69eda93e..ce4591f5a2eb 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.util.List; import java.util.concurrent.Executor; @@ -28,8 +28,8 @@ import org.springframework.amqp.rabbit.retry.RejectAndDontRequeueRecoverer; import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention; import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.boot.amqp.autoconfigure.RabbitProperties.ListenerRetry; -import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Retry; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.ListenerRetry; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.Retry; import org.springframework.boot.retry.RetryPolicySettings; import org.springframework.core.retry.RetryPolicy; import org.springframework.util.Assert; @@ -41,7 +41,7 @@ * @param the container factory type. * @author Gary Russell * @author Stephane Nicoll - * @since 4.0.0 + * @since 4.1.0 */ public abstract class AbstractRabbitListenerContainerFactoryConfigurer> { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/CachingConnectionFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/CachingConnectionFactoryConfigurer.java similarity index 97% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/CachingConnectionFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/CachingConnectionFactoryConfigurer.java index 2221595f78d1..735e5fffa16a 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/CachingConnectionFactoryConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/CachingConnectionFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.time.Duration; @@ -33,7 +33,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb - * @since 4.0.0 + * @since 4.1.0 */ public class CachingConnectionFactoryConfigurer extends AbstractConnectionFactoryConfigurer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionFactoryCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/ConnectionFactoryCustomizer.java similarity index 93% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionFactoryCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/ConnectionFactoryCustomizer.java index 15f2e14dfff8..119e40b34c9d 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionFactoryCustomizer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/ConnectionFactoryCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import com.rabbitmq.client.ConnectionFactory; @@ -23,7 +23,7 @@ * {@link ConnectionFactory} to fine-tune its auto-configuration. * * @author Andy Wilkinson - * @since 4.0.0 + * @since 4.1.0 */ @FunctionalInterface public interface ConnectionFactoryCustomizer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java index e4057b5c2ecf..e87c3aec7b28 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -30,7 +30,7 @@ * * @author Gary Russell * @author Stephane Nicoll - * @since 4.0.0 + * @since 4.1.0 */ public final class DirectRabbitListenerContainerFactoryConfigurer extends AbstractRabbitListenerContainerFactoryConfigurer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/EnvironmentBuilderCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/EnvironmentBuilderCustomizer.java similarity index 94% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/EnvironmentBuilderCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/EnvironmentBuilderCustomizer.java index 8d5f2373cd8e..fce0f7c94182 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/EnvironmentBuilderCustomizer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/EnvironmentBuilderCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import com.rabbitmq.stream.Environment; import com.rabbitmq.stream.EnvironmentBuilder; @@ -25,7 +25,7 @@ * {@link Environment}. * * @author Andy Wilkinson - * @since 4.0.0 + * @since 4.1.0 */ @FunctionalInterface public interface EnvironmentBuilderCustomizer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetails.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetails.java similarity index 94% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetails.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetails.java index e15052bf3303..3e3900f8a475 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetails.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetails.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.util.ArrayList; import java.util.List; import org.jspecify.annotations.Nullable; -import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Ssl; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.Ssl; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.util.Assert; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAnnotationDrivenConfiguration.java similarity index 99% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAnnotationDrivenConfiguration.java index 2c1b260abe0a..a2e01ef3942e 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAnnotationDrivenConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.amqp.rabbit.config.ContainerCustomizer; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfiguration.java similarity index 99% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfiguration.java index e6f98bcbd2a8..ddb31437b55d 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import com.rabbitmq.client.Channel; import com.rabbitmq.client.impl.CredentialsProvider; @@ -72,7 +72,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Scott Frederick - * @since 4.0.0 + * @since 4.1.0 */ @AutoConfiguration @ConditionalOnClass({ RabbitTemplate.class, Channel.class }) diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionDetails.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionDetails.java similarity index 97% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionDetails.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionDetails.java index d6cb1be0512f..86a7cbeb5c7f 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionDetails.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionDetails.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.util.List; @@ -30,7 +30,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb - * @since 4.0.0 + * @since 4.1.0 */ public interface RabbitConnectionDetails extends ConnectionDetails { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java similarity index 97% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java index de03b57613ca..70c6067dd7dd 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.time.Duration; @@ -23,8 +23,8 @@ import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.core.io.ResourceLoader; @@ -44,7 +44,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @author Scott Frederick - * @since 4.0.0 + * @since 4.1.0 */ public class RabbitConnectionFactoryBeanConfigurer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitListenerRetrySettingsCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitListenerRetrySettingsCustomizer.java similarity index 94% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitListenerRetrySettingsCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitListenerRetrySettingsCustomizer.java index 8a3bc26dd23e..eb577d4c5641 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitListenerRetrySettingsCustomizer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitListenerRetrySettingsCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.springframework.boot.retry.RetryPolicySettings; import org.springframework.core.retry.RetryPolicy; @@ -24,7 +24,7 @@ * settings} used by message listeners. * * @author Stephane Nicoll - * @since 4.0.0 + * @since 4.1.0 * @see RetryPolicy */ @FunctionalInterface diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitProperties.java similarity index 99% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitProperties.java index e9fdff70acfa..33001a04e512 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.time.Duration; import java.time.temporal.ChronoUnit; @@ -53,7 +53,7 @@ * @author Lasse Wulff * @author Yanming Zhou * @author Jay Choi - * @since 4.0.0 + * @since 4.1.0 */ @ConfigurationProperties("spring.rabbitmq") public class RabbitProperties { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitRetryTemplateSettings.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitRetryTemplateSettings.java similarity index 95% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitRetryTemplateSettings.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitRetryTemplateSettings.java index 8e29c1d440e0..db1cd3a84600 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitRetryTemplateSettings.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitRetryTemplateSettings.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.util.LinkedList; @@ -26,7 +26,7 @@ * Define the settings of a {@link RetryTemplate}. * * @author Stephane Nicoll - * @since 4.0.0 + * @since 4.1.0 */ public class RabbitRetryTemplateSettings { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfiguration.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfiguration.java index 416a23d8196f..e2545e8f7d68 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import javax.net.ssl.SSLException; @@ -27,13 +27,13 @@ import org.springframework.amqp.rabbit.config.ContainerCustomizer; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Stream; -import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Stream.Ssl; -import org.springframework.boot.amqp.autoconfigure.RabbitProperties.StreamContainer; 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.PropertyMapper; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.Stream; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.Stream.Ssl; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.StreamContainer; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.ssl.SslManagerBundle; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConnectionDetails.java similarity index 97% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConnectionDetails.java index 5de8fc8301cd..e37462a6f866 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConnectionDetails.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.jspecify.annotations.Nullable; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamTemplateConfigurer.java similarity index 98% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamTemplateConfigurer.java index fe7306d3e473..724ad4a992a5 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamTemplateConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.jspecify.annotations.Nullable; @@ -32,7 +32,7 @@ * auto-configuration. * * @author Eddú Meléndez - * @since 4.0.0 + * @since 4.1.0 */ public class RabbitStreamTemplateConfigurer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateConfigurer.java similarity index 98% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateConfigurer.java index 52823cb48d9a..2dda0397ff92 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.time.Duration; import java.util.List; @@ -45,7 +45,7 @@ * * @author Stephane Nicoll * @author Yanming Zhou - * @since 4.0.0 + * @since 4.1.0 */ public class RabbitTemplateConfigurer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateCustomizer.java similarity index 93% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateCustomizer.java index 9015fa8ca49d..8de9c689e3a6 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateCustomizer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -22,7 +22,7 @@ * Callback interface that can be used to customize a {@link RabbitTemplate}. * * @author dang zhicairang - * @since 4.0.0 + * @since 4.1.0 */ @FunctionalInterface public interface RabbitTemplateCustomizer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java similarity index 94% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java index 099b510132bb..9e521ef6cb4c 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.core.retry.RetryTemplate; @@ -24,7 +24,7 @@ * {@linkplain RabbitRetryTemplateSettings retry settings} used by {@link RabbitTemplate}. * * @author Stephane Nicoll - * @since 4.0.0 + * @since 4.1.0 * @see RabbitTemplateConfigurer * @see RetryTemplate */ diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java index 10fac5395c1d..69ff1953f26f 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -30,7 +30,7 @@ * * @author Stephane Nicoll * @author Gary Russell - * @since 4.0.0 + * @since 4.1.0 */ public final class SimpleRabbitListenerContainerFactoryConfigurer extends AbstractRabbitListenerContainerFactoryConfigurer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/SslBundleRabbitConnectionFactoryBean.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SslBundleRabbitConnectionFactoryBean.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/SslBundleRabbitConnectionFactoryBean.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SslBundleRabbitConnectionFactoryBean.java index ae2345fb06bb..da53a2cbe28b 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/SslBundleRabbitConnectionFactoryBean.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SslBundleRabbitConnectionFactoryBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.jspecify.annotations.Nullable; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java similarity index 91% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java index 0abccb7e34cf..2dd0cc3c7a40 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java @@ -14,12 +14,10 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure.health; +package org.springframework.boot.rabbitmq.autoconfigure.health; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.health.RabbitHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -28,13 +26,15 @@ import org.springframework.boot.health.autoconfigure.contributor.CompositeHealthContributorConfiguration; import org.springframework.boot.health.autoconfigure.contributor.ConditionalOnEnabledHealthIndicator; import org.springframework.boot.health.contributor.HealthContributor; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.health.RabbitHealthIndicator; import org.springframework.context.annotation.Bean; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link RabbitHealthIndicator}. * * @author Christian Dupuis - * @since 4.0.0 + * @since 4.1.0 */ @AutoConfiguration(after = RabbitAutoConfiguration.class) @ConditionalOnClass({ RabbitHealthIndicator.class, RabbitTemplate.class, ConditionalOnEnabledHealthIndicator.class }) diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/health/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/package-info.java similarity index 91% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/health/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/package-info.java index bbd0d5e86d39..27f98c583ad9 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/health/package-info.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/package-info.java @@ -18,6 +18,6 @@ * Auto-configuration for RabbitMQ health. */ @NullMarked -package org.springframework.boot.amqp.autoconfigure.health; +package org.springframework.boot.rabbitmq.autoconfigure.health; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java index 9841b953e937..2f9f18802c76 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure.metrics; +package org.springframework.boot.rabbitmq.autoconfigure.metrics; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.MetricsCollector; @@ -24,7 +24,7 @@ import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory; import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.boot.amqp.metrics.RabbitMetrics; +import org.springframework.boot.rabbitmq.metrics.RabbitMetrics; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; import org.springframework.util.StringUtils; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java similarity index 92% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java index 27b5b6ad36ed..53ff9e680bb6 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure.metrics; +package org.springframework.boot.rabbitmq.autoconfigure.metrics; import com.rabbitmq.client.ConnectionFactory; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; 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.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -33,7 +33,7 @@ * {@link ConnectionFactory connection factories}. * * @author Stephane Nicoll - * @since 4.0.0 + * @since 4.1.0 */ @AutoConfiguration( afterName = "org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration", diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/package-info.java similarity index 91% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/package-info.java index ab0814c6f728..94b2c885530b 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/package-info.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/package-info.java @@ -18,6 +18,6 @@ * Auto-configuration for RabbitMQ metrics. */ @NullMarked -package org.springframework.boot.amqp.autoconfigure.metrics; +package org.springframework.boot.rabbitmq.autoconfigure.metrics; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/package-info.java similarity index 92% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/package-info.java index b519c35a2cae..8b021e23bb05 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/package-info.java @@ -18,6 +18,6 @@ * Auto-configuration for RabbitMQ. */ @NullMarked -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java similarity index 92% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java index daa79344ae75..bd662a242aac 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; import java.util.List; import org.jspecify.annotations.Nullable; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; 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; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.ssl.SslBundle; /** diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironment.java similarity index 95% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironment.java index bc776b015dbf..1d37579e1a84 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironment.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; import java.util.Map; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java similarity index 93% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java index 6b19483ff4a9..5b1fbbb87941 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; import org.jspecify.annotations.Nullable; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; 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; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.ssl.SslBundle; /** diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/package-info.java similarity index 92% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/package-info.java index c2d355c0797e..7fffb63a7c4c 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/package-info.java @@ -18,6 +18,6 @@ * Support for Docker Compose RabbitMQ service connections. */ @NullMarked -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/health/RabbitHealthIndicator.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicator.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/health/RabbitHealthIndicator.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicator.java index 045ab2f89c27..c985139e521e 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/health/RabbitHealthIndicator.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicator.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.health; +package org.springframework.boot.rabbitmq.health; import org.jspecify.annotations.Nullable; @@ -29,7 +29,7 @@ * RabbitMQ messaging system. * * @author Christian Dupuis - * @since 4.0.0 + * @since 4.1.0 */ public class RabbitHealthIndicator extends AbstractHealthIndicator { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/health/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/package-info.java similarity index 93% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/health/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/package-info.java index ecb6ea0b1649..83f913732e91 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/health/package-info.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/package-info.java @@ -18,6 +18,6 @@ * Health integration for AMQP and RabbitMQ. */ @NullMarked -package org.springframework.boot.amqp.health; +package org.springframework.boot.rabbitmq.health; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/metrics/RabbitMetrics.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/RabbitMetrics.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/metrics/RabbitMetrics.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/RabbitMetrics.java index 20ae4111b162..1c32dfd28c65 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/metrics/RabbitMetrics.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/RabbitMetrics.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.metrics; +package org.springframework.boot.rabbitmq.metrics; import java.util.Collections; @@ -32,7 +32,7 @@ * * @author Arnaud Cogoluègnes * @author Stephane Nicoll - * @since 4.0.0 + * @since 4.1.0 */ public class RabbitMetrics implements MeterBinder { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/metrics/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/package-info.java similarity index 93% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/metrics/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/package-info.java index e63026906cd8..c40e5aed68fc 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/metrics/package-info.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/package-info.java @@ -18,6 +18,6 @@ * Metrics for AMQP and RabbitMQ. */ @NullMarked -package org.springframework.boot.amqp.metrics; +package org.springframework.boot.rabbitmq.metrics; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java similarity index 95% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java index b641c37e4395..445d8410611a 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.net.URI; import java.util.List; @@ -22,7 +22,7 @@ import org.jspecify.annotations.Nullable; import org.testcontainers.containers.RabbitMQContainer; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactory.java similarity index 94% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactory.java index fd47f98fd478..32a89a3e23ac 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.net.URI; import java.util.List; @@ -22,7 +22,7 @@ import org.jspecify.annotations.Nullable; import org.testcontainers.rabbitmq.RabbitMQContainer; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java similarity index 95% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java index 5fc1321b7427..dccd1d441417 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import org.jspecify.annotations.Nullable; import org.testcontainers.rabbitmq.RabbitMQContainer; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/package-info.java similarity index 92% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/package-info.java index 69c6364ef32f..272d19a6b982 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/package-info.java @@ -18,6 +18,6 @@ * Support for testcontainers RabbitMQ service connections. */ @NullMarked -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories index 17fb2095415a..e3667de2bbcf 100644 --- a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories @@ -1,8 +1,8 @@ # Connection Details Factories org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ -org.springframework.boot.amqp.docker.compose.RabbitDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.amqp.docker.compose.RabbitStreamDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.amqp.testcontainers.DeprecatedRabbitContainerConnectionDetailsFactory,\ -org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory,\ -org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory +org.springframework.boot.rabbitmq.docker.compose.RabbitDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.rabbitmq.docker.compose.RabbitStreamDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.rabbitmq.testcontainers.DeprecatedRabbitContainerConnectionDetailsFactory,\ +org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory,\ +org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory diff --git a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index daf8ae3ba45f..9ec393f97b32 100644 --- a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,3 @@ -org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration -org.springframework.boot.amqp.autoconfigure.health.RabbitHealthContributorAutoConfiguration -org.springframework.boot.amqp.autoconfigure.metrics.RabbitMetricsAutoConfiguration \ No newline at end of file +org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration +org.springframework.boot.rabbitmq.autoconfigure.health.RabbitHealthContributorAutoConfiguration +org.springframework.boot.rabbitmq.autoconfigure.metrics.RabbitMetricsAutoConfiguration \ No newline at end of file diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetailsTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetailsTests.java similarity index 95% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetailsTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetailsTests.java index fb37bd6f2712..aed57edc4e04 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetailsTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetailsTests.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.ssl.DefaultSslBundleRegistry; import org.springframework.boot.ssl.SslBundle; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfigurationTests.java similarity index 99% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfigurationTests.java index 7db86941280d..b25f9cd3c5f9 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.security.NoSuchAlgorithmException; import java.util.Collection; @@ -942,7 +942,7 @@ void enableSslWithInvalidTrustStoreTypeShouldFail() { void enableSslWithBundle() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.bundle=test-bundle", - "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.ssl.bundle.jks.test-bundle.keystore.password=secret") .run((context) -> { com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); @@ -954,9 +954,9 @@ void enableSslWithBundle() { void enableSslWithKeystoreTypeAndTrustStoreTypeShouldWork() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.enabled:true", - "spring.rabbitmq.ssl.key-store=/org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.rabbitmq.ssl.key-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.rabbitmq.ssl.key-store-type=jks", "spring.rabbitmq.ssl.key-store-password=secret", - "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.rabbitmq.ssl.trust-store-type=jks", "spring.rabbitmq.ssl.trust-store-password=secret") .run((context) -> assertThat(context).hasNotFailed()); } @@ -988,10 +988,10 @@ void enableSslWithValidateServerCertificateDefault(CapturedOutput output) { void enableSslWithValidStoreAlgorithmShouldWork() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.enabled:true", - "spring.rabbitmq.ssl.key-store=/org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.rabbitmq.ssl.key-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.rabbitmq.ssl.key-store-type=jks", "spring.rabbitmq.ssl.key-store-password=secret", "spring.rabbitmq.ssl.key-store-algorithm=PKIX", - "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.rabbitmq.ssl.trust-store-type=jks", "spring.rabbitmq.ssl.trust-store-password=secret", "spring.rabbitmq.ssl.trust-store-algorithm=PKIX") .run((context) -> assertThat(context).hasNotFailed()); @@ -1001,7 +1001,7 @@ void enableSslWithValidStoreAlgorithmShouldWork() { void enableSslWithInvalidKeyStoreAlgorithmShouldFail() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.enabled:true", - "spring.rabbitmq.ssl.key-store=/org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.rabbitmq.ssl.key-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.rabbitmq.ssl.key-store-type=jks", "spring.rabbitmq.ssl.key-store-password=secret", "spring.rabbitmq.ssl.key-store-algorithm=test-invalid-algo") .run((context) -> { @@ -1015,7 +1015,7 @@ void enableSslWithInvalidKeyStoreAlgorithmShouldFail() { void enableSslWithInvalidTrustStoreAlgorithmShouldFail() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.enabled:true", - "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.rabbitmq.ssl.trust-store-type=jks", "spring.rabbitmq.ssl.trust-store-password=secret", "spring.rabbitmq.ssl.trust-store-algorithm=test-invalid-algo") .run((context) -> { diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitPropertiesTests.java similarity index 99% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitPropertiesTests.java index 93548c283a41..345ac33ebe53 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitPropertiesTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.util.List; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfigurationTests.java similarity index 98% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfigurationTests.java index 87230b0618c9..6ca687352abf 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.time.Duration; import java.util.List; @@ -34,9 +34,9 @@ import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConfiguration.PropertiesRabbitStreamConnectionDetails; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConfiguration.PropertiesRabbitStreamConnectionDetails; import org.springframework.boot.ssl.NoSuchSslBundleException; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; @@ -352,7 +352,7 @@ void whenStreamSslIsEnabledThenTlsIsUsed() { @Test void whenStreamSslBundleIsConfiguredThenTlsIsUsed() { this.contextRunner.withPropertyValues("spring.rabbitmq.stream.ssl.bundle=test-bundle", - "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.ssl.bundle.jks.test-bundle.keystore.password=secret") .run((context) -> { assertThat(context).hasNotFailed(); @@ -388,7 +388,7 @@ void whenStreamSslIsDisabledWithBundleThenTlsIsStillUsed() { void whenConnectionDetailsSslBundleIsProvidedThenTlsIsUsedWithoutProperties() { this.contextRunner.withUserConfiguration(CustomConnectionDetails.class) .withPropertyValues( - "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.ssl.bundle.jks.test-bundle.keystore.password=secret") .run((context) -> { assertThat(context).hasNotFailed(); diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java similarity index 89% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java index 8739399d0aae..4b3571c2c8c6 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure.health; +package org.springframework.boot.rabbitmq.autoconfigure.health; import org.junit.jupiter.api.Test; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.health.RabbitHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.health.autoconfigure.contributor.HealthContributorAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.health.RabbitHealthIndicator; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java similarity index 94% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java index 8c8c31a499e0..81928ab80091 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure.metrics; +package org.springframework.boot.rabbitmq.autoconfigure.metrics; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.MeterBinder; @@ -22,8 +22,8 @@ import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java similarity index 95% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java index b75b0e1857d9..937a1203a096 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure.metrics; +package org.springframework.boot.rabbitmq.autoconfigure.metrics; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; @@ -22,9 +22,9 @@ import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironmentTests.java similarity index 97% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironmentTests.java index 1ecb0b4d6ff4..356d93ad535e 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironmentTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; import java.util.Collections; import java.util.Map; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/health/RabbitHealthIndicatorTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicatorTests.java similarity index 98% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/health/RabbitHealthIndicatorTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicatorTests.java index 5ce2f8e64a20..161cb2fa6e94 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/health/RabbitHealthIndicatorTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicatorTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.health; +package org.springframework.boot.rabbitmq.health; import java.util.Collections; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/metrics/RabbitMetricsTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/metrics/RabbitMetricsTests.java similarity index 97% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/metrics/RabbitMetricsTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/metrics/RabbitMetricsTests.java index 780de803e4f6..a8b3f5b3c1d3 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/amqp/metrics/RabbitMetricsTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/metrics/RabbitMetricsTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.metrics; +package org.springframework.boot.rabbitmq.metrics; import com.rabbitmq.client.ConnectionFactory; import io.micrometer.core.instrument.Tags; diff --git a/module/spring-boot-rabbitmq/src/test/resources/org/springframework/boot/amqp/autoconfigure/test.jks b/module/spring-boot-rabbitmq/src/test/resources/org/springframework/boot/rabbitmq/autoconfigure/test.jks similarity index 100% rename from module/spring-boot-rabbitmq/src/test/resources/org/springframework/boot/amqp/autoconfigure/test.jks rename to module/spring-boot-rabbitmq/src/test/resources/org/springframework/boot/rabbitmq/autoconfigure/test.jks diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationSslTests.java similarity index 97% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationSslTests.java index e1acf6319d04..f846f70ae495 100644 --- a/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationSslTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.amqp; +package smoketest.rabbitmq; import java.time.Duration; @@ -43,7 +43,7 @@ @SpringBootTest @Testcontainers(disabledWithoutDocker = true) @ExtendWith(OutputCaptureExtension.class) -class SampleAmqpSimpleApplicationSslTests { +class SampleRabbitApplicationSslTests { @Container @ServiceConnection diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationTests.java similarity index 96% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationTests.java index e763ade7aa09..626348198713 100644 --- a/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.amqp; +package smoketest.rabbitmq; import java.time.Duration; @@ -37,7 +37,7 @@ @SpringBootTest @Testcontainers(disabledWithoutDocker = true) @ExtendWith(OutputCaptureExtension.class) -class SampleAmqpSimpleApplicationTests { +class SampleRabbitApplicationTests { @Container @ServiceConnection diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SecureRabbitMqContainer.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SecureRabbitMqContainer.java similarity index 98% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SecureRabbitMqContainer.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SecureRabbitMqContainer.java index 7599d2d4ff21..5dfb9e776c39 100644 --- a/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/amqp/SecureRabbitMqContainer.java +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SecureRabbitMqContainer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.amqp; +package smoketest.rabbitmq; import org.testcontainers.rabbitmq.RabbitMQContainer; import org.testcontainers.utility.DockerImageName; diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/SampleRabbitApplication.java similarity index 87% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/SampleRabbitApplication.java index 7c01337d3cba..da492b1fe4f5 100644 --- a/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/SampleRabbitApplication.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.amqp; +package smoketest.rabbitmq; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -30,9 +30,9 @@ @SpringBootApplication @RabbitListener(queues = "foo") -public class SampleAmqpSimpleApplication { +public class SampleRabbitApplication { - private static final Log logger = LogFactory.getLog(SampleAmqpSimpleApplication.class); + private static final Log logger = LogFactory.getLog(SampleRabbitApplication.class); @Bean public Sender mySender() { @@ -55,7 +55,7 @@ public ApplicationRunner runner(Sender sender) { } public static void main(String[] args) { - SpringApplication.run(SampleAmqpSimpleApplication.class, args); + SpringApplication.run(SampleRabbitApplication.class, args); } } diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/Sender.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/Sender.java similarity index 97% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/Sender.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/Sender.java index 078f2416966c..c3fd88bbed28 100644 --- a/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/Sender.java +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/Sender.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.amqp; +package smoketest.rabbitmq; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/package-info.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/package-info.java similarity index 96% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/package-info.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/package-info.java index bc9e41a079d7..a078a228474f 100644 --- a/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/amqp/package-info.java +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/package-info.java @@ -15,6 +15,6 @@ */ @NullMarked -package smoketest.amqp; +package smoketest.rabbitmq; import org.jspecify.annotations.NullMarked; From e22a2ae360610278b07ff8e1ae6e985c2f7281db Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 19 Mar 2026 09:29:57 +0100 Subject: [PATCH 375/930] Upgrade to Testcontainers 2.0.4 Closes gh-49656 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 20f1f17dbc16..6519005485b8 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2673,7 +2673,7 @@ bom { releaseNotes("https://github.com/xerial/sqlite-jdbc/releases/tag/{version}") } } - library("Testcontainers", "2.0.3") { + library("Testcontainers", "2.0.4") { group("org.testcontainers") { bom("testcontainers-bom") } From c388dc92f1c961b45fc4ef4fe1314e87b5579796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Mar 2026 07:48:05 +0100 Subject: [PATCH 376/930] Add AMQP 1.0 client auto-configuration This commit adds auto-configuration for the generic AMQP 1.0 client in Spring AMQP 4.1, using Qpid ProtonJ. The auto-configuration provides an AmqpConnectionFactory as well as an AmqpClient with standard customizer callbacks. The "spring.amqp" namespace exposes settings to connect to an AMQP 1.0 compliant broker. Docker compose and testcontainers support using RabbitMQ have been added too. Closes gh-49621 --- .../DocumentConfigurationProperties.java | 1 + documentation/spring-boot-docs/build.gradle | 1 + .../antora/modules/ROOT/pages/redirect.adoc | 14 +- .../pages/features/dev-services.adoc | 3 + .../reference/pages/messaging/amqp.adoc | 64 +++- .../pages/testing/testcontainers.adoc | 3 + .../amqp/generic/sending/MyBean.java | 37 +++ .../amqp/{ => rabbitmq}/receiving/MyBean.java | 2 +- .../receiving/custom/MyBean.java | 2 +- .../receiving/custom/MyMessageConverter.java | 2 +- .../custom/MyRabbitConfiguration.java | 2 +- .../amqp/{ => rabbitmq}/sending/MyBean.java | 2 +- .../messaging/amqp/generic/sending/MyBean.kt | 32 ++ .../amqp/{ => rabbitmq}/receiving/MyBean.kt | 2 +- .../{ => rabbitmq}/receiving/custom/MyBean.kt | 2 +- .../receiving/custom/MyMessageConverter.kt | 2 +- .../receiving/custom/MyRabbitConfiguration.kt | 2 +- .../amqp/{ => rabbitmq}/sending/MyBean.kt | 2 +- module/spring-boot-amqp/build.gradle | 61 ++++ ...AmqpAutoConfigurationIntegrationTests.java | 74 +++++ ...nectionDetailsFactoryIntegrationTests.java | 42 +++ ...nectionDetailsFactoryIntegrationTests.java | 71 +++++ .../src/dockerTest/resources/logback-test.xml | 4 + .../amqp/docker/compose/rabbitmq-compose.yaml | 8 + .../dockerTest/resources/spring.properties | 1 + .../autoconfigure/AmqpAutoConfiguration.java | 132 +++++++++ .../autoconfigure/AmqpClientCustomizer.java | 37 +++ .../autoconfigure/AmqpConnectionDetails.java | 62 ++++ .../amqp/autoconfigure/AmqpProperties.java | 125 ++++++++ .../ConnectionOptionsCustomizer.java | 39 +++ .../PropertiesAmqpConnectionDetails.java | 49 ++++ .../boot/amqp/autoconfigure/package-info.java | 23 ++ ...DockerComposeConnectionDetailsFactory.java | 84 ++++++ .../docker/compose/RabbitMqEnvironment.java | 50 ++++ .../amqp/docker/compose/package-info.java | 23 ++ ...itMqContainerConnectionDetailsFactory.java | 71 +++++ .../amqp/testcontainers/package-info.java | 23 ++ .../main/resources/META-INF/spring.factories | 5 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../AmqpAutoConfigurationTests.java | 276 ++++++++++++++++++ .../compose/RabbitMqEnvironmentTests.java | 72 +++++ .../spring-boot-dependencies/build.gradle | 13 + settings.gradle | 3 + .../build.gradle | 26 ++ starter/spring-boot-starter-amqp/build.gradle | 27 ++ .../RabbitMqManagementContainer.java | 57 ++++ .../boot/testsupport/container/TestImage.java | 5 +- 47 files changed, 1611 insertions(+), 28 deletions(-) create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/MyBean.java (92%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyBean.java (91%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyMessageConverter.java (93%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyRabbitConfiguration.java (95%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/sending/MyBean.java (94%) create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/MyBean.kt (92%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyBean.kt (92%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyMessageConverter.kt (92%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyRabbitConfiguration.kt (95%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/sending/MyBean.kt (93%) create mode 100644 module/spring-boot-amqp/build.gradle create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/spring.properties create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironment.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactory.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java create mode 100644 module/spring-boot-amqp/src/main/resources/META-INF/spring.factories create mode 100644 module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java create mode 100644 module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironmentTests.java create mode 100644 starter/spring-boot-starter-amqp-test/build.gradle create mode 100644 starter/spring-boot-starter-amqp/build.gradle create mode 100644 test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.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 2191c1c1a34c..dc56a5b977c8 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 @@ -166,6 +166,7 @@ private void dataMigrationPrefixes(Config prefix) { private void integrationPrefixes(Config prefix) { prefix.accept("spring.activemq"); + prefix.accept("spring.amqp"); prefix.accept("spring.artemis"); prefix.accept("spring.batch"); prefix.accept("spring.integration"); diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index 7f20bc5ed26b..e89fb1017b0e 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -94,6 +94,7 @@ dependencies { implementation(project(path: ":loader:spring-boot-loader-tools")) implementation(project(path: ":module:spring-boot-actuator")) implementation(project(path: ":module:spring-boot-actuator-autoconfigure")) + implementation(project(path: ":module:spring-boot-amqp")) implementation(project(path: ":module:spring-boot-cache")) implementation(project(path: ":module:spring-boot-cache-test")) implementation(project(path: ":module:spring-boot-data-cassandra")) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc index 72414df0faae..42d3b57c37fd 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc @@ -1716,13 +1716,13 @@ * xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#features.messaging.amqp.rabbit] * xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#messaging.amqp.rabbit] * xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#messaging.amqp.rabbitmq] -* xref:reference:messaging/amqp.adoc#messaging.amqp.receiving[#boot-features-using-amqp-receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.receiving[#features.messaging.amqp.receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.receiving[#messaging.amqp.receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.sending-stream[#messaging.amqp.sending-stream] -* xref:reference:messaging/amqp.adoc#messaging.amqp.sending[#boot-features-using-amqp-sending] -* xref:reference:messaging/amqp.adoc#messaging.amqp.sending[#features.messaging.amqp.sending] -* xref:reference:messaging/amqp.adoc#messaging.amqp.sending[#messaging.amqp.sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#boot-features-using-amqp-receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#features.messaging.amqp.receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#messaging.amqp.receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending-stream[#messaging.amqp.sending-stream] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#boot-features-using-amqp-sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#features.messaging.amqp.sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#messaging.amqp.sending] * xref:reference:messaging/amqp.adoc#messaging.amqp[#boot-features-amqp] * xref:reference:messaging/amqp.adoc#messaging.amqp[#features.messaging.amqp] * xref:reference:messaging/amqp.adoc#messaging.amqp[#messaging.amqp] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc index 36e57ff478db..f816ca64d25c 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc @@ -86,6 +86,9 @@ The following service connections are currently supported: | javadoc:org.springframework.boot.activemq.autoconfigure.ActiveMQConnectionDetails[] | Containers named "symptoma/activemq" or "apache/activemq-classic" +| javadoc:org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails[] +| Containers named "rabbitmq" + | javadoc:org.springframework.boot.artemis.autoconfigure.ArtemisConnectionDetails[] | Containers named "apache/activemq-artemis" diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index 3864f7ebe9ea..15cf865bd868 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc @@ -3,7 +3,53 @@ The Advanced Message Queuing Protocol (AMQP) is a platform-neutral, wire-level protocol for message-oriented middleware. The Spring AMQP project applies core Spring concepts to the development of AMQP-based messaging solutions. -Spring Boot offers several conveniences for working with AMQP through RabbitMQ, including the `spring-boot-starter-rabbitmq` starter. +Spring Boot offers several conveniences for working with AMQP: generic AMQP 1.0 support is provided by the `spring-boot-starter-amqp` starter while specific RabbitMQ support is available via the `spring-boot-starter-rabbitmq` starter. + + + +[[messaging.amqp.generic]] +== Generic AMQP 1.0 Support + +AMQP 1.0 is supported by several brokers and messaging services beyond RabbitMQ, including ActiveMQ, Azure Service Bus, and others. +Spring AMQP provides {url-spring-amqp-docs}/amqp10-client.html[generic support for AMQP 1.0] via `org.springframework.amqp:spring-amqp-client` that is based on the https://github.com/apache/qpid-protonj2/blob/main/protonj2-client/README.md[Qpid ProtonJ2 Client Library]. + +AMQP configuration is controlled by external configuration properties in `+spring.amqp.*+`. +For example, you might declare the following in your configuration: + +[configprops,yaml] +---- +spring: + amqp: + host: "localhost" + port: 5672 + username: "admin" + password: "secret" +---- + +To configure javadoc:org.apache.qpid.protonj2.client.ConnectionOptions[connection options] of the auto-configured javadoc:org.springframework.amqp.client.AmqpConnectionFactory[], define a javadoc:org.springframework.boot.amqp.autoconfigure.ConnectionOptionsCustomizer[] bean. + +[[messaging.amqp.generic.sending]] +=== Sending a Message + +Spring's javadoc:org.springframework.amqp.client.AmqpClient[] is auto-configured, and you can autowire it directly into your own beans, as shown in the following example: + +include-code::MyBean[] + +If a javadoc:org.springframework.amqp.support.converter.MessageConverter[] bean is defined, it is associated automatically with the auto-configured javadoc:org.springframework.amqp.client.AmqpClient[]. +If no such converter is defined and Jackson is available, javadoc:org.springframework.amqp.support.converter.JacksonJsonMessageConverter[] is used. + +Client-specific settings can be configured as follows: + +[configprops,yaml] +---- +spring: + amqp: + client: + default-to-address: "/queues/default_queue" + completion-timeout: "500ms" +---- + +To further configure the auto-configured javadoc:org.springframework.amqp.client.AmqpClient[], define a javadoc:org.springframework.boot.amqp.autoconfigure.AmqpClientCustomizer[] bean. @@ -49,8 +95,8 @@ TIP: See https://spring.io/blog/2010/06/14/understanding-amqp-the-protocol-used- -[[messaging.amqp.sending]] -== Sending a Message +[[messaging.amqp.rabbitmq.sending]] +=== Sending a Message Spring's javadoc:org.springframework.amqp.core.AmqpTemplate[] and javadoc:org.springframework.amqp.core.AmqpAdmin[] are auto-configured, and you can autowire them directly into your own beans, as shown in the following example: @@ -82,8 +128,8 @@ If there's a bean of type javadoc:org.springframework.amqp.rabbit.support.microm -[[messaging.amqp.sending-stream]] -== Sending a Message To A Stream +[[messaging.amqp.rabbitmq.sending-stream]] +=== Sending a Message To A Stream To send a message to a particular stream, specify the name of the stream, as shown in the following example: @@ -101,14 +147,14 @@ If you need to create more javadoc:org.springframework.rabbit.stream.producer.Ra -[[messaging.amqp.sending-stream.ssl]] -=== SSL +[[messaging.amqp.rabbitmq.sending-stream.ssl]] +==== SSL To use SSL with RabbitMQ Streams, set configprop:spring.rabbitmq.stream.ssl.enabled[] to `true` or set configprop:spring.rabbitmq.stream.ssl.bundle[] to configure the xref:features/ssl.adoc#features.ssl.bundles[SSL bundle] to use. -[[messaging.amqp.receiving]] -== Receiving a Message +[[messaging.amqp.rabbitmq.receiving]] +=== Receiving a Message When the Rabbit infrastructure is present, any bean can be annotated with javadoc:org.springframework.amqp.rabbit.annotation.RabbitListener[format=annotation] to create a listener endpoint. If no javadoc:org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory[] has been defined, a default javadoc:org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory[] is automatically configured and you can switch to a direct container using the configprop:spring.rabbitmq.listener.type[] property. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc index 72240022f6ad..c925920a077d 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc @@ -122,6 +122,9 @@ The following service connection factories are provided in the `spring-boot-test | javadoc:org.springframework.boot.activemq.autoconfigure.ActiveMQConnectionDetails[] | Containers named "symptoma/activemq" or javadoc:org.testcontainers.activemq.ActiveMQContainer[] +| javadoc:org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails[] +| Containers of type javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] + | javadoc:org.springframework.boot.artemis.autoconfigure.ArtemisConnectionDetails[] | Containers of type javadoc:org.testcontainers.activemq.ArtemisContainer[] diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java new file mode 100644 index 000000000000..8f7ffc09ff5c --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.generic.sending; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final AmqpClient amqpClient; + + public MyBean(AmqpClient amqpClient) { + this.amqpClient = amqpClient; + } + + // @fold:on // ... + public void sendMessage(String msg) { + this.amqpClient.to("/queues/test").body(msg).send(); + } + // @fold:off + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java similarity index 92% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java index d5fd3014a058..56064bfb94ce 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving; +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.java similarity index 91% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.java index 0f0c59f87c99..671de83f0e3d 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom; +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.java similarity index 93% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.java index ba8d391a53f3..18169c7e7aba 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom; +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java similarity index 95% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java index 172c9cbba0d1..318428b08be0 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom; +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/sending/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java similarity index 94% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/sending/MyBean.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java index f2adb052cecb..51b91d35769b 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/sending/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.sending; +package org.springframework.boot.docs.messaging.amqp.rabbitmq.sending; import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.AmqpTemplate; diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt new file mode 100644 index 000000000000..64c5174f4177 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.generic.sending + +import org.springframework.amqp.client.AmqpClient +import org.springframework.stereotype.Component + +@Component +class MyBean(private val amqpClient: AmqpClient) { + + // @fold:on // ... + fun someOtherMethod(msg: String) { + amqpClient.to("/queues/test").body(msg).send() + } + // @fold:off + +} + diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt similarity index 92% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt index a3241ddb95b8..9bac053496d2 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving import org.springframework.amqp.rabbit.annotation.RabbitListener import org.springframework.stereotype.Component diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt similarity index 92% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt index f92ffc012497..3b90bf3dc42d 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom import org.springframework.amqp.rabbit.annotation.RabbitListener import org.springframework.stereotype.Component diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt similarity index 92% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt index 7b6265875ccb..03b165448e08 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom import org.springframework.amqp.core.Message import org.springframework.amqp.core.MessageProperties diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt similarity index 95% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt index 95ca6f610039..900866bf2464 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory import org.springframework.amqp.rabbit.connection.CachingConnectionFactory diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/sending/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt similarity index 93% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/sending/MyBean.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt index e8ec5274920e..6d21de9bce0a 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/sending/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.sending +package org.springframework.boot.docs.messaging.amqp.rabbitmq.sending import org.springframework.amqp.core.AmqpAdmin import org.springframework.amqp.core.AmqpTemplate diff --git a/module/spring-boot-amqp/build.gradle b/module/spring-boot-amqp/build.gradle new file mode 100644 index 000000000000..7aebee0bdcb8 --- /dev/null +++ b/module/spring-boot-amqp/build.gradle @@ -0,0 +1,61 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java-library" + id "org.springframework.boot.docker-test" + id "org.springframework.boot.auto-configuration" + id "org.springframework.boot.configuration-properties" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot support for AMQP 1.0" + +dependencies { + api(project(":core:spring-boot")) + api("org.apache.qpid:protonj2-client") + api("org.springframework:spring-messaging") + api("org.springframework.amqp:spring-amqp-client") + + implementation(project(":module:spring-boot-transaction")) + + optional(project(":core:spring-boot-autoconfigure")) + optional(project(":core:spring-boot-docker-compose")) + optional(project(":module:spring-boot-jackson")) + optional(project(":core:spring-boot-testcontainers")) + optional("org.testcontainers:testcontainers-rabbitmq") + + dockerTestImplementation(project(":core:spring-boot-test")) + dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) + dockerTestImplementation(testFixtures(project(":core:spring-boot-docker-compose"))) + dockerTestImplementation("ch.qos.logback:logback-classic") + dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") + + + testImplementation(project(":core:spring-boot-test")) + testImplementation(project(":test-support:spring-boot-test-support")) + + testRuntimeOnly("ch.qos.logback:logback-classic") +} + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +tasks.named("compileDockerTestJava") { + options.nullability.checking = "tests" +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java new file mode 100644 index 000000000000..d08d29a55980 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.container.RabbitMqManagementContainer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link AmqpAutoConfiguration}. + * + * @author Stephane Nicoll + */ +@Testcontainers(disabledWithoutDocker = true) +class AmqpAutoConfigurationIntegrationTests { + + @Container + static final RabbitMqManagementContainer container = new RabbitMqManagementContainer(); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AmqpAutoConfiguration.class)) + .withPropertyValues("spring.amqp.host=" + container.getHost(), "spring.amqp.port=" + container.getAmqpPort()); + + @Test + void sendAndReceiveUsingAmqpClient() { + String queue = container.createRandomQueue(); + this.contextRunner.run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient.to(queue).body("Hello World").send().get(1, TimeUnit.SECONDS)).isTrue(); + assertThat(amqpClient.from(queue).receiveAndConvert().get(1, TimeUnit.SECONDS)).isEqualTo("Hello World"); + }); + } + + @Test + void sendAndReceiveUsingJson() { + String queue = container.createRandomQueue(); + this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)).run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient.to(queue).body(new TestMessage("hello", 42)).send().get(1, TimeUnit.SECONDS)) + .isTrue(); + assertThat(amqpClient.from(queue).receiveAndConvert().get(1, TimeUnit.SECONDS)) + .isEqualTo(new TestMessage("hello", 42)); + }); + } + + record TestMessage(String value, int counter) { + + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..04198e1e32bd --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails.Address; +import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.testsupport.container.TestImage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link RabbitMqDockerComposeConnectionDetailsFactory}. + * + * @author Stephane Nicoll + */ +class RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests { + + @DockerComposeTest(composeFile = "rabbitmq-compose.yaml", image = TestImage.RABBITMQ) + void runCreatesConnectionDetails(AmqpConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + Address address = connectionDetails.getAddress(); + assertThat(address.host()).isNotNull(); + assertThat(address.port()).isGreaterThan(0); + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..483ec6e90ad3 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.AmqpAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.RabbitMqManagementContainer; +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 RabbitMqContainerConnectionDetailsFactory}. + * + * @author Stephane Nicoll + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class RabbitMqContainerConnectionDetailsFactoryIntegrationTests { + + @Container + @ServiceConnection + static final RabbitMqManagementContainer container = new RabbitMqManagementContainer(); + + @Autowired(required = false) + private AmqpConnectionDetails connectionDetails; + + @Autowired + private AmqpClient amqpClient; + + @Test + void connectionCanBeMadeToRabbitMqContainer() throws Exception { + assertThat(this.connectionDetails).isNotNull(); + container.createQueue("test"); + this.amqpClient.to("/queues/test").body("test message").send(); + Object message = this.amqpClient.from("/queues/test").receiveAndConvert().get(4, TimeUnit.MINUTES); + assertThat(message).isEqualTo("test message"); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(AmqpAutoConfiguration.class) + static class TestConfiguration { + + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml b/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml new file mode 100644 index 000000000000..b8a41480d7d6 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml @@ -0,0 +1,4 @@ + + + + diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml new file mode 100644 index 000000000000..1951fba4bb08 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml @@ -0,0 +1,8 @@ +services: + rabbitmq: + image: '{imageName}' + environment: + - 'RABBITMQ_DEFAULT_USER=myuser' + - 'RABBITMQ_DEFAULT_PASS=secret' + ports: + - '5672' diff --git a/module/spring-boot-amqp/src/dockerTest/resources/spring.properties b/module/spring-boot-amqp/src/dockerTest/resources/spring.properties new file mode 100644 index 000000000000..47dff33f0bb5 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/spring.properties @@ -0,0 +1 @@ +spring.test.context.cache.maxSize=1 \ No newline at end of file diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java new file mode 100644 index 000000000000..24babf2b4883 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java @@ -0,0 +1,132 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import java.util.stream.Stream; + +import org.apache.qpid.protonj2.client.Client; +import org.apache.qpid.protonj2.client.ClientOptions; +import org.apache.qpid.protonj2.client.ConnectionOptions; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.amqp.client.AmqpConnectionFactory; +import org.springframework.amqp.client.SingleAmqpConnectionFactory; +import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails.Address; +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.ConditionalOnSingleCandidate; +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.Configuration; + +/** + * Auto-configuration for generic AMQP 1.0 support. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +@AutoConfiguration(afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration") +@ConditionalOnClass({ Client.class, AmqpConnectionFactory.class }) +@EnableConfigurationProperties(AmqpProperties.class) +public final class AmqpAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + static class AmqpConnectionFactoryConfiguration { + + @Bean + @ConditionalOnMissingBean + Client protonClient() { + return Client.create(new ClientOptions()); + } + + @Bean + @ConditionalOnMissingBean + AmqpConnectionDetails amqpConnectionDetails(AmqpProperties properties) { + return new PropertiesAmqpConnectionDetails(properties); + } + + @Bean + @ConditionalOnMissingBean + AmqpConnectionFactory amqpConnectionFactory(AmqpConnectionDetails connectionDetails, Client protonClient, + ObjectProvider connectionOptionsCustomizers) { + ConnectionOptions connectionOptions = createConnectionOptions(connectionDetails, + connectionOptionsCustomizers.orderedStream()); + return createAmqpConnectionFactory(connectionDetails, protonClient).setConnectionOptions(connectionOptions); + } + + private SingleAmqpConnectionFactory createAmqpConnectionFactory(AmqpConnectionDetails connectionDetails, + Client protonClient) { + SingleAmqpConnectionFactory connectionFactory = new SingleAmqpConnectionFactory(protonClient); + PropertyMapper map = PropertyMapper.get(); + Address address = connectionDetails.getAddress(); + map.from(address::host).to(connectionFactory::setHost); + map.from(address::port).to(connectionFactory::setPort); + return connectionFactory; + } + + private ConnectionOptions createConnectionOptions(AmqpConnectionDetails connectionDetails, + Stream customizers) { + ConnectionOptions connectionOptions = new ConnectionOptions(); + PropertyMapper map = PropertyMapper.get(); + map.from(connectionDetails::getUsername).to(connectionOptions::user); + map.from(connectionDetails::getPassword).to(connectionOptions::password); + customizers.forEach((customizer) -> customizer.customize(connectionOptions)); + return connectionOptions; + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(JsonMapper.class) + @ConditionalOnSingleCandidate(JsonMapper.class) + static class JacksonMessageConverterConfiguration { + + @Bean + @ConditionalOnMissingBean(MessageConverter.class) + JacksonJsonMessageConverter jacksonJsonMessageConverter(JsonMapper jsonMapper) { + return new JacksonJsonMessageConverter(jsonMapper); + } + + } + + @Configuration(proxyBeanMethods = false) + static class AmqpClientConfiguration { + + @Bean + @ConditionalOnMissingBean + AmqpClient amqpClient(AmqpConnectionFactory amqpConnectionFactory, AmqpProperties properties, + ObjectProvider messageConverter, ObjectProvider customizers) { + AmqpClient.Builder builder = AmqpClient.builder(amqpConnectionFactory); + AmqpProperties.Client client = properties.getClient(); + PropertyMapper map = PropertyMapper.get(); + map.from(client.getDefaultToAddress()).to(builder::defaultToAddress); + map.from(client.getCompletionTimeout()).to(builder::completionTimeout); + messageConverter.ifAvailable(builder::messageConverter); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder.build(); + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java new file mode 100644 index 000000000000..b2ab3c2ed766 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.springframework.amqp.client.AmqpClient; + +/** + * Callback interface that can be used to customize the auto-configured + * {@link AmqpClient.Builder}. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +@FunctionalInterface +public interface AmqpClientCustomizer { + + /** + * Callback to customize a {@link AmqpClient.Builder} instance. + * @param amqpClientBuilder the client builder to customize + */ + void customize(AmqpClient.Builder amqpClientBuilder); + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java new file mode 100644 index 000000000000..246183800aea --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to an AMQP broker. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +public interface AmqpConnectionDetails extends ConnectionDetails { + + /** + * Return the {@link Address} of the broker. + * @return the address + */ + Address getAddress(); + + /** + * Login user to authenticate to the broker. + * @return the login user to authenticate to the broker or {@code null} + */ + default @Nullable String getUsername() { + return null; + } + + /** + * Password used to authenticate to the broker. + * @return the password to authenticate to the broker or {@code null} + */ + default @Nullable String getPassword() { + return null; + } + + /** + * An AMQP broker address. + * + * @param host the host + * @param port the port + */ + record Address(String host, int port) { + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java new file mode 100644 index 000000000000..7091fc292252 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import java.time.Duration; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for AMQP 1.0 connectivity. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +@ConfigurationProperties("spring.amqp") +public class AmqpProperties { + + /** + * AMQP broker host. + */ + private String host = "localhost"; + + /** + * AMQP broker port. + */ + private int port = 5672; + + /** + * Login user to authenticate to the broker. + */ + private @Nullable String username; + + /** + * Password used to authenticate to the broker. + */ + private @Nullable String password; + + private final Client client = new Client(); + + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return this.port; + } + + public void setPort(int port) { + this.port = port; + } + + public @Nullable String getUsername() { + return this.username; + } + + public void setUsername(@Nullable String username) { + this.username = username; + } + + public @Nullable String getPassword() { + return this.password; + } + + public void setPassword(@Nullable String password) { + this.password = password; + } + + public Client getClient() { + return this.client; + } + + /** + * Client-level settings. + */ + public static class Client { + + /** + * Default destination address for send operations when none is specified. + */ + private @Nullable String defaultToAddress; + + /** + * Maximum time to wait for request operations to complete. + */ + private Duration completionTimeout = Duration.ofSeconds(60); + + public @Nullable String getDefaultToAddress() { + return this.defaultToAddress; + } + + public void setDefaultToAddress(@Nullable String defaultToAddress) { + this.defaultToAddress = defaultToAddress; + } + + public Duration getCompletionTimeout() { + return this.completionTimeout; + } + + public void setCompletionTimeout(Duration completionTimeout) { + this.completionTimeout = completionTimeout; + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java new file mode 100644 index 000000000000..9d3c54f1cdb5 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.apache.qpid.protonj2.client.ConnectionOptions; + +import org.springframework.amqp.client.AmqpConnectionFactory; + +/** + * Callback interface for customizing {@link ConnectionOptions} on the auto-configured + * {@link AmqpConnectionFactory}. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +@FunctionalInterface +public interface ConnectionOptionsCustomizer { + + /** + * Customize the {@link ConnectionOptions}. + * @param connectionOptions the connection options to customize + */ + void customize(ConnectionOptions connectionOptions); + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java new file mode 100644 index 000000000000..13e47d047a5f --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.Nullable; + +/** + * Adapts {@link AmqpProperties} to {@link AmqpConnectionDetails}. + * + * @author Stephane Nicoll + */ +class PropertiesAmqpConnectionDetails implements AmqpConnectionDetails { + + private final AmqpProperties properties; + + PropertiesAmqpConnectionDetails(AmqpProperties properties) { + this.properties = properties; + } + + @Override + public Address getAddress() { + return new Address(this.properties.getHost(), this.properties.getPort()); + } + + @Override + public @Nullable String getUsername() { + return this.properties.getUsername(); + } + + @Override + public @Nullable String getPassword() { + return this.properties.getPassword(); + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java new file mode 100644 index 000000000000..fef2b40e8d3f --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 generic AMQP 1.0 support. + */ +@NullMarked +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..09a45147c4f2 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +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 AmqpConnectionDetails} + * for a {@code rabbitmq} service. + * + * @author Stephane Nicoll + */ +class RabbitMqDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int RABBITMQ_PORT = 5672; + + protected RabbitMqDockerComposeConnectionDetailsFactory() { + super("rabbitmq"); + } + + @Override + protected @Nullable AmqpConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + try { + return new RabbitMqDockerComposeConnectionDetails(source.getRunningService()); + } + catch (IllegalStateException ex) { + return null; + } + } + + /** + * {@link AmqpConnectionDetails} backed by a {@code rabbitmq} {@link RunningService}. + */ + static class RabbitMqDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements AmqpConnectionDetails { + + private final RabbitMqEnvironment environment; + + private final Address address; + + protected RabbitMqDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new RabbitMqEnvironment(service.env()); + this.address = new Address(service.host(), service.ports().get(RABBITMQ_PORT)); + } + + @Override + public Address getAddress() { + return this.address; + } + + @Override + public @Nullable String getUsername() { + return this.environment.getUsername(); + } + + @Override + public @Nullable String getPassword() { + return this.environment.getPassword(); + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironment.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironment.java new file mode 100644 index 000000000000..0da210981ae9 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironment.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +/** + * RabbitMQ environment details. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Scott Frederick + */ +class RabbitMqEnvironment { + + private final @Nullable String username; + + private final @Nullable String password; + + RabbitMqEnvironment(Map env) { + this.username = env.getOrDefault("RABBITMQ_DEFAULT_USER", env.getOrDefault("RABBITMQ_USERNAME", "guest")); + this.password = env.getOrDefault("RABBITMQ_DEFAULT_PASS", env.getOrDefault("RABBITMQ_PASSWORD", "guest")); + } + + @Nullable String getUsername() { + return this.username; + } + + @Nullable String getPassword() { + return this.password; + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java new file mode 100644 index 000000000000..6408a2ff0030 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 generic AMQP service connections. + */ +@NullMarked +package org.springframework.boot.amqp.docker.compose; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactory.java new file mode 100644 index 000000000000..a36da1c19bee --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactory.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +import java.net.URI; + +import org.testcontainers.rabbitmq.RabbitMQContainer; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +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 AmqpConnectionDetails} from + * a {@link ServiceConnection @ServiceConnection}-annotated {@link RabbitMQContainer}. + * + * @author Stephane Nicoll + */ +class RabbitMqContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory { + + @Override + protected AmqpConnectionDetails getContainerConnectionDetails(ContainerConnectionSource source) { + return new RabbitMqContainerConnectionDetails(source); + } + + /** + * {@link AmqpConnectionDetails} for RabbitMQ backed by a + * {@link ContainerConnectionSource}. + */ + static final class RabbitMqContainerConnectionDetails extends ContainerConnectionDetails + implements AmqpConnectionDetails { + + private RabbitMqContainerConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + @Override + public Address getAddress() { + URI uri = URI.create(getContainer().getAmqpUrl()); + return new Address(uri.getHost(), uri.getPort()); + } + + @Override + public String getUsername() { + return getContainer().getAdminUsername(); + } + + @Override + public String getPassword() { + return getContainer().getAdminPassword(); + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java new file mode 100644 index 000000000000..398ec731847a --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 generic AMQP service connections. + */ +@NullMarked +package org.springframework.boot.amqp.testcontainers; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories b/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..d3f0d273fc15 --- /dev/null +++ b/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories @@ -0,0 +1,5 @@ +# Connection Details Factories +org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ +org.springframework.boot.amqp.docker.compose.RabbitMqDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.amqp.testcontainers.RabbitMqContainerConnectionDetailsFactory + diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000000..1bbaad209b67 --- /dev/null +++ b/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springframework.boot.amqp.autoconfigure.AmqpAutoConfiguration diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java new file mode 100644 index 000000000000..42fe2fa2cef4 --- /dev/null +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java @@ -0,0 +1,276 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import java.time.Duration; + +import org.apache.qpid.protonj2.client.Client; +import org.apache.qpid.protonj2.client.ConnectionOptions; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.amqp.client.AmqpConnectionFactory; +import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails.Address; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AmqpAutoConfiguration}. + * + * @author Stephane Nicoll + */ +class AmqpAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AmqpAutoConfiguration.class)); + + @Test + void autoConfigurationConfiguresConnectionFactoryWithDefaultSettings() { + this.contextRunner.run((context) -> { + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("host", "localhost"); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("port", 5672); + assertThat(connectionFactory) + .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) + .satisfies((connectOptions) -> { + assertThat(connectOptions).hasFieldOrPropertyWithValue("user", null); + assertThat(connectOptions).hasFieldOrPropertyWithValue("password", null); + }); + }); + } + + @Test + void autoConfigurationConfiguresConnectionFactoryWithOverrides() { + this.contextRunner + .withPropertyValues("spring.amqp.host=amqp.example.com", "spring.amqp.port=1234", + "spring.amqp.username=user", "spring.amqp.password=secret") + .run((context) -> { + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("host", "amqp.example.com"); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("port", 1234); + assertThat(connectionFactory) + .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) + .satisfies((connectOptions) -> { + assertThat(connectOptions).hasFieldOrPropertyWithValue("user", "user"); + assertThat(connectOptions).hasFieldOrPropertyWithValue("password", "secret"); + }); + }); + } + + @Test + void autoConfigurationUsesAmqpConnectionDetailsForConnectionFactory() { + AmqpConnectionDetails connectionDetails = mock(); + given(connectionDetails.getAddress()).willReturn(new Address("details.example.com", 9999)); + given(connectionDetails.getUsername()).willReturn("from-details"); + given(connectionDetails.getPassword()).willReturn("details-secret"); + this.contextRunner.withBean(AmqpConnectionDetails.class, () -> connectionDetails) + .withPropertyValues("spring.amqp.host=ignored.example.com", "spring.amqp.port=1111", + "spring.amqp.username=ignored", "spring.amqp.password=ignored") + .run((context) -> { + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("host", "details.example.com"); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("port", 9999); + assertThat(connectionFactory) + .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) + .satisfies((connectOptions) -> { + assertThat(connectOptions).hasFieldOrPropertyWithValue("user", "from-details"); + assertThat(connectOptions).hasFieldOrPropertyWithValue("password", "details-secret"); + }); + }); + } + + @Test + void autoConfigurationAppliesConnectionOptionsCustomizer() { + this.contextRunner + .withBean(ConnectionOptionsCustomizer.class, () -> (connectOptions) -> connectOptions.user("custom-user")) + .withPropertyValues("spring.amqp.username=user") + .run((context) -> { + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + assertThat(connectionFactory) + .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) + .satisfies((connectOptions) -> assertThat(connectOptions).hasFieldOrPropertyWithValue("user", + "custom-user")); + }); + } + + @Test + void autoConfigurationAppliesConnectionOptionsCustomizersInOrder() { + this.contextRunner.withUserConfiguration(ConnectionOptionsCustomizersConfiguration.class).run((context) -> { + ConnectionOptionsCustomizer first = context.getBean("first", ConnectionOptionsCustomizer.class); + ConnectionOptionsCustomizer second = context.getBean("second", ConnectionOptionsCustomizer.class); + InOrder ordered = inOrder(first, second); + ordered.verify(first).customize(any()); + ordered.verify(second).customize(any()); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesProtonClient() { + Client protonClient = mock(); + this.contextRunner.withBean(Client.class, () -> protonClient).run((context) -> { + assertThat(context).hasSingleBean(Client.class); + assertThat(context.getBean(Client.class)).isSameAs(protonClient); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesAmqpConnectionDetails() { + AmqpConnectionDetails connectionDetails = mock(); + given(connectionDetails.getAddress()).willReturn(new Address("details.example.com", 9999)); + this.contextRunner.withBean(AmqpConnectionDetails.class, () -> connectionDetails).run((context) -> { + assertThat(context).hasSingleBean(AmqpConnectionDetails.class); + assertThat(context.getBean(AmqpConnectionDetails.class)).isSameAs(connectionDetails); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesAmqpConnectionFactory() { + AmqpConnectionFactory amqpConnectionFactory = mock(); + this.contextRunner.withBean(AmqpConnectionFactory.class, () -> amqpConnectionFactory).run((context) -> { + assertThat(context).hasSingleBean(AmqpConnectionFactory.class); + assertThat(context.getBean(AmqpConnectionFactory.class)).isSameAs(amqpConnectionFactory); + }); + } + + @Test + void autoConfigurationConfiguresAmqpClientWithOverrides() { + this.contextRunner + .withPropertyValues("spring.amqp.client.default-to-address=/queues/default_queue", + "spring.amqp.client.completion-timeout=20s") + .run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient).hasFieldOrPropertyWithValue("defaultToAddress", "/queues/default_queue"); + assertThat(amqpClient).hasFieldOrPropertyWithValue("completionTimeout", Duration.ofSeconds(20)); + }); + } + + @Test + void autoConfigurationConfiguresAmqpClientWithJacksonMessageConverter() { + this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)).run((context) -> { + assertThat(context).hasSingleBean(JsonMapper.class); + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient) + .extracting("messageConverter", InstanceOfAssertFactories.type(MessageConverter.class)) + .satisfies((messageConverter) -> assertThat(messageConverter) + .isInstanceOf(JacksonJsonMessageConverter.class) + .hasFieldOrPropertyWithValue("objectMapper", context.getBean(JsonMapper.class))); + }); + } + + @Test + void autoConfigurationConfiguresAmqpClientWithMessageConverter() { + MessageConverter messageConverter = mock(MessageConverter.class); + this.contextRunner.withBean(MessageConverter.class, () -> messageConverter).run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient).hasFieldOrPropertyWithValue("messageConverter", messageConverter); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesAmqpClient() { + AmqpClient amqpClient = mock(); + this.contextRunner.withBean(AmqpClient.class, () -> amqpClient).run((context) -> { + assertThat(context).hasSingleBean(AmqpClient.class); + assertThat(context.getBean(AmqpClient.class)).isSameAs(amqpClient); + }); + } + + @Test + void autoConfigurationUsesUserMessageConverterWhenDefinedAlongsideJackson() { + MessageConverter messageConverter = mock(MessageConverter.class); + this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) + .withBean(MessageConverter.class, () -> messageConverter) + .run((context) -> { + assertThat(context).doesNotHaveBean(JacksonJsonMessageConverter.class); + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient).hasFieldOrPropertyWithValue("messageConverter", messageConverter); + }); + } + + @Test + void autoConfigurationAppliesAmqpClientCustomizer() { + this.contextRunner + .withBean(AmqpClientCustomizer.class, + () -> (client) -> client.defaultToAddress("/queues/custom_default_queue")) + .withPropertyValues("spring.amqp.client.default-to-address=/queues/default_queue") + .run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient).hasFieldOrPropertyWithValue("defaultToAddress", "/queues/custom_default_queue"); + }); + } + + @Test + void autoConfigurationAppliesAmqpClientCustomizersInOrder() { + this.contextRunner.withUserConfiguration(AmqpClientCustomizersConfiguration.class).run((context) -> { + AmqpClientCustomizer first = context.getBean("first", AmqpClientCustomizer.class); + AmqpClientCustomizer second = context.getBean("second", AmqpClientCustomizer.class); + InOrder ordered = inOrder(first, second); + ordered.verify(first).customize(any()); + ordered.verify(second).customize(any()); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionOptionsCustomizersConfiguration { + + @Bean + @Order(2) + ConnectionOptionsCustomizer second() { + return mock(); + } + + @Bean + @Order(1) + ConnectionOptionsCustomizer first() { + return mock(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class AmqpClientCustomizersConfiguration { + + @Bean + @Order(2) + AmqpClientCustomizer second() { + return mock(); + } + + @Bean + @Order(1) + AmqpClientCustomizer first() { + return mock(); + } + + } + +} diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironmentTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironmentTests.java new file mode 100644 index 000000000000..01decb6785cb --- /dev/null +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironmentTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +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 RabbitMqEnvironment}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Scott Frederick + */ +class RabbitMqEnvironmentTests { + + @Test + void getUsernameWhenNoRabbitMqDefaultUser() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Collections.emptyMap()); + assertThat(environment.getUsername()).isEqualTo("guest"); + } + + @Test + void getUsernameWhenHasRabbitMqDefaultUser() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_DEFAULT_USER", "me")); + assertThat(environment.getUsername()).isEqualTo("me"); + } + + @Test + void getUsernameWhenHasRabbitMqUsername() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_USERNAME", "me")); + assertThat(environment.getUsername()).isEqualTo("me"); + } + + @Test + void getPasswordWhenNoRabbitMqDefaultPass() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Collections.emptyMap()); + assertThat(environment.getPassword()).isEqualTo("guest"); + } + + @Test + void getPasswordWhenHasRabbitMqDefaultPass() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getPasswordWhenHasRabbitMqPassword() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + +} diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6519005485b8..dd544051fa05 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1870,6 +1870,19 @@ bom { releaseNotes("https://pulsar.apache.org/release-notes/versioned/pulsar-{version}") } } + library("Qpid ProtonJ2", "1.1.0") { + group("org.apache.qpid") { + modules = [ + "protonj2", + "protonj2-client" + ] + } + links { + site("https://qpid.apache.org/proton") + javadoc("https://qpid.apache.org/releases/qpid-protonj2-{version}/api", "org.apache.qpid.protonj2") + releaseNotes("https://qpid.apache.org/releases/qpid-protonj2-{version}/release-notes.html") + } + } library("Quartz", "2.5.2") { group("org.quartz-scheduler") { modules = [ diff --git a/settings.gradle b/settings.gradle index 5fc768cf5728..b7ceb4956e0f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -80,6 +80,7 @@ include "core:spring-boot-testcontainers" include "module:spring-boot-activemq" include "module:spring-boot-actuator" include "module:spring-boot-actuator-autoconfigure" +include "module:spring-boot-amqp" include "module:spring-boot-artemis" include "module:spring-boot-autoconfigure-classic" include "module:spring-boot-autoconfigure-classic-modules" @@ -208,6 +209,8 @@ include "starter:spring-boot-starter-activemq" include "starter:spring-boot-starter-activemq-test" include "starter:spring-boot-starter-actuator" include "starter:spring-boot-starter-actuator-test" +include "starter:spring-boot-starter-amqp" +include "starter:spring-boot-starter-amqp-test" include "starter:spring-boot-starter-artemis" include "starter:spring-boot-starter-artemis-test" include "starter:spring-boot-starter-aspectj" diff --git a/starter/spring-boot-starter-amqp-test/build.gradle b/starter/spring-boot-starter-amqp-test/build.gradle new file mode 100644 index 000000000000..6991f36fc910 --- /dev/null +++ b/starter/spring-boot-starter-amqp-test/build.gradle @@ -0,0 +1,26 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for testing generic AMQP 1.0 support" + +dependencies { + api(project(":starter:spring-boot-starter-amqp")) + api(project(":starter:spring-boot-starter-test")) +} diff --git a/starter/spring-boot-starter-amqp/build.gradle b/starter/spring-boot-starter-amqp/build.gradle new file mode 100644 index 000000000000..39de12fb1139 --- /dev/null +++ b/starter/spring-boot-starter-amqp/build.gradle @@ -0,0 +1,27 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for generic AMQP 1.0 support" + +dependencies { + api(project(":starter:spring-boot-starter")) + + api(project(":module:spring-boot-amqp")) +} diff --git a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.java b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.java new file mode 100644 index 000000000000..baea0dcddd5f --- /dev/null +++ b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.container; + +import java.util.UUID; + +import org.testcontainers.rabbitmq.RabbitMQContainer; + +/** + * A {@link RabbitMQContainer} with management plugin enabled. + * + * @author Stephane Nicoll + */ +public class RabbitMqManagementContainer extends RabbitMQContainer { + + public RabbitMqManagementContainer() { + super(TestImage.RABBITMQ.toString()); + } + + /** + * Create a queue with the given name. + * @param name the name of the queue + * @return the AMQP target of the queue + */ + public String createQueue(String name) { + try { + execInContainer("rabbitmqadmin", "queues", "declare", "--name", name); + return "/queues/" + name; + } + catch (Exception ex) { + throw new RuntimeException("Could not create queue", ex); + } + } + + /** + * Create a queue with a random, unique, name. + * @return the AMQP target of the queue + */ + public String createRandomQueue() { + return createQueue(UUID.randomUUID().toString()); + } + +} diff --git a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java index e64fc32615cf..5f2da85c8b55 100644 --- a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java +++ b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java @@ -245,9 +245,10 @@ public enum TestImage { .withStartupTimeout(Duration.ofMinutes(3))), /** - * A container image suitable for testing RabbitMQ. + * A container image suitable for testing RabbitMQ with support for both AMQP 0.9 and + * 1.0. Queues can be declared using {@code rabbitmqadmin}. */ - RABBITMQ("rabbitmq", "3.11-alpine", () -> RabbitMQContainer.class, + RABBITMQ("rabbitmq", "4.2-management", () -> RabbitMQContainer.class, (container) -> ((RabbitMQContainer) container).withStartupTimeout(Duration.ofMinutes(4))), /** From 5b7f0f726b9be082b09b0714cadb1b00bff79280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 19 Mar 2026 16:03:32 +0100 Subject: [PATCH 377/930] Harmonize Docker compose and testcontainers support for RabbitMQ Given that the prefix for such support refers to the name of the service, it should have been RabbitMq, not Rabbit. This was already applied in the newly introduced AMQP 1.0 support, and this commit adapts the existing support for consistency. Closes gh-49664 --- ...ectionDetailsFactoryIntegrationTests.java} | 11 ++++---- ...ectionDetailsFactoryIntegrationTests.java} | 8 +++--- ...ectionDetailsFactoryIntegrationTests.java} | 8 +++--- ...ectionDetailsFactoryIntegrationTests.java} | 9 +++--- ...nectionDetailsFactoryIntegrationTests.java | 2 +- ...ectionDetailsFactoryIntegrationTests.java} | 4 +-- ...ectionDetailsFactoryIntegrationTests.java} | 10 +++---- ...ectionDetailsFactoryIntegrationTests.java} | 8 +++--- ...ectionDetailsFactoryIntegrationTests.java} | 8 +++--- ...ectionDetailsFactoryIntegrationTests.java} | 10 +++---- ...tmq-stream-separate-services-compose.yaml} | 0 ...bbitmq-stream-single-service-compose.yaml} | 0 ...bit-compose.yaml => rabbitmq-compose.yaml} | 0 ...compose.yaml => rabbitmq-ssl-compose.yaml} | 0 ...pose.yaml => rabbitmq-stream-compose.yaml} | 0 ....yaml => rabbitmq-stream-ssl-compose.yaml} | 0 ...ockerComposeConnectionDetailsFactory.java} | 8 +++--- ...ironment.java => RabbitMqEnvironment.java} | 4 +-- ...ockerComposeConnectionDetailsFactory.java} | 8 +++--- ...bbitContainerConnectionDetailsFactory.java | 2 +- ...tMqContainerConnectionDetailsFactory.java} | 2 +- ...eamContainerConnectionDetailsFactory.java} | 4 +-- .../main/resources/META-INF/spring.factories | 8 +++--- ...sts.java => RabbitMqEnvironmentTests.java} | 28 +++++++++---------- 24 files changed, 72 insertions(+), 70 deletions(-) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/{RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java => RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java} (84%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/{RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java => RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java} (87%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/{RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java => RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java} (85%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/{SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java => SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java} (85%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/{RabbitContainerConnectionDetailsFactoryIntegrationTests.java => RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java} (95%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/{RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java => RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java} (90%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/{RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java => RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java} (92%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/{SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java => SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java} (92%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/{SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java => SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java} (91%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/{rabbit-and-rabbit-stream-separate-services-compose.yaml => rabbitmq-and-rabbitmq-stream-separate-services-compose.yaml} (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/{rabbit-and-rabbit-stream-single-service-compose.yaml => rabbitmq-and-rabbitmq-stream-single-service-compose.yaml} (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/{rabbit-compose.yaml => rabbitmq-compose.yaml} (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/{rabbit-ssl-compose.yaml => rabbitmq-ssl-compose.yaml} (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/{rabbit-stream-compose.yaml => rabbitmq-stream-compose.yaml} (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/{rabbit-stream-ssl-compose.yaml => rabbitmq-stream-ssl-compose.yaml} (100%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/{RabbitDockerComposeConnectionDetailsFactory.java => RabbitMqDockerComposeConnectionDetailsFactory.java} (92%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/{RabbitEnvironment.java => RabbitMqEnvironment.java} (93%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/{RabbitStreamDockerComposeConnectionDetailsFactory.java => RabbitMqStreamDockerComposeConnectionDetailsFactory.java} (92%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/{RabbitContainerConnectionDetailsFactory.java => RabbitMqContainerConnectionDetailsFactory.java} (98%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/{RabbitStreamContainerConnectionDetailsFactory.java => RabbitMqStreamContainerConnectionDetailsFactory.java} (96%) rename module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/{RabbitEnvironmentTests.java => RabbitMqEnvironmentTests.java} (59%) diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 84% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index dcef4737265a..0c4902328afb 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -25,14 +25,14 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory} and - * {@link RabbitStreamDockerComposeConnectionDetailsFactory} when used in combination. + * Integration tests for {@link RabbitMqDockerComposeConnectionDetailsFactory} and + * {@link RabbitMqStreamDockerComposeConnectionDetailsFactory} when used in combination. * * @author Andy Wilkinson */ -class RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests { +class RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests { - @DockerComposeTest(composeFile = "rabbit-and-rabbit-stream-separate-services-compose.yaml", + @DockerComposeTest(composeFile = "rabbitmq-and-rabbitmq-stream-separate-services-compose.yaml", image = TestImage.RABBITMQ) void runCreatesConnectionDetailsFromSeparateServices(RabbitConnectionDetails connectionDetails, RabbitStreamConnectionDetails streamConnectionDetails) { @@ -40,7 +40,8 @@ void runCreatesConnectionDetailsFromSeparateServices(RabbitConnectionDetails con assertConnectionDetails(streamConnectionDetails); } - @DockerComposeTest(composeFile = "rabbit-and-rabbit-stream-single-service-compose.yaml", image = TestImage.RABBITMQ) + @DockerComposeTest(composeFile = "rabbitmq-and-rabbitmq-stream-single-service-compose.yaml", + image = TestImage.RABBITMQ) void runCreatesConnectionDetailsFromSingleService(RabbitConnectionDetails connectionDetails, RabbitStreamConnectionDetails streamConnectionDetails) { assertConnectionDetails(connectionDetails); diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 87% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java index fb1e627f60e4..fb482a6cbb83 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -25,22 +25,22 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory}. + * Integration tests for {@link RabbitMqDockerComposeConnectionDetailsFactory}. * * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb * @author Scott Frederick */ -class RabbitDockerComposeConnectionDetailsFactoryIntegrationTests { +class RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests { - @DockerComposeTest(composeFile = "rabbit-compose.yaml", image = TestImage.RABBITMQ) + @DockerComposeTest(composeFile = "rabbitmq-compose.yaml", image = TestImage.RABBITMQ) void runCreatesConnectionDetails(RabbitConnectionDetails connectionDetails) { assertConnectionDetails(connectionDetails); assertThat(connectionDetails.getSslBundle()).isNull(); } - @DockerComposeTest(composeFile = "rabbit-ssl-compose.yaml", image = TestImage.RABBITMQ, + @DockerComposeTest(composeFile = "rabbitmq-ssl-compose.yaml", image = TestImage.RABBITMQ, additionalResources = { "../../ca.crt", "../../server.crt", "../../server.key", "../../client.crt", "../../client.key", "rabbitmq-ssl.conf" }) void runWithSslCreatesConnectionDetails(RabbitConnectionDetails connectionDetails) { diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 85% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index b54ab7270fb4..1a4d0fdf9817 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -24,18 +24,18 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link RabbitStreamDockerComposeConnectionDetailsFactory}. + * Integration tests for {@link RabbitMqStreamDockerComposeConnectionDetailsFactory}. * * @author Andy Wilkinson */ -class RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests { +class RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests { - @DockerComposeTest(composeFile = "rabbit-stream-compose.yaml", image = TestImage.RABBITMQ) + @DockerComposeTest(composeFile = "rabbitmq-stream-compose.yaml", image = TestImage.RABBITMQ) void runCreatesConnectionDetails(RabbitStreamConnectionDetails connectionDetails) { assertConnectionDetails(connectionDetails); } - @DockerComposeTest(composeFile = "rabbit-stream-ssl-compose.yaml", image = TestImage.RABBITMQ, + @DockerComposeTest(composeFile = "rabbitmq-stream-ssl-compose.yaml", image = TestImage.RABBITMQ, additionalResources = { "../../ca.crt", "../../server.crt", "../../server.key", "../../client.crt", "../../client.key", "rabbitmq-stream-ssl.conf" }) void runWithSslCreatesConnectionDetails(RabbitStreamConnectionDetails connectionDetails) { diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 85% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index ae9d61597f63..af67b52c44b0 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -25,15 +25,16 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory} and - * {@link RabbitStreamDockerComposeConnectionDetailsFactory} when using a single service + * Integration tests for {@link RabbitMqDockerComposeConnectionDetailsFactory} and + * {@link RabbitMqStreamDockerComposeConnectionDetailsFactory} when using a single service * for standard and stream-based messaging. * * @author Andy Wilkinson */ -class SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests { +class SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests { - @DockerComposeTest(composeFile = "rabbit-and-rabbit-stream-single-service-compose.yaml", image = TestImage.RABBITMQ) + @DockerComposeTest(composeFile = "rabbitmq-and-rabbitmq-stream-single-service-compose.yaml", + image = TestImage.RABBITMQ) void runCreatesConnectionDetails(RabbitConnectionDetails connectionDetails, RabbitStreamConnectionDetails streamConnectionDetails) { assertConnectionDetails(connectionDetails); diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java index 91aed53b74b5..61b0a43c5311 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java @@ -48,7 +48,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link RabbitContainerConnectionDetailsFactoryIntegrationTests}. + * {@link RabbitMqContainerConnectionDetailsFactoryIntegrationTests}. */ @SpringJUnitConfig @Testcontainers(disabledWithoutDocker = true) diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java similarity index 95% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java index f371b8562284..0de6be2fad4a 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java @@ -42,7 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitContainerConnectionDetailsFactory}. + * Tests for {@link RabbitMqContainerConnectionDetailsFactory}. * * @author Moritz Halbritter * @author Andy Wilkinson @@ -50,7 +50,7 @@ */ @SpringJUnitConfig @Testcontainers(disabledWithoutDocker = true) -class RabbitContainerConnectionDetailsFactoryIntegrationTests { +class RabbitMqContainerConnectionDetailsFactoryIntegrationTests { @Container @ServiceConnection diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 90% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java index 8820db4606a9..3f3981b727d9 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -36,8 +36,8 @@ import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; @@ -50,8 +50,8 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitStreamContainerConnectionDetailsFactory} with a single container - * that's only used for streams. + * Tests for {@link RabbitMqStreamContainerConnectionDetailsFactory} with a single + * container that's only used for streams. * * @author Eddú Meléndez * @author Andy Wilkinson @@ -60,7 +60,7 @@ @TestPropertySource( properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) @Testcontainers(disabledWithoutDocker = true) -class RabbitStreamContainerConnectionDetailsFactoryIntegrationTests { +class RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests { private static final int RABBITMQ_STREAMS_PORT = 5552; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java similarity index 92% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java index 6eab916aef27..c13ddc3eae7a 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java @@ -37,8 +37,8 @@ import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.PemKeyStore; import org.springframework.boot.testcontainers.service.connection.PemTrustStore; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -53,7 +53,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitStreamContainerConnectionDetailsFactory} with SSL. + * Tests for {@link RabbitMqStreamContainerConnectionDetailsFactory} with SSL. * * @author Eddú Meléndez * @author Andy Wilkinson @@ -62,7 +62,7 @@ @TestPropertySource( properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) @Testcontainers(disabledWithoutDocker = true) -class RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests { +class RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests { private static final int RABBITMQ_STREAMS_TLS_PORT = 5551; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 92% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java index e28c2657a7c8..099cd3816864 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -40,8 +40,8 @@ import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; @@ -54,7 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitStreamContainerConnectionDetailsFactory} with two containers, + * Tests for {@link RabbitMqStreamContainerConnectionDetailsFactory} with two containers, * one for streams and one for standard messaging. * * @author Eddú Meléndez @@ -64,7 +64,7 @@ @TestPropertySource( properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) @Testcontainers(disabledWithoutDocker = true) -class SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests { +class SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests { private static final int RABBITMQ_STREAMS_PORT = 5552; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 91% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java index 3d306284fa4f..686a447bc2e5 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -40,8 +40,8 @@ import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; @@ -54,8 +54,8 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitStreamContainerConnectionDetailsFactory} with a single container - * used for both streams and standard messaging. + * Tests for {@link RabbitMqStreamContainerConnectionDetailsFactory} with a single + * container used for both streams and standard messaging. * * @author Eddú Meléndez * @author Andy Wilkinson @@ -64,7 +64,7 @@ @TestPropertySource( properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) @Testcontainers(disabledWithoutDocker = true) -class SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests { +class SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests { private static final int RABBITMQ_STREAMS_PORT = 5552; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-and-rabbitmq-stream-separate-services-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-and-rabbitmq-stream-separate-services-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-and-rabbitmq-stream-single-service-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-and-rabbitmq-stream-single-service-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-ssl-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-ssl-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-ssl-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-ssl-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-ssl-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-ssl-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-ssl-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-ssl-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java similarity index 92% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java index bd662a242aac..30b7ab878cb1 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java @@ -33,12 +33,12 @@ * * @author Andy Wilkinson */ -class RabbitDockerComposeConnectionDetailsFactory +class RabbitMqDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { private static final int RABBITMQ_PORT = 5672; - protected RabbitDockerComposeConnectionDetailsFactory() { + protected RabbitMqDockerComposeConnectionDetailsFactory() { super("rabbitmq"); } @@ -60,7 +60,7 @@ protected RabbitDockerComposeConnectionDetailsFactory() { static class RabbitDockerComposeConnectionDetails extends DockerComposeConnectionDetails implements RabbitConnectionDetails { - private final RabbitEnvironment environment; + private final RabbitMqEnvironment environment; private final List
    addresses; @@ -68,7 +68,7 @@ static class RabbitDockerComposeConnectionDetails extends DockerComposeConnectio protected RabbitDockerComposeConnectionDetails(RunningService service) { super(service); - this.environment = new RabbitEnvironment(service.env()); + this.environment = new RabbitMqEnvironment(service.env()); this.sslBundle = getSslBundle(service); this.addresses = List.of(new Address(service.host(), service.ports().get(RABBITMQ_PORT))); } diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironment.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironment.java similarity index 93% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironment.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironment.java index 1d37579e1a84..a71a8207ec71 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironment.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironment.java @@ -28,13 +28,13 @@ * @author Phillip Webb * @author Scott Frederick */ -class RabbitEnvironment { +class RabbitMqEnvironment { private final @Nullable String username; private final @Nullable String password; - RabbitEnvironment(Map env) { + RabbitMqEnvironment(Map env) { this.username = env.getOrDefault("RABBITMQ_DEFAULT_USER", env.getOrDefault("RABBITMQ_USERNAME", "guest")); this.password = env.getOrDefault("RABBITMQ_DEFAULT_PASS", env.getOrDefault("RABBITMQ_PASSWORD", "guest")); } diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactory.java similarity index 92% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactory.java index 5b1fbbb87941..9a0b8f22bc31 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactory.java @@ -35,10 +35,10 @@ * @author Scott Frederick * @author Jay Choi */ -class RabbitStreamDockerComposeConnectionDetailsFactory +class RabbitMqStreamDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { - protected RabbitStreamDockerComposeConnectionDetailsFactory() { + protected RabbitMqStreamDockerComposeConnectionDetailsFactory() { super("rabbitmq"); } @@ -64,7 +64,7 @@ static class RabbitStreamDockerComposeConnectionDetails extends DockerComposeCon private static final int STREAMS_TLS_PORT = 5551; - private final RabbitEnvironment environment; + private final RabbitMqEnvironment environment; private final String host; @@ -74,7 +74,7 @@ static class RabbitStreamDockerComposeConnectionDetails extends DockerComposeCon protected RabbitStreamDockerComposeConnectionDetails(RunningService service) { super(service); - this.environment = new RabbitEnvironment(service.env()); + this.environment = new RabbitMqEnvironment(service.env()); this.host = service.host(); this.sslBundle = getSslBundle(service); this.port = service.ports().get((this.sslBundle != null) ? STREAMS_TLS_PORT : STREAMS_PORT); diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java index 445d8410611a..750d611deb67 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java @@ -37,7 +37,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link RabbitContainerConnectionDetailsFactory}. + * {@link RabbitMqContainerConnectionDetailsFactory}. */ @Deprecated(since = "4.0.0", forRemoval = true) class DeprecatedRabbitContainerConnectionDetailsFactory diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactory.java similarity index 98% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactory.java index 32a89a3e23ac..5e13fc5d999c 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactory.java @@ -37,7 +37,7 @@ * @author Andy Wilkinson * @author Phillip Webb */ -class RabbitContainerConnectionDetailsFactory +class RabbitMqContainerConnectionDetailsFactory extends ContainerConnectionDetailsFactory { @Override diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactory.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactory.java index dccd1d441417..17171157b33f 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactory.java @@ -33,10 +33,10 @@ * @author Eddú Meléndez * @author Jay Choi */ -class RabbitStreamContainerConnectionDetailsFactory +class RabbitMqStreamContainerConnectionDetailsFactory extends ContainerConnectionDetailsFactory { - RabbitStreamContainerConnectionDetailsFactory() { + RabbitMqStreamContainerConnectionDetailsFactory() { super(ANY_CONNECTION_NAME, "org.springframework.rabbit.stream.producer.RabbitStreamTemplate"); } diff --git a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories index e3667de2bbcf..a7a1d458e73e 100644 --- a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories @@ -1,8 +1,8 @@ # Connection Details Factories org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ -org.springframework.boot.rabbitmq.docker.compose.RabbitDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.rabbitmq.docker.compose.RabbitStreamDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.rabbitmq.docker.compose.RabbitMqDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.rabbitmq.docker.compose.RabbitMqStreamDockerComposeConnectionDetailsFactory,\ org.springframework.boot.rabbitmq.testcontainers.DeprecatedRabbitContainerConnectionDetailsFactory,\ -org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory,\ -org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory +org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory,\ +org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironmentTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironmentTests.java similarity index 59% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironmentTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironmentTests.java index 356d93ad535e..533ee69f8c24 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironmentTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironmentTests.java @@ -24,48 +24,48 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitEnvironment}. + * Tests for {@link RabbitMqEnvironment}. * * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb * @author Scott Frederick */ -class RabbitEnvironmentTests { +class RabbitMqEnvironmentTests { @Test - void getUsernameWhenNoRabbitmqDefaultUser() { - RabbitEnvironment environment = new RabbitEnvironment(Collections.emptyMap()); + void getUsernameWhenNoRabbitMqDefaultUser() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Collections.emptyMap()); assertThat(environment.getUsername()).isEqualTo("guest"); } @Test - void getUsernameWhenHasRabbitmqDefaultUser() { - RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_DEFAULT_USER", "me")); + void getUsernameWhenHasRabbitMqDefaultUser() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_DEFAULT_USER", "me")); assertThat(environment.getUsername()).isEqualTo("me"); } @Test - void getUsernameWhenHasRabbitmqUsername() { - RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_USERNAME", "me")); + void getUsernameWhenHasRabbitMqUsername() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_USERNAME", "me")); assertThat(environment.getUsername()).isEqualTo("me"); } @Test - void getUsernameWhenNoRabbitmqDefaultPass() { - RabbitEnvironment environment = new RabbitEnvironment(Collections.emptyMap()); + void getUsernameWhenNoRabbitMqDefaultPass() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Collections.emptyMap()); assertThat(environment.getPassword()).isEqualTo("guest"); } @Test - void getUsernameWhenHasRabbitmqDefaultPass() { - RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); + void getUsernameWhenHasRabbitMqDefaultPass() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); assertThat(environment.getPassword()).isEqualTo("secret"); } @Test - void getUsernameWhenHasRabbitmqPassword() { - RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_PASSWORD", "secret")); + void getUsernameWhenHasRabbitMqPassword() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_PASSWORD", "secret")); assertThat(environment.getPassword()).isEqualTo("secret"); } From 8dde82b557d0a3020f89b088d9ba19f5b4b1e9ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 19 Mar 2026 17:17:28 +0100 Subject: [PATCH 378/930] Fix starter names in published POM See gh-49620 --- platform/spring-boot-dependencies/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index dd544051fa05..335343d71e08 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2291,8 +2291,8 @@ bom { "spring-boot-starter-quartz-test", "spring-boot-starter-r2dbc", "spring-boot-starter-r2dbc-test", - "spring-boot-starter-rabbitmqmq", - "spring-boot-starter-rabbitmqmq-test", + "spring-boot-starter-rabbitmq", + "spring-boot-starter-rabbitmq-test", "spring-boot-starter-reactor-netty", "spring-boot-starter-restclient", "spring-boot-starter-restclient-test", From 2d71ef94ed2b5b82070720984baa2ffeb4cc9e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 19 Mar 2026 17:31:27 +0100 Subject: [PATCH 379/930] Add missing AMQP modules See gh-49621 --- module/spring-boot-autoconfigure-classic-modules/build.gradle | 3 +++ platform/spring-boot-dependencies/build.gradle | 3 +++ 2 files changed, 6 insertions(+) diff --git a/module/spring-boot-autoconfigure-classic-modules/build.gradle b/module/spring-boot-autoconfigure-classic-modules/build.gradle index 07ab22f09847..3075fd4970f1 100644 --- a/module/spring-boot-autoconfigure-classic-modules/build.gradle +++ b/module/spring-boot-autoconfigure-classic-modules/build.gradle @@ -28,6 +28,9 @@ dependencies { api(project(":module:spring-boot-activemq")) { transitive = false } + api(project(":module:spring-boot-amqp")) { + transitive = false + } api(project(":module:spring-boot-artemis")) { transitive = false } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 335343d71e08..c97bd74ada1e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2062,6 +2062,7 @@ bom { "spring-boot-activemq", "spring-boot-actuator", "spring-boot-actuator-autoconfigure", + "spring-boot-amqp", "spring-boot-artemis", "spring-boot-autoconfigure", "spring-boot-autoconfigure-classic", @@ -2179,6 +2180,8 @@ bom { "spring-boot-starter-activemq-test", "spring-boot-starter-actuator", "spring-boot-starter-actuator-test", + "spring-boot-starter-amqp", + "spring-boot-starter-amqp-test", "spring-boot-starter-artemis", "spring-boot-starter-artemis-test", "spring-boot-starter-aspectj", From b0c00d76023422bb18f824088ee03aa7a72ad4be Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 9 Mar 2026 12:34:33 -0700 Subject: [PATCH 380/930] Add dependency management for gRPC libraries Closes gh-49670 --- .../boot/build/bom/BomExtension.java | 7 +++ .../DocumentConfigurationProperties.java | 5 ++ gradle.properties | 1 + .../spring-boot-dependencies/build.gradle | 62 +++++++++++++++++++ .../build.gradle | 5 -- 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java index 3d4dc4b6d6c9..7ab8a4c99864 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java @@ -93,6 +93,13 @@ public List getLibraries() { return this.libraries; } + public Library getLibrary(String name) { + return getLibraries().stream() + .filter((library) -> library.getName().equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No library found named '%s'".formatted(name))); + } + public void upgrade(Action action) { action.execute(this.upgradeHandler); } 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 dc56a5b977c8..5eb829029896 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 @@ -63,6 +63,7 @@ void documentConfigurationProperties() throws IOException { Snippets snippets = new Snippets(this.configurationPropertyMetadata, getDeprecated().getOrElse(false)); snippets.add("application-properties.core", "Core Properties", this::corePrefixes); snippets.add("application-properties.cache", "Cache Properties", this::cachePrefixes); + snippets.add("application-properties.grpc", "gRPC Properties", this::grpcPrefixes); snippets.add("application-properties.mail", "Mail Properties", this::mailPrefixes); snippets.add("application-properties.json", "JSON Properties", this::jsonPrefixes); snippets.add("application-properties.data", "Data Properties", this::dataPrefixes); @@ -115,6 +116,10 @@ private void cachePrefixes(Config config) { config.accept("spring.cache"); } + private void grpcPrefixes(Config prefix) { + prefix.accept("spring.grpc"); + } + private void mailPrefixes(Config config) { config.accept("spring.mail"); config.accept("spring.sendgrid"); diff --git a/gradle.properties b/gradle.properties index 11e584a9b01a..32a7df567a2e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,5 +24,6 @@ snakeYamlVersion=2.6 springFrameworkVersion=7.0.6 springFramework60xVersion=6.0.23 tomcatVersion=11.0.18 +protobufGradlePluginVersion=0.9.6 kotlin.stdlib.default.dependency=false diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c97bd74ada1e..1c5ec06915a4 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -562,6 +562,30 @@ bom { site("https://groovy-lang.org") } } + library("Grpc Java", "1.78.0") { + group("io.grpc") { + bom("grpc-bom") + } + links { + github("https://github.com/grpc/grpc-java") + docs("https://grpc.io/docs/languages/java/") + releaseNotes("https://github.com/grpc/grpc-java/releases/tag/v{version}") + } + } + library("Grpc Kotlin", "1.5.0") { + group("io.grpc") { + modules = [ + "grpc-kotlin-stub" { + exclude group: "javax.annotation", module: "javax.annotation-api" + } + ] + } + links { + github("https://github.com/grpc/grpc-kotlin") + docs("https://grpc.io/docs/languages/kotlin/") + releaseNotes("https://github.com/grpc/grpc-kotlin/releases/tag/v{version}") + } + } library("Gson", "2.13.2") { group("com.google.code.gson") { modules = [ @@ -1883,6 +1907,27 @@ bom { releaseNotes("https://qpid.apache.org/releases/qpid-protonj2-{version}/release-notes.html") } } + library("Protobuf Common Protos", "2.65.0") { + group("com.google.api.grpc") { + modules = [ + "proto-google-common-protos" + ] + } + links { + github("https://github.com/googleapis/sdk-platform-java") + releaseNotes("https://github.com/googleapis/sdk-platform-java/releases/tag/v-{version}") + } + } + library("Protobuf Java", "4.34.0") { + group("com.google.protobuf") { + bom("protobuf-bom") + } + links { + site("https://protobuf.dev") + github("https://github.com/protocolbuffers/protobuf") + releaseNotes("https://github.com/protocolbuffers/protobuf/releases/tag/v{version}") + } + } library("Quartz", "2.5.2") { group("org.quartz-scheduler") { modules = [ @@ -2525,6 +2570,23 @@ bom { releaseNotes("https://github.com/spring-projects/spring-graphql/releases/tag/v{version}") } } + library("Spring gRPC", "1.1.0-SNAPSHOT") { + considerSnapshots() + group("org.springframework.grpc") { + modules = [ + "spring-grpc-core" + ] + } + links { + site("https://spring.io/projects/spring-grpc") + github("https://github.com/spring-projects/spring-grpc") + javadoc(version -> "https://docs.spring.io/spring-grpc/docs/%s/api" + .formatted(version.forMajorMinorGeneration()), "org.springframework.grpc") + docs(version -> "https://docs.spring.io/spring-grpc/docs/%s/reference" + .formatted(version.forMajorMinorGeneration())) + releaseNotes("https://github.com/spring-projects/spring-grpc/releases/tag/v{version}") + } + } library("Spring HATEOAS", "3.1.0-M1") { considerSnapshots() group("org.springframework.hateoas") { diff --git a/platform/spring-boot-internal-dependencies/build.gradle b/platform/spring-boot-internal-dependencies/build.gradle index cfdbbf36db8c..dde747796465 100644 --- a/platform/spring-boot-internal-dependencies/build.gradle +++ b/platform/spring-boot-internal-dependencies/build.gradle @@ -94,11 +94,6 @@ bom { ] } } - library("gRPC", "1.73.0") { - group("io.grpc") { - bom("grpc-bom") - } - } library("Janino", "3.1.12") { group("org.codehaus.janino") { bom("janino") { From 32bc51907c0b9b5267a4d808abfd4510b359ece3 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 2 Mar 2026 14:01:03 -0800 Subject: [PATCH 381/930] Provide protobuf-maven-plugin configuration in starter parent Update `spring-boot-starter-parent` to include sensible configuration for the `protobuf-maven-plugin`. Closes gh-49353 --- .../spring-boot-dependencies/build.gradle | 11 +++++++++ .../spring-boot-starter-parent/build.gradle | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1c5ec06915a4..a8ff76a10ed0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1841,6 +1841,17 @@ bom { releaseNotes("https://github.com/oracle/oracle-r2dbc/releases/tag/{version}") } } + library("Protobuf Maven Plugin", "5.0.1") { + group("io.github.ascopes") { + plugins = [ + "protobuf-maven-plugin" + ] + } + links { + site("https://github.com/ascopes/protobuf-maven-plugin") + releaseNotes("https://github.com/ascopes/protobuf-maven-plugin/tag/v{version}") + } + } library("Pooled JMS", "3.2.2") { group("org.messaginghub") { modules = [ diff --git a/starter/spring-boot-starter-parent/build.gradle b/starter/spring-boot-starter-parent/build.gradle index 986865eadadc..dc612f80b939 100644 --- a/starter/spring-boot-starter-parent/build.gradle +++ b/starter/spring-boot-starter-parent/build.gradle @@ -65,6 +65,29 @@ publishing.publications.withType(MavenPublication) { } pluginManagement { plugins { + plugin { + delegate.groupId('io.github.ascopes') + delegate.artifactId('protobuf-maven-plugin') + configuration { + delegate.protoc('${protobuf-java.version}') + plugins { + plugin(kind: 'binary-maven') { + delegate.groupId('io.grpc') + delegate.artifactId('protoc-gen-grpc-java') + delegate.version('${grpc-java.version}') + delegate.options('@generated=omit') + } + } + } + executions { + execution { + delegate.id('generate') + goals { + delegate.goal('generate') + } + } + } + } plugin { delegate.groupId('org.jetbrains.kotlin') delegate.artifactId('kotlin-maven-plugin') From a052376237b06cd988257c84c86b6f06d8e14d6a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Mar 2026 10:12:02 +0000 Subject: [PATCH 382/930] Configure protobuf when com.google.protobuf plugin is applied Update spring-boot-gradle plugin to apply protobuf configuration when the `com.google.protobuf` plugin is detected. Closes gh-49369 --- .../spring-boot-gradle-plugin/build.gradle | 1 + .../modules/gradle-plugin/pages/reacting.adoc | 11 ++ .../gradle/plugin/ProtobufPluginAction.java | 157 ++++++++++++++++++ .../boot/gradle/plugin/SpringBootPlugin.java | 2 +- .../ProtobufPluginActionIntegrationTests.java | 81 +++++++++ .../testkit/PluginClasspathGradleBuild.java | 5 + ...OfGrpcPluginDependencyWhenSpecified.gradle | 37 +++++ ...sionOfProtocDependencyWhenSpecified.gradle | 35 ++++ ...rotobufPluginActionIntegrationTests.gradle | 63 +++++++ .../antora-asciidoc-attributes.properties | 1 + .../build.gradle | 7 + 11 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java create mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.java create mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests-usesVersionOfGrpcPluginDependencyWhenSpecified.gradle create mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests-usesVersionOfProtocDependencyWhenSpecified.gradle create mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle diff --git a/build-plugin/spring-boot-gradle-plugin/build.gradle b/build-plugin/spring-boot-gradle-plugin/build.gradle index 99058c2c1d2a..ac6a02c1d99b 100644 --- a/build-plugin/spring-boot-gradle-plugin/build.gradle +++ b/build-plugin/spring-boot-gradle-plugin/build.gradle @@ -49,6 +49,7 @@ dependencies { implementation("org.apache.commons:commons-compress") implementation("org.springframework:spring-core") + optional("com.google.protobuf:protobuf-gradle-plugin") optional("org.graalvm.buildtools:native-gradle-plugin") optional("org.cyclonedx:cyclonedx-gradle-plugin") { exclude(group: "javax.annotation", module: "javax.annotation-api") diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc index d1ec0b5817b5..5ccd7f80e3f2 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc @@ -104,3 +104,14 @@ When the {url-cyclonedx-docs-gradle-plugin}[CycloneDX plugin] is applied to a pr .. Disable full license texts. . Adds the SBOM under `META-INF/sbom` in the generated jar or war file. . Adds the `Sbom-Format` and `Sbom-Location` to the manifest of the jar or war file. + + + +[[reacting-to-other-plugins.protobuf]] +== Reacting to the Protobuf Plugin + +When the {url-protobuf-docs-gradle-plugin}[Protobuf plugin] is applied to a project, the Spring Boot plugin: + +. Configures `protoc` to use the artifact `com.google.protobuf:protoc`, aligning its version with that of Protobuf dependencies on the runtime classpath. +. Configures the `grpc` plugin to use the artifact `io.grpc:protoc-gen-grpc-java`, aligning its version with that of gRPC dependencies on the runtime classpath. +. Configures the `grpc` plugin of all generate proto tasks with the option `@generated=omit`. diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java new file mode 100644 index 000000000000..1ce24affa7f9 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java @@ -0,0 +1,157 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.plugin; + +import java.util.List; +import java.util.Optional; + +import com.google.protobuf.gradle.ExecutableLocator; +import com.google.protobuf.gradle.GenerateProtoTask; +import com.google.protobuf.gradle.GenerateProtoTask.PluginOptions; +import com.google.protobuf.gradle.ProtobufExtension; +import com.google.protobuf.gradle.ProtobufExtension.GenerateProtoTaskCollection; +import com.google.protobuf.gradle.ProtobufPlugin; +import org.gradle.api.Action; +import org.gradle.api.NamedDomainObjectContainer; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.DependencyResolveDetails; +import org.gradle.api.artifacts.ModuleVersionSelector; +import org.gradle.api.artifacts.component.ModuleComponentIdentifier; +import org.gradle.api.artifacts.result.DependencyResult; +import org.gradle.api.artifacts.result.ResolvedComponentResult; +import org.jspecify.annotations.Nullable; + +/** + * {@link Action} that is executed in response to the {@link ProtobufPlugin} being + * applied. + * + * @author Andy Wilkinson + */ +final class ProtobufPluginAction implements PluginApplicationAction { + + private static final Dependency protocDependency = new Dependency("com.google.protobuf", "protoc"); + + private static final Dependency grpcDependency = new Dependency("io.grpc", "protoc-gen-grpc-java"); + + private static final List versionAlignment = List.of( + protocDependency.alignVersionWith("com.google.protobuf", "protobuf-java-util"), + grpcDependency.alignVersionWith("io.grpc", "grpc-util")); + + @Override + public Class> getPluginClass() { + return ProtobufPlugin.class; + } + + @Override + public void execute(Project project) { + ProtobufExtension protobuf = project.getExtensions().getByType(ProtobufExtension.class); + protobuf.protoc(this::configureProtoc); + protobuf.plugins(this::configurePlugins); + protobuf.generateProtoTasks(this::configureGenerateProtoTasks); + project.getConfigurations() + .named(this::isProtobufToolsLocator) + .configureEach((configuration) -> configureProtobufToolsLocator(project, configuration)); + } + + private void configureProtoc(ExecutableLocator protoc) { + protoc.setArtifact(protocDependency.asDependencySpec()); + } + + private ExecutableLocator configurePlugins(NamedDomainObjectContainer plugins) { + return plugins.create("grpc", (grpc) -> grpc.setArtifact(grpcDependency.asDependencySpec())); + } + + private void configureGenerateProtoTasks(GenerateProtoTaskCollection tasks) { + tasks.all().configureEach(this::configureGenerateProtoTask); + } + + private void configureGenerateProtoTask(GenerateProtoTask task) { + task.plugins((plugins) -> plugins.create("grpc", this::configureGrpcOptions)); + } + + private void configureGrpcOptions(PluginOptions grpc) { + grpc.option("@generated=omit"); + } + + private boolean isProtobufToolsLocator(String name) { + return name.startsWith("protobufToolsLocator_"); + } + + private void configureProtobufToolsLocator(Project project, Configuration configuration) { + configuration.getResolutionStrategy().eachDependency((details) -> { + VersionAlignment versionAlignment = versionAlignmentFor(details); + if (versionAlignment != null) { + versionAlignment.applyIfPossible(project, details); + } + }); + } + + private @Nullable VersionAlignment versionAlignmentFor(DependencyResolveDetails details) { + if ("null".equals(details.getRequested().getVersion())) { + for (VersionAlignment alignment : versionAlignment) { + if (alignment.accepts(details)) { + return alignment; + } + } + } + return null; + } + + private record Dependency(String group, String module) { + + private VersionAlignment alignVersionWith(String group, String module) { + return new VersionAlignment(this, new Dependency(group, module)); + } + + private String asDependencySpec() { + return this.group + ":" + this.module; + } + + } + + private record VersionAlignment(Dependency target, Dependency source) { + + void applyIfPossible(Project project, DependencyResolveDetails details) { + versionFromRuntimeClasspath(project, source()).ifPresent(details::useVersion); + } + + boolean accepts(DependencyResolveDetails details) { + ModuleVersionSelector requested = details.getRequested(); + return target().group().equals(requested.getGroup()) && target().module().equals(requested.getName()); + } + + private Optional versionFromRuntimeClasspath(Project project, Dependency source) { + return project.getConfigurations() + .getByName("runtimeClasspath") + .getIncoming() + .getResolutionResult() + .getAllDependencies() + .stream() + .map(DependencyResult::getFrom) + .map(ResolvedComponentResult::getId) + .filter(ModuleComponentIdentifier.class::isInstance) + .map(ModuleComponentIdentifier.class::cast) + .filter((id) -> id.getGroup().equals(source.group()) && id.getModule().equals(source.module())) + .map(ModuleComponentIdentifier::getVersion) + .findFirst(); + } + + } + +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java index 9c9d9e026c31..48b07bc2fff0 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java @@ -148,7 +148,7 @@ private void registerPluginActions(Project project, Configuration bootArchives) List actions = Arrays.asList(new JavaPluginAction(singlePublishedArtifact), new WarPluginAction(singlePublishedArtifact), new DependencyManagementPluginAction(), new ApplicationPluginAction(), new KotlinPluginAction(), new NativeImagePluginAction(), - new CyclonedxPluginAction()); + new CyclonedxPluginAction(), new ProtobufPluginAction()); for (PluginApplicationAction action : actions) { withPluginClassOfAction(action, (pluginClass) -> project.getPlugins().withType(pluginClass, (plugin) -> action.execute(project))); diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.java new file mode 100644 index 000000000000..1bec7cf1c885 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.plugin; + +import org.junit.jupiter.api.TestTemplate; + +import org.springframework.boot.gradle.junit.GradleCompatibility; +import org.springframework.boot.testsupport.gradle.testkit.GradleBuild; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ProtobufPluginAction}. + * + * @author Andy Wilkinson + */ +@GradleCompatibility +class ProtobufPluginActionIntegrationTests { + + @SuppressWarnings("NullAway.Init") + GradleBuild gradleBuild; + + @TestTemplate + void configuresProtocArtifact() { + assertThat(this.gradleBuild.build("protocArtifact").getOutput()) + .contains("protoc artifact: 'com.google.protobuf:protoc'"); + } + + @TestTemplate + void configuresGrpcPlugin() { + assertThat(this.gradleBuild.build("grpcPlugin").getOutput()) + .contains("grpc plugin artifact: 'io.grpc:protoc-gen-grpc-java'"); + } + + @TestTemplate + void configuresGenerateProtoTasksToOmitGenerated() { + assertThat(this.gradleBuild.build("generateProtoTasksGrpcPluginOptions").getOutput()) + .contains("generateProto: [[@generated=omit]]") + .contains("generateTestProto: [[@generated=omit]]"); + } + + @TestTemplate + void alignsVersionOfProtocDependency() { + assertThat(this.gradleBuild.build("dependencies", "--configuration", "protobufToolsLocator_protoc").getOutput()) + .contains("com.google.protobuf:protoc:null -> 4.34.0"); + } + + @TestTemplate + void alignsVersionOfGrpcDependency() { + assertThat(this.gradleBuild.build("dependencies", "--configuration", "protobufToolsLocator_grpc").getOutput()) + .contains("io.grpc:protoc-gen-grpc-java:null -> 1.79.0"); + } + + @TestTemplate + void usesVersionOfProtocDependencyWhenSpecified() { + assertThat(this.gradleBuild.build("dependencies", "--configuration", "protobufToolsLocator_protoc").getOutput()) + .contains("com.google.protobuf:protoc:4.33.5"); + } + + @TestTemplate + void usesVersionOfGrpcPluginDependencyWhenSpecified() { + assertThat(this.gradleBuild.build("dependencies", "--configuration", "protobufToolsLocator_grpc").getOutput()) + .contains("io.grpc:protoc-gen-grpc-java:1.78.0"); + + } + +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java index 1e7785c35650..d11755d68d68 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java @@ -22,6 +22,7 @@ import java.util.List; import com.fasterxml.jackson.annotation.JsonView; +import com.google.protobuf.gradle.ProtobufPlugin; import com.sun.jna.Platform; import io.spring.gradle.dependencymanagement.DependencyManagementPlugin; import org.antlr.v4.runtime.Lexer; @@ -124,6 +125,10 @@ private List pluginClasspath() { classpath.add(new File(pathOfJarContaining("org.codehaus.plexus.util.xml.pull.XmlPullParserException"))); classpath.add(new File(pathOfJarContaining("org.codehaus.stax2.ri.Stax2WriterAdapter"))); classpath.add(new File(pathOfJarContaining("org.cyclonedx.model.ExternalReference"))); + // Protobuf dependencies + classpath.add(new File(pathOfJarContaining(ProtobufPlugin.class))); + classpath.add(new File(pathOfJarContaining("com.google.gradle.osdetector.OsDetectorPlugin"))); + classpath.add(new File(pathOfJarContaining("kr.motd.maven.os.FileOperationProvider"))); return classpath; } diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests-usesVersionOfGrpcPluginDependencyWhenSpecified.gradle b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests-usesVersionOfGrpcPluginDependencyWhenSpecified.gradle new file mode 100644 index 000000000000..d6b113dc4feb --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests-usesVersionOfGrpcPluginDependencyWhenSpecified.gradle @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +apply plugin: 'com.google.protobuf' + +group = 'com.example' +version = '0.0.1' + +repositories { + mavenCentral() +} + +protobuf { + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:1.78.0" + } + } +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests-usesVersionOfProtocDependencyWhenSpecified.gradle b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests-usesVersionOfProtocDependencyWhenSpecified.gradle new file mode 100644 index 000000000000..f5fea2ed5954 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests-usesVersionOfProtocDependencyWhenSpecified.gradle @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +apply plugin: 'com.google.protobuf' + +group = 'com.example' +version = '0.0.1' + +repositories { + mavenCentral() +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:4.33.5" + } +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle new file mode 100644 index 000000000000..103a6ed72d48 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle @@ -0,0 +1,63 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +apply plugin: 'com.google.protobuf' + +group = 'com.example' +version = '0.0.1' + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.google.protobuf:protobuf-java-util:4.34.0") + implementation("io.grpc:grpc-util:1.79.0") +} + +tasks.register("protocArtifact") { + doFirst { + protobuf { + protoc { + println "protoc artifact: '$artifact'" + } + } + } +} + +tasks.register("grpcPlugin") { + doFirst { + protobuf { + plugins { + grpc { + println "grpc plugin artifact: '$artifact'" + } + } + } + } +} + +tasks.register("generateProtoTasksGrpcPluginOptions") { + doFirst { + tasks.withType(com.google.protobuf.gradle.GenerateProtoTask).each { println "${it.name}: ${it.pluginsForCaching.collect { it.options }}" } + } +} + diff --git a/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties b/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties index e1fef5f204ff..f64a46c277cc 100644 --- a/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties +++ b/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties @@ -35,6 +35,7 @@ url-native-build-tools-docs-gradle-plugin={url-native-build-tools-docs}/gradle-p url-native-build-tools-docs-maven-plugin={url-native-build-tools-docs}/maven-plugin.html url-paketo-docs=https://paketo.io/docs url-paketo-docs-java-buildpack={url-paketo-docs}/buildpacks/language-family-buildpacks/java +url-protobuf-docs-gradle-plugin=https://github.com/google/protobuf-gradle-plugin url-pulsar-client-api-javadoc=https://javadoc.io/doc/org.apache.pulsar/pulsar-client-api/{version-pulsar-client-api} url-spring-boot-for-apache-geode-docs=https://docs.spring.io/spring-boot-data-geode-build/2.0.x/reference/html5 url-spring-boot-for-apache-geode-site=https://github.com/spring-projects/spring-boot-data-geode diff --git a/platform/spring-boot-internal-dependencies/build.gradle b/platform/spring-boot-internal-dependencies/build.gradle index dde747796465..06f29d39e9c1 100644 --- a/platform/spring-boot-internal-dependencies/build.gradle +++ b/platform/spring-boot-internal-dependencies/build.gradle @@ -219,6 +219,13 @@ bom { ] } } + library("Protobuf Gradle Plugin", "${protobufGradlePluginVersion}") { + group("com.google.protobuf") { + modules = [ + "protobuf-gradle-plugin" + ] + } + } library("Simple JNDI", "0.25.0") { group("com.github.h-thurow") { modules = [ From e61bb6df5bedde613e58bdc07c956f60a4d501a7 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Mon, 9 Mar 2026 12:37:30 -0700 Subject: [PATCH 383/930] Add Spring gRPC server support Add support for Spring gRPC server applications. Closes gh-49044 Co-authored-by: Phillip Webb --- documentation/spring-boot-docs/build.gradle | 1 + .../build.gradle | 3 + module/spring-boot-grpc-server/build.gradle | 52 ++ .../grpc/server/GrpcServletRegistration.java | 116 +++++ ...ConditionalOnGrpcServerFactoryEnabled.java | 40 ++ ...ConditionalOnMissingNetworkGrpcServer.java | 47 ++ .../GrpcServerAutoConfiguration.java | 107 ++++ .../GrpcServerBuilderCustomizers.java | 89 ++++ .../GrpcServerCodecConfiguration.java | 80 +++ .../GrpcServerExecutorProvider.java | 36 ++ .../GrpcServerFactoryCustomizer.java | 37 ++ .../autoconfigure/GrpcServerProperties.java | 452 +++++++++++++++++ .../GrpcServerServicesAutoConfiguration.java | 58 +++ .../InProcessGrpcServerConfiguration.java | 71 +++ .../MissingNetworkGrpcServerCondition.java | 47 ++ .../server/autoconfigure/NettyAddress.java | 94 ++++ .../NettyGrpcServerConfiguration.java | 74 +++ .../PropertiesServerBuilderCustomizer.java | 89 ++++ .../autoconfigure/ServerCredentials.java | 66 +++ .../ServletGrpcServerConfiguration.java | 51 ++ .../ShadedNettyGrpcServerConfiguration.java | 74 +++ .../server/autoconfigure/package-info.java | 23 + .../boot/grpc/server/package-info.java | 23 + ...itional-spring-configuration-metadata.json | 29 ++ .../main/resources/META-INF/spring.factories | 2 + ...ot.autoconfigure.AutoConfiguration.imports | 2 + .../server/GrpcServletRegistrationTests.java | 152 ++++++ .../GrpcServerAutoConfigurationTests.java | 465 ++++++++++++++++++ .../GrpcServerBuilderCustomizersTests.java | 155 ++++++ .../GrpcServerCodecConfigurationTests.java | 92 ++++ .../GrpcServerPropertiesTests.java | 169 +++++++ ...cServerServicesAutoConfigurationTests.java | 104 ++++ .../autoconfigure/NettyAddressTests.java | 115 +++++ ...ropertiesServerBuilderCustomizerTests.java | 98 ++++ .../autoconfigure/ServerCredentialsTests.java | 138 ++++++ .../boot/grpc/server/autoconfigure/test.jks | Bin 0 -> 1276 bytes .../spring-boot-dependencies/build.gradle | 2 + settings.gradle | 5 + .../build.gradle | 67 +++ ...GrpcServerNettyShadedApplicationTests.java | 76 +++ .../HelloWorldService.java | 68 +++ ...ampleGrpcServerNettyShadedApplication.java | 29 ++ .../grpcservernettyshaded/package-info.java | 20 + .../src/main/proto/hello.proto | 17 + .../build.gradle | 69 +++ ...mpleGrpcServerServletApplicationTests.java | 57 +++ .../grpcserverservlet/HelloWorldService.java | 68 +++ .../SampleGrpcServerServletApplication.java | 29 ++ .../grpcserverservlet/package-info.java | 20 + .../src/main/proto/hello.proto | 17 + .../src/main/resources/application.properties | 1 + .../build.gradle | 64 +++ .../SampleGrpcServerApplicationTests.java | 76 +++ .../grpcserver/HelloWorldService.java | 68 +++ .../SampleGrpcServerApplication.java | 29 ++ .../smoketest/grpcserver/package-info.java | 20 + .../src/main/proto/hello.proto | 17 + .../build.gradle | 28 ++ 58 files changed, 4098 insertions(+) create mode 100644 module/spring-boot-grpc-server/build.gradle create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/GrpcServletRegistration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerFactoryEnabled.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnMissingNetworkGrpcServer.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerBuilderCustomizers.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerCodecConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerServicesAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/InProcessGrpcServerConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/MissingNetworkGrpcServerCondition.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddress.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyGrpcServerConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizer.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerCredentials.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletGrpcServerConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ShadedNettyGrpcServerConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/package-info.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/package-info.java create mode 100644 module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 module/spring-boot-grpc-server/src/main/resources/META-INF/spring.factories create mode 100644 module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/GrpcServletRegistrationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerBuilderCustomizersTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerCodecConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerServicesAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddressTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizerTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerCredentialsTests.java create mode 100644 module/spring-boot-grpc-server/src/test/resources/org/springframework/boot/grpc/server/autoconfigure/test.jks create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/dockerTest/java/smoketest/grpcservernettyshaded/SampleGrpcServerNettyShadedApplicationTests.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/java/smoketest/grpcservernettyshaded/HelloWorldService.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/java/smoketest/grpcservernettyshaded/SampleGrpcServerNettyShadedApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/java/smoketest/grpcservernettyshaded/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/proto/hello.proto create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-servlet/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/dockerTest/java/smoketest/grpcserverservlet/SampleGrpcServerServletApplicationTests.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/java/smoketest/grpcserverservlet/HelloWorldService.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/java/smoketest/grpcserverservlet/SampleGrpcServerServletApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/java/smoketest/grpcserverservlet/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/proto/hello.proto create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/resources/application.properties create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server/src/dockerTest/java/smoketest/grpcserver/SampleGrpcServerApplicationTests.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/HelloWorldService.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/SampleGrpcServerApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server/src/main/proto/hello.proto create mode 100644 starter/spring-boot-starter-grpc-server/build.gradle diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index e89fb1017b0e..48fc323e992f 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -110,6 +110,7 @@ dependencies { implementation(project(path: ":module:spring-boot-data-redis-test")) implementation(project(path: ":module:spring-boot-devtools")) implementation(project(path: ":module:spring-boot-graphql-test")) + implementation(project(path: ":module:spring-boot-grpc-server")) implementation(project(path: ":module:spring-boot-health")) implementation(project(path: ":module:spring-boot-hibernate")) implementation(project(path: ":module:spring-boot-http-converter")) diff --git a/module/spring-boot-autoconfigure-classic-modules/build.gradle b/module/spring-boot-autoconfigure-classic-modules/build.gradle index 3075fd4970f1..b1050d6f593c 100644 --- a/module/spring-boot-autoconfigure-classic-modules/build.gradle +++ b/module/spring-boot-autoconfigure-classic-modules/build.gradle @@ -97,6 +97,9 @@ dependencies { api(project(":module:spring-boot-graphql")) { transitive = false } + api(project(":module:spring-boot-grpc-server")) { + transitive = false + } api(project(":module:spring-boot-groovy-templates")) { transitive = false } diff --git a/module/spring-boot-grpc-server/build.gradle b/module/spring-boot-grpc-server/build.gradle new file mode 100644 index 000000000000..cd26fcf32e57 --- /dev/null +++ b/module/spring-boot-grpc-server/build.gradle @@ -0,0 +1,52 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java-library" + id "org.springframework.boot.auto-configuration" + id "org.springframework.boot.configuration-properties" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot gRPC Server" + +dependencies { + api(project(":core:spring-boot")) + api("org.springframework.grpc:spring-grpc-core") + + optional(project(":core:spring-boot-autoconfigure")) + optional("io.grpc:grpc-servlet-jakarta") + optional("io.grpc:grpc-services") + optional("io.grpc:grpc-netty") + optional("io.grpc:grpc-netty-shaded") + optional("io.grpc:grpc-inprocess") + optional("io.grpc:grpc-kotlin-stub") { + exclude group: "javax.annotation", module: "javax.annotation-api" + } + optional("jakarta.servlet:jakarta.servlet-api") + + testImplementation(project(":core:spring-boot-test")) + testImplementation(project(":test-support:spring-boot-test-support")) + testImplementation(testFixtures(project(":core:spring-boot-autoconfigure"))) + testImplementation("org.springframework:spring-web") + + testRuntimeOnly("ch.qos.logback:logback-classic") +} + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/GrpcServletRegistration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/GrpcServletRegistration.java new file mode 100644 index 000000000000..1ddbbaf9d9b3 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/GrpcServletRegistration.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import io.grpc.ServerServiceDefinition; +import io.grpc.ServiceDescriptor; +import io.grpc.servlet.jakarta.GrpcServlet; +import io.grpc.servlet.jakarta.ServletServerBuilder; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRegistration.Dynamic; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.web.servlet.DynamicRegistrationBean; +import org.springframework.core.log.LogMessage; +import org.springframework.grpc.server.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.grpc.server.service.GrpcServiceSpec; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * {@link DynamicRegistrationBean} that can be used to register a {@link GrpcServlet}. + * + * @author David Syer + * @author Chris Bono + * @author Toshiaki Maki + * @author Phillip Webb + * @since 4.0.0 + */ +public class GrpcServletRegistration extends DynamicRegistrationBean { + + private static Log logger = LogFactory.getLog(GrpcServletRegistration.class); + + private final GrpcServlet servlet; + + private final String[] urlMappings; + + /** + * Create a new {@link GrpcServletRegistration} instance. + * @param serviceDiscoverer the gRPC service discoverer + * @param serviceConfigurer the gRPC service configurer + */ + public GrpcServletRegistration(GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer) { + this(serviceDiscoverer, serviceConfigurer, null); + } + + /** + * Create a new {@link GrpcServletRegistration} instance. + * @param serviceDiscoverer the gRPC service discoverer + * @param serviceConfigurer the gRPC service configurer + * @param serverBuilderCustomizer an optional customizer to configure the + * {@link ServletServerBuilder} + */ + public GrpcServletRegistration(GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer, + @Nullable Consumer serverBuilderCustomizer) { + Assert.notNull(serviceDiscoverer, "'serviceDiscoverer' must not be null"); + Assert.notNull(serviceConfigurer, "'serviceConfigurer' must not be null"); + ServletServerBuilder builder = new ServletServerBuilder(); + List urlMappings = new ArrayList<>(); + for (GrpcServiceSpec spec : serviceDiscoverer.findServices()) { + ServiceDescriptor descriptor = spec.service().bindService().getServiceDescriptor(); + logger.info(LogMessage.format("Registering servlet gRPC service: %s", descriptor.getName())); + urlMappings.add("/" + descriptor.getName() + "/*"); + ServerServiceDefinition definition = serviceConfigurer.configure(spec, null); + builder.addService(definition); + } + if (serverBuilderCustomizer != null) { + serverBuilderCustomizer.accept(builder); + } + this.servlet = builder.buildServlet(); + this.urlMappings = urlMappings.toArray(String[]::new); + } + + @Override + protected Dynamic addRegistration(String description, ServletContext servletContext) { + return servletContext.addServlet(getName(), this.servlet); + } + + @Override + protected void configure(Dynamic registration) { + super.configure(registration); + if (!ObjectUtils.isEmpty(this.urlMappings)) { + registration.addMapping(this.urlMappings); + } + } + + @Override + protected String getDescription() { + return getName(); + } + + private String getName() { + return getOrDeduceName(this.servlet); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerFactoryEnabled.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerFactoryEnabled.java new file mode 100644 index 000000000000..6a166bd13e3b --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerFactoryEnabled.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +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.condition.ConditionalOnBooleanProperty; +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that matches when + * {@code spring.grpc.server.factory.enabled} is {@code true} or missing. + * + * @author Phillip Webb + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@ConditionalOnBooleanProperty(name = "spring.grpc.server.factory.enabled", matchIfMissing = true) +@interface ConditionalOnGrpcServerFactoryEnabled { + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnMissingNetworkGrpcServer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnMissingNetworkGrpcServer.java new file mode 100644 index 000000000000..fec5c84dfaf9 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnMissingNetworkGrpcServer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +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.grpc.server.GrpcServletRegistration; +import org.springframework.context.annotation.Conditional; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.InProcessGrpcServerFactory; + +/** + * {@link Conditional @Conditional} that matches when no network gRPC server is found. + * Concretely: + *
      + *
    • There are no {@link GrpcServletRegistration} beans.
    • + *
    • There are no {@link GrpcServerFactory} beans (ignoring + * {@link InProcessGrpcServerFactory} beans)
    • + *
    + * + * @author Phillip Webb + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@Conditional(MissingNetworkGrpcServerCondition.class) +@interface ConditionalOnMissingNetworkGrpcServer { + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java new file mode 100644 index 000000000000..c4b872c2a10f --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import java.util.List; + +import io.grpc.BindableService; +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.Grpc; + +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.ConditionalOnBooleanProperty; +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.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.grpc.server.exception.CompositeGrpcExceptionHandler; +import org.springframework.grpc.server.exception.GrpcExceptionHandler; +import org.springframework.grpc.server.exception.GrpcExceptionHandlerInterceptor; +import org.springframework.grpc.server.exception.ReactiveStubBeanDefinitionRegistrar; +import org.springframework.grpc.server.service.DefaultGrpcServiceConfigurer; +import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer; +import org.springframework.grpc.server.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Spring gRPC server-side + * components. + * + * @author David Syer + * @author Chris Bono + * @author Phillip Webb + * @since 4.1.0 + */ +@AutoConfiguration +@ConditionalOnClass({ GrpcServerFactory.class, Grpc.class }) +@ConditionalOnBean(BindableService.class) +@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true) +@EnableConfigurationProperties(GrpcServerProperties.class) +@Import({ GrpcServerCodecConfiguration.class, ServletGrpcServerConfiguration.class, + ShadedNettyGrpcServerConfiguration.class, NettyGrpcServerConfiguration.class, + InProcessGrpcServerConfiguration.class }) +public final class GrpcServerAutoConfiguration { + + @Bean + GrpcServerBuilderCustomizers grpcServerBuilderCustomizers(GrpcServerProperties grpcServerProperties, + ObjectProvider compressorRegistry, + ObjectProvider decompressorRegistry, + ObjectProvider executorProvider, + ObjectProvider> customizers) { + return new GrpcServerBuilderCustomizers(grpcServerProperties, compressorRegistry, decompressorRegistry, + executorProvider, customizers); + } + + @Bean + @ConditionalOnMissingBean(GrpcServiceConfigurer.class) + DefaultGrpcServiceConfigurer grpcServiceConfigurer(ApplicationContext applicationContext) { + return new DefaultGrpcServiceConfigurer(applicationContext); + } + + @Bean + @ConditionalOnMissingBean(GrpcServiceDiscoverer.class) + DefaultGrpcServiceDiscoverer grpcServiceDiscoverer(ApplicationContext applicationContext) { + return new DefaultGrpcServiceDiscoverer(applicationContext); + } + + @Bean + @GlobalServerInterceptor + @ConditionalOnMissingBean + GrpcExceptionHandlerInterceptor globalExceptionHandlerInterceptor(List exceptionHandlers) { + CompositeGrpcExceptionHandler compositeHandler = new CompositeGrpcExceptionHandler( + exceptionHandlers.toArray(GrpcExceptionHandler[]::new)); + return new GrpcExceptionHandlerInterceptor(compositeHandler); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = "com.salesforce.reactivegrpc.common.Function") + @Import(ReactiveStubBeanDefinitionRegistrar.class) + static class ReactiveStubConfiguration { + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerBuilderCustomizers.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerBuilderCustomizers.java new file mode 100644 index 000000000000..5f45bff14de2 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerBuilderCustomizers.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.ServerBuilder; +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.util.LambdaSafe; +import org.springframework.grpc.server.ServerBuilderCustomizer; + +/** + * Invokes the customizations to a {@link ServerBuilder} based on the provided beans. + * + * @author Chris Bono + * @author Phillip Webb + */ +class GrpcServerBuilderCustomizers { + + private final List> customizers; + + GrpcServerBuilderCustomizers(GrpcServerProperties grpcServerProperties, + ObjectProvider compressorRegistry, + ObjectProvider decompressorRegistry, + ObjectProvider executorProvider, + ObjectProvider> customizers) { + this(grpcServerProperties, compressorRegistry.getIfAvailable(), decompressorRegistry.getIfAvailable(), + executorProvider.getIfAvailable(), customizers.orderedStream().toList()); + } + + GrpcServerBuilderCustomizers(List> customizers) { + this(null, null, null, null, customizers); + } + + GrpcServerBuilderCustomizers(@Nullable GrpcServerProperties grpcServerProperties, + @Nullable CompressorRegistry compressorRegistry, @Nullable DecompressorRegistry decompressorRegistry, + @Nullable GrpcServerExecutorProvider executorProvider, + List> customizers) { + List> all = new ArrayList<>(); + addCustomizer(all, compressorRegistry, ServerBuilder::compressorRegistry); + addCustomizer(all, decompressorRegistry, ServerBuilder::decompressorRegistry); + addCustomizer(all, executorProvider, (builder, bean) -> builder.executor(bean.getExecutor())); + if (grpcServerProperties != null) { + all.add(new PropertiesServerBuilderCustomizer<>(grpcServerProperties)); + } + all.addAll(customizers); + this.customizers = List.copyOf(all); + } + + private static , T> void addCustomizer(List> customizers, + @Nullable T bean, BiConsumer action) { + if (bean != null) { + ServerBuilderCustomizer customizer = (builder) -> action.accept(builder, bean); + customizers.add(customizer); + } + } + + > List> forFactory() { + return List.of(this::apply); + } + + @SuppressWarnings("unchecked") + > void apply(T builder) { + LambdaSafe.callbacks(ServerBuilderCustomizer.class, this.customizers, builder) + .withLogger(GrpcServerBuilderCustomizers.class) + .invoke((customizer) -> customizer.customize(builder)); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerCodecConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerCodecConfiguration.java new file mode 100644 index 000000000000..abeed22da135 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerCodecConfiguration.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import java.util.List; + +import io.grpc.Compressor; +import io.grpc.CompressorRegistry; +import io.grpc.Decompressor; +import io.grpc.DecompressorRegistry; +import io.grpc.ServerBuilder; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * The configuration that contains all gRPC codec related beans. + * + * @author Andrei Lisa + */ +@Configuration(proxyBeanMethods = false) +class GrpcServerCodecConfiguration { + + /** + * The compressor registry that is set on the + * {@link ServerBuilder#compressorRegistry(CompressorRegistry) server builder} . + * @param compressors the compressors to use on the registry + * @return a new {@link CompressorRegistry#newEmptyInstance() registry} with the + * specified compressors or the {@link CompressorRegistry#getDefaultInstance() default + * registry} if no custom compressors are available in the application context. + */ + @Bean + @ConditionalOnMissingBean + CompressorRegistry grpcCompressorRegistry(List compressors) { + if (compressors.isEmpty()) { + return CompressorRegistry.getDefaultInstance(); + } + CompressorRegistry registry = CompressorRegistry.newEmptyInstance(); + compressors.forEach(registry::register); + return registry; + } + + /** + * The decompressor registry that is set on the + * {@link ServerBuilder#decompressorRegistry(DecompressorRegistry) server builder}. + * @param decompressors the decompressors to use on the registry + * @return a new {@link DecompressorRegistry#emptyInstance() registry} with the + * specified decompressors or the {@link DecompressorRegistry#getDefaultInstance() + * default registry} if no custom decompressors are available in the application + * context. + */ + @Bean + @ConditionalOnMissingBean + DecompressorRegistry grpcDecompressorRegistry(List decompressors) { + if (decompressors.isEmpty()) { + return DecompressorRegistry.getDefaultInstance(); + } + DecompressorRegistry registry = DecompressorRegistry.emptyInstance(); + for (Decompressor decompressor : decompressors) { + registry = registry.with(decompressor, false); + } + return registry; + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java new file mode 100644 index 000000000000..0c914bc73773 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import java.util.concurrent.Executor; + +/** + * Strategy interface to determine the {@link Executor} to use for the gRPC server. + * + * @author Chris Bono + * @since 4.1.0 + */ +@FunctionalInterface +public interface GrpcServerExecutorProvider { + + /** + * Returns a {@link Executor} for the gRPC server, if it needs to be customized. + * @return the executor to use for the gRPC server + */ + Executor getExecutor(); + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java new file mode 100644 index 000000000000..63e65678d28a --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import org.springframework.grpc.server.GrpcServerFactory; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link GrpcServerFactory server factory} before it is fully initialized. + * + * @author Chris Bono + * @since 4.1.0 + */ +@FunctionalInterface +public interface GrpcServerFactoryCustomizer { + + /** + * Customize the given {@link GrpcServerFactory}. + * @param serverFactory the server factory to customize + */ + void customize(GrpcServerFactory serverFactory); + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java new file mode 100644 index 000000000000..c3f97ea2a507 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java @@ -0,0 +1,452 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import java.net.InetAddress; +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import io.grpc.TlsServerCredentials.ClientAuth; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.convert.DataSizeUnit; +import org.springframework.boot.convert.DurationUnit; +import org.springframework.util.unit.DataSize; +import org.springframework.util.unit.DataUnit; + +/** + * {@link ConfigurationProperties Properties} for Spring gRPC servers. + * + * @author Chris Bono + * @author Phillip Webb + * @since 4.1.0 + */ +@ConfigurationProperties("spring.grpc.server") +public class GrpcServerProperties { + + /** + * Port on which the gRPC server should listen. Use '0' to bind to a dynamic port. + */ + private @Nullable Integer port; + + /** + * Network address to which the gRPC server should bind. + */ + private @Nullable InetAddress address; + + private final Shutdown shutdown = new Shutdown(); + + private final Inbound inbound = new Inbound(); + + private final Inprocess inprocess = new Inprocess(); + + private final Keepalive keepalive = new Keepalive(); + + private final Ssl ssl = new Ssl(); + + private final Netty netty = new Netty(); + + public @Nullable Integer getPort() { + return this.port; + } + + public void setPort(@Nullable Integer port) { + this.port = port; + } + + public @Nullable InetAddress getAddress() { + return this.address; + } + + public void setAddress(@Nullable InetAddress address) { + this.address = address; + } + + public Shutdown getShutdown() { + return this.shutdown; + } + + public Inbound getInbound() { + return this.inbound; + } + + public Inprocess getInprocess() { + return this.inprocess; + } + + public Keepalive getKeepalive() { + return this.keepalive; + } + + public Ssl getSsl() { + return this.ssl; + } + + public Netty getNetty() { + return this.netty; + } + + /** + * Server shutdown properties. + */ + public static class Shutdown { + + /** + * Maximum time to wait for the server to gracefully shutdown. When the value is + * negative, the server waits forever. When the value is 0, the server will force + * shutdown immediately. The default is 30 seconds. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration gracePeriod = Duration.ofSeconds(30); + + public Duration getGracePeriod() { + return this.gracePeriod; + } + + public void setGracePeriod(Duration gracePeriod) { + this.gracePeriod = gracePeriod; + } + + } + + /** + * In-bound properties. + */ + public static class Inbound { + + private final Message message = new Message(); + + private final Metadata metadata = new Metadata(); + + public Message getMessage() { + return this.message; + } + + public Metadata getMetadata() { + return this.metadata; + } + + /** + * In-bound message properties. + */ + public static class Message { + + /** + * Maximum message size allowed to be received by the server (default 4MiB). + */ + @DataSizeUnit(DataUnit.BYTES) + private DataSize maxSize = DataSize.ofBytes(4194304); + + public DataSize getMaxSize() { + return this.maxSize; + } + + public void setMaxSize(DataSize maxSize) { + this.maxSize = maxSize; + } + + } + + /** + * In-bound metadata properties. + */ + public static class Metadata { + + /** + * Maximum metadata size allowed to be received by the server (default 8KiB). + */ + @DataSizeUnit(DataUnit.BYTES) + private DataSize maxSize = DataSize.ofBytes(8192); + + public DataSize getMaxSize() { + return this.maxSize; + } + + public void setMaxSize(DataSize maxSize) { + this.maxSize = maxSize; + } + + } + + } + + /** + * In-process gRPC properties. + */ + public static class Inprocess { + + /** + * The name of the in-process server or null to not start the in-process server. + */ + private @Nullable String name; + + public @Nullable String getName() { + return this.name; + } + + public void setName(@Nullable String name) { + this.name = name; + } + + } + + /** + * Keep-alive properties. + */ + public static class Keepalive { + + /** + * Duration without read activity before sending a keep alive ping (default 2h). + */ + @DurationUnit(ChronoUnit.SECONDS) + private @Nullable Duration time = Duration.ofHours(2); + + /** + * Maximum time to wait for read activity after sending a keep alive ping. If + * sender does not receive an acknowledgment within this time, it will close the + * connection (default 20s). + */ + @DurationUnit(ChronoUnit.SECONDS) + private @Nullable Duration timeout = Duration.ofSeconds(20); + + private final Permit permit = new Permit(); + + private final Connection connection = new Connection(); + + public @Nullable Duration getTime() { + return this.time; + } + + public void setTime(@Nullable Duration time) { + this.time = time; + } + + public @Nullable Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(@Nullable Duration timeout) { + this.timeout = timeout; + } + + public Permit getPermit() { + return this.permit; + } + + public Connection getConnection() { + return this.connection; + } + + /** + * Keep-alive permit properties. + */ + public static class Permit { + + /** + * Maximum keep-alive time clients are permitted to configure (default 5m). + */ + @DurationUnit(ChronoUnit.SECONDS) + private @Nullable Duration time = Duration.ofMinutes(5); + + /** + * Whether clients are permitted to send keep alive pings when there are no + * outstanding RPCs on the connection (default false). + */ + private boolean withoutCalls; + + public @Nullable Duration getTime() { + return this.time; + } + + public void setTime(@Nullable Duration time) { + this.time = time; + } + + public boolean isWithoutCalls() { + return this.withoutCalls; + } + + public void setWithoutCalls(boolean withoutCalls) { + this.withoutCalls = withoutCalls; + } + + } + + /** + * Keep-alive connection properties. + */ + public static class Connection { + + /** + * Maximum time a connection can remain idle before being gracefully + * terminated (default infinite). + */ + @DurationUnit(ChronoUnit.SECONDS) + private @Nullable Duration maxIdleTime; + + /** + * Maximum time a connection may exist before being gracefully terminated + * (default infinite). + */ + @DurationUnit(ChronoUnit.SECONDS) + private @Nullable Duration maxAge; + + /** + * Maximum time for graceful connection termination (default infinite). + */ + @DurationUnit(ChronoUnit.SECONDS) + private @Nullable Duration gracePeriod = Duration.ofSeconds(30); + + public @Nullable Duration getMaxIdleTime() { + return this.maxIdleTime; + } + + public void setMaxIdleTime(@Nullable Duration maxIdleTime) { + this.maxIdleTime = maxIdleTime; + } + + public @Nullable Duration getMaxAge() { + return this.maxAge; + } + + public void setMaxAge(@Nullable Duration maxAge) { + this.maxAge = maxAge; + } + + public @Nullable Duration getGracePeriod() { + return this.gracePeriod; + } + + public void setGracePeriod(@Nullable Duration gracePeriod) { + this.gracePeriod = gracePeriod; + } + + } + + } + + /** + * SSL properties. + */ + public static class Ssl { + + /** + * Whether to enable SSL support. + */ + private @Nullable Boolean enabled; + + /** + * Client authentication mode. + */ + private ClientAuth clientAuth = ClientAuth.NONE; + + /** + * SSL bundle name. Should match a bundle configured in spring.ssl.bundle. + */ + private @Nullable String bundle; + + /** + * Flag to indicate that client authentication is secure (i.e. certificates are + * checked). Do not set this to false in production. + */ + private boolean secure = true; + + public @Nullable Boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(@Nullable Boolean enabled) { + this.enabled = enabled; + } + + public @Nullable String getBundle() { + return this.bundle; + } + + public void setBundle(@Nullable String bundle) { + this.bundle = bundle; + } + + public void setClientAuth(ClientAuth clientAuth) { + this.clientAuth = clientAuth; + } + + public ClientAuth getClientAuth() { + return this.clientAuth; + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + public boolean isSecure() { + return this.secure; + } + + } + + /** + * Netty server properties. + */ + public static class Netty { + + /** + * Transport mechanism used for Netty and Netty Shaded servers. If not specified + * will the appropriate transport will be picked based on the + * 'deomain-socket-path' or 'address/port'. + */ + private @Nullable Transport transport; + + /** + * Path of the domain socket that should be used. + */ + private @Nullable String domainSocketPath; + + public @Nullable Transport getTransport() { + return this.transport; + } + + public void setTransport(@Nullable Transport transport) { + this.transport = transport; + } + + public @Nullable String getDomainSocketPath() { + return this.domainSocketPath; + } + + public void setDomainSocketPath(@Nullable String domainSocketPath) { + this.domainSocketPath = domainSocketPath; + } + + public enum Transport { + + /** + * TCP transport. + */ + TCP, + + /** + * Domain socket transport. + */ + DOMAIN_SOCKET + + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerServicesAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerServicesAutoConfiguration.java new file mode 100644 index 000000000000..46fc58604eb4 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerServicesAutoConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import io.grpc.BindableService; +import io.grpc.protobuf.services.ProtoReflectionServiceV1; + +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.ConditionalOnBooleanProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.GrpcServerFactory; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC server services. + * + * @author Haris Zujo + * @author Dave Syer + * @author Chris Bono + * @author Andrey Litvitski + * @since 4.1.0 + */ +@AutoConfiguration(before = GrpcServerAutoConfiguration.class) +@ConditionalOnClass({ GrpcServerFactory.class, io.grpc.Grpc.class }) +@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true) +public final class GrpcServerServicesAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ProtoReflectionServiceV1.class) + @ConditionalOnBean(BindableService.class) + @ConditionalOnBooleanProperty(name = "spring.grpc.server.reflection.enabled", matchIfMissing = true) + static class GrpcServerReflectionServiceConfiguration { + + @Bean + BindableService grpcServerReflectionService() { + return ProtoReflectionServiceV1.newInstance(); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/InProcessGrpcServerConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/InProcessGrpcServerConfiguration.java new file mode 100644 index 000000000000..649853b821b2 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/InProcessGrpcServerConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +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.ConditionalOnProperty; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.InProcessGrpcServerFactory; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.util.Assert; + +/** + * {@link Configuration @Configuration} for an in-process gRPC server. + * + * @author David Syer + * @author Chris Bono + * @author Toshiaki Maki + * @author Phillip Webb + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(InProcessGrpcServerFactory.class) +@ConditionalOnProperty("spring.grpc.server.inprocess.name") +@ConditionalOnGrpcServerFactoryEnabled +class InProcessGrpcServerConfiguration { + + @Bean + InProcessGrpcServerFactory inProcessGrpcServerFactory(GrpcServerProperties properties, + GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer, + GrpcServerBuilderCustomizers grpcServerBuilderCustomizers, + ObjectProvider customizers) { + String inProcessName = properties.getInprocess().getName(); + Assert.state(inProcessName != null, "No inprocess name provided"); + InProcessGrpcServerFactory factory = new InProcessGrpcServerFactory(inProcessName, + grpcServerBuilderCustomizers.forFactory()); + customizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); + serviceDiscoverer.findServices() + .stream() + .map((spec) -> serviceConfigurer.configure(spec, factory)) + .forEach(factory::addService); + return factory; + } + + @Bean + @ConditionalOnBean(InProcessGrpcServerFactory.class) + @ConditionalOnMissingBean(name = "inProcessGrpcServerLifecycle") + GrpcServerLifecycle inProcessGrpcServerLifecycle(InProcessGrpcServerFactory factory, + GrpcServerProperties properties, ApplicationEventPublisher eventPublisher) { + return new GrpcServerLifecycle(factory, properties.getShutdown().getGracePeriod(), eventPublisher); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/MissingNetworkGrpcServerCondition.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/MissingNetworkGrpcServerCondition.java new file mode 100644 index 000000000000..8dbd39435136 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/MissingNetworkGrpcServerCondition.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import org.springframework.boot.autoconfigure.condition.AllNestedConditions; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.grpc.server.GrpcServletRegistration; +import org.springframework.context.annotation.Condition; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.InProcessGrpcServerFactory; + +/** + * {@link Condition} that matches when no network gRPC server is found. + * + * @author Phillip Webb + */ +class MissingNetworkGrpcServerCondition extends AllNestedConditions { + + MissingNetworkGrpcServerCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnMissingBean(GrpcServletRegistration.class) + static class MissingGrpcServletRegistrationBean { + + } + + @ConditionalOnMissingBean(value = GrpcServerFactory.class, ignored = InProcessGrpcServerFactory.class) + static class MissingGrpcFactoryBean { + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddress.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddress.java new file mode 100644 index 000000000000..4e66805002a4 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddress.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import java.net.InetAddress; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties.Netty.Transport; +import org.springframework.grpc.internal.GrpcUtils; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.util.StringUtils; + +/** + * Address {@link GrpcServerFactory} address. + * + * @author Phillip Webb + * @param transport the transport to use + * @param address the bind address + * @param port the listen port + * @param domainSocketPath the domain socket path + */ +record NettyAddress(@Nullable Transport transport, @Nullable InetAddress address, @Nullable Integer port, + @Nullable String domainSocketPath) { + + @Override + public final String toString() { + Transport transport = (this.transport != null) ? this.transport : deduceTransport(); + return switch (transport) { + case TCP -> tcpAddress(); + case DOMAIN_SOCKET -> domainSocketAddress(); + }; + } + + private Transport deduceTransport() { + MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> { + entries.put("spring.grpc.server.address", this.address); + entries.put("spring.grpc.server.netty.domain-socket-path", this.domainSocketPath); + }); + MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> { + entries.put("spring.grpc.server.port", this.port); + entries.put("spring.grpc.server.netty.domain-socket-path", this.domainSocketPath); + }); + if (this.address != null || this.port != null) { + return Transport.TCP; + } + if (this.domainSocketPath != null) { + return Transport.DOMAIN_SOCKET; + } + return Transport.TCP; + } + + private String tcpAddress() { + String address = (this.address != null) ? toString(this.address) : GrpcUtils.ANY_IP_ADDRESS; + int port = (this.port != null) ? this.port : GrpcUtils.DEFAULT_PORT; + return address + ":" + port; + } + + private String domainSocketAddress() { + if (!StringUtils.hasText(this.domainSocketPath)) { + throw new InvalidConfigurationPropertyValueException("spring.grpc.server.netty.domain-socket-path", + this.domainSocketPath, + "A path is required when spring.grpc.server.netty.transport is set to 'domain-socket'"); + } + return "unix:" + this.domainSocketPath; + } + + private static String toString(InetAddress address) { + String hostName = address.getHostName(); + return (hostName != null) ? hostName : address.getHostAddress(); + } + + static NettyAddress fromProperties(GrpcServerProperties properties) { + return new NettyAddress(properties.getNetty().getTransport(), properties.getAddress(), properties.getPort(), + properties.getNetty().getDomainSocketPath()); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyGrpcServerConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyGrpcServerConfiguration.java new file mode 100644 index 000000000000..a2901f3ee04f --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyGrpcServerConfiguration.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import io.grpc.netty.NettyServerBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.NettyGrpcServerFactory; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; + +/** + * {@link Configuration @Configuration} for a Netty gRPC server. + * + * @author David Syer + * @author Chris Bono + * @author Toshiaki Maki + * @author Phillip Webb + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(NettyServerBuilder.class) +@ConditionalOnMissingNetworkGrpcServer +@ConditionalOnGrpcServerFactoryEnabled +class NettyGrpcServerConfiguration { + + @Bean + NettyGrpcServerFactory nettyGrpcServerFactory(GrpcServerProperties properties, + GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer, + GrpcServerBuilderCustomizers grpcServerBuilderCustomizers, SslBundles bundles, + ObjectProvider customizers) { + NettyAddress address = NettyAddress.fromProperties(properties); + ServerCredentials credentials = ServerCredentials.get(properties.getSsl(), bundles, + InsecureTrustManagerFactory.INSTANCE); + NettyGrpcServerFactory factory = new NettyGrpcServerFactory(address.toString(), + grpcServerBuilderCustomizers.forFactory(), credentials.keyManagerFactory(), + credentials.trustManagerFactory(), credentials.clientAuth()); + customizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); + serviceDiscoverer.findServices() + .stream() + .map((spec) -> serviceConfigurer.configure(spec, factory)) + .forEach(factory::addService); + return factory; + } + + @Bean + @ConditionalOnMissingBean(name = "nettyGrpcServerLifecycle") + GrpcServerLifecycle nettyGrpcServerLifecycle(NettyGrpcServerFactory factory, GrpcServerProperties properties, + ApplicationEventPublisher eventPublisher) { + return new GrpcServerLifecycle(factory, properties.getShutdown().getGracePeriod(), eventPublisher); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizer.java new file mode 100644 index 000000000000..7e32d05a0f7a --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizer.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import io.grpc.ManagedChannelBuilder; +import io.grpc.ServerBuilder; + +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties.Inbound; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties.Keepalive; +import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.util.ClassUtils; +import org.springframework.util.unit.DataSize; + +/** + * {@link ServerBuilderCustomizer} that maps {@link GrpcServerProperties} to a + * {@link ManagedChannelBuilder}. + * + * @param the type of server builder + * @param properties the properties to map + * @author Chris Bono + * @author Phillip Webb + */ +record PropertiesServerBuilderCustomizer>( + GrpcServerProperties properties) implements ServerBuilderCustomizer { + + @Override + public void customize(T builder) { + mapInboundProperties(this.properties.getInbound(), builder); + if (supportsKeepAliveProperties(builder)) { + mapKeepaliveProperties(this.properties.getKeepalive(), builder); + } + } + + private void mapInboundProperties(Inbound properties, T builder) { + PropertyMapper map = PropertyMapper.get(); + map.from(properties.getMessage()::getMaxSize).asInt(DataSize::toBytes).to(builder::maxInboundMessageSize); + map.from(properties.getMetadata()::getMaxSize).asInt(DataSize::toBytes).to(builder::maxInboundMetadataSize); + } + + private void mapKeepaliveProperties(Keepalive properties, T builder) { + PropertyMapper map = PropertyMapper.get(); + map.from(properties::getTime).to(durationProperty(builder::keepAliveTime)); + map.from(properties::getTimeout).to(durationProperty(builder::keepAliveTimeout)); + map.from(properties.getConnection()::getMaxIdleTime).to(durationProperty(builder::maxConnectionIdle)); + map.from(properties.getConnection()::getMaxAge).to(durationProperty(builder::maxConnectionAge)); + map.from(properties.getConnection()::getGracePeriod).to(durationProperty(builder::maxConnectionAgeGrace)); + map.from(properties.getPermit()::getTime).to(durationProperty(builder::permitKeepAliveTime)); + map.from(properties.getPermit()::isWithoutCalls).to(builder::permitKeepAliveWithoutCalls); + } + + private Consumer durationProperty(BiConsumer setter) { + return (duration) -> setter.accept(duration.toNanos(), TimeUnit.NANOSECONDS); + } + + private boolean supportsKeepAliveProperties(T builder) { + return !isInstance("io.grpc.inprocess.InProcessServerBuilder", builder) + && !isInstance("io.grpc.servlet.jakarta.ServletServerBuilder", builder); + } + + private boolean isInstance(String className, T builder) { + try { + return ClassUtils.forName(className, builder.getClass().getClassLoader()).isInstance(builder); + } + catch (ClassNotFoundException | LinkageError ex) { + return false; + } + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerCredentials.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerCredentials.java new file mode 100644 index 000000000000..96354b22618f --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerCredentials.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; + +import io.grpc.TlsServerCredentials.ClientAuth; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.ssl.SslManagerBundle; +import org.springframework.util.Assert; + +/** + * Server credential details to use with gRPC servers. + * + * @author Chris Bono + * @author Phillip Webb + * @param keyManagerFactory the key manager factory to use or {@code null} + * @param trustManagerFactory the trust manager factory to use or {@code null} + * @param clientAuth the client auth to use + */ +record ServerCredentials(@Nullable KeyManagerFactory keyManagerFactory, + @Nullable TrustManagerFactory trustManagerFactory, ClientAuth clientAuth) { + + /** + * Return the credentials to use based on the given properties. + * @param properties the SSL properties + * @param bundles the SSL bundles + * @param insecureTrustManagerFactory the trust manager factory to use for insecure + * connections + * @return the server credentials to use + */ + static ServerCredentials get(GrpcServerProperties.Ssl properties, SslBundles bundles, + TrustManagerFactory insecureTrustManagerFactory) { + Boolean enabled = properties.getEnabled(); + String bundle = properties.getBundle(); + ClientAuth clientAuth = properties.getClientAuth(); + if (Boolean.FALSE.equals(enabled) || (enabled == null && bundle == null)) { + return new ServerCredentials(null, null, clientAuth); + } + Assert.state(bundle != null, + () -> "SSL bundle-name is requested when 'spring.grpc.server.ssl.enabled' is true"); + SslManagerBundle managers = bundles.getBundle(bundle).getManagers(); + KeyManagerFactory keyManagerFactory = managers.getKeyManagerFactory(); + TrustManagerFactory trustManagerFactory = (!properties.isSecure()) ? insecureTrustManagerFactory + : managers.getTrustManagerFactory(); + return new ServerCredentials(keyManagerFactory, trustManagerFactory, clientAuth); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletGrpcServerConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletGrpcServerConfiguration.java new file mode 100644 index 000000000000..95e6ab7e436d --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletGrpcServerConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import io.grpc.servlet.jakarta.GrpcServlet; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.grpc.server.GrpcServletRegistration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; + +/** + * {@link Configuration @Configuration} for a Servlet gRPC server. + * + * @author David Syer + * @author Chris Bono + * @author Toshiaki Maki + * @author Phillip Webb + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnClass(GrpcServlet.class) +@ConditionalOnMissingNetworkGrpcServer +@ConditionalOnBooleanProperty(name = "spring.grpc.server.servlet.enabled", matchIfMissing = true) +class ServletGrpcServerConfiguration { + + @Bean + GrpcServletRegistration grpcServletRegistration(GrpcServiceDiscoverer serviceDiscoverer, + GrpcServiceConfigurer serviceConfigurer, GrpcServerBuilderCustomizers grpcServerBuilderCustomizers) { + return new GrpcServletRegistration(serviceDiscoverer, serviceConfigurer, grpcServerBuilderCustomizers::apply); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ShadedNettyGrpcServerConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ShadedNettyGrpcServerConfiguration.java new file mode 100644 index 000000000000..6c8f836887a7 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ShadedNettyGrpcServerConfiguration.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.ShadedNettyGrpcServerFactory; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; + +/** + * {@link Configuration @Configuration} for a Shaded Netty gRPC server. + * + * @author David Syer + * @author Chris Bono + * @author Toshiaki Maki + * @author Phillip Webb + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(NettyServerBuilder.class) +@ConditionalOnMissingNetworkGrpcServer +@ConditionalOnGrpcServerFactoryEnabled +class ShadedNettyGrpcServerConfiguration { + + @Bean + ShadedNettyGrpcServerFactory shadedNettyGrpcServerFactory(GrpcServerProperties properties, + GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer, + GrpcServerBuilderCustomizers grpcServerBuilderCustomizers, SslBundles bundles, + ObjectProvider customizers) { + NettyAddress address = NettyAddress.fromProperties(properties); + ServerCredentials serverCredentials = ServerCredentials.get(properties.getSsl(), bundles, + InsecureTrustManagerFactory.INSTANCE); + ShadedNettyGrpcServerFactory factory = new ShadedNettyGrpcServerFactory(address.toString(), + grpcServerBuilderCustomizers.forFactory(), serverCredentials.keyManagerFactory(), + serverCredentials.trustManagerFactory(), serverCredentials.clientAuth()); + customizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); + serviceDiscoverer.findServices() + .stream() + .map((spec) -> serviceConfigurer.configure(spec, factory)) + .forEach(factory::addService); + return factory; + } + + @Bean + @ConditionalOnMissingBean(name = "shadedNettyGrpcServerLifecycle") + GrpcServerLifecycle shadedNettyGrpcServerLifecycle(ShadedNettyGrpcServerFactory factory, + GrpcServerProperties properties, ApplicationEventPublisher eventPublisher) { + return new GrpcServerLifecycle(factory, properties.getShutdown().getGracePeriod(), eventPublisher); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/package-info.java new file mode 100644 index 000000000000..70facb5a7102 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 gRPC server. + */ +@NullMarked +package org.springframework.boot.grpc.server.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/package-info.java new file mode 100644 index 000000000000..384fc973d818 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 gRPC server support classes. + */ +@NullMarked +package org.springframework.boot.grpc.server; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 000000000000..d99721c35be3 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,29 @@ +{ + "groups": [], + "properties": [ + { + "name": "spring.grpc.server.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable gRPC server auto-configuration.", + "defaultValue": true + }, + { + "name": "spring.grpc.server.factory.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable gRPC server factory bean auto-configuration.", + "defaultValue": true + }, + { + "name": "spring.grpc.server.reflection.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Reflection on the gRPC server.", + "defaultValue": true + }, + { + "name": "spring.grpc.server.servlet.enabled", + "type": "java.lang.Boolean", + "description": "Whether to use a servlet server in a servlet-based web application. When the value is false, a native gRPC server will be created as long as one is available, and it will listen on its own port. Should only be needed if the GrpcServlet is on the classpath", + "defaultValue": true + } + ] +} diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/spring.factories b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..3ae2a7f4b1dd --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer=\ +org.springframework.boot.grpc.server.autoconfigure.security.GrpcDisableCsrfHttpConfigurer diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000000..b5ab23908422 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration +org.springframework.boot.grpc.server.autoconfigure.GrpcServerServicesAutoConfiguration diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/GrpcServletRegistrationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/GrpcServletRegistrationTests.java new file mode 100644 index 000000000000..765830cc7b7d --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/GrpcServletRegistrationTests.java @@ -0,0 +1,152 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import io.grpc.BindableService; +import io.grpc.ServerInterceptor; +import io.grpc.ServerServiceDefinition; +import io.grpc.servlet.jakarta.GrpcServlet; +import io.grpc.servlet.jakarta.ServletServerBuilder; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRegistration.Dynamic; +import org.junit.jupiter.api.Test; + +import org.springframework.grpc.server.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.grpc.server.service.GrpcServiceInfo; +import org.springframework.grpc.server.service.GrpcServiceSpec; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNoException; +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; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link GrpcServletRegistration}. + * + * @author Phillip Webb + */ +class GrpcServletRegistrationTests { + + private final GrpcServiceConfigurer serviceConfigurer = mock(); + + private final GrpcServiceDiscoverer serviceDiscoverer = mock(); + + @Test + @SuppressWarnings("NullAway") // Test null check + void createWhenServiceDiscovererIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new GrpcServletRegistration(null, this.serviceConfigurer)) + .withMessage("'serviceDiscoverer' must not be null"); + } + + @Test + @SuppressWarnings("NullAway") // Test null check + void createWhenServiceConfigurerIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new GrpcServletRegistration(this.serviceDiscoverer, null)) + .withMessage("'serviceConfigurer' must not be null"); + } + + @Test + void createWhenServerBuilderCustomizerIsNullDoesNotApplyCustomization() { + assertThatNoException() + .isThrownBy(() -> new GrpcServletRegistration(this.serviceDiscoverer, this.serviceConfigurer, null)); + } + + @Test + void createWhenServerBuilderCustomizerIsNotNullAppliesCustomization() { + Consumer serverBuilderCustomizer = mock(); + new GrpcServletRegistration(this.serviceDiscoverer, this.serviceConfigurer, serverBuilderCustomizer); + then(serverBuilderCustomizer).should().accept(any(ServletServerBuilder.class)); + } + + @Test + void addRegistrationAddsBuiltServlet() { + GrpcServletRegistration registration = new GrpcServletRegistration(this.serviceDiscoverer, + this.serviceConfigurer); + ServletContext servletContext = mock(); + Dynamic result = mock(); + given(servletContext.addServlet(eq("grpcServlet"), any(GrpcServlet.class))).willReturn(result); + assertThat(registration.addRegistration("test", servletContext)).isEqualTo(result); + } + + @Test + void onStartupWhenHasServicesRegistersAndAddsUrlMappingsBasedOnDescriptorName() throws ServletException { + BindableService service1 = mock(BindableService.class); + ServerServiceDefinition serviceDefinition1 = ServerServiceDefinition.builder("s1").build(); + given(service1.bindService()).willReturn(serviceDefinition1); + GrpcServiceInfo info1 = new GrpcServiceInfo(emptyServiceInterceptors(), new String[0], false); + BindableService service2 = mock(BindableService.class); + ServerServiceDefinition serviceDefinition2 = ServerServiceDefinition.builder("s2").build(); + given(service2.bindService()).willReturn(serviceDefinition2); + GrpcServiceInfo info2 = new GrpcServiceInfo(emptyServiceInterceptors(), new String[0], false); + List specs = new ArrayList<>(); + specs.add(new GrpcServiceSpec(service1, info1)); + specs.add(new GrpcServiceSpec(service2, info2)); + given(this.serviceDiscoverer.findServices()).willReturn(specs); + given(this.serviceConfigurer.configure(any(GrpcServiceSpec.class), eq(null))).willAnswer((invocation) -> { + GrpcServiceSpec spec = invocation.getArgument(0, GrpcServiceSpec.class); + return spec.service().bindService(); + }); + GrpcServletRegistration registration = new GrpcServletRegistration(this.serviceDiscoverer, + this.serviceConfigurer); + ServletContext servletContext = mock(ServletContext.class); + Dynamic result = mock(Dynamic.class); + given(servletContext.addServlet(eq("grpcServlet"), any(GrpcServlet.class))).willReturn(result); + registration.onStartup(servletContext); + then(result).should().addMapping("/s1/*", "/s2/*"); + } + + @Test + void onStartupWhenHasNoServicesDoesNotAddUrlMappings() throws ServletException { + given(this.serviceDiscoverer.findServices()).willReturn(Collections.emptyList()); + given(this.serviceConfigurer.configure(any(GrpcServiceSpec.class), eq(null))).willAnswer((invocation) -> { + GrpcServiceSpec spec = invocation.getArgument(0, GrpcServiceSpec.class); + return spec.service().bindService(); + }); + GrpcServletRegistration registration = new GrpcServletRegistration(this.serviceDiscoverer, + this.serviceConfigurer); + ServletContext servletContext = mock(ServletContext.class); + Dynamic result = mock(Dynamic.class); + given(servletContext.addServlet(eq("grpcServlet"), any(GrpcServlet.class))).willReturn(result); + registration.onStartup(servletContext); + then(result).should(never()).addMapping(); + } + + @Test + void getDescriptionReturnsDeducedServletName() { + GrpcServletRegistration registration = new GrpcServletRegistration(this.serviceDiscoverer, + this.serviceConfigurer); + assertThat(registration.getDescription()).isEqualTo("grpcServlet"); + } + + @SuppressWarnings("unchecked") + private Class[] emptyServiceInterceptors() { + return (Class[]) new Class[] {}; + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java new file mode 100644 index 000000000000..b0066cb0d19a --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java @@ -0,0 +1,465 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import java.util.concurrent.atomic.AtomicReference; + +import io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; +import io.grpc.ServiceDescriptor; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.internal.GrpcUtil; +import io.grpc.netty.NettyServerBuilder; +import io.grpc.servlet.jakarta.GrpcServlet; +import io.grpc.servlet.jakarta.ServletServerBuilder; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.grpc.server.GrpcServletRegistration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider; +import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.InProcessGrpcServerFactory; +import org.springframework.grpc.server.NettyGrpcServerFactory; +import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.grpc.server.ShadedNettyGrpcServerFactory; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.util.unit.DataSize; + +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; + +/** + * Tests fir {@link GrpcServerAutoConfiguration}. + * + * @author Chris Bono + * @author Andrey Litvitski + * @author Phillip Webb + */ +class GrpcServerAutoConfigurationTests { + + private static final AutoConfigurations autoConfigurations = AutoConfigurations + .of(GrpcServerAutoConfiguration.class, SslAutoConfiguration.class); + + private final BindableService service = mock(); + + private final ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(autoConfigurations) + .with(this::noOpLifecycleBeans) + .with(this::serviceBean); + + @BeforeEach + void setup() { + given(this.service.bindService()).willReturn(this.serviceDefinition); + } + + @Test + void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(BindableService.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class)); + } + + @Test + void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class)); + } + + @Test + void whenNoBindableServicesRegisteredAutoConfigurationIsSkipped() { + new ApplicationContextRunner().withConfiguration(autoConfigurations) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetFalseThenAutoConfigurationIsSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.server.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GrpcServerAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.server.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcServerAutoConfiguration.class)); + } + + @Test + void whenHasUserDefinedGrpcServiceDiscovererDoesNotAutoConfigureBean() { + GrpcServiceDiscoverer customGrpcServiceDiscoverer = mock(GrpcServiceDiscoverer.class); + new ApplicationContextRunner().withConfiguration(autoConfigurations) + .with(this::noOpLifecycleBeans) + .withBean("customGrpcServiceDiscoverer", GrpcServiceDiscoverer.class, () -> customGrpcServiceDiscoverer) + .withPropertyValues("spring.grpc.server.port=0") + .run((context) -> assertThat(context).getBean(GrpcServiceDiscoverer.class) + .isSameAs(customGrpcServiceDiscoverer)); + } + + @Test + void grpcServiceDiscovererAutoConfiguredAsExpected() { + new ApplicationContextRunner().withConfiguration(autoConfigurations) + .with(this::serviceBean) + .run((context) -> assertThat(context).getBean(GrpcServiceDiscoverer.class) + .isInstanceOf(DefaultGrpcServiceDiscoverer.class)); + } + + @Test + void serverBuilderCustomizersAutoConfiguredAsExpected() { + this.contextRunner.withUserConfiguration(ServerBuilderCustomizersConfig.class) + .run((context) -> assertThat(context).getBean(GrpcServerBuilderCustomizers.class) + .extracting("customizers", InstanceOfAssertFactories.list(ServerBuilderCustomizer.class)) + .contains(ServerBuilderCustomizersConfig.bar, ServerBuilderCustomizersConfig.foo)); + } + + @Test + void customizersAreAppliedToNettyServer() { + AtomicReference applied = new AtomicReference<>(); + ServerBuilderCustomizer customizer = applied::set; + this.contextRunner.withBean(ServerBuilderCustomizer.class, () -> customizer) + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .run((context) -> { + context.getBean(GrpcServerFactory.class).createServer(); + assertThat(applied.get()).isInstanceOf(NettyServerBuilder.class); + }); + } + + @Test + void customizersAreAppliedToShadedNettyServer() { + AtomicReference applied = new AtomicReference<>(); + ServerBuilderCustomizer customizer = applied::set; + this.contextRunner.withBean(ServerBuilderCustomizer.class, () -> customizer) + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class)) + .run((context) -> { + context.getBean(GrpcServerFactory.class).createServer(); + assertThat(applied.get()).isInstanceOf(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class); + }); + } + + @Test + void customizersAreAppliedToInProcessServer() { + AtomicReference applied = new AtomicReference<>(); + ServerBuilderCustomizer customizer = applied::set; + this.contextRunner.withBean(ServerBuilderCustomizer.class, () -> customizer) + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withPropertyValues("spring.grpc.server.inprocess.name=test") + .run((context) -> { + context.getBean(GrpcServerFactory.class).createServer(); + assertThat(applied.get()).isInstanceOf(InProcessServerBuilder.class); + }); + } + + @Test + void whenHasUserDefinedServerFactoryDoesNotAutoConfigureBean() { + GrpcServerFactory customServerFactory = mock(GrpcServerFactory.class); + this.contextRunner.withBean("customServerFactory", GrpcServerFactory.class, () -> customServerFactory) + .run((context) -> assertThat(context).getBean(GrpcServerFactory.class).isSameAs(customServerFactory)); + } + + @Test + void userDefinedServerFactoryWithInProcessServerFactory() { + GrpcServerFactory customServerFactory = mock(GrpcServerFactory.class); + this.contextRunner.withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withBean("customServerFactory", GrpcServerFactory.class, () -> customServerFactory) + .run((context) -> assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("customServerFactory", "inProcessGrpcServerFactory")); + } + + @Test + void whenShadedAndNonShadedNettyOnClasspathShadedNettyFactoryIsAutoConfigured() { + this.contextRunner.run((context) -> assertThat(context).getBean(GrpcServerFactory.class) + .isInstanceOf(ShadedNettyGrpcServerFactory.class)); + } + + @Test + void shadedNettyFactoryWithInProcessServerFactory() { + this.contextRunner.withPropertyValues("spring.grpc.server.inprocess.name=foo") + .run((context) -> assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("shadedNettyGrpcServerFactory", "inProcessGrpcServerFactory")); + } + + @Test + void whenOnlyNonShadedNettyOnClasspathNonShadedNettyFactoryIsAutoConfigured() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .run((context) -> assertThat(context).getBean(GrpcServerFactory.class) + .isInstanceOf(NettyGrpcServerFactory.class)); + } + + @Test + void nonShadedNettyFactoryWithInProcessServerFactory() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .run((context) -> assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("nettyGrpcServerFactory", "inProcessGrpcServerFactory")); + } + + @Test + void whenShadedNettyAndNettyNotOnClasspathNoServerFactoryIsAutoConfigured() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerFactory.class)); + } + + @Test + void noServerFactoryWithInProcessServerFactory() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .run((context) -> assertThat(context).getBean(GrpcServerFactory.class) + .isInstanceOf(InProcessGrpcServerFactory.class)); + } + + @Test + void shadedNettyServerFactoryAutoConfiguredWithCustomLifecycle() { + GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class); + new ApplicationContextRunner().withConfiguration(autoConfigurations) + .with(this::serviceBean) + .withBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle) + .run((context) -> { + assertThat(context).getBean(GrpcServerFactory.class).isInstanceOf(ShadedNettyGrpcServerFactory.class); + assertThat(context).getBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class) + .isSameAs(customServerLifecycle); + }); + } + + @Test + void nettyServerFactoryAutoConfiguredWithCustomLifecycle() { + GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class); + new ApplicationContextRunner().withConfiguration(autoConfigurations) + .with(this::serviceBean) + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle) + .run((context) -> { + assertThat(context).getBean(GrpcServerFactory.class).isInstanceOf(NettyGrpcServerFactory.class); + assertThat(context).getBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class) + .isSameAs(customServerLifecycle); + }); + } + + @Test + void inProcessServerFactoryAutoConfiguredWithCustomLifecycle() { + GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class); + new ApplicationContextRunner().withConfiguration(autoConfigurations) + .with(this::serviceBean) + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withBean("inProcessGrpcServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle) + .run((context) -> { + assertThat(context).getBean(GrpcServerFactory.class).isInstanceOf(InProcessGrpcServerFactory.class); + assertThat(context).getBean("inProcessGrpcServerLifecycle", GrpcServerLifecycle.class) + .isSameAs(customServerLifecycle); + }); + } + + @Test + void shadedNettyServerFactoryAutoConfiguredAsExpected() { + this.contextRunner.withPropertyValues("spring.grpc.server.address=192.168.0.1", "spring.grpc.server.port=6160") + .run(assertThatServerIsConfigured(ShadedNettyGrpcServerFactory.class, "192.168.0.1:6160", + "shadedNettyGrpcServerLifecycle")); + } + + @Test + void nettyServerFactoryAutoConfiguredAsExpected() { + this.contextRunner.withPropertyValues("spring.grpc.server.address=192.168.0.1", "spring.grpc.server.port=6160") + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .run(assertThatServerIsConfigured(NettyGrpcServerFactory.class, "192.168.0.1:6160", + "nettyGrpcServerLifecycle")); + } + + @Test + void serverFactoryAutoConfiguredInWebAppWhenServletDisabled() { + new WebApplicationContextRunner().withConfiguration(autoConfigurations) + .with(this::noOpLifecycleBeans) + .with(this::serviceBean) + .withPropertyValues("spring.grpc.server.address=192.168.0.1") + .withPropertyValues("spring.grpc.server.port=6160") + .withPropertyValues("spring.grpc.server.servlet.enabled=false") + .run(assertThatServerIsConfigured(ShadedNettyGrpcServerFactory.class, "192.168.0.1:6160", + "shadedNettyGrpcServerLifecycle")); + } + + @Test + void inProcessServerFactoryAutoConfiguredAsExpected() { + this.contextRunner.withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .run(assertThatServerIsConfigured(InProcessGrpcServerFactory.class, "foo", "inProcessGrpcServerLifecycle")); + } + + @Test + void nettyServerFactoryAutoConfiguredWithSsl() { + this.contextRunner.withPropertyValues("spring.grpc.server.address=192.168.0.1", "spring.grpc.server.port=6160", + "spring.grpc.server.ssl.bundle=ssltest", + "spring.ssl.bundle.jks.ssltest.keystore.location=classpath:org/springframework/boot/grpc/server/autoconfigure/test.jks", + "spring.ssl.bundle.jks.ssltest.keystore.password=secret", + "spring.ssl.bundle.jks.ssltest.key.password=password") + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .run(assertThatServerIsConfigured(NettyGrpcServerFactory.class, "192.168.0.1:6160", + "nettyGrpcServerLifecycle")); + } + + private ContextConsumer> assertThatServerIsConfigured( + Class expectedServerFactoryType, String expectedAddress, String expectedLifecycleBeanName) { + return (context) -> { + assertThat(context).getBean(GrpcServerFactory.class) + .isInstanceOf(expectedServerFactoryType) + .hasFieldOrPropertyWithValue("address", expectedAddress) + .extracting("serviceList", InstanceOfAssertFactories.list(ServerServiceDefinition.class)) + .singleElement() + .extracting(ServerServiceDefinition::getServiceDescriptor) + .extracting(ServiceDescriptor::getName) + .isEqualTo("my-service"); + assertThat(context).getBean(expectedLifecycleBeanName, GrpcServerLifecycle.class).isNotNull(); + }; + } + + private , C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> R serviceBean( + R contextRunner) { + return contextRunner.withBean(BindableService.class, () -> this.service); + } + + private , C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> R noOpLifecycleBeans( + R contextRunner) { + return contextRunner.withBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean("inProcessGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock); + } + + @Nested + class ServletServerAutoConfigurationTests { + + private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(autoConfigurations) + .with(GrpcServerAutoConfigurationTests.this::serviceBean); + + @Test + void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(BindableService.class)) + .run((context) -> assertThat(context).doesNotHaveBean(ServletGrpcServerConfiguration.class) + .doesNotHaveBean(ServletRegistrationBean.class)); + } + + @Test + void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)) + .run((context) -> assertThat(context).doesNotHaveBean(ServletGrpcServerConfiguration.class)); + } + + @Test + void whenNoBindableServicesRegisteredAutoConfigurationIsSkipped() { + new WebApplicationContextRunner().withConfiguration(autoConfigurations) + .run((context) -> assertThat(context).doesNotHaveBean(ServletGrpcServerConfiguration.class) + .doesNotHaveBean(ServletRegistrationBean.class)); + } + + @Test + void whenGrpcServletNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServlet.class)) + .withPropertyValues("spring.grpc.server.port=0") + .run((context) -> assertThat(context).doesNotHaveBean(ServletGrpcServerConfiguration.class) + .doesNotHaveBean(ServletRegistrationBean.class)); + } + + @Test + void whenWebApplicationServletIsAutoConfigured() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GrpcServletRegistration.class)); + } + + @Test + void whenServerBuilderCustomizerIsRegistered() { + ServerBuilderCustomizer customizer = mock(); + this.contextRunner.withBean(ServerBuilderCustomizer.class, () -> customizer) + .run((context) -> then(customizer).should().customize(any(ServletServerBuilder.class))); + } + + @Test + void whenMaxInboundMessageSizeIsSetThenItIsUsed() { + this.contextRunner.withPropertyValues("spring.grpc.server.inbound.message.max-size=10KB") + .run((context) -> assertThat(context).getBean(GrpcServletRegistration.class) + .hasFieldOrPropertyWithValue("servlet.servletAdapter.maxInboundMessageSize", + Math.toIntExact(DataSize.ofKilobytes(10).toBytes()))); + } + + @Test + void whenMaxInboundMessageSizeIsNotSetThenDefaultIsUsed() { + this.contextRunner.run((context) -> assertThat(context).getBean(GrpcServletRegistration.class) + .hasFieldOrPropertyWithValue("servlet.servletAdapter.maxInboundMessageSize", + GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE)); + } + + @Test + void customizersAreAppliedToServletServer() { + AtomicReference applied = new AtomicReference<>(); + ServerBuilderCustomizer customizer = applied::set; + this.contextRunner.withBean(ServerBuilderCustomizer.class, () -> customizer) + .run((context) -> assertThat(applied.get()).isInstanceOf(ServletServerBuilder.class)); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ServerBuilderCustomizersConfig { + + static ServerBuilderCustomizer foo = mock(); + + static ServerBuilderCustomizer bar = mock(); + + @Bean + @Order(200) + ServerBuilderCustomizer customizerFoo() { + return foo; + } + + @Bean + @Order(100) + ServerBuilderCustomizer customizerBar() { + return bar; + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerBuilderCustomizersTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerBuilderCustomizersTests.java new file mode 100644 index 000000000000..37689c5bc662 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerBuilderCustomizersTests.java @@ -0,0 +1,155 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.ServerBuilder; +import io.grpc.netty.NettyServerBuilder; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import org.mockito.Mockito; + +import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.util.unit.DataSize; + +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 GrpcServerBuilderCustomizers}. + * + * @author Chris Bono + * @author Phillip Webb + */ +class GrpcServerBuilderCustomizersTests { + + @Test + void applyWhenEmptyCustomizersDoesNothing() { + ServerBuilder serverBuilder = mock(ServerBuilder.class); + new GrpcServerBuilderCustomizers(Collections.emptyList()).apply(serverBuilder); + then(serverBuilder).shouldHaveNoInteractions(); + } + + @Test + void applyWhenSimpleCustomizer() { + GrpcServerBuilderCustomizers customizers = new GrpcServerBuilderCustomizers( + List.of(new SimpleServerBuilderCustomizer())); + NettyServerBuilder serverBuilder = mock(NettyServerBuilder.class); + customizers.apply(serverBuilder); + then(serverBuilder).should().maxConnectionAge(100L, TimeUnit.SECONDS); + } + + @Test + void applyWhenGenericCustomizersRespectsGeneric() { + List> list = new ArrayList<>(); + list.add(new TestCustomizer<>()); + list.add(new TestNettyServerBuilderCustomizer()); + list.add(new TestShadedNettyServerBuilderCustomizer()); + GrpcServerBuilderCustomizers customizers = new GrpcServerBuilderCustomizers(list); + customizers.apply(mock(ServerBuilder.class)); + assertThat(list.get(0).getCount()).isOne(); + assertThat(list.get(1).getCount()).isZero(); + assertThat(list.get(2).getCount()).isZero(); + customizers.apply(mock(NettyServerBuilder.class)); + assertThat(list.get(0).getCount()).isEqualTo(2); + assertThat(list.get(1).getCount()).isOne(); + assertThat(list.get(2).getCount()).isZero(); + customizers.apply(mock(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)); + assertThat(list.get(0).getCount()).isEqualTo(3); + assertThat(list.get(1).getCount()).isOne(); + assertThat(list.get(2).getCount()).isOne(); + } + + @Test + void applyWhenHasInjectedBeans() { + CompressorRegistry compressorRegistry = mock(); + DecompressorRegistry decompressorRegistry = mock(); + GrpcServerExecutorProvider executorProvider = mock(); + Executor executor = mock(); + given(executorProvider.getExecutor()).willReturn(executor); + GrpcServerProperties properties = new GrpcServerProperties(); + properties.getInbound().getMessage().setMaxSize(DataSize.ofMegabytes(10)); + GrpcServerBuilderCustomizers customizers = new GrpcServerBuilderCustomizers(properties, compressorRegistry, + decompressorRegistry, executorProvider, List.of(new SimpleServerBuilderCustomizer())); + NettyServerBuilder serverBuilder = mock(NettyServerBuilder.class); + customizers.apply(serverBuilder); + InOrder ordered = Mockito.inOrder(serverBuilder); + then(serverBuilder).should(ordered).compressorRegistry(compressorRegistry); + then(serverBuilder).should(ordered).decompressorRegistry(decompressorRegistry); + then(serverBuilder).should().executor(executor); + then(serverBuilder).should(ordered).maxConnectionAge(100L, TimeUnit.SECONDS); + } + + /** + * Test customizer that will match {@link NettyServerBuilder} and apply a simple + * customization. + */ + static class SimpleServerBuilderCustomizer implements ServerBuilderCustomizer { + + @Override + public void customize(NettyServerBuilder serverBuilder) { + serverBuilder.maxConnectionAge(100, TimeUnit.SECONDS); + } + + } + + /** + * Test customizer that will match all {@link ServerBuilderCustomizer}. + * + * @param the builder type + */ + static class TestCustomizer> implements ServerBuilderCustomizer { + + private int count; + + @Override + public void customize(T serverBuilder) { + this.count++; + } + + int getCount() { + return this.count; + } + + } + + /** + * Test customizer that will match only {@link NettyServerBuilder}. + */ + static class TestNettyServerBuilderCustomizer extends TestCustomizer { + + } + + /** + * Test customizer that will match only + * {@link io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder}. + */ + static class TestShadedNettyServerBuilderCustomizer + extends TestCustomizer { + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerCodecConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerCodecConfigurationTests.java new file mode 100644 index 000000000000..dcb780a68504 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerCodecConfigurationTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import io.grpc.Compressor; +import io.grpc.CompressorRegistry; +import io.grpc.Decompressor; +import io.grpc.DecompressorRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link GrpcServerCodecConfiguration}. + * + * @author Andrei Lisa + */ +class GrpcServerCodecConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcServerCodecConfiguration.class)); + + @Test + void compressorRegistryWhenHasUserDefinedRegistryDoesNotAutoConfigureBean() { + CompressorRegistry customRegistry = mock(); + this.contextRunner.withBean("customCompressorRegistry", CompressorRegistry.class, () -> customRegistry) + .run((context) -> assertThat(context).getBean(CompressorRegistry.class).isSameAs(customRegistry)); + } + + @Test + void compressorRegistryWhenNoCompressorsAutoConfiguresDefaultInstance() { + this.contextRunner.run((context) -> assertThat(context).getBean(CompressorRegistry.class) + .isSameAs(CompressorRegistry.getDefaultInstance())); + } + + @Test + void compressorRegistryWhenHasCompressorsAutoConfiguresNewInstance() { + Compressor compressor = mock(); + given(compressor.getMessageEncoding()).willReturn("foo"); + this.contextRunner.withBean(Compressor.class, () -> compressor).run((context) -> { + assertThat(context).hasSingleBean(CompressorRegistry.class); + CompressorRegistry registry = context.getBean(CompressorRegistry.class); + assertThat(registry).isNotSameAs(CompressorRegistry.getDefaultInstance()); + assertThat(registry.lookupCompressor("foo")).isSameAs(compressor); + }); + } + + @Test + void decompressorRegistryWhenHasUserDefinedRegistryDoesNotAutoConfigureBean() { + DecompressorRegistry customRegistry = mock(); + this.contextRunner.withBean("customDecompressorRegistry", DecompressorRegistry.class, () -> customRegistry) + .run((context) -> assertThat(context).getBean(DecompressorRegistry.class).isSameAs(customRegistry)); + } + + @Test + void decompressorRegistryWhenNoDecompressorsAutoConfiguresDefaultInstance() { + this.contextRunner.run((context) -> assertThat(context).getBean(DecompressorRegistry.class) + .isSameAs(DecompressorRegistry.getDefaultInstance())); + } + + @Test + void decompressorRegistryWhenHasDecompressorsAutoConfiguresNewInstance() { + Decompressor decompressor = mock(); + given(decompressor.getMessageEncoding()).willReturn("foo"); + this.contextRunner.withBean(Decompressor.class, () -> decompressor).run((context) -> { + assertThat(context).hasSingleBean(DecompressorRegistry.class); + DecompressorRegistry registry = context.getBean(DecompressorRegistry.class); + assertThat(registry).isNotSameAs(DecompressorRegistry.getDefaultInstance()); + assertThat(registry.lookupDecompressor("foo")).isSameAs(decompressor); + }); + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java new file mode 100644 index 000000000000..f9be50f51309 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java @@ -0,0 +1,169 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import java.net.InetAddress; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import io.grpc.TlsServerCredentials.ClientAuth; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; +import org.springframework.util.unit.DataSize; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link GrpcServerProperties}. + * + * @author Chris Bono + * @author Phillip Webb + */ +class GrpcServerPropertiesTests { + + private GrpcServerProperties bindProperties(Map map) { + return new Binder(new MapConfigurationPropertySource(map)) + .bind("spring.grpc.server", GrpcServerProperties.class) + .get(); + } + + @Test + void bind() throws Exception { + Map map = new HashMap<>(); + map.put("spring.grpc.server.address", "192.168.0.1"); + GrpcServerProperties properties = bindProperties(map); + assertThat(properties.getAddress()).isEqualTo(InetAddress.getByName("192.168.0.1")); + } + + @Test + void defaultAddressIsNull() { + assertThat(new GrpcServerProperties().getAddress()).isNull(); + } + + @Nested + class ShutdownProperties { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.shutdown.grace-period", "10m"); + GrpcServerProperties properties = bindProperties(map); + assertThat(properties.getShutdown().getGracePeriod()).isEqualTo(Duration.ofMinutes(10)); + } + + @Test + void bindWithoutUnits() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.shutdown.grace-period", "10"); + GrpcServerProperties properties = bindProperties(map); + assertThat(properties.getShutdown().getGracePeriod()).isEqualTo(Duration.ofSeconds(10)); + } + + } + + @Nested + class InboundProperties { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.inbound.message.max-size", "20MB"); + map.put("spring.grpc.server.inbound.metadata.max-size", "1MB"); + GrpcServerProperties properties = bindProperties(map); + assertThat(properties.getInbound().getMessage().getMaxSize()).isEqualTo(DataSize.ofMegabytes(20)); + assertThat(properties.getInbound().getMetadata().getMaxSize()).isEqualTo(DataSize.ofMegabytes(1)); + } + + @Test + void bindWithoutUnits() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.inbound.message.max-size", "1048576"); + map.put("spring.grpc.server.inbound.metadata.max-size", "1024"); + GrpcServerProperties properties = bindProperties(map); + assertThat(properties.getInbound().getMessage().getMaxSize()).isEqualTo(DataSize.ofMegabytes(1)); + assertThat(properties.getInbound().getMetadata().getMaxSize()).isEqualTo(DataSize.ofKilobytes(1)); + } + + } + + @Nested + class KeepAliveProperties { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.keepalive.time", "45m"); + map.put("spring.grpc.server.keepalive.timeout", "40s"); + map.put("spring.grpc.server.keepalive.permit.time", "33s"); + map.put("spring.grpc.server.keepalive.permit.without-calls", "true"); + map.put("spring.grpc.server.keepalive.connection.max-idle-time", "1h"); + map.put("spring.grpc.server.keepalive.connection.max-age", "3h"); + map.put("spring.grpc.server.keepalive.connection.grace-period", "21s"); + GrpcServerProperties.Keepalive properties = bindProperties(map).getKeepalive(); + assertThatPropertiesSetAsExpected(properties); + } + + @Test + void bindWithoutUnits() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.keepalive.time", "2700"); + map.put("spring.grpc.server.keepalive.timeout", "40"); + map.put("spring.grpc.server.keepalive.permit.time", "33"); + map.put("spring.grpc.server.keepalive.permit.without-calls", "true"); + map.put("spring.grpc.server.keepalive.connection.max-idle-time", "3600"); + map.put("spring.grpc.server.keepalive.connection.max-age", "10800"); + map.put("spring.grpc.server.keepalive.connection.grace-period", "21"); + GrpcServerProperties.Keepalive properties = bindProperties(map).getKeepalive(); + assertThatPropertiesSetAsExpected(properties); + } + + private void assertThatPropertiesSetAsExpected(GrpcServerProperties.Keepalive properties) { + assertThat(properties.getTime()).isEqualTo(Duration.ofMinutes(45)); + assertThat(properties.getTimeout()).isEqualTo(Duration.ofSeconds(40)); + assertThat(properties.getPermit().getTime()).isEqualTo(Duration.ofSeconds(33)); + assertThat(properties.getPermit().isWithoutCalls()).isTrue(); + assertThat(properties.getConnection().getMaxIdleTime()).isEqualTo(Duration.ofHours(1)); + assertThat(properties.getConnection().getMaxAge()).isEqualTo(Duration.ofHours(3)); + assertThat(properties.getConnection().getGracePeriod()).isEqualTo(Duration.ofSeconds(21)); + } + + } + + @Nested + class SslProperties { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.ssl.enabled", "true"); + map.put("spring.grpc.server.ssl.client-auth", "require"); + map.put("spring.grpc.server.ssl.bundle", "test"); + map.put("spring.grpc.server.ssl.secure", "false"); + GrpcServerProperties.Ssl properties = bindProperties(map).getSsl(); + assertThat(properties.getEnabled()).isTrue(); + assertThat(properties.getClientAuth()).isEqualTo(ClientAuth.REQUIRE); + assertThat(properties.getBundle()).isEqualTo("test"); + assertThat(properties.isSecure()).isFalse(); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerServicesAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerServicesAutoConfigurationTests.java new file mode 100644 index 000000000000..508101606d78 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerServicesAutoConfigurationTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import io.grpc.BindableService; +import io.grpc.protobuf.services.ProtoReflectionServiceV1; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerServicesAutoConfiguration.GrpcServerReflectionServiceConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link GrpcServerServicesAutoConfiguration}. + * + * @author Haris Zujo + * @author Chris Bono + * @author Andrey Litvitski + * @author Phillip Webb + */ +class GrpcServerServicesAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcServerServicesAutoConfiguration.class)) + .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean(BindableService.class, Mockito::mock); + + @Nested + class GrpcServerReflectionServiceConfigurationTests { + + private final ApplicationContextRunner contextRunner = GrpcServerServicesAutoConfigurationTests.this.contextRunner; + + @Test + void whenAutoConfigurationIsNotSkippedCreatesReflectionServiceBean() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(GrpcServerReflectionServiceConfiguration.class); + assertThat(context).hasBean("grpcServerReflectionService"); + }); + } + + @Test + void whenGrpcServicesNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(ProtoReflectionServiceV1.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerReflectionServiceConfiguration.class)); + } + + @Test + void whenNoBindableServiceDefinedAutoConfigurationIsSkipped() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcServerServicesAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerReflectionServiceConfiguration.class)); + } + + @Test + void whenReflectionEnabledPropertyIsTrueAutoConfigurationIsNotSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.server.reflection.enabled=true") + .run((context) -> assertThat(context).hasBean("grpcServerReflectionService")); + } + + @Test + void whenReflectionEnabledPropertyIsFalseAutoConfigurationIsSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.server.reflection.enabled=false").run((context) -> { + assertThat(context).doesNotHaveBean("grpcServerReflectionService"); + assertThat(context).doesNotHaveBean(GrpcServerReflectionServiceConfiguration.class); + }); + } + + @Test + void whenServerEnabledPropertyIsTrueAutoConfigurationIsNotSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.server.enabled=true") + .run((context) -> assertThat(context).hasBean("grpcServerReflectionService")); + } + + @Test + void whenServerEnabledPropertyIsFalseAutoConfigurationIsSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.server.enabled=false").run((context) -> { + assertThat(context).doesNotHaveBean("grpcServerReflectionService"); + assertThat(context).doesNotHaveBean(GrpcServerReflectionServiceConfiguration.class); + }); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddressTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddressTests.java new file mode 100644 index 000000000000..c0e392068b91 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddressTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import java.net.InetAddress; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties.Netty.Transport; +import org.springframework.grpc.internal.GrpcUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link NettyAddress}. + * + * @author Phillip Webb + */ +class NettyAddressTests { + + @Test + void whenNoTransportAndNoAddressOrPortOrDomainSocketPathBindsToDefault() { + NettyAddress address = new NettyAddress(null, null, null, null); + assertThat(address).hasToString(GrpcUtils.ANY_IP_ADDRESS + ":" + GrpcUtils.DEFAULT_PORT); + } + + @Test + void whenNoTransportAndOnlyPortBindsToAllAddressesUsingPort() { + NettyAddress address = new NettyAddress(null, null, 1234, null); + assertThat(address).hasToString(GrpcUtils.ANY_IP_ADDRESS + ":1234"); + } + + @Test + void whenNoTransportAndOnlyAddressBindsToAddressUsingPort9090() throws Exception { + InetAddress inetAddress = InetAddress.getByName("localhost"); + NettyAddress address = new NettyAddress(null, inetAddress, null, null); + assertThat(address).hasToString("localhost:" + GrpcUtils.DEFAULT_PORT); + } + + @Test + void whenNoTransportAndOnlyAddressWithoutNameBindsToAddressUsingPort9090() throws Exception { + InetAddress inetAddress = InetAddress.getByName("192.168.1.0"); + NettyAddress address = new NettyAddress(null, inetAddress, null, null); + assertThat(address).hasToString("192.168.1.0:" + GrpcUtils.DEFAULT_PORT); + } + + @Test + void whenNoTransportAndOnlyDomainSocketPathBindsToDomainSocket() { + NettyAddress address = new NettyAddress(null, null, null, "/ds"); + assertThat(address).hasToString("unix:/ds"); + } + + @Test + void whenNoTransportAndPortAndDomainSocketPathThrowsException() { + NettyAddress address = new NettyAddress(null, null, 1234, "/ds"); + assertThatExceptionOfType(MutuallyExclusiveConfigurationPropertiesException.class) + .isThrownBy(() -> address.toString()) + .withMessage( + "The configuration properties 'spring.grpc.server.port, spring.grpc.server.netty.domain-socket-path' " + + "are mutually exclusive and 'spring.grpc.server.port, spring.grpc.server.netty.domain-socket-path' " + + "have been configured together"); + } + + @Test + void whenNoTransportAndAddressAndDomainSocketPathThrowsException() throws Exception { + InetAddress inetAddress = InetAddress.getByName("192.168.1.0"); + NettyAddress address = new NettyAddress(null, inetAddress, null, "/ds"); + assertThatExceptionOfType(MutuallyExclusiveConfigurationPropertiesException.class) + .isThrownBy(() -> address.toString()) + .withMessage( + "The configuration properties 'spring.grpc.server.address, spring.grpc.server.netty.domain-socket-path' " + + "are mutually exclusive and 'spring.grpc.server.address, spring.grpc.server.netty.domain-socket-path' " + + "have been configured together"); + } + + @Test + void whenTcpTransportBindsToTcp() throws Exception { + InetAddress inetAddress = InetAddress.getByName("192.168.1.0"); + NettyAddress address = new NettyAddress(Transport.TCP, inetAddress, 1234, "/ds"); + assertThat(address).hasToString("192.168.1.0:1234"); + } + + @Test + void whenDomainSocketTransportAndNoDomainPathThrowsException() { + NettyAddress address = new NettyAddress(Transport.DOMAIN_SOCKET, null, null, ""); + assertThatExceptionOfType(InvalidConfigurationPropertyValueException.class).isThrownBy(() -> address.toString()) + .withMessage("Property spring.grpc.server.netty.domain-socket-path with value '' is invalid: " + + "A path is required when spring.grpc.server.netty.transport is set to 'domain-socket'"); + } + + @Test + void whenDomainSocketTransportAndDomainPathBindsToDomainPath() throws Exception { + InetAddress inetAddress = InetAddress.getByName("192.168.1.0"); + NettyAddress address = new NettyAddress(Transport.DOMAIN_SOCKET, inetAddress, 1234, "/ds"); + assertThat(address).hasToString("unix:/ds"); + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizerTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizerTests.java new file mode 100644 index 000000000000..1a0d74875dac --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizerTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import io.grpc.ServerBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.netty.NettyServerBuilder; +import io.grpc.servlet.jakarta.ServletServerBuilder; +import org.junit.jupiter.api.Test; +import org.mockito.verification.VerificationMode; + +import org.springframework.util.unit.DataSize; + +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link PropertiesServerBuilderCustomizer}. + * + * @author Chris Bono + * @author Phillip Webb + */ +class PropertiesServerBuilderCustomizerTests { + + @Test + void customizeWhenNettyServerBuilder() { + NettyServerBuilder builder = mock(); + PropertiesServerBuilderCustomizer customizer = new PropertiesServerBuilderCustomizer<>( + getProperties()); + customizer.customize(builder); + assertExpectedMapping(builder, atLeastOnce()); + } + + @Test + void customizeWhenInProcessServerBuilder() { + InProcessServerBuilder builder = mock(); + PropertiesServerBuilderCustomizer customizer = new PropertiesServerBuilderCustomizer<>( + getProperties()); + customizer.customize(builder); + assertExpectedMapping(builder, never()); + } + + @Test + void customizerWhenServletServerBuilder() { + ServletServerBuilder builder = mock(); + PropertiesServerBuilderCustomizer customizer = new PropertiesServerBuilderCustomizer<>( + getProperties()); + customizer.customize(builder); + assertExpectedMapping(builder, never()); + } + + private GrpcServerProperties getProperties() { + GrpcServerProperties properties = new GrpcServerProperties(); + properties.getInbound().getMessage().setMaxSize(DataSize.ofMegabytes(333)); + properties.getInbound().getMetadata().setMaxSize(DataSize.ofKilobytes(111)); + properties.getKeepalive().setTime(Duration.ofHours(1)); + properties.getKeepalive().setTimeout(Duration.ofSeconds(10)); + properties.getKeepalive().getConnection().setMaxIdleTime(Duration.ofHours(2)); + properties.getKeepalive().getConnection().setMaxAge(Duration.ofHours(3)); + properties.getKeepalive().getConnection().setGracePeriod(Duration.ofSeconds(45)); + properties.getKeepalive().getPermit().setTime(Duration.ofMinutes(7)); + properties.getKeepalive().getPermit().setWithoutCalls(true); + return properties; + } + + private void assertExpectedMapping(ServerBuilder builder, VerificationMode keepAliveMode) { + then(builder).should().maxInboundMessageSize(Math.toIntExact(DataSize.ofMegabytes(333).toBytes())); + then(builder).should().maxInboundMetadataSize(Math.toIntExact(DataSize.ofKilobytes(111).toBytes())); + then(builder).should(keepAliveMode).keepAliveTime(Duration.ofHours(1).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should(keepAliveMode).keepAliveTimeout(Duration.ofSeconds(10).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should(keepAliveMode).maxConnectionIdle(Duration.ofHours(2).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should(keepAliveMode).maxConnectionAge(Duration.ofHours(3).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should(keepAliveMode) + .maxConnectionAgeGrace(Duration.ofSeconds(45).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should(keepAliveMode).permitKeepAliveTime(Duration.ofMinutes(7).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should(keepAliveMode).permitKeepAliveWithoutCalls(true); + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerCredentialsTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerCredentialsTests.java new file mode 100644 index 000000000000..c9794c200375 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerCredentialsTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; + +import io.grpc.TlsServerCredentials.ClientAuth; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties.Ssl; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.ssl.SslManagerBundle; + +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 ServerCredentials}. + * + * @author Phillip Webb + */ +class ServerCredentialsTests { + + private final TrustManagerFactory insecureTrustManagerFactory = mock(); + + private final SslBundles bundles = mock(); + + private final TrustManagerFactory bundleTrustManagerFactory = mock(); + + private final KeyManagerFactory bundleKeyManagerFactory = mock(); + + ServerCredentialsTests() { + SslBundle bundle = mock(); + SslManagerBundle managers = mock(); + given(this.bundles.getBundle("test")).willReturn(bundle); + given(bundle.getManagers()).willReturn(managers); + given(managers.getTrustManagerFactory()).willReturn(this.bundleTrustManagerFactory); + given(managers.getKeyManagerFactory()).willReturn(this.bundleKeyManagerFactory); + } + + @Test + void getWhenNotEnabledAndNoBundleReturnsNullManagers() { + ServerCredentials credentials = get((properties) -> { + }); + assertThat(credentials.keyManagerFactory()).isNull(); + assertThat(credentials.trustManagerFactory()).isNull(); + assertThat(credentials.clientAuth()).isEqualTo(ClientAuth.NONE); + } + + @Test + void getWhenDisabledReturnsNullManagers() { + ServerCredentials credentials = get((properties) -> { + properties.put("spring.grpc.server.ssl.enabled", "false"); + properties.put("spring.grpc.server.ssl.client-auth", "require"); + }); + assertThat(credentials.keyManagerFactory()).isNull(); + assertThat(credentials.trustManagerFactory()).isNull(); + assertThat(credentials.clientAuth()).isEqualTo(ClientAuth.REQUIRE); + } + + @Test + void getWhenEnabledTrueAndNoBundleNameThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> get((properties) -> { + properties.put("spring.grpc.server.ssl.enabled", "true"); + properties.put("spring.grpc.server.ssl.client-auth", "require"); + })).withMessage("SSL bundle-name is requested when 'spring.grpc.server.ssl.enabled' is true"); + } + + @Test + void getWhenHasBundleName() { + ServerCredentials credentials = get((properties) -> { + properties.put("spring.grpc.server.ssl.bundle", "test"); + properties.put("spring.grpc.server.ssl.client-auth", "require"); + }); + assertThat(credentials.keyManagerFactory()).isEqualTo(this.bundleKeyManagerFactory); + assertThat(credentials.trustManagerFactory()).isEqualTo(this.bundleTrustManagerFactory); + assertThat(credentials.clientAuth()).isEqualTo(ClientAuth.REQUIRE); + } + + @Test + void getWhenHasBundleNameAndEnabled() { + ServerCredentials credentials = get((properties) -> { + properties.put("spring.grpc.server.ssl.enabled", "true"); + properties.put("spring.grpc.server.ssl.bundle", "test"); + properties.put("spring.grpc.server.ssl.client-auth", "require"); + }); + assertThat(credentials.keyManagerFactory()).isEqualTo(this.bundleKeyManagerFactory); + assertThat(credentials.trustManagerFactory()).isEqualTo(this.bundleTrustManagerFactory); + assertThat(credentials.clientAuth()).isEqualTo(ClientAuth.REQUIRE); + } + + @Test + void getWhenHasBundleNameAndSecureFalse() { + ServerCredentials credentials = get((properties) -> { + properties.put("spring.grpc.server.ssl.enabled", "true"); + properties.put("spring.grpc.server.ssl.bundle", "test"); + properties.put("spring.grpc.server.ssl.secure", "false"); + }); + assertThat(credentials.keyManagerFactory()).isEqualTo(this.bundleKeyManagerFactory); + assertThat(credentials.trustManagerFactory()).isEqualTo(this.insecureTrustManagerFactory); + assertThat(credentials.clientAuth()).isEqualTo(ClientAuth.NONE); + } + + private ServerCredentials get(Consumer> properties) { + Map map = new HashMap<>(); + properties.accept(map); + Ssl ssl = new Binder(new MapConfigurationPropertySource(map)) + .bind("spring.grpc.server", GrpcServerProperties.class) + .orElseGet(GrpcServerProperties::new) + .getSsl(); + return ServerCredentials.get(ssl, this.bundles, this.insecureTrustManagerFactory); + } + +} diff --git a/module/spring-boot-grpc-server/src/test/resources/org/springframework/boot/grpc/server/autoconfigure/test.jks b/module/spring-boot-grpc-server/src/test/resources/org/springframework/boot/grpc/server/autoconfigure/test.jks new file mode 100644 index 0000000000000000000000000000000000000000..0fc3e802f75461dd074facb9611d350db4d5960f GIT binary patch literal 1276 zcmezO_TO6u1_mZ5W@O+hNi8nXP0YzmEM{O}OjTL=XFE`?-k{cikBv*4jgf^>i%F1? zk(GfZ`?F{4vBFug6<%MmmXJ+)mEgQoslXV6!5_H^yana6V1?kw1w zbE0Bi&GHlLHzfosgjrwL)qKcc5I;l0LF?W2lpQg%-cHp$l(#o)?Jkath1@gQN{hG8 zig@wKv#0R7vd_QC=JG%%Ffy=4=$RT=0v*d`(8R=M(8RcU0W%XL6BCP-)w&Y~JZv0V zZ64=rS(uqv84M~6g$xAPm_u3EggJBalM{0?@{3DgVjNh+*s+LlVG-lTBF2m)W*{fd zYiMC$VQ64zW@K(?5e4L0B5?=MWswHLZ0z7LVq$~_7BeF|vl9agPmO-znfkD()@R+bGrT$(AXmN24#zv*XRX z#$UNu(Lmln78u;Jd@N!tBKmU@J0!OJc3G%!N>OO@P1n+F-CorAVRmOQaA8six!iWP z)M3lXpnJ*TI=kIlH(Yxia-ls?xvctEx&P5B6()tKm`>%bo~@fX9{j%TtMU1G!|pw& zZ6BRjIqQ^`bIxR@OmMno&8^H%tpq36Esh&T(+MJ_la+#pV>+3scS% container = new GenericContainer<>( + DockerImageName.parse("fullstorydev/grpcurl:v1.9.3")) + .withCommand("-d", "{\"name\": \"spring\"}", "--plaintext", address, "HelloWorld/SayHello") + .withStartupCheckStrategy(new IndefiniteWaitOneShotStartupCheckStrategy())) { + container.start(); + assertThat(container.getLogs()).contains("\"message\": \"Hello 'spring'\""); + } + + } + + static class GrpcServerStartedEventListener implements ApplicationListener { + + private int port; + + @Override + public void onApplicationEvent(GrpcServerStartedEvent event) { + this.port = event.getPort(); + } + + int getPort() { + return this.port; + } + + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/java/smoketest/grpcservernettyshaded/HelloWorldService.java b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/java/smoketest/grpcservernettyshaded/HelloWorldService.java new file mode 100644 index 000000000000..d17147df0374 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/java/smoketest/grpcservernettyshaded/HelloWorldService.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcservernettyshaded; + +import io.grpc.stub.StreamObserver; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import smoketest.grpcservernettyshaded.proto.HelloReply; +import smoketest.grpcservernettyshaded.proto.HelloRequest; +import smoketest.grpcservernettyshaded.proto.HelloWorldGrpc; + +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +@Service +public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase { + + private static Log logger = LogFactory.getLog(HelloWorldService.class); + + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + String name = request.getName(); + logger.info("sayHello " + name); + Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name); + Assert.state(!name.startsWith("internal"), "Internal error"); + String message = "Hello '%s'".formatted(name); + HelloReply reply = HelloReply.newBuilder().setMessage(message).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + + @Override + public void streamHello(HelloRequest request, StreamObserver responseObserver) { + String name = request.getName(); + logger.info("streamHello " + name); + int count = 0; + while (count < 10) { + String message = "Hello(" + count + ") '%s'".formatted(name); + HelloReply reply = HelloReply.newBuilder().setMessage(message).build(); + responseObserver.onNext(reply); + count++; + try { + Thread.sleep(100L); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + responseObserver.onError(ex); + return; + } + } + responseObserver.onCompleted(); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/java/smoketest/grpcservernettyshaded/SampleGrpcServerNettyShadedApplication.java b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/java/smoketest/grpcservernettyshaded/SampleGrpcServerNettyShadedApplication.java new file mode 100644 index 000000000000..67aa1184c13d --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/java/smoketest/grpcservernettyshaded/SampleGrpcServerNettyShadedApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcservernettyshaded; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleGrpcServerNettyShadedApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleGrpcServerNettyShadedApplication.class, args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/java/smoketest/grpcservernettyshaded/package-info.java b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/java/smoketest/grpcservernettyshaded/package-info.java new file mode 100644 index 000000000000..6d4a3f27ec95 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/java/smoketest/grpcservernettyshaded/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NullMarked +package smoketest.grpcservernettyshaded; + +import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/proto/hello.proto b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/proto/hello.proto new file mode 100644 index 000000000000..7d56b5fc5df1 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/main/proto/hello.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "smoketest.grpcservernettyshaded.proto"; +option java_multiple_files = true; + +service HelloWorld { + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc StreamHello(HelloRequest) returns (stream HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-servlet/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/build.gradle new file mode 100644 index 000000000000..c9389b5491d4 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/build.gradle @@ -0,0 +1,69 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java" + id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "org.springframework.boot.docker-test" +} + +description = "Spring Boot gRPC server servlet smoke test" + + +dependencies { + implementation(project(":starter:spring-boot-starter-grpc-server")) { + exclude(group: "io.grpc", module: "grpc-netty-shaded") + } + implementation(project(":starter:spring-boot-starter-tomcat")) + implementation("io.grpc:grpc-servlet-jakarta") + + dockerTestImplementation(project(":starter:spring-boot-starter-test")) + dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") +} + +def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom") +def grpcJava = dependenciesBom.getLibrary("Grpc Java") +def protobufJava = dependenciesBom.getLibrary("Protobuf Java") + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +nullability { + requireExplicitNullMarking = false +} + +configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all { + extendsFrom(configurations.dependencyManagement) +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protobufJava.version}" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}" + } + } + generateProtoTasks { + all()*.plugins { + grpc { + option '@generated=omit' + } + } + } +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/dockerTest/java/smoketest/grpcserverservlet/SampleGrpcServerServletApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/dockerTest/java/smoketest/grpcserverservlet/SampleGrpcServerServletApplicationTests.java new file mode 100644 index 000000000000..b2aaa1a2ab3b --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/dockerTest/java/smoketest/grpcserverservlet/SampleGrpcServerServletApplicationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcserverservlet; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.startupcheck.IndefiniteWaitOneShotStartupCheckStrategy; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for Spring gRPC with a servlet server. + * + * @author Phillip Webb + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@Testcontainers(disabledWithoutDocker = true) +class SampleGrpcServerServletApplicationTests { + + @LocalServerPort + private String localServerPort; + + @Test + @SuppressWarnings("resource") + void test() { + String address = "host.docker.internal:" + this.localServerPort; + try (GenericContainer container = new GenericContainer<>( + DockerImageName.parse("fullstorydev/grpcurl:v1.9.3")) + .withCommand("-d", "{\"name\": \"spring\"}", "--plaintext", address, "HelloWorld/SayHello") + .withStartupCheckStrategy(new IndefiniteWaitOneShotStartupCheckStrategy())) { + container.start(); + assertThat(container.getLogs()).contains("\"message\": \"Hello 'spring'\""); + } + + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/java/smoketest/grpcserverservlet/HelloWorldService.java b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/java/smoketest/grpcserverservlet/HelloWorldService.java new file mode 100644 index 000000000000..37e4ca236200 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/java/smoketest/grpcserverservlet/HelloWorldService.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcserverservlet; + +import io.grpc.stub.StreamObserver; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import smoketest.grpcserverservlet.proto.HelloReply; +import smoketest.grpcserverservlet.proto.HelloRequest; +import smoketest.grpcserverservlet.proto.HelloWorldGrpc; + +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +@Service +public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase { + + private static Log logger = LogFactory.getLog(HelloWorldService.class); + + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + String name = request.getName(); + logger.info("sayHello " + name); + Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name); + Assert.state(!name.startsWith("internal"), "Internal error"); + String message = "Hello '%s'".formatted(name); + HelloReply reply = HelloReply.newBuilder().setMessage(message).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + + @Override + public void streamHello(HelloRequest request, StreamObserver responseObserver) { + String name = request.getName(); + logger.info("streamHello " + name); + int count = 0; + while (count < 10) { + String message = "Hello(" + count + ") '%s'".formatted(name); + HelloReply reply = HelloReply.newBuilder().setMessage(message).build(); + responseObserver.onNext(reply); + count++; + try { + Thread.sleep(100L); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + responseObserver.onError(ex); + return; + } + } + responseObserver.onCompleted(); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/java/smoketest/grpcserverservlet/SampleGrpcServerServletApplication.java b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/java/smoketest/grpcserverservlet/SampleGrpcServerServletApplication.java new file mode 100644 index 000000000000..87a8ba6b03f8 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/java/smoketest/grpcserverservlet/SampleGrpcServerServletApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcserverservlet; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleGrpcServerServletApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleGrpcServerServletApplication.class, args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/java/smoketest/grpcserverservlet/package-info.java b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/java/smoketest/grpcserverservlet/package-info.java new file mode 100644 index 000000000000..a86ba9557b46 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/java/smoketest/grpcserverservlet/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NullMarked +package smoketest.grpcserverservlet; + +import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/proto/hello.proto b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/proto/hello.proto new file mode 100644 index 000000000000..8ba784139a7c --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/proto/hello.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "smoketest.grpcserverservlet.proto"; +option java_multiple_files = true; + +service HelloWorld { + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc StreamHello(HelloRequest) returns (stream HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/resources/application.properties b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/resources/application.properties new file mode 100644 index 000000000000..63158dce2424 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/main/resources/application.properties @@ -0,0 +1 @@ +server.http2.enabled=true diff --git a/smoke-test/spring-boot-smoke-test-grpc-server/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server/build.gradle new file mode 100644 index 000000000000..2f9fe70d0fac --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server/build.gradle @@ -0,0 +1,64 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java" + id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "org.springframework.boot.docker-test" +} + +description = "Spring Boot gRPC server smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-grpc-server")) + + dockerTestImplementation(project(":starter:spring-boot-starter-test")) + dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") +} + +def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom") +def grpcJava = dependenciesBom.getLibrary("Grpc Java") +def protobufJava = dependenciesBom.getLibrary("Protobuf Java") + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +nullability { + requireExplicitNullMarking = false +} + +configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all { + extendsFrom(configurations.dependencyManagement) +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protobufJava.version}" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}" + } + } + generateProtoTasks { + all()*.plugins { + grpc { + option '@generated=omit' + } + } + } +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server/src/dockerTest/java/smoketest/grpcserver/SampleGrpcServerApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-server/src/dockerTest/java/smoketest/grpcserver/SampleGrpcServerApplicationTests.java new file mode 100644 index 000000000000..0e34103552bb --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server/src/dockerTest/java/smoketest/grpcserver/SampleGrpcServerApplicationTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcserver; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.startupcheck.IndefiniteWaitOneShotStartupCheckStrategy; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import smoketest.grpcserver.SampleGrpcServerApplicationTests.GrpcServerStartedEventListener; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Import; +import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for the default Spring gRPC netty server. + * + * @author Phillip Webb + */ +@SpringBootTest(properties = "spring.grpc.server.port=0") +@Testcontainers(disabledWithoutDocker = true) +@Import(GrpcServerStartedEventListener.class) +class SampleGrpcServerApplicationTests { + + @Autowired + private GrpcServerStartedEventListener startedEventListener; + + @Test + @SuppressWarnings("resource") + void test() { + String address = "host.docker.internal:" + this.startedEventListener.getPort(); + try (GenericContainer container = new GenericContainer<>( + DockerImageName.parse("fullstorydev/grpcurl:v1.9.3")) + .withCommand("-d", "{\"name\": \"spring\"}", "--plaintext", address, "HelloWorld/SayHello") + .withStartupCheckStrategy(new IndefiniteWaitOneShotStartupCheckStrategy())) { + container.start(); + assertThat(container.getLogs()).contains("\"message\": \"Hello 'spring'\""); + } + + } + + static class GrpcServerStartedEventListener implements ApplicationListener { + + private int port; + + @Override + public void onApplicationEvent(GrpcServerStartedEvent event) { + this.port = event.getPort(); + } + + int getPort() { + return this.port; + } + + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/HelloWorldService.java b/smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/HelloWorldService.java new file mode 100644 index 000000000000..e8d5de4c01e5 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/HelloWorldService.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcserver; + +import io.grpc.stub.StreamObserver; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import smoketest.grpcserver.proto.HelloReply; +import smoketest.grpcserver.proto.HelloRequest; +import smoketest.grpcserver.proto.HelloWorldGrpc; + +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +@Service +public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase { + + private static Log logger = LogFactory.getLog(HelloWorldService.class); + + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + String name = request.getName(); + logger.info("sayHello " + name); + Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name); + Assert.state(!name.startsWith("internal"), "Internal error"); + String message = "Hello '%s'".formatted(name); + HelloReply reply = HelloReply.newBuilder().setMessage(message).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + + @Override + public void streamHello(HelloRequest request, StreamObserver responseObserver) { + String name = request.getName(); + logger.info("streamHello " + name); + int count = 0; + while (count < 10) { + String message = "Hello(" + count + ") '%s'".formatted(name); + HelloReply reply = HelloReply.newBuilder().setMessage(message).build(); + responseObserver.onNext(reply); + count++; + try { + Thread.sleep(100L); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + responseObserver.onError(ex); + return; + } + } + responseObserver.onCompleted(); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/SampleGrpcServerApplication.java b/smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/SampleGrpcServerApplication.java new file mode 100644 index 000000000000..749d395252d0 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/SampleGrpcServerApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcserver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleGrpcServerApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleGrpcServerApplication.class, args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/package-info.java b/smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/package-info.java new file mode 100644 index 000000000000..7d6c0d0e9c19 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NullMarked +package smoketest.grpcserver; + +import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-grpc-server/src/main/proto/hello.proto b/smoke-test/spring-boot-smoke-test-grpc-server/src/main/proto/hello.proto new file mode 100644 index 000000000000..636901c419dc --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server/src/main/proto/hello.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "smoketest.grpcserver.proto"; +option java_multiple_files = true; + +service HelloWorld { + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc StreamHello(HelloRequest) returns (stream HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/starter/spring-boot-starter-grpc-server/build.gradle b/starter/spring-boot-starter-grpc-server/build.gradle new file mode 100644 index 000000000000..24f9a1f88a34 --- /dev/null +++ b/starter/spring-boot-starter-grpc-server/build.gradle @@ -0,0 +1,28 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring gRPC server" + +dependencies { + api(project(":starter:spring-boot-starter")) + api(project(":module:spring-boot-grpc-server")) + api("io.grpc:grpc-netty") + api("io.grpc:grpc-services") +} From c35b21adb40fc7e5a267523fd542f6946f60af94 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Tue, 10 Feb 2026 18:50:58 -0800 Subject: [PATCH 384/930] Add Spring gRPC client support Add support for Spring gRPC client applications. Closes gh-49045 Co-authored-by: Phillip Webb --- documentation/spring-boot-docs/build.gradle | 1 + .../build.gradle | 3 + module/spring-boot-grpc-client/build.gradle | 53 +++ ...positeChannelFactoryAutoConfiguration.java | 73 ++++ ...onalOnGrpcClientChannelFactoryEnabled.java | 40 ++ .../GrpcChannelBuilderCustomizers.java | 113 +++++ .../GrpcChannelFactoryCustomizer.java | 38 ++ .../GrpcClientAutoConfiguration.java | 98 +++++ .../GrpcClientCodecConfiguration.java | 83 ++++ ...cClientDefaultServiceConfigCustomizer.java | 49 +++ .../autoconfigure/GrpcClientProperties.java | 369 ++++++++++++++++ .../InProcessGrpcClientConfiguration.java | 56 +++ .../NettyGrpcClientConfiguration.java | 60 +++ .../PropertiesChannelCredentialsProvider.java | 65 +++ ...ropertiesGrpcChannelBuilderCustomizer.java | 94 +++++ ...cClientDefaultServiceConfigCustomizer.java | 44 ++ .../PropertiesVirtualTargets.java | 59 +++ .../ShadedNettyGrpcClientConfiguration.java | 58 +++ .../client/autoconfigure/package-info.java | 23 + ...itional-spring-configuration-metadata.json | 23 + ...ot.autoconfigure.AutoConfiguration.imports | 2 + ...eChannelFactoryAutoConfigurationTests.java | 140 +++++++ .../GrpcChannelBuilderCustomizersTests.java | 242 +++++++++++ .../GrpcClientAutoConfigurationTests.java | 396 ++++++++++++++++++ .../GrpcClientPropertiesTests.java | 158 +++++++ .../GrpcCodecConfigurationTests.java | 100 +++++ ...ertiesChannelCredentialsProviderTests.java | 179 ++++++++ ...tiesGrpcChannelBuilderCustomizerTests.java | 209 +++++++++ .../PropertiesVirtualTargetsTests.java | 129 ++++++ .../test/scan/DummyBlockingGrpc.java | 46 ++ .../test/scan/DummyBlockingV2Grpc.java | 46 ++ .../autoconfigure/test/scan/package-info.java | 20 + .../spring-boot-dependencies/build.gradle | 2 + settings.gradle | 3 + .../build.gradle | 63 +++ .../SampleGrpcClientApplication.java | 46 ++ .../smoketest/grpcclient/package-info.java | 20 + .../src/main/proto/hello.proto | 17 + .../src/main/resources/application.yaml | 6 + .../SampleGrpcClientApplicationTests.java | 89 ++++ .../build.gradle | 28 ++ 41 files changed, 3343 insertions(+) create mode 100644 module/spring-boot-grpc-client/build.gradle create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConditionalOnGrpcClientChannelFactoryEnabled.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizers.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientCodecConfiguration.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientDefaultServiceConfigCustomizer.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/InProcessGrpcClientConfiguration.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NettyGrpcClientConfiguration.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesChannelCredentialsProvider.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcChannelBuilderCustomizer.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcClientDefaultServiceConfigCustomizer.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesVirtualTargets.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ShadedNettyGrpcClientConfiguration.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/package-info.java create mode 100644 module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizersTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesChannelCredentialsProviderTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcChannelBuilderCustomizerTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesVirtualTargetsTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/test/scan/DummyBlockingGrpc.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/test/scan/DummyBlockingV2Grpc.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/test/scan/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-client/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-grpc-client/src/main/java/smoketest/grpcclient/SampleGrpcClientApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-client/src/main/java/smoketest/grpcclient/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-client/src/main/proto/hello.proto create mode 100644 smoke-test/spring-boot-smoke-test-grpc-client/src/main/resources/application.yaml create mode 100644 smoke-test/spring-boot-smoke-test-grpc-client/src/test/java/smoketest/grpcclient/SampleGrpcClientApplicationTests.java create mode 100644 starter/spring-boot-starter-grpc-client/build.gradle diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index 48fc323e992f..f18dcbcec105 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -110,6 +110,7 @@ dependencies { implementation(project(path: ":module:spring-boot-data-redis-test")) implementation(project(path: ":module:spring-boot-devtools")) implementation(project(path: ":module:spring-boot-graphql-test")) + implementation(project(path: ":module:spring-boot-grpc-client")) implementation(project(path: ":module:spring-boot-grpc-server")) implementation(project(path: ":module:spring-boot-health")) implementation(project(path: ":module:spring-boot-hibernate")) diff --git a/module/spring-boot-autoconfigure-classic-modules/build.gradle b/module/spring-boot-autoconfigure-classic-modules/build.gradle index b1050d6f593c..2b45c0d9ec76 100644 --- a/module/spring-boot-autoconfigure-classic-modules/build.gradle +++ b/module/spring-boot-autoconfigure-classic-modules/build.gradle @@ -97,6 +97,9 @@ dependencies { api(project(":module:spring-boot-graphql")) { transitive = false } + api(project(":module:spring-boot-grpc-client")) { + transitive = false + } api(project(":module:spring-boot-grpc-server")) { transitive = false } diff --git a/module/spring-boot-grpc-client/build.gradle b/module/spring-boot-grpc-client/build.gradle new file mode 100644 index 000000000000..4bf990aff43d --- /dev/null +++ b/module/spring-boot-grpc-client/build.gradle @@ -0,0 +1,53 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java-library" + id "org.springframework.boot.auto-configuration" + id "org.springframework.boot.configuration-properties" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot gRPC Client" + +dependencies { + api(project(":core:spring-boot")) + api("org.springframework.grpc:spring-grpc-core") + + optional(project(":core:spring-boot-autoconfigure")) + optional(project(":module:spring-boot-micrometer-observation")) + optional("io.grpc:grpc-stub") + optional("io.grpc:grpc-netty") + optional("io.grpc:grpc-netty-shaded") + optional("io.grpc:grpc-inprocess") + optional("io.grpc:grpc-kotlin-stub") { + exclude group: "javax.annotation", module: "javax.annotation-api" + } + optional("io.grpc:grpc-xds") { + exclude group: "javax.annotation", module: "javax.annotation-api" + } + + testImplementation(project(":core:spring-boot-test")) + testImplementation(project(":test-support:spring-boot-test-support")) + testImplementation("org.yaml:snakeyaml") + + testRuntimeOnly("ch.qos.logback:logback-classic") +} + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java new file mode 100644 index 000000000000..35ec955fb808 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import io.grpc.stub.AbstractStub; + +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.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.condition.NoneNestedConditions; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Primary; +import org.springframework.grpc.client.CompositeGrpcChannelFactory; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; +import org.springframework.grpc.client.GrpcChannelFactory; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for a + * {@link CompositeGrpcChannelFactory}. + * + * @author Chris Bono + * @since 4.1.0 + */ +@AutoConfiguration +@ConditionalOnClass({ AbstractStub.class, GrpcChannelBuilderCustomizer.class }) +@ConditionalOnProperty(name = "spring.grpc.client.enabled", matchIfMissing = true) +@Conditional(CompositeChannelFactoryAutoConfiguration.MultipleNonPrimaryChannelFactoriesCondition.class) +public final class CompositeChannelFactoryAutoConfiguration { + + @Bean + @Primary + CompositeGrpcChannelFactory compositeChannelFactory(ObjectProvider channelFactoriesProvider) { + return new CompositeGrpcChannelFactory(channelFactoriesProvider.orderedStream().toList()); + } + + static class MultipleNonPrimaryChannelFactoriesCondition extends NoneNestedConditions { + + MultipleNonPrimaryChannelFactoriesCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnMissingBean(GrpcChannelFactory.class) + static class NoChannelFactoryCondition { + + } + + @ConditionalOnSingleCandidate(GrpcChannelFactory.class) + static class SingleInjectableChannelFactoryCondition { + + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConditionalOnGrpcClientChannelFactoryEnabled.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConditionalOnGrpcClientChannelFactoryEnabled.java new file mode 100644 index 000000000000..2d4dc03cc2a2 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConditionalOnGrpcClientChannelFactoryEnabled.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +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.condition.ConditionalOnBooleanProperty; +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that matches when + * {@code spring.grpc.client.channelfactory.enabled} is {@code true} or missing. + * + * @author Phillip Webb + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@ConditionalOnBooleanProperty(name = "spring.grpc.client.channelfactory.enabled", matchIfMissing = true) +@interface ConditionalOnGrpcClientChannelFactoryEnabled { + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizers.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizers.java new file mode 100644 index 000000000000..f09f34f8abbb --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizers.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; + +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.ManagedChannelBuilder; +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.util.LambdaSafe; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; + +/** + * Invokes the customizations to a {@link ManagedChannelBuilder} based on the provided + * beans. + * + * @author Chris Bono + * @author Phillip Webb + */ +@Order(Ordered.LOWEST_PRECEDENCE) +class GrpcChannelBuilderCustomizers { + + private final List> customizers; + + GrpcChannelBuilderCustomizers(GrpcClientProperties grpcClientProperties, + ObjectProvider compressorRegistry, + ObjectProvider decompressorRegistry, + ObjectProvider> customizers, + ObjectProvider defaultServiceConfigCustomizers) { + this(grpcClientProperties, compressorRegistry.getIfAvailable(), decompressorRegistry.getIfAvailable(), + customizers.orderedStream().toList(), defaultServiceConfigCustomizers.orderedStream().toList()); + } + + GrpcChannelBuilderCustomizers(List> customizers) { + this(null, null, null, customizers, Collections.emptyList()); + } + + GrpcChannelBuilderCustomizers(@Nullable GrpcClientProperties grpcClientProperties, + @Nullable CompressorRegistry compressorRegistry, @Nullable DecompressorRegistry decompressorRegistry, + List> customizers, + List defaultServiceConfigCustomizers) { + List> all = new ArrayList<>(); + addCustomizer(all, compressorRegistry, ManagedChannelBuilder::compressorRegistry); + addCustomizer(all, decompressorRegistry, ManagedChannelBuilder::decompressorRegistry); + if (grpcClientProperties != null) { + all.add(new PropertiesGrpcChannelBuilderCustomizer<>(grpcClientProperties)); + } + all.addAll(customizers); + all.add(customizeDefaultServiceConfig(grpcClientProperties, defaultServiceConfigCustomizers)); + this.customizers = List.copyOf(all); + } + + private static , T> void addCustomizer( + List> customizers, @Nullable T bean, BiConsumer action) { + if (bean != null) { + GrpcChannelBuilderCustomizer customizer = (target, builder) -> action.accept(builder, bean); + customizers.add(customizer); + } + } + + private > GrpcChannelBuilderCustomizer customizeDefaultServiceConfig( + @Nullable GrpcClientProperties properties, + List customizers) { + PropertiesGrpcClientDefaultServiceConfigCustomizer propertiesCustomizer = (properties != null) + ? new PropertiesGrpcClientDefaultServiceConfigCustomizer(properties) : null; + return (target, builder) -> { + Map defaultServiceConfig = new LinkedHashMap<>(); + if (propertiesCustomizer != null) { + propertiesCustomizer.customize(target, defaultServiceConfig); + } + customizers.forEach((customizer) -> customizer.customize(target, defaultServiceConfig)); + if (!defaultServiceConfig.isEmpty()) { + builder.defaultServiceConfig(defaultServiceConfig); + } + }; + } + + > List> forFactory() { + return List.of(this::apply); + } + + @SuppressWarnings("unchecked") + > void apply(String target, T builder) { + LambdaSafe.callbacks(GrpcChannelBuilderCustomizer.class, this.customizers, builder) + .withLogger(GrpcChannelBuilderCustomizers.class) + .invoke((customizer) -> customizer.customize(target, builder)); + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java new file mode 100644 index 000000000000..321097d3312c --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import org.springframework.grpc.client.GrpcChannelFactory; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link GrpcChannelFactory} before it is fully initialized, in particular to tune its + * configuration. + * + * @author Chris Bono + * @since 4.1.0 + */ +@FunctionalInterface +public interface GrpcChannelFactoryCustomizer { + + /** + * Customize the given {@link GrpcChannelFactory}. + * @param factory the factory to customize + */ + void customize(GrpcChannelFactory factory); + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java new file mode 100644 index 000000000000..e16476d8f239 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.ManagedChannelBuilder; +import io.grpc.stub.AbstractStub; + +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.context.properties.EnableConfigurationProperties; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.grpc.client.ChannelCredentialsProvider; +import org.springframework.grpc.client.ClientInterceptorsConfigurer; +import org.springframework.grpc.client.CoroutineStubFactory; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC clients. + * + * @author Dave Syer + * @author Chris Bono + * @author Phillip Webb + * @since 4.1.0 + */ +@AutoConfiguration(before = CompositeChannelFactoryAutoConfiguration.class) +@ConditionalOnClass({ AbstractStub.class, GrpcChannelBuilderCustomizer.class }) +@ConditionalOnProperty(name = "spring.grpc.client.enabled", matchIfMissing = true) +@EnableConfigurationProperties(GrpcClientProperties.class) +@Import({ GrpcClientCodecConfiguration.class, ShadedNettyGrpcClientConfiguration.class, + NettyGrpcClientConfiguration.class, InProcessGrpcClientConfiguration.class }) +public final class GrpcClientAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + ClientInterceptorsConfigurer grpcClientInterceptorsConfigurer(ApplicationContext applicationContext) { + return new ClientInterceptorsConfigurer(applicationContext); + } + + @Bean + @ConditionalOnMissingBean(ChannelCredentialsProvider.class) + PropertiesChannelCredentialsProvider grpcChannelCredentialsProvider(SslBundles bundles, + GrpcClientProperties properties) { + return new PropertiesChannelCredentialsProvider(properties, bundles); + } + + @Bean + > PropertiesGrpcChannelBuilderCustomizer grpcClientPropertiesChannelCustomizer( + GrpcClientProperties properties) { + return new PropertiesGrpcChannelBuilderCustomizer<>(properties); + } + + @Bean + GrpcChannelBuilderCustomizers grpcDefaultServicesChannelBuilderCustomizer(GrpcClientProperties grpcClientProperties, + ObjectProvider compressorRegistry, + ObjectProvider decompressorRegistry, + ObjectProvider> customizers, + ObjectProvider defaultServiceConfigCustomizers) { + return new GrpcChannelBuilderCustomizers(grpcClientProperties, compressorRegistry, decompressorRegistry, + customizers, defaultServiceConfigCustomizers); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = "io.grpc.kotlin.AbstractCoroutineStub") + static class GrpcClientCoroutineStubConfiguration { + + @Bean + @ConditionalOnMissingBean + CoroutineStubFactory coroutineStubFactory() { + return new CoroutineStubFactory(); + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientCodecConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientCodecConfiguration.java new file mode 100644 index 000000000000..0ed44cb87fb1 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientCodecConfiguration.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import java.util.List; + +import io.grpc.Codec; +import io.grpc.Compressor; +import io.grpc.CompressorRegistry; +import io.grpc.Decompressor; +import io.grpc.DecompressorRegistry; +import io.grpc.ServerBuilder; + +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; + +/** + * The configuration that contains all codec related beans for clients. + * + * @author Andrei Lisa + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(Codec.class) +class GrpcClientCodecConfiguration { + + /** + * The compressor registry that is set on the + * {@link ServerBuilder#compressorRegistry(CompressorRegistry) server builder} . + * @param compressors the compressors to use on the registry + * @return a new {@link CompressorRegistry#newEmptyInstance() registry} with the + * specified compressors or the {@link CompressorRegistry#getDefaultInstance() default + * registry} if no custom compressors are available in the application context. + */ + @Bean + @ConditionalOnMissingBean + CompressorRegistry grpcCompressorRegistry(List compressors) { + if (compressors.isEmpty()) { + return CompressorRegistry.getDefaultInstance(); + } + CompressorRegistry registry = CompressorRegistry.newEmptyInstance(); + compressors.forEach(registry::register); + return registry; + } + + /** + * The decompressor registry that is set on the + * {@link ServerBuilder#decompressorRegistry(DecompressorRegistry) server builder}. + * @param decompressors the decompressors to use on the registry + * @return a new {@link DecompressorRegistry#emptyInstance() registry} with the + * specified decompressors or the {@link DecompressorRegistry#getDefaultInstance() + * default registry} if no custom decompressors are available in the application + * context. + */ + @Bean + @ConditionalOnMissingBean + DecompressorRegistry grpcDecompressorRegistry(List decompressors) { + if (decompressors.isEmpty()) { + return DecompressorRegistry.getDefaultInstance(); + } + DecompressorRegistry registry = DecompressorRegistry.emptyInstance(); + for (Decompressor decompressor : decompressors) { + registry = registry.with(decompressor, false); + } + return registry; + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientDefaultServiceConfigCustomizer.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientDefaultServiceConfigCustomizer.java new file mode 100644 index 000000000000..2b39c51f24b4 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientDefaultServiceConfigCustomizer.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import java.util.Map; + +import io.grpc.ManagedChannelBuilder; + +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; +import org.springframework.grpc.client.VirtualTargets; + +/** + * Callback interface that can be used to customize the default service config of the + * {@link GrpcChannelBuilderCustomizer}. + *

    + * This customizer should be used instead of calling + * {@link ManagedChannelBuilder#defaultServiceConfig(Map)} from a + * {@link GrpcChannelBuilderCustomizer} since it allows multiple customizers to update the + * same default service config, rather than having a "last wins" outcome. + * + * @author Phillip Webb + * @since 4.1.0 + * @see GrpcChannelBuilderCustomizer + */ +@FunctionalInterface +public interface GrpcClientDefaultServiceConfigCustomizer { + + /** + * Customize the given default service config. + * @param target the target (which may be a {@link VirtualTargets virtual target}). + * @param defaultServiceConfig the default service config to customize + */ + void customize(String target, Map defaultServiceConfig); + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java new file mode 100644 index 000000000000..f015938d1bef --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java @@ -0,0 +1,369 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.Name; +import org.springframework.boot.convert.DurationUnit; +import org.springframework.util.unit.DataSize; + +/** + * Configuration properties for gRPC clients. + * + * @author Chris Bono + * @author Phillip Webb + * @since 4.1.0 + */ +@ConfigurationProperties("spring.grpc.client") +public class GrpcClientProperties { + + /** + * Map of channel configured by name. + */ + private final Map channel = new LinkedHashMap<>(); + + public Map getChannel() { + return this.channel; + } + + /** + * Channel Properties. + */ + public static class Channel { + + static final String DEFAULT_TARGET = "static://localhost:9090"; + + /** + * The channel target address. + */ + private String target = DEFAULT_TARGET; + + /** + * The custom User-Agent for the channel. + */ + private @Nullable String userAgent; + + /** + * Bypass certificate validation for easier testing (so the remote certificate + * could be anonymous). Should not be set in production. + */ + private boolean bypassCertificateValidation; + + private final Inbound inbound = new Inbound(); + + @Name("default") + private final Default defaultProperties = new Default(); + + private final Idle idle = new Idle(); + + private final Keepalive keepalive = new Keepalive(); + + private final Ssl ssl = new Ssl(); + + private final Health health = new Health(); + + public String getTarget() { + return this.target; + } + + public void setTarget(String target) { + this.target = target; + } + + public @Nullable String getUserAgent() { + return this.userAgent; + } + + public void setUserAgent(@Nullable String userAgent) { + this.userAgent = userAgent; + } + + public boolean isBypassCertificateValidation() { + return this.bypassCertificateValidation; + } + + public void setBypassCertificateValidation(boolean bypassCertificateValidation) { + this.bypassCertificateValidation = bypassCertificateValidation; + } + + public Inbound getInbound() { + return this.inbound; + } + + public Default getDefault() { + return this.defaultProperties; + } + + public Idle getIdle() { + return this.idle; + } + + public Keepalive getKeepalive() { + return this.keepalive; + } + + public Ssl getSsl() { + return this.ssl; + } + + public Health getHealth() { + return this.health; + } + + /** + * In-bound properties. + */ + public static class Inbound { + + private final Message message = new Message(); + + private final Metadata metadata = new Metadata(); + + public Message getMessage() { + return this.message; + } + + public Metadata getMetadata() { + return this.metadata; + } + + /** + * In-bound message properties. + */ + public static class Message { + + /** + * Maximum message size allowed to be received by the channel. Set to '-1' + * to use the highest possible limit (not recommended). + */ + private DataSize maxSize = DataSize.ofBytes(4194304); + + public DataSize getMaxSize() { + return this.maxSize; + } + + public void setMaxSize(DataSize maxSize) { + this.maxSize = maxSize; + } + + } + + /** + * In-bound metadata properties. + */ + public static class Metadata { + + /** + * Maximum metadata size allowed to be received by the channel. Set to + * '-1' to use the highest possible limit (not recommended). + */ + private DataSize maxSize = DataSize.ofBytes(8192); + + public DataSize getMaxSize() { + return this.maxSize; + } + + public void setMaxSize(DataSize maxSize) { + this.maxSize = maxSize; + } + + } + + } + + /** + * Properties for client defaults. + */ + public static class Default { + + /** + * The default deadline for RPCs performed on this channel. + */ + private @Nullable Duration deadline; + + /** + * The load balancing policy the channel should use. + */ + private String loadBalancingPolicy = "round_robin"; + + public @Nullable Duration getDeadline() { + return this.deadline; + } + + public void setDeadline(@Nullable Duration deadline) { + this.deadline = deadline; + } + + public String getLoadBalancingPolicy() { + return this.loadBalancingPolicy; + } + + public void setLoadBalancingPolicy(String loadBalancingPolicy) { + this.loadBalancingPolicy = loadBalancingPolicy; + } + + } + + /** + * Idle properties. + */ + public static class Idle { + + /** + * The duration without ongoing RPCs before going to idle mode. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration timeout = Duration.ofSeconds(20); + + public Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + + } + + /** + * Keep-alive properties. + */ + public static class Keepalive { + + /** + * The delay before sending a keepAlive. Note that shorter intervals increase + * the network burden for the server and this value can not be lower than + * 'permitKeepAliveTime' on the server. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration time = Duration.ofMinutes(5); + + /** + * The default timeout for a keepAlives ping request. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration timeout = Duration.ofSeconds(20); + + /** + * Whether a keepAlive will be performed when there are no outstanding RPC on + * a connection. + */ + private boolean withoutCalls; + + public Duration getTime() { + return this.time; + } + + public void setTime(Duration time) { + this.time = time; + } + + public Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + + public boolean isWithoutCalls() { + return this.withoutCalls; + } + + public void setWithoutCalls(boolean withoutCalls) { + this.withoutCalls = withoutCalls; + } + + } + + /** + * Health properties. + */ + public static class Health { + + /** + * Whether to enable client-side health check for the channel. + */ + private boolean enabled; + + /** + * Name of the service to check health on. + */ + private @Nullable String serviceName; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public @Nullable String getServiceName() { + return this.serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + } + + /** + * SSL properties. + */ + public static class Ssl { + + /** + * Whether to enable SSL support. Enabled automatically if "bundle" is + * provided unless specified otherwise. + */ + private @Nullable Boolean enabled; + + /** + * SSL bundle name. + */ + private @Nullable String bundle; + + public @Nullable Boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(@Nullable Boolean enabled) { + this.enabled = enabled; + } + + public @Nullable String getBundle() { + return this.bundle; + } + + public void setBundle(@Nullable String bundle) { + this.bundle = bundle; + } + + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/InProcessGrpcClientConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/InProcessGrpcClientConfiguration.java new file mode 100644 index 000000000000..6227c48e2671 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/InProcessGrpcClientConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import io.grpc.inprocess.InProcessChannelBuilder; + +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.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.client.ClientInterceptorFilter; +import org.springframework.grpc.client.ClientInterceptorsConfigurer; +import org.springframework.grpc.client.InProcessGrpcChannelFactory; + +/** + * {@link Configuration @Configuration} for an in-process gRPC client. + * + * @author Chris Bono + * @author Phillip Webb + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(InProcessChannelBuilder.class) +@ConditionalOnMissingBean(InProcessGrpcChannelFactory.class) +@ConditionalOnGrpcClientChannelFactoryEnabled +@ConditionalOnProperty(name = "spring.grpc.client.inprocess.enabled", havingValue = "true", matchIfMissing = true) +class InProcessGrpcClientConfiguration { + + @Bean + InProcessGrpcChannelFactory inProcessGrpcChannelFactory(GrpcChannelBuilderCustomizers grpcChannelBuilderCustomizers, + ClientInterceptorsConfigurer interceptorsConfigurer, + ObjectProvider interceptorFilter, + ObjectProvider channelFactoryCustomizers) { + InProcessGrpcChannelFactory factory = new InProcessGrpcChannelFactory( + grpcChannelBuilderCustomizers.forFactory(), interceptorsConfigurer); + interceptorFilter.ifAvailable(factory::setInterceptorFilter); + channelFactoryCustomizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); + return factory; + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NettyGrpcClientConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NettyGrpcClientConfiguration.java new file mode 100644 index 000000000000..f97641a398fc --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NettyGrpcClientConfiguration.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import io.grpc.Channel; +import io.grpc.netty.NettyChannelBuilder; + +import org.springframework.beans.factory.ObjectProvider; +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.env.Environment; +import org.springframework.grpc.client.ChannelCredentialsProvider; +import org.springframework.grpc.client.ClientInterceptorsConfigurer; +import org.springframework.grpc.client.GrpcChannelFactory; +import org.springframework.grpc.client.InProcessGrpcChannelFactory; +import org.springframework.grpc.client.NettyGrpcChannelFactory; + +/** + * {@link Configuration @Configuration} for a Netty gRPC client. + * + * @author Chris Bono + * @author Phillip Webb + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ Channel.class, NettyChannelBuilder.class }) +@ConditionalOnMissingBean(value = GrpcChannelFactory.class, ignored = InProcessGrpcChannelFactory.class) +@ConditionalOnGrpcClientChannelFactoryEnabled +class NettyGrpcClientConfiguration { + + @Bean + NettyGrpcChannelFactory nettyGrpcChannelFactory(Environment environment, GrpcClientProperties properties, + GrpcChannelBuilderCustomizers grpcChannelBuilderCustomizers, + ClientInterceptorsConfigurer interceptorsConfigurer, + ObjectProvider channelFactoryCustomizers, + ChannelCredentialsProvider credentials) { + NettyGrpcChannelFactory factory = new NettyGrpcChannelFactory(grpcChannelBuilderCustomizers.forFactory(), + interceptorsConfigurer); + factory.setCredentialsProvider(credentials); + factory.setVirtualTargets(new PropertiesVirtualTargets(environment, properties)); + channelFactoryCustomizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); + return factory; + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesChannelCredentialsProvider.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesChannelCredentialsProvider.java new file mode 100644 index 000000000000..1ad894b40bf8 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesChannelCredentialsProvider.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import io.grpc.ChannelCredentials; +import io.grpc.InsecureChannelCredentials; +import io.grpc.TlsChannelCredentials; +import io.grpc.TlsChannelCredentials.Builder; + +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel; +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel.Ssl; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.grpc.client.ChannelCredentialsProvider; +import org.springframework.grpc.internal.InsecureTrustManagerFactory; + +/** + * {@link ChannelCredentialsProvider} backed by {@link GrpcClientProperties}. + * + * @param properties the client properties + * @param bundles the SSL bundles + * @author David Syer + * @author Phillip Webb + */ +record PropertiesChannelCredentialsProvider(GrpcClientProperties properties, + SslBundles bundles) implements ChannelCredentialsProvider { + + @Override + public ChannelCredentials getChannelCredentials(String target) { + Channel channel = this.properties.getChannel().get(target); + channel = (channel != null) ? channel : this.properties.getChannel().get("default"); + if (channel == null || isInsecure(channel.getSsl())) { + return InsecureChannelCredentials.create(); + } + Builder builder = TlsChannelCredentials.newBuilder(); + if (channel.getSsl().getBundle() != null) { + SslBundle bundle = this.bundles.getBundle(channel.getSsl().getBundle()); + builder.trustManager(bundle.getManagers().getTrustManagerFactory().getTrustManagers()); + builder.keyManager(bundle.getManagers().getKeyManagerFactory().getKeyManagers()); + } + if (channel.isBypassCertificateValidation()) { + builder.trustManager(InsecureTrustManagerFactory.INSTANCE.getTrustManagers()); + } + return builder.build(); + } + + private boolean isInsecure(Ssl ssl) { + return Boolean.FALSE.equals(ssl.getEnabled()) || (ssl.getBundle() == null && ssl.getEnabled() == null); + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcChannelBuilderCustomizer.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcChannelBuilderCustomizer.java new file mode 100644 index 000000000000..3f3ff314ecac --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcChannelBuilderCustomizer.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import io.grpc.ManagedChannelBuilder; + +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; +import org.springframework.grpc.client.interceptor.DefaultDeadlineSetupClientInterceptor; +import org.springframework.util.Assert; +import org.springframework.util.unit.DataSize; + +/** + * {@link GrpcChannelBuilderCustomizer} that maps {@link GrpcClientProperties} to a + * {@link ManagedChannelBuilder}. + * + * @param the type of the builder + * @param properties the properties to map + * @author David Syer + * @author Chris Bono + * @author Phillip Webb + */ +record PropertiesGrpcChannelBuilderCustomizer>( + GrpcClientProperties properties) implements GrpcChannelBuilderCustomizer { + + private static final Channel STOCK_DEFAULT_CHANNEL = new Channel(); + + @Override + public void customize(String target, T builder) { + Channel channel = getChannel(target); + PropertyMapper map = PropertyMapper.get(); + map.from(channel::getUserAgent).to(builder::userAgent); + map.from(channel.getInbound().getMessage()::getMaxSize).asInt(this::maxSize).to(builder::maxInboundMessageSize); + map.from(channel.getInbound().getMetadata()::getMaxSize) + .asInt(this::maxSize) + .to(builder::maxInboundMetadataSize); + map.from(channel.getDefault()::getDeadline) + .when((deadline) -> deadline.toMillis() > 0L) + .as(DefaultDeadlineSetupClientInterceptor::new) + .to(builder::intercept); + map.from(channel.getDefault()::getLoadBalancingPolicy) + .when((policy) -> supportsLoadBalancing(target, channel)) + .to(builder::defaultLoadBalancingPolicy); + map.from(channel.getIdle()::getTimeout).to(durationProperty(builder::idleTimeout)); + map.from(channel.getKeepalive()::getTime).to(durationProperty(builder::keepAliveTime)); + map.from(channel.getKeepalive()::getTimeout).to(durationProperty(builder::keepAliveTimeout)); + map.from(channel.getKeepalive()::isWithoutCalls).to(builder::keepAliveWithoutCalls); + } + + private Channel getChannel(String target) { + Channel channel = this.properties.getChannel().get(target); + channel = (channel != null) ? channel : this.properties.getChannel().get("default"); + return (channel != null) ? channel : STOCK_DEFAULT_CHANNEL; + } + + private boolean supportsLoadBalancing(String target, Channel channel) { + return !(isUnixOrInProcessTarget(target) || isUnixOrInProcessTarget(channel.getTarget())); + } + + private boolean isUnixOrInProcessTarget(String target) { + return target.startsWith("unix:") || target.startsWith("in-process:"); + } + + Consumer durationProperty(BiConsumer setter) { + return (duration) -> setter.accept(duration.toNanos(), TimeUnit.NANOSECONDS); + } + + private int maxSize(DataSize maxSize) { + long bytes = maxSize.toBytes(); + Assert.state(bytes >= 0 || bytes == -1, () -> "Unsupported max size value " + maxSize); + return (bytes >= 0 && bytes <= Integer.MAX_VALUE) ? (int) bytes : Integer.MAX_VALUE; + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcClientDefaultServiceConfigCustomizer.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcClientDefaultServiceConfigCustomizer.java new file mode 100644 index 000000000000..58026ae3e93b --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcClientDefaultServiceConfigCustomizer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import java.util.Map; + +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel; + +/** + * {@link GrpcClientDefaultServiceConfigCustomizer} to apply {@link GrpcClientProperties}. + * + * @author Chris Bono + * @author Phillip Webb + * @param properties the client properties + */ +record PropertiesGrpcClientDefaultServiceConfigCustomizer( + GrpcClientProperties properties) implements GrpcClientDefaultServiceConfigCustomizer { + + @Override + public void customize(String target, Map defaultServiceConfig) { + Channel channel = this.properties.getChannel().get(target); + channel = (channel != null) ? channel : this.properties.getChannel().get("default"); + if (channel != null && channel.getHealth().isEnabled()) { + String serviceName = channel.getHealth().getServiceName(); + Map healthCheckConfig = Map.of("serviceName", (serviceName != null) ? serviceName : ""); + defaultServiceConfig.put("healthCheckConfig", healthCheckConfig); + } + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesVirtualTargets.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesVirtualTargets.java new file mode 100644 index 000000000000..458925dab47d --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesVirtualTargets.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel; +import org.springframework.core.env.PropertyResolver; +import org.springframework.grpc.client.VirtualTargets; + +/** + * {@link VirtualTargets} supporting named channels from {@link GrpcClientProperties} and + * directly specified targets (which may include property placeholders). + * + * @param propertyResolver the property resolver + * @param properties the client properties + * @author Chris Bono + * @author Phillip Webb + */ +record PropertiesVirtualTargets(PropertyResolver propertyResolver, + GrpcClientProperties properties) implements VirtualTargets { + + @Override + public String getTarget(String target) { + Channel channel = this.properties.getChannel().get(target); + if (channel != null) { + return clean(this.propertyResolver.resolvePlaceholders(channel.getTarget())); + } + if ("default".equals(target)) { + return clean(Channel.DEFAULT_TARGET); + } + target = this.propertyResolver.resolvePlaceholders(target); + if (target.contains(":/") || target.startsWith("unix:")) { + return clean(target); + } + return target; + } + + private String clean(String target) { + if (target.startsWith("static:") || target.startsWith("tcp:")) { + String withoutScheme = target.substring(target.indexOf(":") + 1); + return withoutScheme.replaceFirst("/*", ""); + } + return target; + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ShadedNettyGrpcClientConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ShadedNettyGrpcClientConfiguration.java new file mode 100644 index 000000000000..e63b9659f047 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ShadedNettyGrpcClientConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import org.springframework.beans.factory.ObjectProvider; +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.env.Environment; +import org.springframework.grpc.client.ChannelCredentialsProvider; +import org.springframework.grpc.client.ClientInterceptorsConfigurer; +import org.springframework.grpc.client.GrpcChannelFactory; +import org.springframework.grpc.client.InProcessGrpcChannelFactory; +import org.springframework.grpc.client.ShadedNettyGrpcChannelFactory; + +/** + * {@link Configuration @Configuration} for a Shaded Netty gRPC client. + * + * @author Chris Bono + * @author Phillip Webb + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ io.grpc.netty.shaded.io.netty.channel.Channel.class, + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class }) +@ConditionalOnMissingBean(value = GrpcChannelFactory.class, ignored = InProcessGrpcChannelFactory.class) +@ConditionalOnGrpcClientChannelFactoryEnabled +class ShadedNettyGrpcClientConfiguration { + + @Bean + ShadedNettyGrpcChannelFactory shadedNettyGrpcChannelFactory(Environment environment, + GrpcClientProperties properties, GrpcChannelBuilderCustomizers grpcChannelBuilderCustomizers, + ClientInterceptorsConfigurer interceptorsConfigurer, + ObjectProvider channelFactoryCustomizers, + ChannelCredentialsProvider credentials) { + ShadedNettyGrpcChannelFactory factory = new ShadedNettyGrpcChannelFactory( + grpcChannelBuilderCustomizers.forFactory(), interceptorsConfigurer); + factory.setCredentialsProvider(credentials); + factory.setVirtualTargets(new PropertiesVirtualTargets(environment, properties)); + channelFactoryCustomizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); + return factory; + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/package-info.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/package-info.java new file mode 100644 index 000000000000..66f01b0431b2 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 gRPC clients. + */ +@NullMarked +package org.springframework.boot.grpc.client.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 000000000000..0b2cfcc4bd2f --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,23 @@ +{ + "groups": [], + "properties": [ + { + "name": "spring.grpc.client.channelfactory.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable gRPC channel factory bean auto-configuration.", + "defaultValue": true + }, + { + "name": "spring.grpc.client.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable gRPC client auto-configuration.", + "defaultValue": true + }, + { + "name": "spring.grpc.client.inprocess.enabled", + "type": "java.lang.Boolean", + "description": "Whether to configure the in-process channel factory.", + "defaultValue": true + } + ] +} diff --git a/module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000000..e2d9c74b8c48 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +org.springframework.boot.grpc.client.autoconfigure.CompositeChannelFactoryAutoConfiguration +org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java new file mode 100644 index 000000000000..e3be4daf5cdb --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.netty.NettyChannelBuilder; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +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.core.annotation.Order; +import org.springframework.grpc.client.CompositeGrpcChannelFactory; +import org.springframework.grpc.client.GrpcChannelFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link CompositeChannelFactoryAutoConfiguration}. + * + * @author Chris Bono + */ +class CompositeChannelFactoryAutoConfigurationTests { + + private ApplicationContextRunner contextRunnerWithoutChannelFactories() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class, SslAutoConfiguration.class, + CompositeChannelFactoryAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class, + NettyChannelBuilder.class, InProcessChannelBuilder.class)); + } + + @Test + void whenNoChannelFactoriesDoesNotAutoconfigureComposite() { + this.contextRunnerWithoutChannelFactories() + .run((context) -> assertThat(context).doesNotHaveBean(GrpcChannelFactory.class)); + } + + @Test + void whenSingleChannelFactoryDoesNotAutoconfigureComposite() { + GrpcChannelFactory channelFactory1 = mock(); + this.contextRunnerWithoutChannelFactories() + .withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1) + .run((context) -> assertThat(context).hasSingleBean(GrpcChannelFactory.class) + .getBean(GrpcChannelFactory.class) + .isNotInstanceOf(CompositeGrpcChannelFactory.class) + .isSameAs(channelFactory1)); + } + + @Test + void whenMultipleChannelFactoriesWithPrimaryDoesNotAutoconfigureComposite() { + GrpcChannelFactory channelFactory1 = mock(); + GrpcChannelFactory channelFactory2 = mock(); + this.contextRunnerWithoutChannelFactories() + .withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1) + .withBean("channelFactory2", GrpcChannelFactory.class, () -> channelFactory2, (bd) -> bd.setPrimary(true)) + .run((context) -> { + assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("channelFactory1", "channelFactory2"); + assertThat(context).getBean(GrpcChannelFactory.class) + .isNotInstanceOf(CompositeGrpcChannelFactory.class) + .isSameAs(channelFactory2); + }); + } + + @Test + void whenMultipleChannelFactoriesDoesAutoconfigureComposite() { + GrpcChannelFactory channelFactory1 = mock(); + GrpcChannelFactory channelFactory2 = mock(); + this.contextRunnerWithoutChannelFactories() + .withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1) + .withBean("channelFactory2", GrpcChannelFactory.class, () -> channelFactory2) + .run((context) -> { + assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("channelFactory1", "channelFactory2", "compositeChannelFactory"); + assertThat(context).getBean(GrpcChannelFactory.class).isInstanceOf(CompositeGrpcChannelFactory.class); + }); + } + + @Test + void compositeAutoconfiguredAsExpected() { + this.contextRunnerWithoutChannelFactories() + .withUserConfiguration(MultipleFactoriesTestConfig.class) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(CompositeGrpcChannelFactory.class) + .extracting("channelFactories") + .asInstanceOf(InstanceOfAssertFactories.list(GrpcChannelFactory.class)) + .containsExactly(MultipleFactoriesTestConfig.CHANNEL_FACTORY_BAR, + MultipleFactoriesTestConfig.CHANNEL_FACTORY_BAZ, + MultipleFactoriesTestConfig.CHANNEL_FACTORY_FOO)); + + } + + @Configuration(proxyBeanMethods = false) + static class MultipleFactoriesTestConfig { + + static GrpcChannelFactory CHANNEL_FACTORY_FOO = mock(); + static GrpcChannelFactory CHANNEL_FACTORY_BAR = mock(); + static GrpcChannelFactory CHANNEL_FACTORY_BAZ = mock(); + + @Bean + @Order(3) + GrpcChannelFactory channelFactoryFoo() { + return CHANNEL_FACTORY_FOO; + } + + @Bean + @Order(1) + GrpcChannelFactory channelFactoryBar() { + return CHANNEL_FACTORY_BAR; + } + + @Bean + @Order(2) + GrpcChannelFactory channelFactoryBaz() { + return CHANNEL_FACTORY_BAZ; + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizersTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizersTests.java new file mode 100644 index 000000000000..d49b4bfc5857 --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizersTests.java @@ -0,0 +1,242 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.ManagedChannelBuilder; +import io.grpc.netty.NettyChannelBuilder; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; + +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; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link GrpcChannelBuilderCustomizers}. + * + * @author Chris Bono + * @author Phillip Webb + */ +class GrpcChannelBuilderCustomizersTests { + + @Test + void applyWhenHasProperties() { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channel = new Channel(); + channel.setUserAgent("spring-boot"); + properties.getChannel().put("target", channel); + GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null, + Collections.emptyList(), Collections.emptyList()); + NettyChannelBuilder builder = mock(NettyChannelBuilder.class); + customizers.apply("target", builder); + then(builder).should().userAgent("spring-boot"); + } + + @Test + void applyWhenHasCompressorRegistry() { + CompressorRegistry compressorRegistry = mock(); + GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(null, compressorRegistry, null, + Collections.emptyList(), Collections.emptyList()); + NettyChannelBuilder builder = mock(NettyChannelBuilder.class); + customizers.apply("target", builder); + then(builder).should().compressorRegistry(compressorRegistry); + } + + @Test + void applyWhenHasDecompressorRegistry() { + DecompressorRegistry decompressorRegistry = mock(); + GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(null, null, decompressorRegistry, + Collections.emptyList(), Collections.emptyList()); + NettyChannelBuilder builder = mock(NettyChannelBuilder.class); + customizers.apply("target", builder); + then(builder).should().decompressorRegistry(decompressorRegistry); + } + + @Test + void applyWhenEmptyCustomizersDoesNothing() { + NettyChannelBuilder builder = mock(NettyChannelBuilder.class); + new GrpcChannelBuilderCustomizers(Collections.emptyList()).apply("target", builder); + then(builder).shouldHaveNoInteractions(); + } + + @Test + void applyWhenSimpleChannelBuilder() { + GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers( + List.of(new SimpleChannelBuilderCustomizer())); + NettyChannelBuilder builder = mock(NettyChannelBuilder.class); + customizers.apply("target", builder); + then(builder).should().flowControlWindow(100); + } + + @Test + @SuppressWarnings("unchecked") + void applyWhenGenericCustomizersRespectsGeneric() { + List> list = new ArrayList<>(); + list.add(new TestCustomizer<>()); + list.add(new TestNettyChannelBuilderCustomizer()); + list.add(new TestShadedNettyChannelBuilderCustomizer()); + GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(list); + customizers.apply("target", mock(ManagedChannelBuilder.class)); + assertThat(list.get(0).getCount()).isOne(); + assertThat(list.get(1).getCount()).isZero(); + assertThat(list.get(2).getCount()).isZero(); + customizers.apply("target", mock(NettyChannelBuilder.class)); + assertThat(list.get(0).getCount()).isEqualTo(2); + assertThat(list.get(1).getCount()).isOne(); + assertThat(list.get(2).getCount()).isZero(); + customizers.apply("target", mock(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)); + assertThat(list.get(0).getCount()).isEqualTo(3); + assertThat(list.get(1).getCount()).isOne(); + assertThat(list.get(2).getCount()).isOne(); + } + + @Test + void applyWhenHasGrpcClientDefaultServiceConfigCustomizers() { + GrpcClientDefaultServiceConfigCustomizer defaultConfigCustomizer1 = (target, defaultServiceConfig) -> { + defaultServiceConfig.put("c", "v1"); + defaultServiceConfig.put("c1", "v1"); + }; + GrpcClientDefaultServiceConfigCustomizer defaultConfigCustomizer2 = (target, defaultServiceConfig) -> { + defaultServiceConfig.put("c", "v2"); + defaultServiceConfig.put("c2", "v2"); + }; + GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(null, null, null, + Collections.emptyList(), List.of(defaultConfigCustomizer1, defaultConfigCustomizer2)); + NettyChannelBuilder builder = mock(NettyChannelBuilder.class); + customizers.apply("target", builder); + Map expected = new LinkedHashMap<>(); + expected.put("c", "v2"); + expected.put("c1", "v1"); + expected.put("c2", "v2"); + then(builder).should().defaultServiceConfig(expected); + } + + @Test + void applyWhenHasChannelHealthAddsHealthServiceConfig() { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channel = new Channel(); + channel.getHealth().setEnabled(true); + channel.getHealth().setServiceName("testservice"); + properties.getChannel().put("target", channel); + GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null, + Collections.emptyList(), Collections.emptyList()); + NettyChannelBuilder builder = mock(NettyChannelBuilder.class); + customizers.apply("target", builder); + Map expected = new LinkedHashMap<>(); + expected.put("healthCheckConfig", Map.of("serviceName", "testservice")); + then(builder).should().defaultServiceConfig(expected); + } + + @Test + void applyWhenHasDefaultHealthAddsHealthServiceConfig() { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channel = new Channel(); + channel.getHealth().setEnabled(true); + channel.getHealth().setServiceName("testdefaultservice"); + properties.getChannel().put("default", channel); + GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null, + Collections.emptyList(), Collections.emptyList()); + NettyChannelBuilder builder = mock(NettyChannelBuilder.class); + customizers.apply("target", builder); + Map expected = new LinkedHashMap<>(); + expected.put("healthCheckConfig", Map.of("serviceName", "testdefaultservice")); + then(builder).should().defaultServiceConfig(expected); + } + + @Test + void applyWhenHealthEnabledAndNoServiceNameAddsHealthConfig() { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channel = new Channel(); + channel.getHealth().setEnabled(true); + properties.getChannel().put("target", channel); + GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null, + Collections.emptyList(), Collections.emptyList()); + NettyChannelBuilder builder = mock(NettyChannelBuilder.class); + customizers.apply("target", builder); + Map expected = new LinkedHashMap<>(); + expected.put("healthCheckConfig", Map.of("serviceName", "")); + then(builder).should().defaultServiceConfig(expected); + } + + @Test + void applyWhenNoCustomizersOrHealthDoesSetDefaultServiceConfig() { + GrpcClientProperties properties = new GrpcClientProperties(); + GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null, + Collections.emptyList(), Collections.emptyList()); + NettyChannelBuilder builder = mock(NettyChannelBuilder.class); + customizers.apply("target", builder); + then(builder).should(never()).defaultServiceConfig(any()); + } + + static class SimpleChannelBuilderCustomizer implements GrpcChannelBuilderCustomizer { + + @Override + public void customize(String target, NettyChannelBuilder channelBuilder) { + channelBuilder.flowControlWindow(100); + } + + } + + /** + * Test customizer that will match any {@link GrpcChannelBuilderCustomizer}. + * + * @param the builder type + */ + static class TestCustomizer> implements GrpcChannelBuilderCustomizer { + + private int count; + + @Override + public void customize(String targetOrChannelName, T channelBuilder) { + this.count++; + } + + int getCount() { + return this.count; + } + + } + + /** + * Test customizer that will match only {@link NettyChannelBuilder}. + */ + static class TestNettyChannelBuilderCustomizer extends TestCustomizer { + + } + + /** + * Test customizer that will match only + * {@link io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder}. + */ + static class TestShadedNettyChannelBuilderCustomizer + extends TestCustomizer { + + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java new file mode 100644 index 000000000000..1aecb618ec7d --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java @@ -0,0 +1,396 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import io.grpc.Codec; +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.ManagedChannelBuilder; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.kotlin.AbstractCoroutineStub; +import io.grpc.netty.NettyChannelBuilder; +import io.grpc.stub.AbstractStub; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.AutoConfigurationPackage; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration.GrpcClientCoroutineStubConfiguration; +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel; +import org.springframework.boot.grpc.client.autoconfigure.test.scan.DummyBlockingGrpc; +import org.springframework.boot.ssl.SslBundles; +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.core.annotation.Order; +import org.springframework.grpc.client.ChannelCredentialsProvider; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; +import org.springframework.grpc.client.GrpcChannelFactory; +import org.springframework.grpc.client.InProcessGrpcChannelFactory; +import org.springframework.grpc.client.NettyGrpcChannelFactory; +import org.springframework.grpc.client.ShadedNettyGrpcChannelFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link GrpcClientAutoConfiguration}. + * + * @author Chris Bono + * @author Phillip Webb + */ +@SuppressWarnings({ "unchecked", "rawtypes" }) +class GrpcClientAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class, SslAutoConfiguration.class)); + + private final ApplicationContextRunner contextRunnerWithoutInProcessChannelFactory = this.contextRunner + .withPropertyValues("spring.grpc.client.inprocess.enabled=false"); + + @Test + void whenGrpcStubNotOnClasspathThenAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(AbstractStub.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientAutoConfiguration.class)); + } + + @Test + void whenGrpcKotlinIsNotOnClasspathThenAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(AbstractCoroutineStub.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientCoroutineStubConfiguration.class)); + } + + @Test + void whenClientEnabledPropertySetFalseThenAutoConfigurationIsSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.client.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientAutoConfiguration.class)); + } + + @Test + void whenClientEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GrpcClientAutoConfiguration.class)); + } + + @Test + void whenClientEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.client.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcClientAutoConfiguration.class)); + } + + @Test + void whenHasUserDefinedCredentialsProviderDoesNotAutoConfigureBean() { + ChannelCredentialsProvider customCredentialsProvider = mock(ChannelCredentialsProvider.class); + this.contextRunner + .withBean("customCredentialsProvider", ChannelCredentialsProvider.class, () -> customCredentialsProvider) + .run((context) -> assertThat(context).getBean(ChannelCredentialsProvider.class) + .isSameAs(customCredentialsProvider)); + } + + @Test + void credentialsProviderAutoConfiguredAsExpected() { + this.contextRunner.run((context) -> assertThat(context).getBean(PropertiesChannelCredentialsProvider.class) + .hasFieldOrPropertyWithValue("properties", context.getBean(GrpcClientProperties.class)) + .extracting("bundles") + .isInstanceOf(SslBundles.class)); + } + + @Test + void clientPropertiesAutoConfiguredResolvesPlaceholders() { + this.contextRunner + .withPropertyValues("spring.grpc.client.channel.c1.target=my-server-${channelName}:8888", "channelName=foo") + .run((context) -> assertThat(context).getBean(GrpcClientProperties.class).satisfies((properties) -> { + Channel channel = properties.getChannel().get("c1"); + assertThat(channel).isNotNull(); + assertThat(channel.getTarget()).isEqualTo("my-server-foo:8888"); + })); + } + + @Test + void clientPropertiesChannelCustomizerAutoConfiguredWithHealthAsExpected() { + this.contextRunner + .withPropertyValues("spring.grpc.client.channel.test.health.enabled=true", + "spring.grpc.client.channel.test.health.service-name=my-service") + .run((context) -> { + GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class); + ManagedChannelBuilder builder = Mockito.mock(); + customizers.apply("test", builder); + Map healthCheckConfig = Map.of("healthCheckConfig", Map.of("serviceName", "my-service")); + then(builder).should().defaultServiceConfig(healthCheckConfig); + }); + } + + @Test + void clientPropertiesChannelCustomizerAutoConfiguredWithoutHealthAsExpected() { + this.contextRunner.run((context) -> { + GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class); + ManagedChannelBuilder builder = Mockito.mock(); + customizers.apply("test", builder); + then(builder).should(never()).defaultServiceConfig(anyMap()); + }); + } + + @Test + void compressionCustomizerAutoConfiguredAsExpected() { + this.contextRunner.run((context) -> { + GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class); + CompressorRegistry compressorRegistry = context.getBean(CompressorRegistry.class); + ManagedChannelBuilder builder = Mockito.mock(); + customizers.apply("testChannel", builder); + then(builder).should().compressorRegistry(compressorRegistry); + }); + } + + @Test + void compressionCustomizerWhenNoRegistrry() { + // Codec class guards the imported GrpcCodecConfiguration to hide registry + this.contextRunner.withClassLoader(new FilteredClassLoader(Codec.class)).run((context) -> { + GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class); + ManagedChannelBuilder builder = Mockito.mock(); + customizers.apply("testChannel", builder); + then(builder).should(never()).compressorRegistry(any()); + }); + } + + @Test + void decompressionCustomizerAutoConfiguredAsExpected() { + this.contextRunner.run((context) -> { + GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class); + DecompressorRegistry decompressorRegistry = context.getBean(DecompressorRegistry.class); + ManagedChannelBuilder builder = Mockito.mock(); + customizers.apply("testChannel", builder); + then(builder).should().decompressorRegistry(decompressorRegistry); + }); + } + + @Test + void whenNoDecompressorRegistryThenDecompressionCustomizerIsNotConfigured() { + // Codec class guards the imported GrpcCodecConfiguration to hide registry + this.contextRunner.withClassLoader(new FilteredClassLoader(Codec.class)).run((context) -> { + GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class); + ManagedChannelBuilder builder = Mockito.mock(); + customizers.apply("testChannel", builder); + then(builder).should(never()).compressorRegistry(any()); + }); + } + + @Test + void whenInProcessEnabledPropNotSetDoesAutoconfigureInProcess() { + this.contextRunner.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class) + .containsKey("inProcessGrpcChannelFactory")); + } + + @Test + void whenInProcessEnabledPropSetToTrueDoesAutoconfigureInProcess() { + this.contextRunner.withPropertyValues("spring.grpc.client.inprocess.enabled=true") + .run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class) + .containsKey("inProcessGrpcChannelFactory")); + } + + @Test + void whenInProcessEnabledPropSetToFalseDoesNotAutoconfigureInProcess() { + this.contextRunner.withPropertyValues("spring.grpc.client.inprocess.enabled=false") + .run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class) + .doesNotContainKey("inProcessGrpcChannelFactory")); + } + + @Test + void whenInProcessIsNotOnClasspathDoesNotAutoconfigureInProcess() { + this.contextRunner.withClassLoader(new FilteredClassLoader(InProcessChannelBuilder.class)) + .run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class) + .doesNotContainKey("inProcessGrpcChannelFactory")); + } + + @Test + void whenHasUserDefinedInProcessChannelFactoryDoesNotAutoConfigureBean() { + InProcessGrpcChannelFactory customChannelFactory = mock(); + this.contextRunner + .withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .withBean("customChannelFactory", InProcessGrpcChannelFactory.class, () -> customChannelFactory) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class).isSameAs(customChannelFactory)); + } + + @Test + void whenHasUserDefinedChannelFactoryDoesNotAutoConfigureNettyOrShadedNetty() { + GrpcChannelFactory customChannelFactory = mock(); + this.contextRunnerWithoutInProcessChannelFactory + .withBean("customChannelFactory", GrpcChannelFactory.class, () -> customChannelFactory) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class).isSameAs(customChannelFactory)); + } + + @Test + void userDefinedChannelFactoryWithInProcessChannelFactory() { + GrpcChannelFactory customChannelFactory = mock(); + this.contextRunner.withBean("customChannelFactory", GrpcChannelFactory.class, () -> customChannelFactory) + .run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("customChannelFactory", "inProcessGrpcChannelFactory")); + } + + @Test + void whenShadedAndNonShadedNettyOnClasspathShadedNettyFactoryIsAutoConfigured() { + this.contextRunnerWithoutInProcessChannelFactory + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(ShadedNettyGrpcChannelFactory.class)); + } + + @Test + void shadedNettyWithInProcessChannelFactory() { + this.contextRunner.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("shadedNettyGrpcChannelFactory", "inProcessGrpcChannelFactory")); + } + + @Test + void whenOnlyNonShadedNettyOnClasspathNonShadedNettyFactoryIsAutoConfigured() { + this.contextRunnerWithoutInProcessChannelFactory + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(NettyGrpcChannelFactory.class)); + } + + @Test + void nonShadedNettyWithInProcessChannelFactory() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("nettyGrpcChannelFactory", "inProcessGrpcChannelFactory")); + } + + @Test + void whenShadedNettyAndNettyNotOnClasspathNoChannelFactoryIsAutoConfigured() { + this.contextRunnerWithoutInProcessChannelFactory + .withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcChannelFactory.class)); + } + + @Test + void noChannelFactoryWithInProcessChannelFactory() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(InProcessGrpcChannelFactory.class)); + } + + @Test + void shadedNettyChannelFactoryAutoConfiguredAsExpected() { + this.contextRunnerWithoutInProcessChannelFactory + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(ShadedNettyGrpcChannelFactory.class) + .hasFieldOrPropertyWithValue("credentials", context.getBean(PropertiesChannelCredentialsProvider.class)) + .extracting("targets") + .isInstanceOf(PropertiesVirtualTargets.class)); + } + + @Test + void nettyChannelFactoryAutoConfiguredAsExpected() { + this.contextRunnerWithoutInProcessChannelFactory + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(NettyGrpcChannelFactory.class) + .hasFieldOrPropertyWithValue("credentials", context.getBean(PropertiesChannelCredentialsProvider.class)) + .extracting("targets") + .isInstanceOf(PropertiesVirtualTargets.class)); + } + + @Test + void inProcessChannelFactoryAutoConfiguredAsExpected() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(InProcessGrpcChannelFactory.class) + .extracting("credentials") + .isSameAs(ChannelCredentialsProvider.INSECURE)); + } + + @Test + void shadedNettyChannelFactoryAutoConfiguredWithCustomizers() { + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder builder = mock(); + channelFactoryAutoConfiguredWithCustomizers(this.contextRunnerWithoutInProcessChannelFactory, builder, + ShadedNettyGrpcChannelFactory.class); + } + + @Test + void nettyChannelFactoryAutoConfiguredWithCustomizers() { + NettyChannelBuilder builder = mock(); + channelFactoryAutoConfiguredWithCustomizers( + this.contextRunnerWithoutInProcessChannelFactory.withClassLoader( + new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)), + builder, NettyGrpcChannelFactory.class); + } + + @Test + void inProcessChannelFactoryAutoConfiguredWithCustomizers() { + InProcessChannelBuilder builder = mock(); + channelFactoryAutoConfiguredWithCustomizers( + this.contextRunner.withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)), + builder, InProcessGrpcChannelFactory.class); + } + + private > void channelFactoryAutoConfiguredWithCustomizers( + ApplicationContextRunner contextRunner, ManagedChannelBuilder mockChannelBuilder, + Class expectedChannelFactoryType) { + contextRunner.withUserConfiguration(ChannelBuilderCustomizersConfig.class) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(expectedChannelFactoryType) + .extracting("globalCustomizers", InstanceOfAssertFactories.list(GrpcChannelBuilderCustomizer.class)) + .satisfies((allCustomizers) -> { + allCustomizers.forEach((c) -> c.customize("channel1", mockChannelBuilder)); + InOrder ordered = inOrder(mockChannelBuilder); + ordered.verify(mockChannelBuilder).keepAliveTime(40L, TimeUnit.SECONDS); + ordered.verify(mockChannelBuilder).keepAliveTime(50L, TimeUnit.SECONDS); + })); + } + + @Configuration(proxyBeanMethods = false) + @AutoConfigurationPackage(basePackageClasses = DummyBlockingGrpc.class) + static class AutoConfigurePackagesConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + static class ChannelBuilderCustomizersConfig { + + @Bean + @Order(100) + > GrpcChannelBuilderCustomizer customizerOne() { + return (target, builder) -> builder.keepAliveTime(40L, TimeUnit.SECONDS); + } + + @Bean + @Order(200) + > GrpcChannelBuilderCustomizer customizerTwo() { + return (target, builder) -> builder.keepAliveTime(50L, TimeUnit.SECONDS); + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java new file mode 100644 index 000000000000..5989f9d34f39 --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.env.YamlPropertySourceLoader; +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel; +import org.springframework.boot.testsupport.classpath.resources.WithResource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.unit.DataSize; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link GrpcClientProperties}. + * + * @author Chris Bono + * @author Phillip Webb + */ +class GrpcClientPropertiesTests { + + @Test + @WithResource(name = "client.yaml", content = """ + channel: + a: + target: static://my-server:8888 + b: + user-agent: me""") + void defaultValues() throws Exception { + GrpcClientProperties properties = bind(); + Channel channelA = properties.getChannel().get("a"); + assertThat(channelA).isNotNull(); + assertThat(channelA.getUserAgent()).isNull(); + assertThat(channelA.isBypassCertificateValidation()).isFalse(); + assertThat(channelA.getInbound().getMessage().getMaxSize()).isEqualTo(DataSize.ofBytes(4194304)); + assertThat(channelA.getInbound().getMetadata().getMaxSize()).isEqualTo(DataSize.ofBytes(8192)); + assertThat(channelA.getDefault().getDeadline()).isNull(); + assertThat(channelA.getIdle().getTimeout()).isEqualTo(Duration.ofSeconds(20)); + assertThat(channelA.getKeepalive().getTime()).isEqualTo(Duration.ofMinutes(5)); + assertThat(channelA.getKeepalive().getTimeout()).isEqualTo(Duration.ofSeconds(20)); + assertThat(channelA.getKeepalive().isWithoutCalls()).isFalse(); + assertThat(channelA.getSsl().getEnabled()).isNull(); + assertThat(channelA.getSsl().getBundle()).isNull(); + Channel channelB = properties.getChannel().get("b"); + assertThat(channelB).isNotNull(); + assertThat(channelB.getTarget()).isEqualTo("static://localhost:9090"); + } + + @Test + @WithResource(name = "client.yaml", content = """ + channel: + test: + target: static://my-server:8888 + user-agent: me + bypass-certificate-validation: true + inbound: + message: + max-size: 200MB + metadata: + max-size: 1GB + default: + deadline: 1s + load-balancing-policy: pick_first + idle: + timeout: 1m + keepalive: + time: 200s + timeout: 60000ms + without-calls: true + ssl: + enabled: true + bundle: my-bundle + health: + enabled: true + service-name: my-service""") + void specificProperties() throws Exception { + GrpcClientProperties properties = bind(); + Channel channel = properties.getChannel().get("test"); + assertThat(channel).isNotNull(); + assertThat(channel.getTarget()).isEqualTo("static://my-server:8888"); + assertThat(channel.getUserAgent()).isEqualTo("me"); + assertThat(channel.isBypassCertificateValidation()).isTrue(); + assertThat(channel.getInbound().getMessage().getMaxSize()).isEqualTo(DataSize.ofMegabytes(200)); + assertThat(channel.getInbound().getMetadata().getMaxSize()).isEqualTo(DataSize.ofGigabytes(1)); + assertThat(channel.getDefault().getDeadline()).isEqualTo(Duration.ofSeconds(1)); + assertThat(channel.getIdle().getTimeout()).isEqualTo(Duration.ofMinutes(1)); + assertThat(channel.getKeepalive().getTime()).isEqualTo(Duration.ofSeconds(200)); + assertThat(channel.getKeepalive().getTimeout()).isEqualTo(Duration.ofMillis(60000)); + assertThat(channel.getKeepalive().isWithoutCalls()).isTrue(); + assertThat(channel.getSsl().getEnabled()).isTrue(); + assertThat(channel.getSsl().getBundle()).isEqualTo("my-bundle"); + } + + @Test + @WithResource(name = "client.yaml", content = """ + channel: + test: + idle: + timeout: 1 + keepalive: + time: 60 + timeout: 5""") + void withoutKeepAliveUnitsSpecified() throws Exception { + GrpcClientProperties properties = bind(); + Channel channel = properties.getChannel().get("test"); + assertThat(channel).isNotNull(); + assertThat(channel.getIdle().getTimeout()).isEqualTo(Duration.ofSeconds(1)); + assertThat(channel.getKeepalive().getTime()).isEqualTo(Duration.ofSeconds(60)); + assertThat(channel.getKeepalive().getTimeout()).isEqualTo(Duration.ofSeconds(5)); + } + + @Test + @WithResource(name = "client.yaml", content = """ + channel: + test: + inbound: + message: + max-size: 1000 + metadata: + max-size: 256""") + void withoutInboundSizeUnitsSpecified() throws Exception { + GrpcClientProperties properties = bind(); + Channel channel = properties.getChannel().get("test"); + assertThat(channel).isNotNull(); + assertThat(channel.getInbound().getMessage().getMaxSize()).isEqualTo(DataSize.ofBytes(1000)); + assertThat(channel.getInbound().getMetadata().getMaxSize()).isEqualTo(DataSize.ofBytes(256)); + } + + private GrpcClientProperties bind() throws Exception { + StandardEnvironment environment = new StandardEnvironment(); + new YamlPropertySourceLoader().load("client.yaml", new ClassPathResource("client.yaml")) + .forEach(environment.getPropertySources()::addLast); + return Binder.get(environment) + .bind("", Bindable.of(GrpcClientProperties.class)) + .orElseGet(GrpcClientProperties::new); + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java new file mode 100644 index 000000000000..247168ee92f4 --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import io.grpc.Codec; +import io.grpc.Compressor; +import io.grpc.CompressorRegistry; +import io.grpc.Decompressor; +import io.grpc.DecompressorRegistry; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link GrpcClientCodecConfiguration}. + * + * @author Andrei Lisa + */ +class GrpcCodecConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcClientCodecConfiguration.class)); + + @Test + void whenCodecNotOnClasspathThenAutoconfigurationSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(Codec.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientCodecConfiguration.class)); + } + + @Test + void whenHasCustomCompressorRegistryDoesNotAutoConfigureBean() { + CompressorRegistry customRegistry = mock(); + this.contextRunner.withBean("customCompressorRegistry", CompressorRegistry.class, () -> customRegistry) + .run((context) -> assertThat(context).getBean(CompressorRegistry.class).isSameAs(customRegistry)); + } + + @Test + void compressorRegistryAutoConfiguredAsExpected() { + this.contextRunner.run((context) -> assertThat(context).getBean(CompressorRegistry.class) + .isSameAs(CompressorRegistry.getDefaultInstance())); + } + + @Test + void whenCustomCompressorsThenCompressorRegistryIsNewInstance() { + Compressor compressor = mock(); + given(compressor.getMessageEncoding()).willReturn("foo"); + this.contextRunner.withBean(Compressor.class, () -> compressor).run((context) -> { + assertThat(context).hasSingleBean(CompressorRegistry.class); + CompressorRegistry registry = context.getBean(CompressorRegistry.class); + assertThat(registry).isNotSameAs(CompressorRegistry.getDefaultInstance()); + assertThat(registry.lookupCompressor("foo")).isSameAs(compressor); + }); + } + + @Test + void whenHasCustomDecompressorRegistryDoesNotAutoConfigureBean() { + DecompressorRegistry customRegistry = mock(); + this.contextRunner.withBean("customDecompressorRegistry", DecompressorRegistry.class, () -> customRegistry) + .run((context) -> assertThat(context).getBean(DecompressorRegistry.class).isSameAs(customRegistry)); + } + + @Test + void decompressorRegistryAutoConfiguredAsExpected() { + this.contextRunner.run((context) -> assertThat(context).getBean(DecompressorRegistry.class) + .isSameAs(DecompressorRegistry.getDefaultInstance())); + } + + @Test + void whenCustomDecompressorsThenDecompressorRegistryIsNewInstance() { + Decompressor decompressor = mock(); + given(decompressor.getMessageEncoding()).willReturn("foo"); + this.contextRunner.withBean(Decompressor.class, () -> decompressor).run((context) -> { + assertThat(context).hasSingleBean(DecompressorRegistry.class); + DecompressorRegistry registry = context.getBean(DecompressorRegistry.class); + assertThat(registry).isNotSameAs(DecompressorRegistry.getDefaultInstance()); + assertThat(registry.lookupDecompressor("foo")).isSameAs(decompressor); + }); + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesChannelCredentialsProviderTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesChannelCredentialsProviderTests.java new file mode 100644 index 000000000000..45e08db1dcdb --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesChannelCredentialsProviderTests.java @@ -0,0 +1,179 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; + +import io.grpc.ChannelCredentials; +import io.grpc.InsecureChannelCredentials; +import io.grpc.TlsChannelCredentials; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.ssl.SslManagerBundle; +import org.springframework.grpc.client.ChannelCredentialsProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PropertiesChannelCredentialsProvider}. + * + * @author Phillip Webb + */ +class PropertiesChannelCredentialsProviderTests { + + private final TrustManager[] trustManagers = { mock() }; + + private final KeyManager[] keyManagers = { mock() }; + + @Test + void getChannelCredentialsWhenTargetMatchesChannel() { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channelProperties = new Channel(); + channelProperties.getSsl().setEnabled(true); + properties.getChannel().put("test", channelProperties); + SslBundles sslBundles = new DefaultSslBundleRegistry(); + ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles); + TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test"); + assertThat(credentials.getTrustManagers()).isNull(); + assertThat(credentials.getKeyManagers()).isNull(); + } + + @Test + void getChannelCredentialsWhenTargetDoesNotMatchChannelAndHasDefault() { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channelProperties = new Channel(); + channelProperties.getSsl().setEnabled(true); + properties.getChannel().put("default", channelProperties); + SslBundles sslBundles = new DefaultSslBundleRegistry(); + ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles); + TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test"); + assertThat(credentials.getTrustManagers()).isNull(); + assertThat(credentials.getKeyManagers()).isNull(); + } + + @Test + void getChannelCredentialsWhenTargetDoesNotMatchChannelAndHasNoDefaultUsesInsecure() { + GrpcClientProperties properties = new GrpcClientProperties(); + SslBundles sslBundles = new DefaultSslBundleRegistry(); + ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles); + ChannelCredentials credentials = provider.getChannelCredentials("test"); + assertThat(credentials).isInstanceOf(InsecureChannelCredentials.class); + } + + @Test + void getChannelCredentialsWhenSslExplictlyDisabled() { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channelProperties = new Channel(); + channelProperties.getSsl().setEnabled(false); + properties.getChannel().put("test", channelProperties); + SslBundles sslBundles = new DefaultSslBundleRegistry(); + ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles); + ChannelCredentials credentials = provider.getChannelCredentials("test"); + assertThat(credentials).isInstanceOf(InsecureChannelCredentials.class); + } + + @Test + void getChannelCredentialsWhenSslExplictlyEnabledAndNoBundle() { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channelProperties = new Channel(); + channelProperties.getSsl().setEnabled(true); + properties.getChannel().put("test", channelProperties); + SslBundles sslBundles = new DefaultSslBundleRegistry(); + ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles); + TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test"); + assertThat(credentials.getTrustManagers()).isNull(); + assertThat(credentials.getKeyManagers()).isNull(); + } + + @Test + void getChannelCredentialsWhenNoSslEnabledSetButHasBundle() { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channelProperties = new Channel(); + channelProperties.getSsl().setBundle("test"); + properties.getChannel().put("test", channelProperties); + SslBundles sslBundles = new DefaultSslBundleRegistry("test", mockBundle()); + ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles); + TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test"); + assertThat(credentials.getTrustManagers()).containsExactly(this.trustManagers); + assertThat(credentials.getKeyManagers()).containsExactly(this.keyManagers); + } + + @Test + void getChannelCredentialsWhenNoSslEnabledSetAndNoBundle() { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channelProperties = new Channel(); + properties.getChannel().put("test", channelProperties); + SslBundles sslBundles = new DefaultSslBundleRegistry(); + ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles); + ChannelCredentials credentials = provider.getChannelCredentials("test"); + assertThat(credentials).isInstanceOf(InsecureChannelCredentials.class); + } + + @Test + void getChannelCredentialsWhenSslEnabledAndHasBundle() { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channelProperties = new Channel(); + channelProperties.getSsl().setEnabled(true); + channelProperties.getSsl().setBundle("test"); + properties.getChannel().put("test", channelProperties); + SslBundles sslBundles = new DefaultSslBundleRegistry("test", mockBundle()); + ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles); + TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test"); + assertThat(credentials.getTrustManagers()).containsExactly(this.trustManagers); + assertThat(credentials.getKeyManagers()).containsExactly(this.keyManagers); + } + + @Test + void getChannelCredentialsWhenBypassCertificateValidation() { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channelProperties = new Channel(); + channelProperties.setBypassCertificateValidation(true); + channelProperties.getSsl().setBundle("test"); + properties.getChannel().put("test", channelProperties); + SslBundles sslBundles = new DefaultSslBundleRegistry("test", mockBundle()); + ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles); + TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test"); + TrustManager trustManager = credentials.getTrustManagers().get(0); + assertThat(trustManager.getClass().getName()).contains("InsecureTrustManager"); + assertThat(((X509ExtendedTrustManager) trustManager).getAcceptedIssuers()).isEmpty(); + assertThat(credentials.getKeyManagers()).containsExactly(this.keyManagers); + } + + private SslBundle mockBundle() { + SslBundle bundle = mock(); + SslManagerBundle managerBundle = mock(); + TrustManagerFactory trustManagerFactory = mock(); + KeyManagerFactory keyManagerFactory = mock(); + given(bundle.getManagers()).willReturn(managerBundle); + given(managerBundle.getTrustManagerFactory()).willReturn(trustManagerFactory); + given(managerBundle.getKeyManagerFactory()).willReturn(keyManagerFactory); + given(trustManagerFactory.getTrustManagers()).willReturn(this.trustManagers); + given(keyManagerFactory.getKeyManagers()).willReturn(this.keyManagers); + return bundle; + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcChannelBuilderCustomizerTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcChannelBuilderCustomizerTests.java new file mode 100644 index 000000000000..1c29640301ad --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcChannelBuilderCustomizerTests.java @@ -0,0 +1,209 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import io.grpc.ClientInterceptor; +import io.grpc.ManagedChannelBuilder; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; +import org.springframework.grpc.client.interceptor.DefaultDeadlineSetupClientInterceptor; +import org.springframework.util.unit.DataSize; + +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.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link PropertiesGrpcChannelBuilderCustomizer}. + * + * @author Phillip Webb + */ +class PropertiesGrpcChannelBuilderCustomizerTests { + + @Test + > void customizeWhenHasMatchingChannel() { + GrpcClientProperties properties = new GrpcClientProperties(); + properties.getChannel().put("test", createTestChannelProperties()); + GrpcChannelBuilderCustomizer customizer = new PropertiesGrpcChannelBuilderCustomizer<>(properties); + T builder = mock(); + customizer.customize("test", builder); + assertMapped(builder); + } + + @Test + > void customizeWhenHasDefaultChannel() { + GrpcClientProperties properties = new GrpcClientProperties(); + properties.getChannel().put("default", createTestChannelProperties()); + GrpcChannelBuilderCustomizer customizer = new PropertiesGrpcChannelBuilderCustomizer<>(properties); + T builder = mock(); + customizer.customize("test", builder); + assertMapped(builder); + } + + @Test + > void customizeWhenHasNoMatchAndNoDefault() { + GrpcClientProperties properties = new GrpcClientProperties(); + properties.getChannel().put("other", createTestChannelProperties()); + GrpcChannelBuilderCustomizer customizer = new PropertiesGrpcChannelBuilderCustomizer<>(properties); + T builder = mock(); + customizer.customize("test", builder); + assertMappedStockDefaults(builder); + } + + @Test + > void customizeWhenChannelDoesNotSupportLoadBalancingDoesNotMapDefaultLoadBalancer() { + assertNoLoadBalancerMappedBasedOnChannel("unix:test"); + assertNoLoadBalancerMappedBasedOnChannel("in-process:test"); + } + + private > void assertNoLoadBalancerMappedBasedOnChannel(String target) { + T builder = getBuilder((channelProperties) -> { + channelProperties.setTarget(target); + channelProperties.getDefault().setLoadBalancingPolicy("testlbp"); + }); + then(builder).should(never()).defaultLoadBalancingPolicy(any()); + } + + @Test + > void customizeWhenTargetDoesNotSupportLoadBalancingDoesNotMapDefaultLoadBalancer() { + GrpcChannelBuilderCustomizer customizer = getCustomizer( + (channelProperties) -> channelProperties.setTarget("static://localhost:1234")); + assertNoLoadBalancerMappedBasedOnTarget(customizer, "unix:test"); + assertNoLoadBalancerMappedBasedOnTarget(customizer, "in-process:test"); + } + + private > void assertNoLoadBalancerMappedBasedOnTarget( + GrpcChannelBuilderCustomizer customizer, String target) { + T builder = mock(); + customizer.customize(target, builder); + then(builder).should(never()).defaultLoadBalancingPolicy(any()); + } + + @Test + > void customizeWhenMaxMessageSizeIsMinusOneUsesMaxValue() { + T builder = getBuilder( + (channelProperties) -> channelProperties.getInbound().getMessage().setMaxSize(DataSize.ofBytes(-1))); + then(builder).should().maxInboundMessageSize(Integer.MAX_VALUE); + } + + @Test + > void customizeWhenMaxMessageSizeIsTooBigUsesMaxValue() { + T builder = getBuilder((channelProperties) -> channelProperties.getInbound() + .getMessage() + .setMaxSize(DataSize.ofBytes((long) Integer.MAX_VALUE + 100))); + then(builder).should().maxInboundMessageSize(Integer.MAX_VALUE); + } + + @Test + > void customizeWhenMaxMessageSizeIsNegativeAndNotMinusOneThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> getBuilder( + (channelProperties) -> channelProperties.getInbound().getMessage().setMaxSize(DataSize.ofBytes(-2)))) + .withMessage("Unsupported max size value -2B"); + } + + @Test + > void customizeWhenMaxMetadataSizeIsMinusOneUsesMaxValue() { + T builder = getBuilder( + (channelProperties) -> channelProperties.getInbound().getMetadata().setMaxSize(DataSize.ofBytes(-1))); + then(builder).should().maxInboundMetadataSize(Integer.MAX_VALUE); + } + + @Test + > void customizeWhenMaxMetadataSizeIsTooBigUsesMaxValue() { + T builder = getBuilder((channelProperties) -> channelProperties.getInbound() + .getMetadata() + .setMaxSize(DataSize.ofBytes((long) Integer.MAX_VALUE + 100))); + then(builder).should().maxInboundMetadataSize(Integer.MAX_VALUE); + } + + @Test + > void customizeWhenMaxMetadataSizeIsNegativeAndNotMinusOneThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> getBuilder( + (channelProperties) -> channelProperties.getInbound().getMetadata().setMaxSize(DataSize.ofBytes(-2)))) + .withMessage("Unsupported max size value -2B"); + } + + private > T getBuilder(Consumer setup) { + GrpcChannelBuilderCustomizer customizer = getCustomizer(setup); + T builder = mock(); + customizer.customize("test", builder); + return builder; + } + + private > GrpcChannelBuilderCustomizer getCustomizer( + Consumer setup) { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channelProperties = new Channel(); + setup.accept(channelProperties); + properties.getChannel().put("test", channelProperties); + GrpcChannelBuilderCustomizer customizer = new PropertiesGrpcChannelBuilderCustomizer<>(properties); + return customizer; + } + + private Channel createTestChannelProperties() { + Channel properties = new Channel(); + properties.setUserAgent("testua"); + properties.getInbound().getMessage().setMaxSize(DataSize.ofBytes(10)); + properties.getInbound().getMetadata().setMaxSize(DataSize.ofBytes(20)); + properties.getDefault().setDeadline(Duration.ofMinutes(5)); + properties.getDefault().setLoadBalancingPolicy("testlbp"); + properties.getIdle().setTimeout(Duration.ofMinutes(6)); + properties.getKeepalive().setTime(Duration.ofMinutes(7)); + properties.getKeepalive().setTimeout(Duration.ofMinutes(8)); + properties.getKeepalive().setWithoutCalls(true); + return properties; + } + + private > void assertMapped(T builder) { + then(builder).should().userAgent("testua"); + then(builder).should().maxInboundMessageSize(10); + then(builder).should().maxInboundMetadataSize(20); + ArgumentCaptor interceptors = ArgumentCaptor.captor(); + then(builder).should().intercept(interceptors.capture()); + ClientInterceptor interceptor = interceptors.getValue()[0]; + assertThat(interceptor).isInstanceOf(DefaultDeadlineSetupClientInterceptor.class) + .extracting("defaultDeadline") + .isEqualTo(Duration.ofMinutes(5)); + then(builder).should().defaultLoadBalancingPolicy("testlbp"); + then(builder).should().idleTimeout(360000000000L, TimeUnit.NANOSECONDS); + then(builder).should().keepAliveTime(420000000000L, TimeUnit.NANOSECONDS); + then(builder).should().keepAliveTimeout(480000000000L, TimeUnit.NANOSECONDS); + then(builder).should().keepAliveWithoutCalls(true); + } + + private > void assertMappedStockDefaults(T builder) { + then(builder).should().maxInboundMessageSize(4194304); + then(builder).should().maxInboundMetadataSize(8192); + then(builder).should().defaultLoadBalancingPolicy("round_robin"); + then(builder).should().idleTimeout(20000000000L, TimeUnit.NANOSECONDS); + then(builder).should().keepAliveTime(300000000000L, TimeUnit.NANOSECONDS); + then(builder).should().keepAliveTimeout(20000000000L, TimeUnit.NANOSECONDS); + then(builder).should().keepAliveWithoutCalls(false); + then(builder).shouldHaveNoMoreInteractions(); + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesVirtualTargetsTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesVirtualTargetsTests.java new file mode 100644 index 000000000000..ebd497660b23 --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesVirtualTargetsTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel; +import org.springframework.grpc.client.VirtualTargets; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PropertiesVirtualTargets}. + * + * @author Chris Bono + * @author Phillip Webb + */ +class PropertiesVirtualTargetsTests { + + @Test + void getTargetWhenHasMatchingChannel() { + GrpcClientProperties properties = createProperties("test", "my-server:8888"); + VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties); + assertThat(targets.getTarget("test")).isEqualTo("my-server:8888"); + } + + @Test + void getTargetWhenDefaultAndDefaultChannelDefined() { + GrpcClientProperties properties = createProperties("default", "my-server:8888"); + VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties); + assertThat(targets.getTarget("default")).isEqualTo("my-server:8888"); + } + + @Test + void getTargetWhenDefaultAndNoDefaultChannelDefined() { + GrpcClientProperties properties = new GrpcClientProperties(); + VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties); + assertThat(targets.getTarget("default")).isEqualTo("localhost:9090"); + } + + @Test + void getTargetWhenChannelHasStaticTargetReturnsStrippedTarget() { + GrpcClientProperties properties = createProperties("test", "static://my-server:8888"); + VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties); + assertThat(targets.getTarget("test")).isEqualTo("my-server:8888"); + } + + @Test + void getTargetWhenChannelHasTcpTargetReturnsStrippedTarget() { + GrpcClientProperties properties = createProperties("test", "tcp://my-server:8888"); + VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties); + assertThat(targets.getTarget("test")).isEqualTo("my-server:8888"); + } + + @Test + void getTargetWhenChannelHasOtherUrlTarget() { + GrpcClientProperties properties = createProperties("test", "foo://my-server:8888"); + VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties); + assertThat(targets.getTarget("test")).isEqualTo("foo://my-server:8888"); + } + + @Test + void getTargetWhenStaticReturnsStripped() { + GrpcClientProperties properties = new GrpcClientProperties(); + VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties); + assertThat(targets.getTarget("static://my-server:8888")).isEqualTo("my-server:8888"); + } + + @Test + void getTargetWhenTcpReturnsStripped() { + GrpcClientProperties properties = new GrpcClientProperties(); + VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties); + assertThat(targets.getTarget("tcp://my-server:8888")).isEqualTo("my-server:8888"); + } + + @Test + void getTargetWhenUnixUrlDoesNotPrependStatic() { + GrpcClientProperties properties = new GrpcClientProperties(); + VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties); + assertThat(targets.getTarget("foo://bar")).isEqualTo("foo://bar"); + } + + @Test + void getTargetWhenUrlReturnsAsIs() { + GrpcClientProperties properties = new GrpcClientProperties(); + VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties); + assertThat(targets.getTarget("unix:foo")).isEqualTo("unix:foo"); + } + + @Test + void getTargetUrlWhenHasColonWithoutSlashReturnsAsIs() { + GrpcClientProperties properties = new GrpcClientProperties(); + VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties); + assertThat(targets.getTarget("localhost:123/bar")).isEqualTo("localhost:123/bar"); + } + + @Test + void getTargetWhenNotChannelNameResolvesPlaceholders() { + GrpcClientProperties properties = new GrpcClientProperties(); + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("channelName", "foo"); + VirtualTargets targets = new PropertiesVirtualTargets(environment, properties); + assertThat(targets.getTarget("my-server-${channelName}:8888")).isEqualTo("my-server-foo:8888"); + } + + private GrpcClientProperties createProperties(String name, String target) { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channel = new Channel(); + channel.setTarget(target); + properties.getChannel().put(name, channel); + return properties; + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/test/scan/DummyBlockingGrpc.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/test/scan/DummyBlockingGrpc.java new file mode 100644 index 000000000000..a54e133f3422 --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/test/scan/DummyBlockingGrpc.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure.test.scan; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.stub.AbstractBlockingStub; +import io.grpc.stub.AbstractStub.StubFactory; + +public final class DummyBlockingGrpc { + + private DummyBlockingGrpc() { + } + + public static DummyBlockingStub newBlockingStub(io.grpc.Channel channel) { + return AbstractBlockingStub.newStub((StubFactory) DummyBlockingStub::new, channel); + } + + public static class DummyBlockingStub extends AbstractBlockingStub { + + protected DummyBlockingStub(Channel channel, CallOptions callOptions) { + super(channel, callOptions); + } + + @Override + protected DummyBlockingStub build(Channel channel, CallOptions callOptions) { + return new DummyBlockingStub(channel, callOptions); + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/test/scan/DummyBlockingV2Grpc.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/test/scan/DummyBlockingV2Grpc.java new file mode 100644 index 000000000000..a405adc2adfd --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/test/scan/DummyBlockingV2Grpc.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure.test.scan; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.stub.AbstractBlockingStub; +import io.grpc.stub.AbstractStub.StubFactory; + +public final class DummyBlockingV2Grpc { + + private DummyBlockingV2Grpc() { + } + + public static DummyBlockingV2Stub newBlockingV2Stub(io.grpc.Channel channel) { + return AbstractBlockingStub.newStub((StubFactory) DummyBlockingV2Stub::new, channel); + } + + public static class DummyBlockingV2Stub extends AbstractBlockingStub { + + protected DummyBlockingV2Stub(Channel channel, CallOptions callOptions) { + super(channel, callOptions); + } + + @Override + protected DummyBlockingV2Stub build(Channel channel, CallOptions callOptions) { + return new DummyBlockingV2Stub(channel, callOptions); + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/test/scan/package-info.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/test/scan/package-info.java new file mode 100644 index 000000000000..caad90364c6a --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/test/scan/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NullMarked +package org.springframework.boot.grpc.client.autoconfigure.test.scan; + +import org.jspecify.annotations.NullMarked; diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ceba585781f3..3569519a5d6f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2163,6 +2163,7 @@ bom { "spring-boot-freemarker", "spring-boot-graphql", "spring-boot-graphql-test", + "spring-boot-grpc-client", "spring-boot-grpc-server", "spring-boot-groovy-templates", "spring-boot-gson", @@ -2294,6 +2295,7 @@ bom { "spring-boot-starter-freemarker-test", "spring-boot-starter-graphql", "spring-boot-starter-graphql-test", + "spring-boot-starter-grpc-client", "spring-boot-starter-grpc-server", "spring-boot-starter-groovy-templates", "spring-boot-starter-groovy-templates-test", diff --git a/settings.gradle b/settings.gradle index da12a2937fd1..8b44e0b5e4ea 100644 --- a/settings.gradle +++ b/settings.gradle @@ -120,6 +120,7 @@ include "module:spring-boot-flyway" include "module:spring-boot-freemarker" include "module:spring-boot-graphql" include "module:spring-boot-graphql-test" +include "module:spring-boot-grpc-client" include "module:spring-boot-grpc-server" include "module:spring-boot-groovy-templates" include "module:spring-boot-gson" @@ -269,6 +270,7 @@ include "starter:spring-boot-starter-freemarker" include "starter:spring-boot-starter-freemarker-test" include "starter:spring-boot-starter-graphql" include "starter:spring-boot-starter-graphql-test" +include "starter:spring-boot-starter-grpc-client" include "starter:spring-boot-starter-grpc-server" include "starter:spring-boot-starter-groovy-templates" include "starter:spring-boot-starter-groovy-templates-test" @@ -423,6 +425,7 @@ include ":smoke-test:spring-boot-smoke-test-data-rest" include ":smoke-test:spring-boot-smoke-test-devtools" include ":smoke-test:spring-boot-smoke-test-flyway" include ":smoke-test:spring-boot-smoke-test-graphql" +include ":smoke-test:spring-boot-smoke-test-grpc-client" include ":smoke-test:spring-boot-smoke-test-grpc-server" include ":smoke-test:spring-boot-smoke-test-grpc-server-netty-shaded" include ":smoke-test:spring-boot-smoke-test-grpc-server-servlet" diff --git a/smoke-test/spring-boot-smoke-test-grpc-client/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-client/build.gradle new file mode 100644 index 000000000000..a0e75c6aff3d --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-client/build.gradle @@ -0,0 +1,63 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java" + id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "org.springframework.boot.docker-test" +} + +description = "Spring Boot gRPC client smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-grpc-client")) + + testImplementation(project(":starter:spring-boot-starter-test")) +} + +def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom") +def grpcJava = dependenciesBom.getLibrary("Grpc Java") +def protobufJava = dependenciesBom.getLibrary("Protobuf Java") + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +nullability { + requireExplicitNullMarking = false +} + +configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all { + extendsFrom(configurations.dependencyManagement) +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protobufJava.version}" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}" + } + } + generateProtoTasks { + all()*.plugins { + grpc { + option '@generated=omit' + } + } + } +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-client/src/main/java/smoketest/grpcclient/SampleGrpcClientApplication.java b/smoke-test/spring-boot-smoke-test-grpc-client/src/main/java/smoketest/grpcclient/SampleGrpcClientApplication.java new file mode 100644 index 000000000000..98480abf4873 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-client/src/main/java/smoketest/grpcclient/SampleGrpcClientApplication.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcclient; + +import smoketest.grpcclient.proto.HelloReply; +import smoketest.grpcclient.proto.HelloRequest; +import smoketest.grpcclient.proto.HelloWorldGrpc.HelloWorldBlockingStub; + +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.grpc.client.ImportGrpcClients; + +@SpringBootApplication +@ImportGrpcClients(types = HelloWorldBlockingStub.class) +public class SampleGrpcClientApplication { + + @Bean + ApplicationRunner applicationRunner(HelloWorldBlockingStub hello) { + return (args) -> { + HelloRequest request = HelloRequest.newBuilder().setName("Spring").build(); + HelloReply reply = hello.sayHello(request); + System.out.println(">>> " + reply.getMessage()); + }; + } + + public static void main(String[] args) { + SpringApplication.run(SampleGrpcClientApplication.class, args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-client/src/main/java/smoketest/grpcclient/package-info.java b/smoke-test/spring-boot-smoke-test-grpc-client/src/main/java/smoketest/grpcclient/package-info.java new file mode 100644 index 000000000000..56b3d8318d74 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-client/src/main/java/smoketest/grpcclient/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NullMarked +package smoketest.grpcclient; + +import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-grpc-client/src/main/proto/hello.proto b/smoke-test/spring-boot-smoke-test-grpc-client/src/main/proto/hello.proto new file mode 100644 index 000000000000..8c0bed63c617 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-client/src/main/proto/hello.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "smoketest.grpcclient.proto"; +option java_multiple_files = true; + +service HelloWorld { + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc StreamHello(HelloRequest) returns (stream HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-client/src/main/resources/application.yaml b/smoke-test/spring-boot-smoke-test-grpc-client/src/main/resources/application.yaml new file mode 100644 index 000000000000..0ec63d466687 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-client/src/main/resources/application.yaml @@ -0,0 +1,6 @@ +spring: + grpc: + client: + channel: + default: + target: "static://localhost:9090" diff --git a/smoke-test/spring-boot-smoke-test-grpc-client/src/test/java/smoketest/grpcclient/SampleGrpcClientApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-client/src/test/java/smoketest/grpcclient/SampleGrpcClientApplicationTests.java new file mode 100644 index 000000000000..a9020ba2c965 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-client/src/test/java/smoketest/grpcclient/SampleGrpcClientApplicationTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcclient; + +import java.time.Duration; +import java.util.Collections; +import java.util.Map; + +import io.grpc.stub.StreamObserver; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import smoketest.grpcclient.SampleGrpcClientApplicationTests.MockServerInitializer; +import smoketest.grpcclient.proto.HelloReply; +import smoketest.grpcclient.proto.HelloRequest; +import smoketest.grpcclient.proto.HelloWorldGrpc; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.MapPropertySource; +import org.springframework.grpc.server.NettyGrpcServerFactory; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ContextConfiguration(initializers = MockServerInitializer.class) +@ExtendWith(OutputCaptureExtension.class) +class SampleGrpcClientApplicationTests { + + @Test + void applicationRunsAndCallsGrpcServer(CapturedOutput output) { + assertThat(output).contains(">>> Hello 'Spring'"); + } + + static class MockServerInitializer implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + NettyGrpcServerFactory serverFactory = new NettyGrpcServerFactory("*:0", Collections.emptyList(), null, + null, null); + HelloWorldService helloWorldService = new HelloWorldService(); + serverFactory.addService(helloWorldService.bindService()); + GrpcServerLifecycle lifecycle = new GrpcServerLifecycle(serverFactory, Duration.ofSeconds(30), + applicationContext); + lifecycle.start(); + String target = "static://localhost:%s".formatted(lifecycle.getPort()); + applicationContext.getEnvironment() + .getPropertySources() + .addFirst(new MapPropertySource("grpc", Map.of("spring.grpc.client.channel.default.target", target))); + applicationContext.getBeanFactory().registerSingleton("grpcServerLifecyce", lifecycle); + } + + } + + static class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase { + + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + HelloReply reply = HelloReply.newBuilder().setMessage("Hello '%s'".formatted(request.getName())).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + + @Override + public void streamHello(HelloRequest request, StreamObserver responseObserver) { + throw new IllegalStateException(); + } + + } + +} diff --git a/starter/spring-boot-starter-grpc-client/build.gradle b/starter/spring-boot-starter-grpc-client/build.gradle new file mode 100644 index 000000000000..cdc21b419edb --- /dev/null +++ b/starter/spring-boot-starter-grpc-client/build.gradle @@ -0,0 +1,28 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring gRPC client" + +dependencies { + api(project(":starter:spring-boot-starter")) + api(project(":module:spring-boot-grpc-client")) + api("io.grpc:grpc-netty") + api("io.grpc:grpc-stub") +} From be3a1b71178d4c674d4636a236ebea9d850f21b4 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Thu, 12 Feb 2026 16:15:13 -0800 Subject: [PATCH 385/930] Add Spring gRPC test support Add a testing module that can be used for testing Spring gRPC server or client applications. Closes gh-49046 Co-authored-by: Phillip Webb --- documentation/spring-boot-docs/build.gradle | 1 + .../reference/pages/testing/test-modules.adoc | 3 + module/spring-boot-grpc-test/build.gradle | 43 +++++++ .../AutoConfigureTestGrpcTransport.java | 77 ++++++++++++ ...PortInfoApplicationContextInitializer.java | 106 +++++++++++++++++ .../autoconfigure/LocalGrpcServerPort.java | 43 +++++++ .../autoconfigure/TestGrpcChannelFactory.java | 51 ++++++++ .../autoconfigure/TestGrpcServerFactory.java | 43 +++++++ .../TestGrpcTransportAutoConfiguration.java | 112 ++++++++++++++++++ .../grpc/test/autoconfigure/package-info.java | 23 ++++ .../main/resources/META-INF/spring.factories | 3 + ...ure.AutoConfigureTestGrpcTransport.imports | 1 + ...nfigureTestGrpcTransportOverrideTests.java | 44 +++++++ .../AutoConfigureTestGrpcTransportTests.java | 44 +++++++ ...nfoApplicationContextInitializerTests.java | 92 ++++++++++++++ .../TestGrpcChannelFactoryTests.java | 47 ++++++++ .../TestGrpcServerFactoryTests.java | 40 +++++++ ...stGrpcTransportAutoConfigurationTests.java | 107 +++++++++++++++++ .../build.gradle | 3 + .../spring-boot-dependencies/build.gradle | 2 + settings.gradle | 4 + .../build.gradle | 63 ++++++++++ .../SampleGrpcClientTestApplication.java | 46 +++++++ .../grpcclienttest/package-info.java | 20 ++++ .../src/main/proto/hello.proto | 17 +++ .../SampleGrpcClientTestApplicationTests.java | 58 +++++++++ .../build.gradle | 63 ++++++++++ .../grpcservertest/HelloWorldService.java | 70 +++++++++++ .../SampleGrpcServerTestApplication.java | 29 +++++ .../StandardExceptionHandler.java | 41 +++++++ .../grpcservertest/package-info.java | 20 ++++ .../src/main/proto/hello.proto | 17 +++ ...ServerTestApplicationIntegrationTests.java | 54 +++++++++ .../SampleGrpcServerTestApplicationTests.java | 63 ++++++++++ .../build.gradle | 29 +++++ 35 files changed, 1479 insertions(+) create mode 100644 module/spring-boot-grpc-test/build.gradle create mode 100644 module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureTestGrpcTransport.java create mode 100644 module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializer.java create mode 100644 module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/LocalGrpcServerPort.java create mode 100644 module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcChannelFactory.java create mode 100644 module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcServerFactory.java create mode 100644 module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcTransportAutoConfiguration.java create mode 100644 module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/package-info.java create mode 100644 module/spring-boot-grpc-test/src/main/resources/META-INF/spring.factories create mode 100644 module/spring-boot-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport.imports create mode 100644 module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureTestGrpcTransportOverrideTests.java create mode 100644 module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureTestGrpcTransportTests.java create mode 100644 module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializerTests.java create mode 100644 module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcChannelFactoryTests.java create mode 100644 module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcServerFactoryTests.java create mode 100644 module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcTransportAutoConfigurationTests.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-client-test/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-grpc-client-test/src/main/java/smoketest/grpcclienttest/SampleGrpcClientTestApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-client-test/src/main/java/smoketest/grpcclienttest/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-client-test/src/main/proto/hello.proto create mode 100644 smoke-test/spring-boot-smoke-test-grpc-client-test/src/test/java/smoketest/grpcclienttest/SampleGrpcClientTestApplicationTests.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-test/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/HelloWorldService.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/SampleGrpcServerTestApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/StandardExceptionHandler.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/proto/hello.proto create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-test/src/test/java/smoketest/grpcservertest/SampleGrpcServerTestApplicationIntegrationTests.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-test/src/test/java/smoketest/grpcservertest/SampleGrpcServerTestApplicationTests.java create mode 100644 starter/spring-boot-starter-grpc-test/build.gradle diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index f18dcbcec105..fe01de87d6fb 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -112,6 +112,7 @@ dependencies { implementation(project(path: ":module:spring-boot-graphql-test")) implementation(project(path: ":module:spring-boot-grpc-client")) implementation(project(path: ":module:spring-boot-grpc-server")) + implementation(project(path: ":module:spring-boot-grpc-test")) implementation(project(path: ":module:spring-boot-health")) implementation(project(path: ":module:spring-boot-hibernate")) implementation(project(path: ":module:spring-boot-http-converter")) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-modules.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-modules.adoc index 8a16fbe5f7c6..9d00d5e08b4f 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-modules.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-modules.adoc @@ -43,6 +43,9 @@ Spring Boot offers several focused, feature-specific `-test` modules: |`spring-boot-graphql-test` |Testing applications that use Spring GraphQL. Provides the `@GraphQlTest` test slice. +|`spring-boot-grpc-test` +|Testing applications that use Spring gRPC. + |`spring-boot-jdbc-test` |Testing applications that using Spring JDBC. Provides the `@JdbcTest` test slice. diff --git a/module/spring-boot-grpc-test/build.gradle b/module/spring-boot-grpc-test/build.gradle new file mode 100644 index 000000000000..f2d3e8e39a54 --- /dev/null +++ b/module/spring-boot-grpc-test/build.gradle @@ -0,0 +1,43 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java-library" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot gRPC Test" + +dependencies { + api(project(":core:spring-boot-test-autoconfigure")) + api(project(":module:spring-boot-grpc-client")) + api(project(":module:spring-boot-grpc-server")) + + optional(project(":core:spring-boot-autoconfigure")) + optional("io.grpc:grpc-inprocess") + optional("io.grpc:grpc-stub") + optional("org.junit.jupiter:junit-jupiter-api") + + testImplementation(project(":test-support:spring-boot-test-support")) + testImplementation("io.grpc:grpc-netty") + + testRuntimeOnly("ch.qos.logback:logback-classic") +} + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} diff --git a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureTestGrpcTransport.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureTestGrpcTransport.java new file mode 100644 index 000000000000..b45d6dade896 --- /dev/null +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureTestGrpcTransport.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.test.autoconfigure; + +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.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.grpc.server.GrpcServletRegistration; +import org.springframework.boot.test.context.PropertyMapping; +import org.springframework.grpc.client.GrpcChannelFactory; +import org.springframework.grpc.server.GrpcServerFactory; + +/** + * Annotation that can be applied to a test class to enable test in-process gRPC + * transport. Starts a test in-process gRPC server and configures a + * {@code GrpcChannelFactory} that will connect all targets to it. + * + * @author Dave Syer + * @author Chris Bono + * @author Phillip Webb + * @since 4.1.0 + * @see TestGrpcTransportAutoConfiguration + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ImportAutoConfiguration +public @interface AutoConfigureTestGrpcTransport { + + /** + * Enables auto-configuration of the {@link GrpcServletRegistration}. Defaults to + * {@code false} since servlet registration is unnecessary when using test in-process + * transport. + * @return if servlet support is enabled + */ + @PropertyMapping("spring.grpc.server.servlet.enabled") + boolean enableServlet() default false; + + /** + * Enables auto-configuration of {@link GrpcServerFactory} beans. Defaults to + * {@code false} since additional server factories are unnecessary when using test + * in-process transport. + * @return if server factories are enabled + */ + @PropertyMapping("spring.grpc.server.factory.enabled") + boolean enableServerFactory() default false; + + /** + * Enables auto-configuration of {@link GrpcChannelFactory} beans. Defaults to + * {@code false} since additional channel factories are unnecessary when using test + * in-process transport. + * @return if channel factories are enabled + */ + @PropertyMapping("spring.grpc.client.channelfactory.enabled") + boolean enableChannelFactory() default false; + +} diff --git a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializer.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializer.java new file mode 100644 index 000000000000..76f1e0a30673 --- /dev/null +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializer.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.test.autoconfigure; + +import java.util.HashMap; +import java.util.Map; + +import io.grpc.Server; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.InProcessGrpcServerFactory; +import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent; + +/** + * {@link ApplicationContextInitializer} that sets {@link Environment} properties for the + * ports that {@link Server gRPC servers} are actually listening on. The property + * {@literal "local.grpc.server.port"} can be injected directly into tests using + * {@link Value @Value} or obtained through the {@link Environment}. + *

    + * Properties are automatically propagated up to any parent context. + * + * @author Dave Syer + * @author Chris Bono + * @author Phillip Webb + */ +class GrpcPortInfoApplicationContextInitializer + implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + applicationContext.addApplicationListener(new Listener(applicationContext)); + } + + private static class Listener implements ApplicationListener { + + private static final String PROPERTY_NAME = "local.grpc.server.port"; + + private static final String PROPERTY_SOURCE_NAME = "server.ports"; + + private final ConfigurableApplicationContext applicationContext; + + Listener(ConfigurableApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public void onApplicationEvent(GrpcServerStartedEvent event) { + GrpcServerFactory factory = event.getSource().getFactory(); + if (factory instanceof InProcessGrpcServerFactory || factory instanceof TestGrpcServerFactory + || event.getPort() == -1) { + return; + } + setPortProperty(this.applicationContext, event.getPort()); + } + + private void setPortProperty(ApplicationContext context, int port) { + if (context instanceof ConfigurableApplicationContext configurableContext) { + setPortProperty(configurableContext.getEnvironment(), port); + } + if (context.getParent() != null) { + setPortProperty(context.getParent(), port); + } + } + + private void setPortProperty(ConfigurableEnvironment environment, int port) { + MutablePropertySources sources = environment.getPropertySources(); + PropertySource source = sources.get(PROPERTY_SOURCE_NAME); + if (source == null) { + source = new MapPropertySource(PROPERTY_SOURCE_NAME, new HashMap<>()); + sources.addFirst(source); + } + setPortProperty(port, source); + } + + @SuppressWarnings("unchecked") + private void setPortProperty(int port, PropertySource source) { + ((Map) source.getSource()).put(PROPERTY_NAME, port); + } + + } + +} diff --git a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/LocalGrpcServerPort.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/LocalGrpcServerPort.java new file mode 100644 index 000000000000..41fed407bef4 --- /dev/null +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/LocalGrpcServerPort.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.test.autoconfigure; + +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.Value; + +/** + * Annotation at the field or method/constructor parameter level that injects the gRPC + * server port that was allocated at runtime. Provides a convenient alternative for + * @Value("${local.grpc.server.port}"). + * + * @author Dave Syer + * @author Chris Bono + * @author Phillip Webb + * @since 4.1.0 + */ +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Value("${local.grpc.server.port}") +public @interface LocalGrpcServerPort { + +} diff --git a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcChannelFactory.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcChannelFactory.java new file mode 100644 index 000000000000..e00d3e74c922 --- /dev/null +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcChannelFactory.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.test.autoconfigure; + +import java.util.Collections; + +import io.grpc.ChannelCredentials; +import io.grpc.inprocess.InProcessChannelBuilder; + +import org.springframework.grpc.client.ClientInterceptorsConfigurer; +import org.springframework.grpc.client.DefaultGrpcChannelFactory; +import org.springframework.grpc.client.GrpcChannelFactory; + +/** + * {@link GrpcChannelFactory} for testing with in-process transport. + * + * @author Chris Bono + * @author Phillip Webb + */ +class TestGrpcChannelFactory extends DefaultGrpcChannelFactory { + + TestGrpcChannelFactory(String address, ClientInterceptorsConfigurer interceptorsConfigurer) { + super(Collections.emptyList(), interceptorsConfigurer); + setVirtualTargets((path) -> address); + } + + @Override + public boolean supports(String target) { + return true; + } + + @Override + protected InProcessChannelBuilder newChannelBuilder(String target, ChannelCredentials creds) { + return InProcessChannelBuilder.forName(target); + } + +} diff --git a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcServerFactory.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcServerFactory.java new file mode 100644 index 000000000000..42630cae7cac --- /dev/null +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcServerFactory.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.test.autoconfigure; + +import java.util.Collections; + +import io.grpc.inprocess.InProcessServerBuilder; + +import org.springframework.grpc.server.DefaultGrpcServerFactory; +import org.springframework.grpc.server.GrpcServerFactory; + +/** + * {@link GrpcServerFactory} for testing with in-process transport. + * + * @author Chris Bono + * @author Phillip Webb + */ +class TestGrpcServerFactory extends DefaultGrpcServerFactory { + + TestGrpcServerFactory(String address) { + super(address, Collections.emptyList(), null, null, null); + } + + @Override + protected InProcessServerBuilder newServerBuilder() { + return InProcessServerBuilder.forName(address()); + } + +} diff --git a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcTransportAutoConfiguration.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcTransportAutoConfiguration.java new file mode 100644 index 000000000000..6ef189c4c879 --- /dev/null +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcTransportAutoConfiguration.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.test.autoconfigure; + +import java.util.List; + +import io.grpc.BindableService; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.AbstractStub; +import org.jspecify.annotations.Nullable; + +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.grpc.client.autoconfigure.GrpcClientAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEventPublisher; +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.grpc.client.ClientInterceptorsConfigurer; +import org.springframework.grpc.client.GrpcChannelFactory; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.InProcessGrpcServerFactory; +import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.grpc.server.ServerServiceDefinitionFilter; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; + +/** + * Auto-configuration for an in-process test gRPC transport. + * + * @author Chris Bono + * @author Dave Syer + * @author Andrey Litvitski + * @author Phillip Webb + * @since 4.1.0 + * @see AutoConfigureTestGrpcTransport + */ +@AutoConfiguration(before = { GrpcServerAutoConfiguration.class, GrpcClientAutoConfiguration.class }) +@ConditionalOnClass({ InProcessServerBuilder.class, InProcessGrpcServerFactory.class }) +public final class TestGrpcTransportAutoConfiguration { + + private static final String address = InProcessServerBuilder.generateName(); + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(GrpcServerFactory.class) + @ConditionalOnBean(BindableService.class) + static class TestGrpcServerTransportAutoConfiguration { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + TestGrpcServerFactory testGrpcServerFactory(GrpcServiceDiscoverer serviceDiscoverer, + GrpcServiceConfigurer serviceConfigurer, + List> customizers, + @Nullable ServerServiceDefinitionFilter serviceFilter) { + TestGrpcServerFactory factory = new TestGrpcServerFactory(address); + serviceDiscoverer.findServices() + .stream() + .map((spec) -> serviceConfigurer.configure(spec, factory)) + .forEach(factory::addService); + return factory; + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + GrpcServerLifecycle testGrpcServerLifecycle(TestGrpcServerFactory testGrpcServerFactory, + GrpcServerProperties properties, ApplicationEventPublisher eventPublisher) { + return new GrpcServerLifecycle(testGrpcServerFactory, properties.getShutdown().getGracePeriod(), + eventPublisher); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ AbstractStub.class, GrpcChannelFactory.class }) + static class TestGrpcClientTransportAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + ClientInterceptorsConfigurer grpcClientInterceptorsConfigurer(ApplicationContext applicationContext) { + return new ClientInterceptorsConfigurer(applicationContext); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + TestGrpcChannelFactory testGrpcChannelFactory(ClientInterceptorsConfigurer interceptorsConfigurer) { + return new TestGrpcChannelFactory(address, interceptorsConfigurer); + } + + } + +} diff --git a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/package-info.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/package-info.java new file mode 100644 index 000000000000..c6785eb5dd75 --- /dev/null +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 gRPC tests. + */ +@NullMarked +package org.springframework.boot.grpc.test.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-test/src/main/resources/META-INF/spring.factories b/module/spring-boot-grpc-test/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..a9432eff7478 --- /dev/null +++ b/module/spring-boot-grpc-test/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Application Context Initializers +org.springframework.context.ApplicationContextInitializer=\ +org.springframework.boot.grpc.test.autoconfigure.GrpcPortInfoApplicationContextInitializer diff --git a/module/spring-boot-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport.imports b/module/spring-boot-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport.imports new file mode 100644 index 000000000000..dcf95ea3d0b8 --- /dev/null +++ b/module/spring-boot-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport.imports @@ -0,0 +1 @@ +org.springframework.boot.grpc.test.autoconfigure.TestGrpcTransportAutoConfiguration \ No newline at end of file diff --git a/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureTestGrpcTransportOverrideTests.java b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureTestGrpcTransportOverrideTests.java new file mode 100644 index 000000000000..fc2f363c347b --- /dev/null +++ b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureTestGrpcTransportOverrideTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.test.autoconfigure; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutoConfigureTestGrpcTransport} with override attributes. + * + * @author Phillip Webb + */ +@ExtendWith(SpringExtension.class) +@AutoConfigureTestGrpcTransport(enableServlet = true, enableServerFactory = true, enableChannelFactory = true) +class AutoConfigureTestGrpcTransportOverrideTests { + + @Test + void setsEnabledPropertiesToTrue(@Autowired Environment environment) { + assertThat(environment.getProperty("spring.grpc.server.servlet.enabled", Boolean.class)).isTrue(); + assertThat(environment.getProperty("spring.grpc.server.factory.enabled", Boolean.class)).isTrue(); + assertThat(environment.getProperty("spring.grpc.client.channelfactory.enabled", Boolean.class)).isTrue(); + } + +} diff --git a/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureTestGrpcTransportTests.java b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureTestGrpcTransportTests.java new file mode 100644 index 000000000000..86150d104d47 --- /dev/null +++ b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureTestGrpcTransportTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.test.autoconfigure; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutoConfigureTestGrpcTransport}. + * + * @author Phillip Webb + */ +@ExtendWith(SpringExtension.class) +@AutoConfigureTestGrpcTransport +class AutoConfigureTestGrpcTransportTests { + + @Test + void setsEnabledPropertiesToFalse(@Autowired Environment environment) { + assertThat(environment.getProperty("spring.grpc.server.servlet.enabled", Boolean.class)).isFalse(); + assertThat(environment.getProperty("spring.grpc.server.factory.enabled", Boolean.class)).isFalse(); + assertThat(environment.getProperty("spring.grpc.client.channelfactory.enabled", Boolean.class)).isFalse(); + } + +} diff --git a/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializerTests.java b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializerTests.java new file mode 100644 index 000000000000..bb5ffdb80cc1 --- /dev/null +++ b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializerTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.test.autoconfigure; + +import io.grpc.Server; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.InProcessGrpcServerFactory; +import org.springframework.grpc.server.NettyGrpcServerFactory; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link GrpcPortInfoApplicationContextInitializer}. + * + * @author Phillip Webb + */ +class GrpcPortInfoApplicationContextInitializerTests { + + private static final String PORT_PROPERTY = "local.grpc.server.port"; + + @Test + void whenServerHasAddressInitializerSetsPortProperty() { + NettyGrpcServerFactory factory = mock(); + testListener(factory, 65535, "65535"); + } + + @Test + void whenServerHasNoAddressInitializerSetsNoPortProperty() { + NettyGrpcServerFactory factory = mock(); + testListener(factory, -1, null); + } + + @Test + void whenInProcessGrpcServerFactorySetsNoPortProperty() { + InProcessGrpcServerFactory factory = mock(); + testListener(factory, 65535, null); + } + + @Test + void whenTestGrpcServerFactorySetsNoPortProperty() { + TestGrpcServerFactory factory = mock(); + testListener(factory, 65535, null); + } + + private void testListener(GrpcServerFactory factory, int port, @Nullable String expected) { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(Config.class)) { + context.getBean(GrpcPortInfoApplicationContextInitializer.class).initialize(context); + GrpcServerLifecycle lifecyle = mock(); + Server server = mock(); + given(lifecyle.getFactory()).willReturn(factory); + GrpcServerStartedEvent event = new GrpcServerStartedEvent(lifecyle, server, "localhost", port); + context.publishEvent(event); + assertThat(context.getEnvironment().getProperty(PORT_PROPERTY)).isEqualTo(expected); + } + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + GrpcPortInfoApplicationContextInitializer grpcPortInfoApplicationContextInitializer() { + return new GrpcPortInfoApplicationContextInitializer(); + } + + } + +} diff --git a/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcChannelFactoryTests.java b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcChannelFactoryTests.java new file mode 100644 index 000000000000..021d28b4b398 --- /dev/null +++ b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcChannelFactoryTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.test.autoconfigure; + +import io.grpc.ManagedChannel; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link TestGrpcChannelFactory}. + * + * @author Phillip Webb + */ +class TestGrpcChannelFactoryTests { + + private TestGrpcChannelFactory factory = new TestGrpcChannelFactory("test", mock()); + + @Test + void supportsReturnsTrue() { + assertThat(this.factory.supports("inprocess:test")).isTrue(); + assertThat(this.factory.supports("static:*:0")).isTrue(); + } + + @Test + void createChannelReturnsInProcessChannel() { + ManagedChannel channel = this.factory.createChannel("static:*:0"); + assertThat(channel).extracting("delegate.originalTransportFactory") + .satisfies((transportFactory) -> assertThat(transportFactory.getClass().getName()).contains("InProcess")); + } + +} diff --git a/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcServerFactoryTests.java b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcServerFactoryTests.java new file mode 100644 index 000000000000..27c367a80e76 --- /dev/null +++ b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcServerFactoryTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.test.autoconfigure; + +import io.grpc.Server; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TestGrpcServerFactory}. + * + * @author Phillip Webb + */ +class TestGrpcServerFactoryTests { + + private TestGrpcServerFactory factory = new TestGrpcServerFactory("test"); + + @Test + void createServerReturnsInProcessServer() { + Server server = this.factory.createServer(); + assertThat(server).extracting("transportServer") + .satisfies((transportServer) -> assertThat(transportServer.getClass().getName()).contains("InProcess")); + } + +} diff --git a/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcTransportAutoConfigurationTests.java b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcTransportAutoConfigurationTests.java new file mode 100644 index 000000000000..48dc8d8cea1e --- /dev/null +++ b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcTransportAutoConfigurationTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.test.autoconfigure; + +import io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; +import io.grpc.stub.AbstractStub; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerServicesAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.grpc.client.GrpcChannelFactory; +import org.springframework.grpc.server.GrpcServerFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link TestGrpcTransportAutoConfiguration}. + * + * @author Chris Bono + * @author Phillip Webb + */ +class TestGrpcTransportAutoConfigurationTests { + + private static final AutoConfigurations autoConfiguration = AutoConfigurations.of( + TestGrpcTransportAutoConfiguration.class, GrpcServerAutoConfiguration.class, + GrpcServerServicesAutoConfiguration.class, SslAutoConfiguration.class, GrpcClientAutoConfiguration.class); + + private final BindableService service = mock(); + + private final ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(autoConfiguration) + .withBean(BindableService.class, () -> this.service); + + @BeforeEach + void setup() { + given(this.service.bindService()).willReturn(this.serviceDefinition); + } + + @Test + void createsTestGrpcServerFactoryBean() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(TestGrpcServerFactory.class)); + } + + @Test + void createsGrpcServerLifecycleBean() { + this.contextRunner.run((context) -> assertThat(context).hasBean("testGrpcServerLifecycle")); + } + + @Test + void whenNoBindableServiceDoesNotCreateServerBeans() { + new ApplicationContextRunner().withConfiguration(autoConfiguration).run((context) -> { + assertThat(context).doesNotHaveBean(TestGrpcServerFactory.class); + assertThat(context).doesNotHaveBean("testGrpcServerLifecycle"); + }); + } + + @Test + void whenNoGrpcServerFactoryClassDoesNotCreateServerBeans() { + this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)).run((context) -> { + assertThat(context).doesNotHaveBean(TestGrpcServerFactory.class); + assertThat(context).doesNotHaveBean("testGrpcServerLifecycle"); + }); + } + + @Test + void createsTestGrpcChannelFactoryBean() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(TestGrpcChannelFactory.class)); + } + + @Test + void whenNoAbstractStubClassDoesNotCreateClientBeans() { + this.contextRunner.withClassLoader(new FilteredClassLoader(AbstractStub.class)) + .run((context) -> assertThat(context).doesNotHaveBean(TestGrpcChannelFactory.class)); + } + + @Test + void whenNoGrpcChannelFactoryClassDoesNotCreateClientBeans() { + this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcChannelFactory.class)) + .run((context) -> assertThat(context).doesNotHaveBean(TestGrpcChannelFactory.class)); + } + +} diff --git a/module/spring-boot-test-classic-modules/build.gradle b/module/spring-boot-test-classic-modules/build.gradle index 0b3932661eaf..72e340c4f00f 100644 --- a/module/spring-boot-test-classic-modules/build.gradle +++ b/module/spring-boot-test-classic-modules/build.gradle @@ -67,6 +67,9 @@ dependencies { api(project(":module:spring-boot-graphql-test")) { transitive = false } + api(project(":module:spring-boot-grpc-test")) { + transitive = false + } api(project(":module:spring-boot-jdbc-test")) { transitive = false } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3569519a5d6f..4f5925d72a9d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2165,6 +2165,7 @@ bom { "spring-boot-graphql-test", "spring-boot-grpc-client", "spring-boot-grpc-server", + "spring-boot-grpc-test", "spring-boot-groovy-templates", "spring-boot-gson", "spring-boot-h2console", @@ -2297,6 +2298,7 @@ bom { "spring-boot-starter-graphql-test", "spring-boot-starter-grpc-client", "spring-boot-starter-grpc-server", + "spring-boot-starter-grpc-test", "spring-boot-starter-groovy-templates", "spring-boot-starter-groovy-templates-test", "spring-boot-starter-gson", diff --git a/settings.gradle b/settings.gradle index 8b44e0b5e4ea..28dffcc3a6a0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -122,6 +122,7 @@ include "module:spring-boot-graphql" include "module:spring-boot-graphql-test" include "module:spring-boot-grpc-client" include "module:spring-boot-grpc-server" +include "module:spring-boot-grpc-test" include "module:spring-boot-groovy-templates" include "module:spring-boot-gson" include "module:spring-boot-h2console" @@ -272,6 +273,7 @@ include "starter:spring-boot-starter-graphql" include "starter:spring-boot-starter-graphql-test" include "starter:spring-boot-starter-grpc-client" include "starter:spring-boot-starter-grpc-server" +include "starter:spring-boot-starter-grpc-test" include "starter:spring-boot-starter-groovy-templates" include "starter:spring-boot-starter-groovy-templates-test" include "starter:spring-boot-starter-gson" @@ -426,9 +428,11 @@ include ":smoke-test:spring-boot-smoke-test-devtools" include ":smoke-test:spring-boot-smoke-test-flyway" include ":smoke-test:spring-boot-smoke-test-graphql" include ":smoke-test:spring-boot-smoke-test-grpc-client" +include ":smoke-test:spring-boot-smoke-test-grpc-client-test" include ":smoke-test:spring-boot-smoke-test-grpc-server" include ":smoke-test:spring-boot-smoke-test-grpc-server-netty-shaded" include ":smoke-test:spring-boot-smoke-test-grpc-server-servlet" +include ":smoke-test:spring-boot-smoke-test-grpc-server-test" include ":smoke-test:spring-boot-smoke-test-hateoas" include ":smoke-test:spring-boot-smoke-test-hibernate" include ":smoke-test:spring-boot-smoke-test-integration" diff --git a/smoke-test/spring-boot-smoke-test-grpc-client-test/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-client-test/build.gradle new file mode 100644 index 000000000000..9f70966c6ee9 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-client-test/build.gradle @@ -0,0 +1,63 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java" + id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "org.springframework.boot.docker-test" +} + +description = "Spring Boot gRPC client and test smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-grpc-client")) + + testImplementation(project(":starter:spring-boot-starter-grpc-test")) +} + +def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom") +def grpcJava = dependenciesBom.getLibrary("Grpc Java") +def protobufJava = dependenciesBom.getLibrary("Protobuf Java") + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +nullability { + requireExplicitNullMarking = false +} + +configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all { + extendsFrom(configurations.dependencyManagement) +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protobufJava.version}" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}" + } + } + generateProtoTasks { + all()*.plugins { + grpc { + option '@generated=omit' + } + } + } +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-client-test/src/main/java/smoketest/grpcclienttest/SampleGrpcClientTestApplication.java b/smoke-test/spring-boot-smoke-test-grpc-client-test/src/main/java/smoketest/grpcclienttest/SampleGrpcClientTestApplication.java new file mode 100644 index 000000000000..ecf355e3a8e6 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-client-test/src/main/java/smoketest/grpcclienttest/SampleGrpcClientTestApplication.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcclienttest; + +import smoketest.grpcclienttest.proto.HelloReply; +import smoketest.grpcclienttest.proto.HelloRequest; +import smoketest.grpcclienttest.proto.HelloWorldGrpc.HelloWorldBlockingStub; + +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.grpc.client.ImportGrpcClients; + +@SpringBootApplication +@ImportGrpcClients(types = HelloWorldBlockingStub.class) +public class SampleGrpcClientTestApplication { + + @Bean + ApplicationRunner applicationRunner(HelloWorldBlockingStub hello) { + return (args) -> { + HelloRequest request = HelloRequest.newBuilder().setName("Spring").build(); + HelloReply reply = hello.sayHello(request); + System.out.println(">>> " + reply.getMessage()); + }; + } + + public static void main(String[] args) { + SpringApplication.run(SampleGrpcClientTestApplication.class, args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-client-test/src/main/java/smoketest/grpcclienttest/package-info.java b/smoke-test/spring-boot-smoke-test-grpc-client-test/src/main/java/smoketest/grpcclienttest/package-info.java new file mode 100644 index 000000000000..ae7921291b19 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-client-test/src/main/java/smoketest/grpcclienttest/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NullMarked +package smoketest.grpcclienttest; + +import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-grpc-client-test/src/main/proto/hello.proto b/smoke-test/spring-boot-smoke-test-grpc-client-test/src/main/proto/hello.proto new file mode 100644 index 000000000000..ee3a07893644 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-client-test/src/main/proto/hello.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "smoketest.grpcclienttest.proto"; +option java_multiple_files = true; + +service HelloWorld { + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc StreamHello(HelloRequest) returns (stream HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-client-test/src/test/java/smoketest/grpcclienttest/SampleGrpcClientTestApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-client-test/src/test/java/smoketest/grpcclienttest/SampleGrpcClientTestApplicationTests.java new file mode 100644 index 000000000000..621a17753ffc --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-client-test/src/test/java/smoketest/grpcclienttest/SampleGrpcClientTestApplicationTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcclienttest; + +import io.grpc.stub.StreamObserver; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import smoketest.grpcclienttest.SampleGrpcClientTestApplicationTests.HelloWorldService; +import smoketest.grpcclienttest.proto.HelloReply; +import smoketest.grpcclienttest.proto.HelloRequest; +import smoketest.grpcclienttest.proto.HelloWorldGrpc; + +import org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.context.annotation.Import; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@AutoConfigureTestGrpcTransport +@Import(HelloWorldService.class) +@ExtendWith(OutputCaptureExtension.class) +class SampleGrpcClientTestApplicationTests { + + @Test + void callsService(CapturedOutput output) { + assertThat(output).contains(">>> Test hello 'Spring'"); + } + + static class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase { + + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + String message = "Test hello '%s'".formatted(request.getName()); + HelloReply reply = HelloReply.newBuilder().setMessage(message).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-test/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-test/build.gradle new file mode 100644 index 000000000000..8f28729142eb --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-test/build.gradle @@ -0,0 +1,63 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java" + id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "org.springframework.boot.docker-test" +} + +description = "Spring Boot gRPC server and test smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-grpc-server")) + + testImplementation(project(":starter:spring-boot-starter-grpc-test")) +} + +def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom") +def grpcJava = dependenciesBom.getLibrary("Grpc Java") +def protobufJava = dependenciesBom.getLibrary("Protobuf Java") + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +nullability { + requireExplicitNullMarking = false +} + +configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all { + extendsFrom(configurations.dependencyManagement) +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protobufJava.version}" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}" + } + } + generateProtoTasks { + all()*.plugins { + grpc { + option '@generated=omit' + } + } + } +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/HelloWorldService.java b/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/HelloWorldService.java new file mode 100644 index 000000000000..abcd239e3e3d --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/HelloWorldService.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcservertest; + +import io.grpc.stub.StreamObserver; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import smoketest.grpcservertest.proto.HelloReply; +import smoketest.grpcservertest.proto.HelloRequest; +import smoketest.grpcservertest.proto.HelloWorldGrpc; + +import org.springframework.grpc.server.advice.GrpcAdvice; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +@Service +@GrpcAdvice +public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase { + + private static Log logger = LogFactory.getLog(HelloWorldService.class); + + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + String name = request.getName(); + logger.info("sayHello " + name); + Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name); + Assert.state(!name.startsWith("internal"), "Internal error"); + String message = "Hello '%s'".formatted(name); + HelloReply reply = HelloReply.newBuilder().setMessage(message).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + + @Override + public void streamHello(HelloRequest request, StreamObserver responseObserver) { + String name = request.getName(); + logger.info("streamHello " + name); + int count = 0; + while (count < 10) { + String message = "Hello(" + count + ") '%s'".formatted(name); + HelloReply reply = HelloReply.newBuilder().setMessage(message).build(); + responseObserver.onNext(reply); + count++; + try { + Thread.sleep(100L); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + responseObserver.onError(ex); + return; + } + } + responseObserver.onCompleted(); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/SampleGrpcServerTestApplication.java b/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/SampleGrpcServerTestApplication.java new file mode 100644 index 000000000000..ae8a12f20977 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/SampleGrpcServerTestApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcservertest; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleGrpcServerTestApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleGrpcServerTestApplication.class, args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/StandardExceptionHandler.java b/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/StandardExceptionHandler.java new file mode 100644 index 000000000000..762d51a705db --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/StandardExceptionHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcservertest; + +import io.grpc.Status; +import io.grpc.Status.Code; +import io.grpc.StatusException; +import org.jspecify.annotations.Nullable; + +import org.springframework.grpc.server.exception.GrpcExceptionHandler; +import org.springframework.stereotype.Component; + +@Component +public class StandardExceptionHandler implements GrpcExceptionHandler { + + @Override + public @Nullable StatusException handleException(Throwable exception) { + if (exception instanceof IllegalArgumentException) { + return new StatusException(Status.fromCode(Code.INVALID_ARGUMENT).withDescription(exception.getMessage())); + } + if (exception instanceof IllegalStateException) { + return new StatusException(Status.fromCode(Code.INTERNAL).withDescription(exception.getMessage())); + } + return null; + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/package-info.java b/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/package-info.java new file mode 100644 index 000000000000..9fe37fa1c63a --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/java/smoketest/grpcservertest/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NullMarked +package smoketest.grpcservertest; + +import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/proto/hello.proto b/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/proto/hello.proto new file mode 100644 index 000000000000..fdfb5c0456cc --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-test/src/main/proto/hello.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "smoketest.grpcservertest.proto"; +option java_multiple_files = true; + +service HelloWorld { + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc StreamHello(HelloRequest) returns (stream HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-test/src/test/java/smoketest/grpcservertest/SampleGrpcServerTestApplicationIntegrationTests.java b/smoke-test/spring-boot-smoke-test-grpc-server-test/src/test/java/smoketest/grpcservertest/SampleGrpcServerTestApplicationIntegrationTests.java new file mode 100644 index 000000000000..ba9956f9fdbb --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-test/src/test/java/smoketest/grpcservertest/SampleGrpcServerTestApplicationIntegrationTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcservertest; + +import io.grpc.ManagedChannel; +import io.grpc.netty.NettyChannelBuilder; +import org.junit.jupiter.api.Test; +import smoketest.grpcservertest.proto.HelloReply; +import smoketest.grpcservertest.proto.HelloRequest; +import smoketest.grpcservertest.proto.HelloWorldGrpc; +import smoketest.grpcservertest.proto.HelloWorldGrpc.HelloWorldBlockingStub; + +import org.springframework.boot.grpc.test.autoconfigure.LocalGrpcServerPort; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = "spring.grpc.server.port=0") +class SampleGrpcServerTestApplicationIntegrationTests { + + @LocalGrpcServerPort + private int port; + + @Test + void callsService() { + ManagedChannel channel = NettyChannelBuilder.forTarget("localhost:%s".formatted(this.port)) + .usePlaintext() + .build(); + try { + HelloWorldBlockingStub hello = HelloWorldGrpc.newBlockingStub(channel); + HelloRequest request = HelloRequest.newBuilder().setName("Spring").build(); + HelloReply reply = hello.sayHello(request); + assertThat(reply.getMessage()).isEqualTo("Hello 'Spring'"); + } + finally { + channel.shutdown(); + } + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-test/src/test/java/smoketest/grpcservertest/SampleGrpcServerTestApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-server-test/src/test/java/smoketest/grpcservertest/SampleGrpcServerTestApplicationTests.java new file mode 100644 index 000000000000..c62593e6f73c --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-test/src/test/java/smoketest/grpcservertest/SampleGrpcServerTestApplicationTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcservertest; + +import io.grpc.Status.Code; +import io.grpc.StatusRuntimeException; +import org.junit.jupiter.api.Test; +import smoketest.grpcservertest.proto.HelloReply; +import smoketest.grpcservertest.proto.HelloRequest; +import smoketest.grpcservertest.proto.HelloWorldGrpc.HelloWorldBlockingStub; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.grpc.client.ImportGrpcClients; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +@SpringBootTest +@AutoConfigureTestGrpcTransport +@ImportGrpcClients(types = HelloWorldBlockingStub.class) +class SampleGrpcServerTestApplicationTests { + + @Autowired + private HelloWorldBlockingStub hello; + + @Test + void sayHello() { + HelloRequest request = HelloRequest.newBuilder().setName("Spring").build(); + HelloReply reply = this.hello.sayHello(request); + assertThat(reply.getMessage()).isEqualTo("Hello 'Spring'"); + } + + @Test + void sayHelloWhenBadName() { + HelloRequest request = HelloRequest.newBuilder().setName("errorThing").build(); + assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> this.hello.sayHello(request)) + .satisfies((ex) -> assertThat(ex.getStatus().getCode()).isEqualTo(Code.INVALID_ARGUMENT)); + } + + @Test + void sayHelloWhenInternalError() { + HelloRequest request = HelloRequest.newBuilder().setName("internal").build(); + assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> this.hello.sayHello(request)) + .satisfies((ex) -> assertThat(ex.getStatus().getCode()).isEqualTo(Code.INTERNAL)); + } + +} diff --git a/starter/spring-boot-starter-grpc-test/build.gradle b/starter/spring-boot-starter-grpc-test/build.gradle new file mode 100644 index 000000000000..d0f2bf871d4c --- /dev/null +++ b/starter/spring-boot-starter-grpc-test/build.gradle @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for testing gRPC" + +dependencies { + api(project(":starter:spring-boot-starter-test")) + + api(project(":module:spring-boot-grpc-test")) + api("io.grpc:grpc-inprocess") + api("io.grpc:grpc-stub") +} From b650f3c5581b3e50c01ba5405dbad1827053a6fc Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 22 Feb 2026 11:02:25 -0800 Subject: [PATCH 386/930] Rework spring-boot-health for reuse Update `spring-boot-health` to allow for better reuse outside of the actuator module. See gh-49048 --- .../HealthEndpointDocumentationTests.java | 6 +- .../health/TestHealthEndpointGroup.java | 6 +- .../endpoint/HttpCodeStatusMapper.java | 30 ++++ .../endpoint/SimpleHttpCodeStatusMapper.java | 35 ++-- .../endpoint/SimpleStatusAggregator.java | 78 ++++----- .../actuate/endpoint/StatusAggregator.java | 58 ++++++- .../AutoConfiguredHealthEndpointGroup.java | 14 +- .../AutoConfiguredHealthEndpointGroups.java | 63 ++++--- ...AvailabilityProbesHealthEndpointGroup.java | 2 +- .../endpoint/HealthEndpointConfiguration.java | 120 ++------------ ...oSuchHealthContributorFailureAnalyzer.java | 38 ----- .../HealthContributorMembership.java | 59 +++++++ .../HealthContributorMembershipValidator.java | 140 ++++++++++++++++ ...deExcludeHealthContributorMembership.java} | 9 +- .../boot/health/contributor/Status.java | 8 + .../main/resources/META-INF/spring.factories | 3 - .../endpoint/HttpCodeStatusMapperTests.java | 65 ++++++++ .../SimpleHttpCodeStatusMapperTests.java | 1 + .../endpoint/SimpleStatusAggregatorTests.java | 1 + .../endpoint/StatusAggregatorTests.java | 70 ++++++++ .../endpoint/TestHealthEndpointGroup.java | 4 +- ...utoConfiguredHealthEndpointGroupTests.java | 4 +- ...toConfiguredHealthEndpointGroupsTests.java | 10 +- ...abilityProbesHealthEndpointGroupTests.java | 2 +- .../HealthEndpointAutoConfigurationTests.java | 12 +- ...cludeExcludeGroupMemberPredicateTests.java | 144 ---------------- ...HealthContributorFailureAnalyzerTests.java | 66 -------- .../HealthContributorMembershipTests.java | 155 ++++++++++++++++++ ...thContributorMembershipValidatorTests.java | 97 +++++++++++ 29 files changed, 794 insertions(+), 506 deletions(-) delete mode 100644 module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/NoSuchHealthContributorFailureAnalyzer.java create mode 100644 module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembership.java create mode 100644 module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembershipValidator.java rename module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/{actuate/endpoint/IncludeExcludeGroupMemberPredicate.java => contributor/IncludeExcludeHealthContributorMembership.java} (88%) delete mode 100644 module/spring-boot-health/src/main/resources/META-INF/spring.factories create mode 100644 module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/HttpCodeStatusMapperTests.java create mode 100644 module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/StatusAggregatorTests.java delete mode 100644 module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/IncludeExcludeGroupMemberPredicateTests.java delete mode 100644 module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/NoSuchHealthContributorFailureAnalyzerTests.java create mode 100644 module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembershipTests.java create mode 100644 module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembershipValidatorTests.java diff --git a/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/health/HealthEndpointDocumentationTests.java b/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/health/HealthEndpointDocumentationTests.java index ddf01dbc92b2..5db10993d81a 100644 --- a/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/health/HealthEndpointDocumentationTests.java +++ b/documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/health/HealthEndpointDocumentationTests.java @@ -34,8 +34,6 @@ import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroup; import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroups; import org.springframework.boot.health.actuate.endpoint.HttpCodeStatusMapper; -import org.springframework.boot.health.actuate.endpoint.SimpleHttpCodeStatusMapper; -import org.springframework.boot.health.actuate.endpoint.SimpleStatusAggregator; import org.springframework.boot.health.actuate.endpoint.StatusAggregator; import org.springframework.boot.health.application.DiskSpaceHealthIndicator; import org.springframework.boot.health.autoconfigure.registry.HealthContributorNameGenerator; @@ -136,9 +134,9 @@ CompositeHealthContributor brokerHealthContributor() { private static final class TestHealthEndpointGroup implements HealthEndpointGroup { - private final StatusAggregator statusAggregator = new SimpleStatusAggregator(); + private final StatusAggregator statusAggregator = StatusAggregator.getDefault(); - private final HttpCodeStatusMapper httpCodeStatusMapper = new SimpleHttpCodeStatusMapper(); + private final HttpCodeStatusMapper httpCodeStatusMapper = HttpCodeStatusMapper.getDefault(); @Override public boolean isMember(String name) { diff --git a/integration-test/spring-boot-actuator-integration-tests/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java b/integration-test/spring-boot-actuator-integration-tests/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java index d020daffcd50..f88911196b44 100644 --- a/integration-test/spring-boot-actuator-integration-tests/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java +++ b/integration-test/spring-boot-actuator-integration-tests/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java @@ -23,8 +23,6 @@ import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroup; import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroups; import org.springframework.boot.health.actuate.endpoint.HttpCodeStatusMapper; -import org.springframework.boot.health.actuate.endpoint.SimpleHttpCodeStatusMapper; -import org.springframework.boot.health.actuate.endpoint.SimpleStatusAggregator; import org.springframework.boot.health.actuate.endpoint.StatusAggregator; /** @@ -34,9 +32,9 @@ */ class TestHealthEndpointGroup implements HealthEndpointGroup { - private final StatusAggregator statusAggregator = new SimpleStatusAggregator(); + private final StatusAggregator statusAggregator = StatusAggregator.getDefault(); - private final HttpCodeStatusMapper httpCodeStatusMapper = new SimpleHttpCodeStatusMapper(); + private final HttpCodeStatusMapper httpCodeStatusMapper = HttpCodeStatusMapper.getDefault(); private final Predicate memberPredicate; diff --git a/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/HttpCodeStatusMapper.java b/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/HttpCodeStatusMapper.java index f5f2cc14e6b7..fea88387b14b 100644 --- a/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/HttpCodeStatusMapper.java +++ b/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/HttpCodeStatusMapper.java @@ -16,7 +16,12 @@ package org.springframework.boot.health.actuate.endpoint; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + import org.springframework.boot.health.contributor.Status; +import org.springframework.util.CollectionUtils; /** * Strategy used to map a {@link Status health status} to an HTTP status code. @@ -30,7 +35,10 @@ public interface HttpCodeStatusMapper { /** * An {@link HttpCodeStatusMapper} instance using default mappings. + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of #getDefault() */ + @SuppressWarnings("removal") + @Deprecated(since = "4.1.0", forRemoval = true) HttpCodeStatusMapper DEFAULT = new SimpleHttpCodeStatusMapper(); /** @@ -41,4 +49,26 @@ public interface HttpCodeStatusMapper { */ int getStatusCode(Status status); + /** + * Create a new {@link HttpCodeStatusMapper} with the specified mappings. + * @param mappings the mappings to use or {@code null} to use the default mappings + * @return a {@link HttpCodeStatusMapper} or {@link #getDefault()} + * @since 4.1.0 + */ + @SuppressWarnings("removal") + static HttpCodeStatusMapper of(@Nullable Map mappings) { + return CollectionUtils.isEmpty(mappings) ? SimpleHttpCodeStatusMapper.DEFAULT_MAPPINGS + : new SimpleHttpCodeStatusMapper(mappings); + } + + /** + * Return an {@link HttpCodeStatusMapper} instance using default mappings. + * @return a mapper using default mappings + * @since 4.1.0 + */ + @SuppressWarnings("removal") + static HttpCodeStatusMapper getDefault() { + return SimpleHttpCodeStatusMapper.DEFAULT_MAPPINGS; + } + } diff --git a/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/SimpleHttpCodeStatusMapper.java b/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/SimpleHttpCodeStatusMapper.java index 74a7664f2287..f6284ef85f18 100644 --- a/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/SimpleHttpCodeStatusMapper.java +++ b/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/SimpleHttpCodeStatusMapper.java @@ -25,6 +25,7 @@ import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.health.contributor.Status; +import org.springframework.lang.Contract; import org.springframework.util.CollectionUtils; /** @@ -34,15 +35,18 @@ * @author Stephane Nicoll * @author Phillip Webb * @since 4.0.0 + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link HttpCodeStatusMapper#of} */ +@Deprecated(since = "4.1.0", forRemoval = true) public class SimpleHttpCodeStatusMapper implements HttpCodeStatusMapper { - private static final Map DEFAULT_MAPPINGS; + static final SimpleHttpCodeStatusMapper DEFAULT_MAPPINGS; static { - Map defaultMappings = new HashMap<>(); - defaultMappings.put(Status.DOWN.getCode(), WebEndpointResponse.STATUS_SERVICE_UNAVAILABLE); - defaultMappings.put(Status.OUT_OF_SERVICE.getCode(), WebEndpointResponse.STATUS_SERVICE_UNAVAILABLE); - DEFAULT_MAPPINGS = getUniformMappings(defaultMappings); + Map mappings = new HashMap<>(); + mappings.put(Status.DOWN.getCode(), WebEndpointResponse.STATUS_SERVICE_UNAVAILABLE); + mappings.put(Status.OUT_OF_SERVICE.getCode(), WebEndpointResponse.STATUS_SERVICE_UNAVAILABLE); + DEFAULT_MAPPINGS = new SimpleHttpCodeStatusMapper(mappings); } private final Map mappings; @@ -51,7 +55,7 @@ public class SimpleHttpCodeStatusMapper implements HttpCodeStatusMapper { * Create a new {@link SimpleHttpCodeStatusMapper} instance using default mappings. */ public SimpleHttpCodeStatusMapper() { - this(null); + this((Map) null); } /** @@ -59,7 +63,7 @@ public SimpleHttpCodeStatusMapper() { * @param mappings the mappings to use or {@code null} to use the default mappings */ public SimpleHttpCodeStatusMapper(@Nullable Map mappings) { - this.mappings = CollectionUtils.isEmpty(mappings) ? DEFAULT_MAPPINGS : getUniformMappings(mappings); + this.mappings = CollectionUtils.isEmpty(mappings) ? DEFAULT_MAPPINGS.mappings : getUniformMappings(mappings); } @Override @@ -79,18 +83,13 @@ private static Map getUniformMappings(Map mapp return Collections.unmodifiableMap(result); } + @Contract("!null -> !null") private static @Nullable String getUniformCode(@Nullable String code) { - if (code == null) { - return null; - } - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < code.length(); i++) { - char ch = code.charAt(i); - if (Character.isAlphabetic(ch) || Character.isDigit(ch)) { - builder.append(Character.toLowerCase(ch)); - } - } - return builder.toString(); + return (code != null) ? code.codePoints() + .filter(Character::isLetterOrDigit) + .map(Character::toLowerCase) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString() : null; } } diff --git a/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/SimpleStatusAggregator.java b/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/SimpleStatusAggregator.java index 671efed02092..2c3dde2213e6 100644 --- a/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/SimpleStatusAggregator.java +++ b/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/SimpleStatusAggregator.java @@ -16,9 +16,7 @@ package org.springframework.boot.health.actuate.endpoint; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; @@ -36,42 +34,39 @@ * * @author Phillip Webb * @since 4.0.0 + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of {@link StatusAggregator#of} */ +@Deprecated(since = "4.1.0", forRemoval = true) public class SimpleStatusAggregator implements StatusAggregator { - private static final List DEFAULT_ORDER; - - static final StatusAggregator INSTANCE; - - static { - List defaultOrder = new ArrayList<>(); - defaultOrder.add(Status.DOWN.getCode()); - defaultOrder.add(Status.OUT_OF_SERVICE.getCode()); - defaultOrder.add(Status.UP.getCode()); - defaultOrder.add(Status.UNKNOWN.getCode()); - DEFAULT_ORDER = Collections.unmodifiableList(getUniformCodes(defaultOrder.stream())); - INSTANCE = new SimpleStatusAggregator(); - } + static final SimpleStatusAggregator DEFAULT_ORDER = new SimpleStatusAggregator(); private final List order; - private final Comparator comparator = new StatusComparator(); + private final Comparator comparator = Comparator.comparingInt(this::orderIndex) + .thenComparing(Status::getCode); public SimpleStatusAggregator() { - this.order = DEFAULT_ORDER; + this(Status.DEFAULT_ORDER.stream().map(Status::getCode)); } public SimpleStatusAggregator(Status... order) { - this.order = ObjectUtils.isEmpty(order) ? DEFAULT_ORDER - : getUniformCodes(Arrays.stream(order).map(Status::getCode)); + this.order = ObjectUtils.isEmpty(order) ? DEFAULT_ORDER.order + : Arrays.stream(order).map(SimpleStatusAggregator::getUniformCode).toList(); } public SimpleStatusAggregator(String... order) { - this.order = ObjectUtils.isEmpty(order) ? DEFAULT_ORDER : getUniformCodes(Arrays.stream(order)); + this.order = ObjectUtils.isEmpty(order) ? DEFAULT_ORDER.order + : Arrays.stream(order).map(SimpleStatusAggregator::getUniformCode).toList(); } public SimpleStatusAggregator(List order) { - this.order = CollectionUtils.isEmpty(order) ? DEFAULT_ORDER : getUniformCodes(order.stream()); + this.order = CollectionUtils.isEmpty(order) ? DEFAULT_ORDER.order + : order.stream().map(SimpleStatusAggregator::getUniformCode).toList(); + } + + SimpleStatusAggregator(Stream order) { + this.order = order.map(SimpleStatusAggregator::getUniformCode).toList(); } @Override @@ -80,41 +75,24 @@ public Status getAggregateStatus(Set statuses) { } private boolean contains(Status status) { - return this.order.contains(getUniformCode(status.getCode())); + return this.order.contains(getUniformCode(status)); } - private static List getUniformCodes(Stream codes) { - return codes.map(SimpleStatusAggregator::getUniformCode).toList(); + private int orderIndex(Status status) { + return this.order.indexOf(getUniformCode(status)); } - @Contract("!null -> !null") - private static @Nullable String getUniformCode(@Nullable String code) { - if (code == null) { - return null; - } - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < code.length(); i++) { - char ch = code.charAt(i); - if (Character.isAlphabetic(ch) || Character.isDigit(ch)) { - builder.append(Character.toLowerCase(ch)); - } - } - return builder.toString(); + private static @Nullable String getUniformCode(Status status) { + return getUniformCode(status.getCode()); } - /** - * {@link Comparator} used to order {@link Status}. - */ - private final class StatusComparator implements Comparator { - - @Override - public int compare(Status s1, Status s2) { - List order = SimpleStatusAggregator.this.order; - int i1 = order.indexOf(getUniformCode(s1.getCode())); - int i2 = order.indexOf(getUniformCode(s2.getCode())); - return (i1 < i2) ? -1 : (i1 != i2) ? 1 : s1.getCode().compareTo(s2.getCode()); - } - + @Contract("!null -> !null") + private static @Nullable String getUniformCode(@Nullable String code) { + return (code != null) ? code.codePoints() + .filter(Character::isLetterOrDigit) + .map(Character::toLowerCase) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString() : null; } } diff --git a/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/StatusAggregator.java b/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/StatusAggregator.java index c3a5907feafa..07b9dbeb3c75 100644 --- a/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/StatusAggregator.java +++ b/module/spring-boot-health/src/main/java/org/springframework/boot/health/actuate/endpoint/StatusAggregator.java @@ -18,16 +18,18 @@ import java.util.Arrays; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import org.springframework.boot.health.contributor.Health; import org.springframework.boot.health.contributor.Status; +import org.springframework.util.ObjectUtils; /** * Strategy used to aggregate {@link Status} instances. *

    * This is required in order to combine subsystem states expressed through - * {@link Health#getStatus()} into one state for the entire system. + * {@link Health#getStatus()} into one state for the health endpoint. * * @author Phillip Webb * @since 4.0.0 @@ -35,14 +37,6 @@ @FunctionalInterface public interface StatusAggregator { - /** - * Return {@link StatusAggregator} instance using default ordering rules. - * @return a {@code StatusAggregator} with default ordering rules. - */ - static StatusAggregator getDefault() { - return SimpleStatusAggregator.INSTANCE; - } - /** * Return the aggregate status for the given set of statuses. * @param statuses the statuses to aggregate @@ -59,4 +53,50 @@ default Status getAggregateStatus(Status... statuses) { */ Status getAggregateStatus(Set statuses); + /** + * Return a {@link StatusAggregator} backed by the given ordered status list. + * @param order the status order + * @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is + * provided + * @since 4.1.0 + */ + @SuppressWarnings("removal") + static StatusAggregator of(Status... order) { + return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(Arrays.stream(order).map(Status::getCode)) + : getDefault(); + } + + /** + * Return a {@link StatusAggregator} backed by the given ordered status list. + * @param order the status order + * @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is + * provided + * @since 4.1.0 + */ + @SuppressWarnings("removal") + static StatusAggregator of(String... order) { + return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(Arrays.stream(order)) : getDefault(); + } + + /** + * Return a {@link StatusAggregator} backed by the given ordered status list. + * @param order the status order + * @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is + * provided + * @since 4.1.0 + */ + @SuppressWarnings("removal") + static StatusAggregator of(List order) { + return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(order.stream()) : getDefault(); + } + + /** + * Return {@link StatusAggregator} instance using default ordering rules. + * @return a {@code StatusAggregator} with default ordering rules. + */ + @SuppressWarnings("removal") + static StatusAggregator getDefault() { + return SimpleStatusAggregator.DEFAULT_ORDER; + } + } diff --git a/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroup.java b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroup.java index df56619a28fe..584d1333f0b2 100644 --- a/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroup.java +++ b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroup.java @@ -17,7 +17,6 @@ package org.springframework.boot.health.autoconfigure.actuate.endpoint; import java.util.Collection; -import java.util.function.Predicate; import org.jspecify.annotations.Nullable; @@ -27,9 +26,10 @@ import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroup; import org.springframework.boot.health.actuate.endpoint.HttpCodeStatusMapper; import org.springframework.boot.health.actuate.endpoint.StatusAggregator; +import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembership; /** - * Auto-configured {@link HealthEndpointGroup} backed by {@link HealthProperties}. + * Auto-configured {@link HealthEndpointGroup}. * * @author Phillip Webb * @author Andy Wilkinson @@ -37,7 +37,7 @@ */ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup { - private final Predicate members; + private final HealthContributorMembership membership; private final StatusAggregator statusAggregator; @@ -53,7 +53,7 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup { /** * Create a new {@link AutoConfiguredHealthEndpointGroup} instance. - * @param members a predicate used to test for group membership + * @param membership used to test for group membership * @param statusAggregator the status aggregator to use * @param httpCodeStatusMapper the HTTP code status mapper to use * @param showComponents the show components setting @@ -61,10 +61,10 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup { * @param roles the roles to match * @param additionalPath the additional path to use for this group */ - AutoConfiguredHealthEndpointGroup(Predicate members, StatusAggregator statusAggregator, + AutoConfiguredHealthEndpointGroup(HealthContributorMembership membership, StatusAggregator statusAggregator, HttpCodeStatusMapper httpCodeStatusMapper, @Nullable Show showComponents, Show showDetails, Collection roles, @Nullable AdditionalHealthEndpointPath additionalPath) { - this.members = members; + this.membership = membership; this.statusAggregator = statusAggregator; this.httpCodeStatusMapper = httpCodeStatusMapper; this.showComponents = showComponents; @@ -75,7 +75,7 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup { @Override public boolean isMember(String name) { - return this.members.test(name); + return this.membership.isMember(name); } @Override diff --git a/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroups.java b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroups.java index 68f45c8b2641..d92291352f41 100644 --- a/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroups.java +++ b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroups.java @@ -23,7 +23,6 @@ import java.util.Objects; import java.util.Set; import java.util.TreeMap; -import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; @@ -43,11 +42,10 @@ import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroup; import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroups; import org.springframework.boot.health.actuate.endpoint.HttpCodeStatusMapper; -import org.springframework.boot.health.actuate.endpoint.SimpleHttpCodeStatusMapper; -import org.springframework.boot.health.actuate.endpoint.SimpleStatusAggregator; import org.springframework.boot.health.actuate.endpoint.StatusAggregator; import org.springframework.boot.health.autoconfigure.actuate.endpoint.HealthEndpointProperties.Group; import org.springframework.boot.health.autoconfigure.actuate.endpoint.HealthProperties.Status; +import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembership; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.util.CollectionUtils; @@ -61,8 +59,6 @@ */ class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups, AdditionalPathsMapper { - private static final Predicate ALL = (name) -> true; - private final HealthEndpointGroup primaryGroup; private final Map groups; @@ -78,22 +74,18 @@ class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups, Additi Show showComponents = properties.getShowComponents(); Show showDetails = properties.getShowDetails(); Set roles = properties.getRoles(); - StatusAggregator statusAggregator = getNonQualifiedBean(beanFactory, StatusAggregator.class); - if (statusAggregator == null) { - statusAggregator = new SimpleStatusAggregator(properties.getStatus().getOrder()); - } - HttpCodeStatusMapper httpCodeStatusMapper = getNonQualifiedBean(beanFactory, HttpCodeStatusMapper.class); - if (httpCodeStatusMapper == null) { - httpCodeStatusMapper = new SimpleHttpCodeStatusMapper(properties.getStatus().getHttpMapping()); - } - this.primaryGroup = new AutoConfiguredHealthEndpointGroup(ALL, statusAggregator, httpCodeStatusMapper, - showComponents, showDetails, roles, null); - this.groups = createGroups(properties.getGroup(), beanFactory, statusAggregator, httpCodeStatusMapper, - showComponents, showDetails, roles); + StatusAggregator statusAggregator = getNonQualifiedBean(beanFactory, StatusAggregator.class, + () -> StatusAggregator.of(properties.getStatus().getOrder())); + HttpCodeStatusMapper statusMapper = getNonQualifiedBean(beanFactory, HttpCodeStatusMapper.class, + () -> HttpCodeStatusMapper.of(properties.getStatus().getHttpMapping())); + this.primaryGroup = new AutoConfiguredHealthEndpointGroup(HealthContributorMembership.always(), + statusAggregator, statusMapper, showComponents, showDetails, roles, null); + this.groups = createGroups(properties.getGroup(), beanFactory, statusAggregator, statusMapper, showComponents, + showDetails, roles); } private Map createGroups(Map groupProperties, BeanFactory beanFactory, - StatusAggregator defaultStatusAggregator, HttpCodeStatusMapper defaultHttpCodeStatusMapper, + StatusAggregator defaultStatusAggregator, HttpCodeStatusMapper defaultStatusMapper, @Nullable Show defaultShowComponents, Show defaultShowDetails, Set defaultRoles) { Map groups = new TreeMap<>(); groupProperties.forEach((groupName, group) -> { @@ -102,29 +94,30 @@ private Map createGroups(Map groupPr : defaultShowComponents; Show showDetails = (group.getShowDetails() != null) ? group.getShowDetails() : defaultShowDetails; Set roles = !CollectionUtils.isEmpty(group.getRoles()) ? group.getRoles() : defaultRoles; - StatusAggregator statusAggregator = getQualifiedBean(beanFactory, StatusAggregator.class, groupName, () -> { - if (!CollectionUtils.isEmpty(status.getOrder())) { - return new SimpleStatusAggregator(status.getOrder()); - } - return defaultStatusAggregator; - }); - HttpCodeStatusMapper httpCodeStatusMapper = getQualifiedBean(beanFactory, HttpCodeStatusMapper.class, - groupName, () -> { - if (!CollectionUtils.isEmpty(status.getHttpMapping())) { - return new SimpleHttpCodeStatusMapper(status.getHttpMapping()); - } - return defaultHttpCodeStatusMapper; - }); - Predicate members = new IncludeExcludeGroupMemberPredicate(group.getInclude(), group.getExclude()); + StatusAggregator statusAggregator = getQualifiedBean(beanFactory, StatusAggregator.class, groupName, + () -> createStatusAggregator(status.getOrder(), defaultStatusAggregator)); + HttpCodeStatusMapper statusMapper = getQualifiedBean(beanFactory, HttpCodeStatusMapper.class, groupName, + () -> createStatusMapper(status.getHttpMapping(), defaultStatusMapper)); + HealthContributorMembership membership = HealthContributorMembership.byIncludeExclude(group.getInclude(), + group.getExclude()); AdditionalHealthEndpointPath additionalPath = (group.getAdditionalPath() != null) ? AdditionalHealthEndpointPath.from(group.getAdditionalPath()) : null; - groups.put(groupName, new AutoConfiguredHealthEndpointGroup(members, statusAggregator, httpCodeStatusMapper, + groups.put(groupName, new AutoConfiguredHealthEndpointGroup(membership, statusAggregator, statusMapper, showComponents, showDetails, roles, additionalPath)); }); return Collections.unmodifiableMap(groups); } - private @Nullable T getNonQualifiedBean(ListableBeanFactory beanFactory, Class type) { + private StatusAggregator createStatusAggregator(List order, StatusAggregator defaultStatusAggregator) { + return (!CollectionUtils.isEmpty(order)) ? StatusAggregator.of(order) : defaultStatusAggregator; + } + + private HttpCodeStatusMapper createStatusMapper(Map mapping, + HttpCodeStatusMapper defaultStatusMapper) { + return (!CollectionUtils.isEmpty(mapping)) ? HttpCodeStatusMapper.of(mapping) : defaultStatusMapper; + } + + private T getNonQualifiedBean(ListableBeanFactory beanFactory, Class type, Supplier fallback) { List candidates = new ArrayList<>(); for (String beanName : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) { String[] aliases = beanFactory.getAliases(beanName); @@ -135,7 +128,7 @@ private Map createGroups(Map groupPr } } if (candidates.isEmpty()) { - return null; + return fallback.get(); } if (candidates.size() == 1) { return beanFactory.getBean(candidates.get(0), type); diff --git a/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AvailabilityProbesHealthEndpointGroup.java b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AvailabilityProbesHealthEndpointGroup.java index 8ae893c94d62..6c0feef871ba 100644 --- a/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AvailabilityProbesHealthEndpointGroup.java +++ b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AvailabilityProbesHealthEndpointGroup.java @@ -67,7 +67,7 @@ public StatusAggregator getStatusAggregator() { @Override public HttpCodeStatusMapper getHttpCodeStatusMapper() { - return HttpCodeStatusMapper.DEFAULT; + return HttpCodeStatusMapper.getDefault(); } @Override diff --git a/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/HealthEndpointConfiguration.java b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/HealthEndpointConfiguration.java index 43f74c19a8fd..563fb7712623 100644 --- a/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/HealthEndpointConfiguration.java +++ b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/HealthEndpointConfiguration.java @@ -16,14 +16,8 @@ package org.springframework.boot.health.autoconfigure.actuate.endpoint; -import java.util.Set; -import java.util.function.BiFunction; - -import org.jspecify.annotations.Nullable; - import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -31,17 +25,13 @@ import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroups; import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroupsPostProcessor; import org.springframework.boot.health.actuate.endpoint.HttpCodeStatusMapper; -import org.springframework.boot.health.actuate.endpoint.SimpleHttpCodeStatusMapper; -import org.springframework.boot.health.actuate.endpoint.SimpleStatusAggregator; import org.springframework.boot.health.actuate.endpoint.StatusAggregator; -import org.springframework.boot.health.contributor.HealthContributors; -import org.springframework.boot.health.contributor.ReactiveHealthContributors; +import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembershipValidator; import org.springframework.boot.health.registry.HealthContributorRegistry; import org.springframework.boot.health.registry.ReactiveHealthContributorRegistry; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.util.CollectionUtils; /** * Configuration for {@link HealthEndpoint} infrastructure beans. @@ -52,16 +42,18 @@ @Configuration(proxyBeanMethods = false) class HealthEndpointConfiguration { + private static final String VALIDATE_MEMBERSHIP_PROPERTY = "management.endpoint.health.validate-group-membership"; + @Bean @ConditionalOnMissingBean StatusAggregator healthStatusAggregator(HealthEndpointProperties properties) { - return new SimpleStatusAggregator(properties.getStatus().getOrder()); + return StatusAggregator.of(properties.getStatus().getOrder()); } @Bean @ConditionalOnMissingBean HttpCodeStatusMapper healthHttpCodeStatusMapper(HealthEndpointProperties properties) { - return new SimpleHttpCodeStatusMapper(properties.getStatus().getHttpMapping()); + return HttpCodeStatusMapper.of(properties.getStatus().getHttpMapping()); } @Bean @@ -78,12 +70,17 @@ GroupsHealthContributorNameValidator groupsHealthContributorNameValidator( } @Bean - @ConditionalOnBooleanProperty(name = "management.endpoint.health.validate-group-membership", matchIfMissing = true) - HealthEndpointGroupMembershipValidator healthEndpointGroupMembershipValidator(HealthEndpointProperties properties, + @ConditionalOnBooleanProperty(name = VALIDATE_MEMBERSHIP_PROPERTY, matchIfMissing = true) + HealthContributorMembershipValidator healthEndpointGroupMembershipValidator(HealthEndpointProperties properties, HealthContributorRegistry healthContributorRegistry, ObjectProvider reactiveHealthContributorRegistry) { - return new HealthEndpointGroupMembershipValidator(properties, healthContributorRegistry, - reactiveHealthContributorRegistry.getIfAvailable()); + return new HealthContributorMembershipValidator(healthContributorRegistry, + reactiveHealthContributorRegistry.getIfAvailable(), VALIDATE_MEMBERSHIP_PROPERTY, + (members) -> properties.getGroup().forEach((groupName, group) -> { + String property = "management.endpoint.health.group." + groupName; + members.member(property + ".include".formatted(groupName), group.getInclude()); + members.member(property + ".exclude".formatted(groupName), group.getExclude()); + })); } @Bean @@ -131,93 +128,4 @@ private Object applyPostProcessors(HealthEndpointGroups bean) { } - /** - * {@link SmartInitializingSingleton} that validates health endpoint group membership, - * throwing a {@link NoSuchHealthContributorException} if an included or excluded - * contributor does not exist. - */ - static class HealthEndpointGroupMembershipValidator implements SmartInitializingSingleton { - - private final HealthEndpointProperties properties; - - private final HealthContributorRegistry registry; - - @Nullable ReactiveHealthContributorRegistry fallbackRegistry; - - HealthEndpointGroupMembershipValidator(HealthEndpointProperties properties, HealthContributorRegistry registry, - @Nullable ReactiveHealthContributorRegistry fallbackRegistry) { - this.properties = properties; - this.registry = registry; - this.fallbackRegistry = fallbackRegistry; - } - - @Override - public void afterSingletonsInstantiated() { - validateGroups(); - } - - private void validateGroups() { - this.properties.getGroup().forEach((name, group) -> { - validate(group.getInclude(), "Included", name); - validate(group.getExclude(), "Excluded", name); - }); - } - - private void validate(@Nullable Set names, String type, String group) { - if (CollectionUtils.isEmpty(names)) { - return; - } - for (String name : names) { - if ("*".equals(name)) { - return; - } - String[] path = name.split("/"); - if (!contributorExists(path)) { - throw new NoSuchHealthContributorException(type, name, group); - } - } - } - - private boolean contributorExists(String[] path) { - return contributorExistsInMainRegistry(path) || contributorExistsInFallbackRegistry(path); - } - - private boolean contributorExistsInMainRegistry(String[] path) { - return contributorExists(path, this.registry, HealthContributors.class, HealthContributors::getContributor); - } - - private boolean contributorExistsInFallbackRegistry(String[] path) { - return contributorExists(path, this.fallbackRegistry, ReactiveHealthContributors.class, - ReactiveHealthContributors::getContributor); - } - - @SuppressWarnings("unchecked") - private boolean contributorExists(String[] path, @Nullable Object registry, Class collectionType, - BiFunction getFromCollection) { - int pathOffset = 0; - Object contributor = registry; - while (pathOffset < path.length) { - if (contributor == null || !collectionType.isInstance(contributor)) { - return false; - } - contributor = getFromCollection.apply((C) contributor, path[pathOffset]); - pathOffset++; - } - return (contributor != null); - } - - /** - * Thrown when a contributor that does not exist is included in or excluded from a - * group. - */ - static class NoSuchHealthContributorException extends RuntimeException { - - NoSuchHealthContributorException(String type, String name, String group) { - super(type + " health contributor '" + name + "' in group '" + group + "' does not exist"); - } - - } - - } - } diff --git a/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/NoSuchHealthContributorFailureAnalyzer.java b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/NoSuchHealthContributorFailureAnalyzer.java deleted file mode 100644 index 30a240de7fce..000000000000 --- a/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/NoSuchHealthContributorFailureAnalyzer.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.health.autoconfigure.actuate.endpoint; - -import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; -import org.springframework.boot.diagnostics.FailureAnalysis; -import org.springframework.boot.health.autoconfigure.actuate.endpoint.HealthEndpointConfiguration.HealthEndpointGroupMembershipValidator.NoSuchHealthContributorException; - -/** - * An {@link AbstractFailureAnalyzer} that performs analysis of failures caused by a - * {@link NoSuchHealthContributorException}. - * - * @author Moritz Halbritter - */ -class NoSuchHealthContributorFailureAnalyzer extends AbstractFailureAnalyzer { - - @Override - protected FailureAnalysis analyze(Throwable rootFailure, NoSuchHealthContributorException cause) { - return new FailureAnalysis(cause.getMessage(), "Update your application to correct the invalid configuration.\n" - + "You can also set 'management.endpoint.health.validate-group-membership' to false to disable the validation.", - cause); - } - -} diff --git a/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembership.java b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembership.java new file mode 100644 index 000000000000..451d8855c562 --- /dev/null +++ b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembership.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.health.autoconfigure.contributor; + +import java.util.Set; + +import org.jspecify.annotations.Nullable; + +/** + * Determines if a health contributor is a member of a group or collection. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 4.1.0 + */ +@FunctionalInterface +public interface HealthContributorMembership { + + /** + * Returns {@code true} if the given contributor is a member. + * @param name the contributor name + * @return {@code true} if the contributor is a member + */ + boolean isMember(String name); + + /** + * Return a {@link HealthContributorMembership} that always matches. + * @return a {@link HealthContributorMembership} instance that always matches + */ + static HealthContributorMembership always() { + return (name) -> true; + } + + /** + * Return a {@link HealthContributorMembership} instance that matches based on include + * and exclude patterns. Support {@code '*'} patterns and {@code '/'} separated names. + * @param include the include patterns or {@code null} + * @param exclude the exclude patterns of {@code null} + * @return a new {@link HealthContributorMembership} instance + */ + static HealthContributorMembership byIncludeExclude(@Nullable Set include, @Nullable Set exclude) { + return new IncludeExcludeHealthContributorMembership(include, exclude); + } + +} diff --git a/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembershipValidator.java b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembershipValidator.java new file mode 100644 index 000000000000..e6180c7da422 --- /dev/null +++ b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembershipValidator.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.health.autoconfigure.contributor; + +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.boot.diagnostics.FailureAnalyzedException; +import org.springframework.boot.health.contributor.HealthContributors; +import org.springframework.boot.health.contributor.ReactiveHealthContributors; +import org.springframework.boot.health.registry.HealthContributorRegistry; +import org.springframework.boot.health.registry.ReactiveHealthContributorRegistry; +import org.springframework.util.CollectionUtils; + +/** + * {@link SmartInitializingSingleton} that validates health membership, throwing a + * {@link FailureAnalyzedException} if an included or excluded contributor does not exist. + * This implementation supports the same include/exclude patterns as + * {@link HealthContributorMembership#byIncludeExclude(Set, Set)} + * + * @author Andy Wilkinson + * @author Phillip Webb + * @since 4.1.0 + */ +public class HealthContributorMembershipValidator implements SmartInitializingSingleton { + + private final HealthContributorRegistry registry; + + private final @Nullable ReactiveHealthContributorRegistry fallbackRegistry; + + private final String disableValidationProperty; + + private final Consumer members; + + /** + * Create a new {@link HealthContributorMembershipValidator} instance. + * @param registry the health registry + * @param fallbackRegistry the fallback registry or {@code null} + * @param disableValidationProperty the property that can be used to disable + * validation + * @param members consumer used to provide the members + */ + public HealthContributorMembershipValidator(HealthContributorRegistry registry, + @Nullable ReactiveHealthContributorRegistry fallbackRegistry, String disableValidationProperty, + Consumer members) { + this.registry = registry; + this.fallbackRegistry = fallbackRegistry; + this.disableValidationProperty = disableValidationProperty; + this.members = members; + } + + @Override + public void afterSingletonsInstantiated() { + validateMembers(); + } + + private void validateMembers() { + this.members.accept(this::validate); + } + + private void validate(String property, @Nullable Set names) { + if (CollectionUtils.isEmpty(names)) { + return; + } + for (String name : names) { + if ("*".equals(name)) { + return; + } + String[] path = name.split("/"); + if (!contributorExists(path)) { + String description = "Health contributor '%s' defined in '%s' does not exist".formatted(name, property); + String action = "Update your application to correct the invalid configuration.\nYou can also set '%s' to false to disable the validation." + .formatted(this.disableValidationProperty); + throw new FailureAnalyzedException(description, action); + } + } + } + + private boolean contributorExists(String[] path) { + return contributorExistsInMainRegistry(path) || contributorExistsInFallbackRegistry(path); + } + + private boolean contributorExistsInMainRegistry(String[] path) { + return contributorExists(path, this.registry, HealthContributors.class, HealthContributors::getContributor); + } + + private boolean contributorExistsInFallbackRegistry(String[] path) { + return contributorExists(path, this.fallbackRegistry, ReactiveHealthContributors.class, + ReactiveHealthContributors::getContributor); + } + + @SuppressWarnings("unchecked") + private boolean contributorExists(String[] path, @Nullable Object registry, Class collectionType, + BiFunction getFromCollection) { + int pathOffset = 0; + Object contributor = registry; + while (pathOffset < path.length) { + if (contributor == null || !collectionType.isInstance(contributor)) { + return false; + } + contributor = getFromCollection.apply((C) contributor, path[pathOffset]); + pathOffset++; + } + return (contributor != null); + } + + /** + * Callback used to provide member information. + */ + @FunctionalInterface + public interface Members { + + /** + * Provide members. + * @param property the property providing the members + * @param members the members + */ + void member(String property, @Nullable Set members); + + } + +} diff --git a/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/IncludeExcludeGroupMemberPredicate.java b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/contributor/IncludeExcludeHealthContributorMembership.java similarity index 88% rename from module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/IncludeExcludeGroupMemberPredicate.java rename to module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/contributor/IncludeExcludeHealthContributorMembership.java index 9cd8b1874539..4ad8c7f021fd 100644 --- a/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/IncludeExcludeGroupMemberPredicate.java +++ b/module/spring-boot-health/src/main/java/org/springframework/boot/health/autoconfigure/contributor/IncludeExcludeHealthContributorMembership.java @@ -14,12 +14,11 @@ * limitations under the License. */ -package org.springframework.boot.health.autoconfigure.actuate.endpoint; +package org.springframework.boot.health.autoconfigure.contributor; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; -import java.util.function.Predicate; import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; @@ -32,19 +31,19 @@ * @author Phillip Webb * @author Madhura Bhave */ -class IncludeExcludeGroupMemberPredicate implements Predicate { +class IncludeExcludeHealthContributorMembership implements HealthContributorMembership { private final Set include; private final Set exclude; - IncludeExcludeGroupMemberPredicate(@Nullable Set include, @Nullable Set exclude) { + IncludeExcludeHealthContributorMembership(@Nullable Set include, @Nullable Set exclude) { this.include = clean(include); this.exclude = clean(exclude); } @Override - public boolean test(String name) { + public boolean isMember(String name) { name = clean(name); return isIncluded(name) && !isExcluded(name); } diff --git a/module/spring-boot-health/src/main/java/org/springframework/boot/health/contributor/Status.java b/module/spring-boot-health/src/main/java/org/springframework/boot/health/contributor/Status.java index 07fbfefd78da..3aab863ecf37 100644 --- a/module/spring-boot-health/src/main/java/org/springframework/boot/health/contributor/Status.java +++ b/module/spring-boot-health/src/main/java/org/springframework/boot/health/contributor/Status.java @@ -16,6 +16,8 @@ package org.springframework.boot.health.contributor; +import java.util.List; + import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; @@ -60,6 +62,12 @@ public final class Status { */ public static final Status OUT_OF_SERVICE = new Status("OUT_OF_SERVICE"); + /** + * The default order use to prioritize standard status instances. + * @since 4.1.0 + */ + public static final List DEFAULT_ORDER = List.of(DOWN, OUT_OF_SERVICE, UP, UNKNOWN); + private final String code; private final String description; diff --git a/module/spring-boot-health/src/main/resources/META-INF/spring.factories b/module/spring-boot-health/src/main/resources/META-INF/spring.factories deleted file mode 100644 index ec9d4ccc7fe2..000000000000 --- a/module/spring-boot-health/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Failure Analyzers -org.springframework.boot.diagnostics.FailureAnalyzer=\ -org.springframework.boot.health.autoconfigure.actuate.endpoint.NoSuchHealthContributorFailureAnalyzer diff --git a/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/HttpCodeStatusMapperTests.java b/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/HttpCodeStatusMapperTests.java new file mode 100644 index 000000000000..6f4f6cebd7b2 --- /dev/null +++ b/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/HttpCodeStatusMapperTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.health.actuate.endpoint; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.boot.health.contributor.Status; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link HttpCodeStatusMapper}. + * + * @author Phillip Webb + */ +class HttpCodeStatusMapperTests { + + @Test + void createWhenMappingsAreNullUsesDefaultMappings() { + HttpCodeStatusMapper mapper = HttpCodeStatusMapper.of(null); + assertThat(mapper.getStatusCode(Status.UNKNOWN)).isEqualTo(WebEndpointResponse.STATUS_OK); + assertThat(mapper.getStatusCode(Status.UP)).isEqualTo(WebEndpointResponse.STATUS_OK); + assertThat(mapper.getStatusCode(Status.DOWN)).isEqualTo(WebEndpointResponse.STATUS_SERVICE_UNAVAILABLE); + assertThat(mapper.getStatusCode(Status.OUT_OF_SERVICE)) + .isEqualTo(WebEndpointResponse.STATUS_SERVICE_UNAVAILABLE); + } + + @Test + void getStatusCodeReturnsMappedStatus() { + Map map = new LinkedHashMap<>(); + map.put("up", 123); + map.put("down", 456); + HttpCodeStatusMapper mapper = HttpCodeStatusMapper.of(map); + assertThat(mapper.getStatusCode(Status.UP)).isEqualTo(123); + assertThat(mapper.getStatusCode(Status.DOWN)).isEqualTo(456); + assertThat(mapper.getStatusCode(Status.OUT_OF_SERVICE)).isEqualTo(200); + } + + @Test + void getStatusCodeWhenMappingsAreNotUniformReturnsMappedStatus() { + Map map = new LinkedHashMap<>(); + map.put("out-of-service", 123); + HttpCodeStatusMapper mapper = HttpCodeStatusMapper.of(map); + assertThat(mapper.getStatusCode(Status.OUT_OF_SERVICE)).isEqualTo(123); + } + +} diff --git a/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/SimpleHttpCodeStatusMapperTests.java b/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/SimpleHttpCodeStatusMapperTests.java index 6d6d8ee1511b..00f83ab78a75 100644 --- a/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/SimpleHttpCodeStatusMapperTests.java +++ b/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/SimpleHttpCodeStatusMapperTests.java @@ -31,6 +31,7 @@ * * @author Phillip Webb */ +@SuppressWarnings("removal") class SimpleHttpCodeStatusMapperTests { @Test diff --git a/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/SimpleStatusAggregatorTests.java b/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/SimpleStatusAggregatorTests.java index f29b34507122..901cd2c08ec2 100644 --- a/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/SimpleStatusAggregatorTests.java +++ b/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/SimpleStatusAggregatorTests.java @@ -28,6 +28,7 @@ * @author Phillip Webb * @author Christian Dupuis */ +@SuppressWarnings("removal") class SimpleStatusAggregatorTests { @Test diff --git a/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/StatusAggregatorTests.java b/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/StatusAggregatorTests.java new file mode 100644 index 000000000000..2de07d4d1cd3 --- /dev/null +++ b/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/StatusAggregatorTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.health.actuate.endpoint; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.health.contributor.Status; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StatusAggregator}. + * + * @author Phillip Webb + */ +class StatusAggregatorTests { + + @Test + void ofAndGetAggregateStatusWhenUsingDefaultInstance() { + StatusAggregator aggregator = StatusAggregator.getDefault(); + Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE); + assertThat(status).isEqualTo(Status.DOWN); + } + + @Test + void ofAndGetAggregateStatusWhenUsingCustomOrder() { + StatusAggregator aggregator = StatusAggregator.of(Status.UNKNOWN, Status.UP, Status.OUT_OF_SERVICE, + Status.DOWN); + Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE); + assertThat(status).isEqualTo(Status.UNKNOWN); + } + + @Test + void ofAndGetAggregateStatusWhenHasCustomStatusAndUsingDefaultOrder() { + StatusAggregator aggregator = StatusAggregator.getDefault(); + Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE, + new Status("CUSTOM")); + assertThat(status).isEqualTo(Status.DOWN); + } + + @Test + void ofAndGetAggregateStatusWhenHasCustomStatusAndUsingCustomOrder() { + StatusAggregator aggregator = StatusAggregator.of("DOWN", "OUT_OF_SERVICE", "UP", "UNKNOWN", "CUSTOM"); + Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE, + new Status("CUSTOM")); + assertThat(status).isEqualTo(Status.DOWN); + } + + @Test + void ofWithNonUniformCodes() { + StatusAggregator aggregator = StatusAggregator.of("out-of-service", "up"); + Status status = aggregator.getAggregateStatus(Status.UP, Status.OUT_OF_SERVICE); + assertThat(status).isEqualTo(Status.OUT_OF_SERVICE); + } + +} diff --git a/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/TestHealthEndpointGroup.java b/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/TestHealthEndpointGroup.java index 7b3a323beaed..e367bc89570b 100644 --- a/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/TestHealthEndpointGroup.java +++ b/module/spring-boot-health/src/test/java/org/springframework/boot/health/actuate/endpoint/TestHealthEndpointGroup.java @@ -29,9 +29,9 @@ */ class TestHealthEndpointGroup implements HealthEndpointGroup { - private final StatusAggregator statusAggregator = new SimpleStatusAggregator(); + private final StatusAggregator statusAggregator = StatusAggregator.getDefault(); - private final HttpCodeStatusMapper httpCodeStatusMapper = new SimpleHttpCodeStatusMapper(); + private final HttpCodeStatusMapper httpCodeStatusMapper = HttpCodeStatusMapper.getDefault(); private final Predicate memberPredicate; diff --git a/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroupTests.java b/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroupTests.java index e981cf9efd0e..d246773584d0 100644 --- a/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroupTests.java +++ b/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroupTests.java @@ -61,7 +61,7 @@ class AutoConfiguredHealthEndpointGroupTests { private Principal principal; @Test - void isMemberWhenMemberPredicateMatchesAcceptsTrue() { + void isMemberWhenMembershipMatchesAcceptsTrue() { AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> name.startsWith("a"), this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet(), null); assertThat(group.isMember("albert")).isTrue(); @@ -69,7 +69,7 @@ void isMemberWhenMemberPredicateMatchesAcceptsTrue() { } @Test - void isMemberWhenMemberPredicateRejectsReturnsTrue() { + void isMemberWhenMembershipRejectsReturnsTrue() { AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> name.startsWith("a"), this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet(), null); assertThat(group.isMember("bert")).isFalse(); diff --git a/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroupsTests.java b/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroupsTests.java index f8c6dbcc216d..6866e63ec1e8 100644 --- a/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroupsTests.java +++ b/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AutoConfiguredHealthEndpointGroupsTests.java @@ -31,8 +31,6 @@ import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroup; import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroups; import org.springframework.boot.health.actuate.endpoint.HttpCodeStatusMapper; -import org.springframework.boot.health.actuate.endpoint.SimpleHttpCodeStatusMapper; -import org.springframework.boot.health.actuate.endpoint.SimpleStatusAggregator; import org.springframework.boot.health.actuate.endpoint.StatusAggregator; import org.springframework.boot.health.contributor.Status; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -394,7 +392,7 @@ static class CustomStatusAggregatorConfiguration { @Bean @Primary StatusAggregator statusAggregator() { - return new SimpleStatusAggregator(Status.UNKNOWN, Status.UP, Status.DOWN); + return StatusAggregator.of(Status.UNKNOWN, Status.UP, Status.DOWN); } } @@ -405,7 +403,7 @@ static class CustomStatusAggregatorGroupAConfiguration { @Bean @Qualifier("a") StatusAggregator statusAggregator() { - return new SimpleStatusAggregator(Status.UNKNOWN, Status.UP, Status.DOWN); + return StatusAggregator.of(Status.UNKNOWN, Status.UP, Status.DOWN); } } @@ -416,7 +414,7 @@ static class CustomHttpCodeStatusMapperConfiguration { @Bean @Primary HttpCodeStatusMapper httpCodeStatusMapper() { - return new SimpleHttpCodeStatusMapper(Collections.singletonMap(Status.DOWN.getCode(), 200)); + return HttpCodeStatusMapper.of(Collections.singletonMap(Status.DOWN.getCode(), 200)); } } @@ -427,7 +425,7 @@ static class CustomHttpCodeStatusMapperGroupAConfiguration { @Bean @Qualifier("a") HttpCodeStatusMapper httpCodeStatusMapper() { - return new SimpleHttpCodeStatusMapper(Collections.singletonMap(Status.DOWN.getCode(), 200)); + return HttpCodeStatusMapper.of(Collections.singletonMap(Status.DOWN.getCode(), 200)); } } diff --git a/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AvailabilityProbesHealthEndpointGroupTests.java b/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AvailabilityProbesHealthEndpointGroupTests.java index 35bce043018d..dec2eae6276d 100644 --- a/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AvailabilityProbesHealthEndpointGroupTests.java +++ b/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/AvailabilityProbesHealthEndpointGroupTests.java @@ -63,7 +63,7 @@ void getStatusAggregatorReturnsDefaultStatusAggregator() { @Test void getHttpCodeStatusMapperReturnsDefaultHttpCodeStatusMapper() { - assertThat(this.group.getHttpCodeStatusMapper()).isEqualTo(HttpCodeStatusMapper.DEFAULT); + assertThat(this.group.getHttpCodeStatusMapper()).isEqualTo(HttpCodeStatusMapper.getDefault()); } } diff --git a/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/HealthEndpointAutoConfigurationTests.java b/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/HealthEndpointAutoConfigurationTests.java index 960723cbe386..1187b943c452 100644 --- a/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/HealthEndpointAutoConfigurationTests.java +++ b/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/HealthEndpointAutoConfigurationTests.java @@ -30,6 +30,7 @@ import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.diagnostics.FailureAnalyzedException; import org.springframework.boot.health.actuate.endpoint.CompositeHealthDescriptor; import org.springframework.boot.health.actuate.endpoint.HealthDescriptor; import org.springframework.boot.health.actuate.endpoint.HealthEndpoint; @@ -40,7 +41,6 @@ import org.springframework.boot.health.actuate.endpoint.IndicatedHealthDescriptor; import org.springframework.boot.health.actuate.endpoint.ReactiveHealthEndpointWebExtension; import org.springframework.boot.health.actuate.endpoint.StatusAggregator; -import org.springframework.boot.health.autoconfigure.actuate.endpoint.HealthEndpointConfiguration.HealthEndpointGroupMembershipValidator.NoSuchHealthContributorException; import org.springframework.boot.health.autoconfigure.contributor.HealthContributorAutoConfiguration; import org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration; import org.springframework.boot.health.contributor.CompositeHealthContributor; @@ -166,8 +166,9 @@ void runFailsWhenHealthEndpointGroupIncludesContributorThatDoesNotExist() { .withPropertyValues("management.endpoint.health.group.ready.include=composite/b/c,nope") .run((context) -> { assertThat(context).hasFailed(); - assertThat(context.getStartupFailure()).isInstanceOf(NoSuchHealthContributorException.class) - .hasMessage("Included health contributor 'nope' in group 'ready' does not exist"); + assertThat(context.getStartupFailure()).isInstanceOf(FailureAnalyzedException.class) + .hasMessage("Health contributor 'nope' defined in " + + "'management.endpoint.health.group.ready.include' does not exist"); }); } @@ -178,8 +179,9 @@ void runFailsWhenHealthEndpointGroupExcludesContributorThatDoesNotExist() { "management.endpoint.health.group.ready.include=*") .run((context) -> { assertThat(context).hasFailed(); - assertThat(context.getStartupFailure()).isInstanceOf(NoSuchHealthContributorException.class) - .hasMessage("Excluded health contributor 'composite/b/d' in group 'ready' does not exist"); + assertThat(context.getStartupFailure()).isInstanceOf(FailureAnalyzedException.class) + .hasMessage("Health contributor 'composite/b/d' defined in " + + "'management.endpoint.health.group.ready.exclude' does not exist"); }); } diff --git a/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/IncludeExcludeGroupMemberPredicateTests.java b/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/IncludeExcludeGroupMemberPredicateTests.java deleted file mode 100644 index 324ae758d28b..000000000000 --- a/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/IncludeExcludeGroupMemberPredicateTests.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.health.autoconfigure.actuate.endpoint; - -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Set; -import java.util.function.Predicate; - -import org.jspecify.annotations.Nullable; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link IncludeExcludeGroupMemberPredicate}. - * - * @author Phillip Webb - * @author Madhura Bhave - */ -class IncludeExcludeGroupMemberPredicateTests { - - @Test - void testWhenEmptyIncludeAndExcludeAcceptsAll() { - Predicate predicate = new IncludeExcludeGroupMemberPredicate(null, null); - assertThat(predicate).accepts("a", "b", "c"); - } - - @Test - void testWhenStarIncludeAndEmptyExcludeAcceptsAll() { - Predicate predicate = include("*").exclude(); - assertThat(predicate).accepts("a", "b", "c"); - } - - @Test - void testWhenEmptyIncludeAndNonEmptyExcludeAcceptsAllButExclude() { - Predicate predicate = new IncludeExcludeGroupMemberPredicate(null, Collections.singleton("c")); - assertThat(predicate).accepts("a", "b"); - } - - @Test - void testWhenStarIncludeAndSpecificExcludeDoesNotAcceptExclude() { - Predicate predicate = include("*").exclude("c"); - assertThat(predicate).accepts("a", "b").rejects("c"); - } - - @Test - void testWhenSpecificIncludeAcceptsOnlyIncluded() { - Predicate predicate = include("a", "b").exclude(); - assertThat(predicate).accepts("a", "b").rejects("c"); - } - - @Test - void testWhenSpecifiedIncludeAndSpecifiedExcludeAcceptsAsExpected() { - Predicate predicate = include("a", "b", "c").exclude("c"); - assertThat(predicate).accepts("a", "b").rejects("c", "d"); - } - - @Test - void testWhenSpecifiedIncludeAndStarExcludeRejectsAll() { - Predicate predicate = include("a", "b", "c").exclude("*"); - assertThat(predicate).rejects("a", "b", "c", "d"); - } - - @Test - void testWhenCamelCaseIncludeAcceptsOnlyIncluded() { - Predicate predicate = include("myEndpoint").exclude(); - assertThat(predicate).accepts("myEndpoint").rejects("d"); - } - - @Test - void testWhenHyphenCaseIncludeAcceptsOnlyIncluded() { - Predicate predicate = include("my-endpoint").exclude(); - assertThat(predicate).accepts("my-endpoint").rejects("d"); - } - - @Test - void testWhenExtraWhitespaceAcceptsTrimmedVersion() { - Predicate predicate = include(" myEndpoint ").exclude(); - assertThat(predicate).accepts("myEndpoint").rejects("d"); - } - - @Test - void testWhenSpecifiedIncludeWithSlash() { - Predicate predicate = include("test/a").exclude(); - assertThat(predicate).accepts("test/a").rejects("test").rejects("test/b"); - } - - @Test - void specifiedIncludeShouldIncludeNested() { - Predicate predicate = include("test").exclude(); - assertThat(predicate).accepts("test/a/d").accepts("test/b").rejects("foo"); - } - - @Test - void specifiedIncludeShouldNotIncludeExcludedNested() { - Predicate predicate = include("test").exclude("test/b"); - assertThat(predicate).accepts("test/a").rejects("test/b").rejects("foo"); - } - - @Test // gh-29251 - void specifiedExcludeShouldExcludeNestedChildren() { - Predicate predicate = include("*").exclude("test"); - assertThat(predicate).rejects("test").rejects("test/a").rejects("test/a").accepts("other"); - } - - private Builder include(String... include) { - return new Builder(include); - } - - private static class Builder { - - private final String[] include; - - Builder(String[] include) { - this.include = include; - } - - Predicate exclude(String... exclude) { - return new IncludeExcludeGroupMemberPredicate(asSet(this.include), asSet(exclude)); - } - - private @Nullable Set asSet(String @Nullable [] names) { - return (names != null) ? new LinkedHashSet<>(Arrays.asList(names)) : null; - } - - } - -} diff --git a/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/NoSuchHealthContributorFailureAnalyzerTests.java b/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/NoSuchHealthContributorFailureAnalyzerTests.java deleted file mode 100644 index a4483fe9c469..000000000000 --- a/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/actuate/endpoint/NoSuchHealthContributorFailureAnalyzerTests.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.health.autoconfigure.actuate.endpoint; - -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.diagnostics.FailureAnalysis; -import org.springframework.boot.health.autoconfigure.actuate.endpoint.HealthEndpointConfiguration.HealthEndpointGroupMembershipValidator.NoSuchHealthContributorException; -import org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link NoSuchHealthContributorFailureAnalyzer}. - * - * @author Moritz Halbritter - */ -class NoSuchHealthContributorFailureAnalyzerTests { - - private final ApplicationContextRunner runner = new ApplicationContextRunner().withConfiguration(AutoConfigurations - .of(HealthEndpointAutoConfiguration.class, HealthContributorRegistryAutoConfiguration.class)); - - @Test - void analyzesMissingRequiredConfiguration() throws Throwable { - FailureAnalysis analysis = new NoSuchHealthContributorFailureAnalyzer().analyze(createFailure()); - assertThat(analysis).isNotNull(); - assertThat(analysis.getDescription()) - .isEqualTo("Included health contributor 'dummy' in group 'readiness' does not exist"); - assertThat(analysis.getAction()).isEqualTo("Update your application to correct the invalid configuration.\n" - + "You can also set 'management.endpoint.health.validate-group-membership' to false to disable the validation."); - } - - private Throwable createFailure() throws Throwable { - AtomicReference failure = new AtomicReference<>(); - this.runner.withPropertyValues("management.endpoint.health.group.readiness.include=dummy").run((context) -> { - assertThat(context).hasFailed(); - Throwable startupFailure = context.getStartupFailure(); - assertThat(startupFailure).isNotNull(); - failure.set(startupFailure); - }); - Throwable throwable = failure.get(); - if (throwable instanceof NoSuchHealthContributorException) { - return throwable; - } - throw throwable; - } - -} diff --git a/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembershipTests.java b/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembershipTests.java new file mode 100644 index 000000000000..f180d154ca68 --- /dev/null +++ b/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembershipTests.java @@ -0,0 +1,155 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.health.autoconfigure.contributor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.Predicate; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link HealthContributorMembership}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class HealthContributorMembershipTests { + + @Nested + class ByIncludeExcludeTests { + + @Test + void isMemberWhenEmptyIncludeAndExcludeAcceptsAll() { + HealthContributorMembership membership = HealthContributorMembership.byIncludeExclude(null, null); + assertThat(asPredicate(membership)).accepts("a", "b", "c"); + } + + @Test + void isMemberWhenStarIncludeAndEmptyExcludeAcceptsAll() { + HealthContributorMembership membership = include("*").exclude(); + assertThat(asPredicate(membership)).accepts("a", "b", "c"); + } + + @Test + void isMemberWhenEmptyIncludeAndNonEmptyExcludeAcceptsAllButExclude() { + HealthContributorMembership membership = HealthContributorMembership.byIncludeExclude(null, + Collections.singleton("c")); + assertThat(asPredicate(membership)).accepts("a", "b"); + } + + @Test + void isMemberWhenStarIncludeAndSpecificExcludeDoesNotAcceptExclude() { + HealthContributorMembership membership = include("*").exclude("c"); + assertThat(asPredicate(membership)).accepts("a", "b").rejects("c"); + } + + @Test + void isMemberWhenSpecificIncludeAcceptsOnlyIncluded() { + HealthContributorMembership membership = include("a", "b").exclude(); + assertThat(asPredicate(membership)).accepts("a", "b").rejects("c"); + } + + @Test + void isMemberWhenSpecifiedIncludeAndSpecifiedExcludeAcceptsAsExpected() { + HealthContributorMembership membership = include("a", "b", "c").exclude("c"); + assertThat(asPredicate(membership)).accepts("a", "b").rejects("c", "d"); + } + + @Test + void isMemberWhenSpecifiedIncludeAndStarExcludeRejectsAll() { + HealthContributorMembership membership = include("a", "b", "c").exclude("*"); + assertThat(asPredicate(membership)).rejects("a", "b", "c", "d"); + } + + @Test + void isMemberWhenCamelCaseIncludeAcceptsOnlyIncluded() { + HealthContributorMembership membership = include("myEndpoint").exclude(); + assertThat(asPredicate(membership)).accepts("myEndpoint").rejects("d"); + } + + @Test + void isMemberWhenHyphenCaseIncludeAcceptsOnlyIncluded() { + HealthContributorMembership membership = include("my-endpoint").exclude(); + assertThat(asPredicate(membership)).accepts("my-endpoint").rejects("d"); + } + + @Test + void isMemberWhenExtraWhitespaceAcceptsTrimmedVersion() { + HealthContributorMembership membership = include(" myEndpoint ").exclude(); + assertThat(asPredicate(membership)).accepts("myEndpoint").rejects("d"); + } + + @Test + void isMemberWhenSpecifiedIncludeWithSlash() { + HealthContributorMembership membership = include("test/a").exclude(); + assertThat(asPredicate(membership)).accepts("test/a").rejects("test").rejects("test/b"); + } + + @Test + void specifiedIncludeShouldIncludeNested() { + HealthContributorMembership membership = include("test").exclude(); + assertThat(asPredicate(membership)).accepts("test/a/d").accepts("test/b").rejects("foo"); + } + + @Test + void specifiedIncludeShouldNotIncludeExcludedNested() { + HealthContributorMembership membership = include("test").exclude("test/b"); + assertThat(asPredicate(membership)).accepts("test/a").rejects("test/b").rejects("foo"); + } + + @Test // gh-29251 + void specifiedExcludeShouldExcludeNestedChildren() { + HealthContributorMembership membership = include("*").exclude("test"); + assertThat(asPredicate(membership)).rejects("test").rejects("test/a").rejects("test/a").accepts("other"); + } + + private Predicate asPredicate(HealthContributorMembership membership) { + return membership::isMember; + } + + private Builder include(String... include) { + return new Builder(include); + } + + private static class Builder { + + private final String[] include; + + Builder(String[] include) { + this.include = include; + } + + HealthContributorMembership exclude(String... exclude) { + return HealthContributorMembership.byIncludeExclude(asSet(this.include), asSet(exclude)); + } + + private @Nullable Set asSet(String @Nullable [] names) { + return (names != null) ? new LinkedHashSet<>(Arrays.asList(names)) : null; + } + + } + + } + +} diff --git a/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembershipValidatorTests.java b/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembershipValidatorTests.java new file mode 100644 index 000000000000..23bf207fc130 --- /dev/null +++ b/module/spring-boot-health/src/test/java/org/springframework/boot/health/autoconfigure/contributor/HealthContributorMembershipValidatorTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.health.autoconfigure.contributor; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.diagnostics.FailureAnalyzedException; +import org.springframework.boot.health.contributor.CompositeHealthContributor; +import org.springframework.boot.health.contributor.Health; +import org.springframework.boot.health.contributor.HealthIndicator; +import org.springframework.boot.health.contributor.ReactiveHealthContributor; +import org.springframework.boot.health.registry.DefaultHealthContributorRegistry; +import org.springframework.boot.health.registry.DefaultReactiveHealthContributorRegistry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link HealthContributorMembershipValidator}. + * + * @author Phillip Webb + */ +class HealthContributorMembershipValidatorTests { + + private static final HealthIndicator UP = () -> Health.up().build(); + + private final DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry(); + + private final DefaultReactiveHealthContributorRegistry fallbackRegistry = new DefaultReactiveHealthContributorRegistry(); + + @Test + void validateWhenNoNamesDoesNotThrow() { + HealthContributorMembershipValidator validator = new HealthContributorMembershipValidator(this.registry, null, + "my.disable-validation", (members) -> members.member("include", Collections.emptySet())); + validator.afterSingletonsInstantiated(); + } + + @Test + void validateWhenNameIncludesAsteriskDoesNotThrow() { + HealthContributorMembershipValidator validator = new HealthContributorMembershipValidator(this.registry, null, + "my.disable-validation", (members) -> members.member("include", Set.of("*"))); + validator.afterSingletonsInstantiated(); + } + + @Test + void validateWhenContributorDoesNotExistThrows() { + HealthContributorMembershipValidator validator = new HealthContributorMembershipValidator(this.registry, null, + "my.disable-validation", (members) -> members.member("include", Set.of("test"))); + assertThatExceptionOfType(FailureAnalyzedException.class) + .isThrownBy(() -> validator.afterSingletonsInstantiated()) + .withMessage("Health contributor 'test' defined in 'include' does not exist") + .satisfies((ex) -> assertThat(ex.analysis().getAction()).contains("my.disable-validation")); + } + + @Test + void validateWhenContributorExistsInRegistryDoesNotThrow() { + this.registry.registerContributor("test", UP); + HealthContributorMembershipValidator validator = new HealthContributorMembershipValidator(this.registry, null, + "my.disable-validation", (members) -> members.member("include", Set.of("test"))); + validator.afterSingletonsInstantiated(); + } + + @Test + void validateWhenContributorExistsInFallbackRegistryDoesNotThrow() { + this.fallbackRegistry.registerContributor("test", ReactiveHealthContributor.adapt(UP)); + HealthContributorMembershipValidator validator = new HealthContributorMembershipValidator(this.registry, + this.fallbackRegistry, "my.disable-validation", (members) -> members.member("include", Set.of("test"))); + validator.afterSingletonsInstantiated(); + } + + @Test + void validateWhenNestedContributorExistsInRegistryDoesNotThrow() { + this.registry.registerContributor("test", CompositeHealthContributor.fromMap(Map.of("nested", UP))); + HealthContributorMembershipValidator validator = new HealthContributorMembershipValidator(this.registry, null, + "my.disable-validation", (members) -> members.member("include", Set.of("test/nested"))); + validator.afterSingletonsInstantiated(); + } + +} From 67ac57fb77560dcd86ac100da5ba318869a73b58 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 22 Feb 2026 11:02:36 -0800 Subject: [PATCH 387/930] Add Spring gRPC server health check support Add integration between Spring Boot's health module and gRPC health checking service support. Closes gh-49048 Co-authored-by: Chris Bono --- module/spring-boot-grpc-server/build.gradle | 3 + ...oConfiguredHealthCheckedGrpcComponent.java | 59 +++ ...ConfiguredHealthCheckedGrpcComponents.java | 144 ++++++ .../GrpcServerHealthAutoConfiguration.java | 142 ++++++ .../health/GrpcServerHealthProperties.java | 248 ++++++++++ .../health/GrpcServerHealthScheduler.java | 69 +++ ...erverHealthSchedulerAutoConfiguration.java | 58 +++ .../autoconfigure/health/package-info.java | 23 + .../grpc/server/health/GrpcServerHealth.java | 122 +++++ .../health/HealthCheckedGrpcComponent.java | 46 ++ .../health/HealthCheckedGrpcComponents.java | 54 +++ .../health/SimpleServingStatusMapper.java | 79 +++ .../server/health/SimpleStatusAggregator.java | 74 +++ .../grpc/server/health/StatusAggregator.java | 95 ++++ .../boot/grpc/server/health/StatusMapper.java | 62 +++ .../boot/grpc/server/health/package-info.java | 23 + ...ot.autoconfigure.AutoConfiguration.imports | 2 + ...iguredHealthCheckedGrpcComponentTests.java | 75 +++ ...guredHealthCheckedGrpcComponentsTests.java | 437 +++++++++++++++++ ...rpcServerHealthAutoConfigurationTests.java | 458 ++++++++++++++++++ .../GrpcServerHealthSchedulerTests.java | 79 +++ .../server/health/GrpcServerHealthTests.java | 293 +++++++++++ .../server/health/StatusAggregatorTests.java | 71 +++ .../grpc/server/health/StatusMapperTests.java | 64 +++ 24 files changed, 2780 insertions(+) create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponent.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/package-info.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/GrpcServerHealth.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/HealthCheckedGrpcComponent.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/HealthCheckedGrpcComponents.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/SimpleServingStatusMapper.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/SimpleStatusAggregator.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/StatusAggregator.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/StatusMapper.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/package-info.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentsTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/GrpcServerHealthTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusAggregatorTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusMapperTests.java diff --git a/module/spring-boot-grpc-server/build.gradle b/module/spring-boot-grpc-server/build.gradle index cd26fcf32e57..3f730227c6a4 100644 --- a/module/spring-boot-grpc-server/build.gradle +++ b/module/spring-boot-grpc-server/build.gradle @@ -29,6 +29,9 @@ dependencies { api("org.springframework.grpc:spring-grpc-core") optional(project(":core:spring-boot-autoconfigure")) + optional(project(":module:spring-boot-health")) + optional("com.fasterxml.jackson.core:jackson-annotations") + optional("io.projectreactor:reactor-core") optional("io.grpc:grpc-servlet-jakarta") optional("io.grpc:grpc-services") optional("io.grpc:grpc-netty") diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponent.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponent.java new file mode 100644 index 000000000000..b523e6113d32 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponent.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.health; + +import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponent; +import org.springframework.boot.grpc.server.health.StatusAggregator; +import org.springframework.boot.grpc.server.health.StatusMapper; +import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembership; + +/** + * Auto-configured {@link HealthCheckedGrpcComponent}. + * + * @author Phillip Webb + */ +class AutoConfiguredHealthCheckedGrpcComponent implements HealthCheckedGrpcComponent { + + private final HealthContributorMembership membership; + + private final StatusAggregator statusAggregator; + + private final StatusMapper statusMapper; + + AutoConfiguredHealthCheckedGrpcComponent(HealthContributorMembership membership, StatusAggregator statusAggregator, + StatusMapper statusMapper) { + this.membership = membership; + this.statusAggregator = statusAggregator; + this.statusMapper = statusMapper; + } + + @Override + public boolean isMember(String healthContributorName) { + return this.membership.isMember(healthContributorName); + } + + @Override + public StatusAggregator getStatusAggregator() { + return this.statusAggregator; + } + + @Override + public StatusMapper getStatusMapper() { + return this.statusMapper; + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java new file mode 100644 index 000000000000..98248c2b885b --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java @@ -0,0 +1,144 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.health; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Supplier; + +import io.grpc.health.v1.HealthCheckResponse.ServingStatus; +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; +import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthProperties.Service; +import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthProperties.Status; +import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponent; +import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponents; +import org.springframework.boot.grpc.server.health.StatusAggregator; +import org.springframework.boot.grpc.server.health.StatusMapper; +import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembership; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +/** + * Auto-configured {@link HealthCheckedGrpcComponents}. + * + * @author Phillip Webb + */ +class AutoConfiguredHealthCheckedGrpcComponents implements HealthCheckedGrpcComponents { + + private final HealthCheckedGrpcComponent server; + + private final Map services; + + /** + * Create a new {@link AutoConfiguredHealthCheckedGrpcComponents} instance. + * @param applicationContext the application context used to check for override beans + * @param properties the grpc server health properties + */ + AutoConfiguredHealthCheckedGrpcComponents(ApplicationContext applicationContext, + GrpcServerHealthProperties properties) { + ListableBeanFactory beanFactory = (applicationContext instanceof ConfigurableApplicationContext configurableContext) + ? configurableContext.getBeanFactory() : applicationContext; + StatusAggregator statusAggregator = getNonQualifiedBean(beanFactory, StatusAggregator.class, + () -> StatusAggregator.of(properties.getStatus().getOrder())); + StatusMapper statusMapper = getNonQualifiedBean(beanFactory, StatusMapper.class, + () -> StatusMapper.of(properties.getStatus().getMapping())); + this.server = new AutoConfiguredHealthCheckedGrpcComponent(HealthContributorMembership.always(), + statusAggregator, statusMapper); + this.services = createServices(properties.getService(), beanFactory, statusAggregator, statusMapper); + } + + private Map createServices(Map serviceProperties, + BeanFactory beanFactory, StatusAggregator defaultStatusAggregator, StatusMapper defaultStatusMapper) { + Map services = new TreeMap<>(); + serviceProperties.forEach((serviceName, service) -> { + Status status = service.getStatus(); + StatusAggregator statusAggregator = getQualifiedBean(beanFactory, StatusAggregator.class, serviceName, + () -> createStatusAggregator(status.getOrder(), defaultStatusAggregator)); + StatusMapper statusMapper = getQualifiedBean(beanFactory, StatusMapper.class, serviceName, + () -> createStatusMapper(status.getMapping(), defaultStatusMapper)); + HealthContributorMembership membership = HealthContributorMembership.byIncludeExclude(service.getInclude(), + service.getExclude()); + services.put(serviceName, + new AutoConfiguredHealthCheckedGrpcComponent(membership, statusAggregator, statusMapper)); + }); + return Collections.unmodifiableMap(services); + } + + private StatusAggregator createStatusAggregator(List order, StatusAggregator defaultStatusAggregator) { + return (!CollectionUtils.isEmpty(order)) ? StatusAggregator.of(order) : defaultStatusAggregator; + } + + private StatusMapper createStatusMapper(Map mapping, StatusMapper defaultStatusMapper) { + return (!CollectionUtils.isEmpty(mapping)) ? StatusMapper.of(mapping) : defaultStatusMapper; + } + + private T getNonQualifiedBean(ListableBeanFactory beanFactory, Class type, Supplier fallback) { + List candidates = new ArrayList<>(); + for (String beanName : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) { + String[] aliases = beanFactory.getAliases(beanName); + if (!BeanFactoryAnnotationUtils.isQualifierMatch( + (qualifier) -> !qualifier.equals(beanName) && !ObjectUtils.containsElement(aliases, qualifier), + beanName, beanFactory)) { + candidates.add(beanName); + } + } + if (candidates.isEmpty()) { + return fallback.get(); + } + if (candidates.size() == 1) { + return beanFactory.getBean(candidates.get(0), type); + } + return beanFactory.getBean(type); + } + + private T getQualifiedBean(BeanFactory beanFactory, Class type, String qualifier, Supplier fallback) { + try { + return BeanFactoryAnnotationUtils.qualifiedBeanOfType(beanFactory, type, qualifier); + } + catch (NoSuchBeanDefinitionException ex) { + return fallback.get(); + } + } + + @Override + public @Nullable HealthCheckedGrpcComponent getServer() { + return this.server; + } + + @Override + public Set getServiceNames() { + return this.services.keySet(); + } + + @Override + public @Nullable HealthCheckedGrpcComponent getService(String serviceName) { + return this.services.get(serviceName); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java new file mode 100644 index 000000000000..f525a6e77b61 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java @@ -0,0 +1,142 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.health; + +import io.grpc.BindableService; +import io.grpc.Grpc; +import io.grpc.protobuf.services.HealthStatusManager; + +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.ConditionalOnBooleanProperty; +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.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration.NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition; +import org.springframework.boot.grpc.server.health.GrpcServerHealth; +import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponents; +import org.springframework.boot.grpc.server.health.StatusAggregator; +import org.springframework.boot.grpc.server.health.StatusMapper; +import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembershipValidator; +import org.springframework.boot.health.registry.HealthContributorRegistry; +import org.springframework.boot.health.registry.ReactiveHealthContributorRegistry; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.GrpcServerFactory; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side health service. + * + * @author Daniel Theuke + * @author Chris Bono + * @author Phillip Webb + * @since 4.1.0 + */ +@AutoConfiguration( + afterName = "org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration") +@ConditionalOnClass({ GrpcServerFactory.class, Grpc.class, HealthStatusManager.class }) +@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true) +@Conditional(NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition.class) +@EnableConfigurationProperties(GrpcServerHealthProperties.class) +public final class GrpcServerHealthAutoConfiguration { + + @Bean(destroyMethod = "enterTerminalState") + @ConditionalOnMissingBean + HealthStatusManager grpcServerHealthStatusManager() { + return new HealthStatusManager(); + } + + @Bean + BindableService grpcServerHealthService(HealthStatusManager healthStatusManager) { + return healthStatusManager.getHealthService(); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(type = "org.springframework.boot.health.registry.HealthContributorRegistry") + static class GrpcServerHealthContributorConfiguration { + + static final String VALIDATE_MEMBERSHIP_PROPERTY = "spring.grpc.server.health.services.validate-membership"; + + @Bean + @ConditionalOnMissingBean + StatusAggregator grpcServerHealthStatusAggregator(GrpcServerHealthProperties properties) { + return StatusAggregator.of(properties.getStatus().getOrder()); + } + + @Bean + @ConditionalOnMissingBean + StatusMapper grpcServerHealthHttpCodeStatusMapper(GrpcServerHealthProperties properties) { + return StatusMapper.of(properties.getStatus().getMapping()); + } + + @Bean + @ConditionalOnMissingBean(HealthCheckedGrpcComponents.class) + AutoConfiguredHealthCheckedGrpcComponents grpcServerHealthCheckedGrpcComponents( + ApplicationContext applicationContext, GrpcServerHealthProperties properties) { + return new AutoConfiguredHealthCheckedGrpcComponents(applicationContext, properties); + } + + @Bean + @ConditionalOnMissingBean + GrpcServerHealth grpcServerHealth(HealthContributorRegistry healthContributorRegistry, + ObjectProvider reactiveHealthContributorRegistry, + HealthCheckedGrpcComponents healthCheckedGrpcComponents) { + return new GrpcServerHealth(healthContributorRegistry, reactiveHealthContributorRegistry.getIfAvailable(), + healthCheckedGrpcComponents); + } + + @Bean + @ConditionalOnBooleanProperty(name = VALIDATE_MEMBERSHIP_PROPERTY, matchIfMissing = true) + HealthContributorMembershipValidator grpcServerHealthServiceMembershipValidator( + GrpcServerHealthProperties properties, HealthContributorRegistry healthContributorRegistry, + ObjectProvider reactiveHealthContributorRegistry) { + return new HealthContributorMembershipValidator(healthContributorRegistry, + reactiveHealthContributorRegistry.getIfAvailable(), VALIDATE_MEMBERSHIP_PROPERTY, + (members) -> properties.getService().forEach((serviceName, service) -> { + String property = "spring.grpc.server.health.service." + serviceName; + members.member(property + ".include".formatted(serviceName), service.getInclude()); + members.member(property + ".exclude".formatted(serviceName), service.getExclude()); + })); + } + + } + + static class NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition extends AnyNestedCondition { + + NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnBean(BindableService.class) + @ConditionalOnBooleanProperty(name = "spring.grpc.server.health.enabled", matchIfMissing = true) + static class NotDisabledAndHasBindableService { + + } + + @ConditionalOnBooleanProperty(name = "spring.grpc.server.health.enabled") + static class ExplicitlyEnabled { + + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java new file mode 100644 index 000000000000..fc80154becc0 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java @@ -0,0 +1,248 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.health; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.grpc.health.v1.HealthCheckResponse.ServingStatus; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.util.CollectionUtils; + +/** + * {@link ConfigurationProperties Properties} for Spring gRPC server health. + * + * @author Phillip Webb + * @author Chris Bono + * @since 4.1.0 + */ +@ConfigurationProperties("spring.grpc.server.health") +public class GrpcServerHealthProperties { + + /** + * Whether to auto-configure Health feature on the gRPC server. + */ + private @Nullable Boolean enabled; + + /** + * Whether to include the overall server health. + */ + private boolean includeOverallHealth = true; + + /** + * Properties that apply to all services. + */ + private final Services services = new Services(); + + /** + * Service specific health reporting. + */ + private final Map service = new LinkedHashMap<>(); + + /** + * Status configuration. + */ + private final Status status = new Status(); + + /** + * Schedule configuration. + */ + private final Schedule schedule = new Schedule(); + + public @Nullable Boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(@Nullable Boolean enabled) { + this.enabled = enabled; + } + + public boolean isIncludeOverallHealth() { + return this.includeOverallHealth; + } + + public void setIncludeOverallHealth(boolean includeOverallHealth) { + this.includeOverallHealth = includeOverallHealth; + } + + public Services getServices() { + return this.services; + } + + public Map getService() { + return this.service; + } + + public Status getStatus() { + return this.status; + } + + public Schedule getSchedule() { + return this.schedule; + } + + /** + * Properties applying to all services. + */ + public static class Services { + + /** + * Whether to validate health membership for services on startup. Validation fails + * if a service includes or excludes a health contributor that does not exist. + */ + private boolean validateMembership = true; + + public boolean isValidateMembership() { + return this.validateMembership; + } + + public void setValidateMembership(boolean validateMembership) { + this.validateMembership = validateMembership; + } + + } + + /** + * A health for a specific service. + */ + public static class Service { + + /** + * Health indicator IDs that should be included or '*' for all. + */ + private @Nullable Set include; + + /** + * Health indicator IDs that should be excluded or '*' for all. + */ + private @Nullable Set exclude; + + /** + * Status configuration. + */ + @NestedConfigurationProperty + private final Status status = new Status(); + + public Status getStatus() { + return this.status; + } + + public @Nullable Set getInclude() { + return this.include; + } + + public void setInclude(@Nullable Set include) { + this.include = include; + } + + public @Nullable Set getExclude() { + return this.exclude; + } + + public void setExclude(@Nullable Set exclude) { + this.exclude = exclude; + } + + } + + /** + * Status properties for the group. + */ + public static class Status { + + /** + * List of health statuses in order of severity. + */ + private List order = new ArrayList<>(); + + /** + * Mapping of health statuses to gRPC service status. By default, registered + * health statuses map to sensible defaults (for example, UP maps to SERVING). + */ + private final Map mapping = new HashMap<>(); + + public List getOrder() { + return this.order; + } + + public void setOrder(List statusOrder) { + if (!CollectionUtils.isEmpty(statusOrder)) { + this.order = statusOrder; + } + } + + public Map getMapping() { + return this.mapping; + } + + } + + /** + * Health task scheduling. + */ + public static class Schedule { + + /** + * Whether to schedule updates to gRPC server health based on application health. + */ + private boolean enabled = true; + + /** + * How often to update the health status. + */ + private Duration period = Duration.ofSeconds(5); + + /** + * The initial delay before updating the health status the very first time. + */ + private Duration delay = Duration.ofSeconds(5); + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Duration getPeriod() { + return this.period; + } + + public void setPeriod(Duration period) { + this.period = period; + } + + public Duration getDelay() { + return this.delay; + } + + public void setDelay(Duration delay) { + this.delay = delay; + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java new file mode 100644 index 000000000000..3bf7cb8a0af2 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.health; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.ScheduledFuture; + +import io.grpc.protobuf.services.HealthStatusManager; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.grpc.server.health.GrpcServerHealth; +import org.springframework.context.ApplicationListener; +import org.springframework.core.log.LogMessage; +import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.util.function.SingletonSupplier; + +/** + * Schedules gRPC health updates one the gRPC server has been started. + * + * @author Phillip Webb + * @author Chris Bono + */ +class GrpcServerHealthScheduler implements ApplicationListener { + + private static final Log logger = LogFactory.getLog(GrpcServerHealthScheduler.class); + + private final SingletonSupplier> scheduleHealth; + + GrpcServerHealthScheduler(GrpcServerHealth grpcServerHealth, HealthStatusManager grpcServerHealthStatusManager, + TaskScheduler taskScheduler, Duration period, Duration delay) { + this(Clock.systemDefaultZone(), grpcServerHealth, grpcServerHealthStatusManager, taskScheduler, period, delay); + } + + GrpcServerHealthScheduler(Clock clock, GrpcServerHealth grpcServerHealth, + HealthStatusManager grpcServerHealthStatusManager, TaskScheduler taskScheduler, Duration period, + Duration delay) { + this.scheduleHealth = SingletonSupplier.of(() -> { + logger.debug(LogMessage + .of(() -> "Scheduling gRPC server health updates every %s seconds (after a delay of %s seconds)" + .formatted((period.toMillis() / 1000.0), delay.toMillis() / 1000.0))); + Runnable task = () -> grpcServerHealth.update(grpcServerHealthStatusManager); + return taskScheduler.scheduleAtFixedRate(task, Instant.now(clock).plus(delay), period); + }); + } + + @Override + public void onApplicationEvent(GrpcServerStartedEvent event) { + this.scheduleHealth.get(); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerAutoConfiguration.java new file mode 100644 index 000000000000..972d4a79d3fa --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerAutoConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.health; + +import io.grpc.Grpc; +import io.grpc.protobuf.services.HealthStatusManager; + +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.ConditionalOnBooleanProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.task.DefaultTaskSchedulerConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthProperties.Schedule; +import org.springframework.boot.grpc.server.health.GrpcServerHealth; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.scheduling.TaskScheduler; + +/** + * {@link EnableAutoConfiguration Auto-configuration} to invoke {@link GrpcServerHealth} + * updates using a {@link TaskScheduler}. + * + * @author Phillip Webb + * @since 4.1.0 + */ +@AutoConfiguration(after = GrpcServerHealthAutoConfiguration.class) +@ConditionalOnBooleanProperty(name = "spring.grpc.server.health.schedule.enabled", matchIfMissing = true) +@Import(DefaultTaskSchedulerConfiguration.class) +@ConditionalOnClass({ GrpcServerFactory.class, Grpc.class, HealthStatusManager.class }) +public final class GrpcServerHealthSchedulerAutoConfiguration { + + @Bean + @ConditionalOnBean({ TaskScheduler.class, GrpcServerHealth.class }) + GrpcServerHealthScheduler grpcServerHealthScheduler(GrpcServerHealth grpcServerHealth, + HealthStatusManager grpcServerHealthStatusManager, TaskScheduler taskScheduler, + GrpcServerHealthProperties properties) { + Schedule schedule = properties.getSchedule(); + return new GrpcServerHealthScheduler(grpcServerHealth, grpcServerHealthStatusManager, taskScheduler, + schedule.getPeriod(), schedule.getDelay()); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/package-info.java new file mode 100644 index 000000000000..764a25d911e1 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 gRPC server health checks. + */ +@NullMarked +package org.springframework.boot.grpc.server.autoconfigure.health; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/GrpcServerHealth.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/GrpcServerHealth.java new file mode 100644 index 000000000000..0ff80ca4cc69 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/GrpcServerHealth.java @@ -0,0 +1,122 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.health; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +import io.grpc.health.v1.HealthCheckResponse.ServingStatus; +import io.grpc.protobuf.services.HealthStatusManager; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.health.contributor.CompositeHealthContributor; +import org.springframework.boot.health.contributor.Health; +import org.springframework.boot.health.contributor.HealthContributors; +import org.springframework.boot.health.contributor.HealthIndicator; +import org.springframework.boot.health.contributor.Status; +import org.springframework.boot.health.registry.HealthContributorRegistry; +import org.springframework.boot.health.registry.ReactiveHealthContributorRegistry; +import org.springframework.util.Assert; + +/** + * Provides health information for a gRPC server that can be used to update a + * {@link HealthStatusManager}. + * + * @author Phillip Webb + * @since 4.1.0 + */ +public class GrpcServerHealth { + + private HealthContributorRegistry registry; + + private @Nullable ReactiveHealthContributorRegistry fallbackRegistry; + + private HealthCheckedGrpcComponents components; + + /** + * Create a new {@link GrpcServerHealth} instance. + * @param registry the health contributor registry + * @param fallbackRegistry the fallback registry or {@code null} + * @param components the components used to provide the server health + */ + public GrpcServerHealth(HealthContributorRegistry registry, + @Nullable ReactiveHealthContributorRegistry fallbackRegistry, HealthCheckedGrpcComponents components) { + Assert.notNull(registry, "'registry' must not be null"); + Assert.notNull(components, "'components' must not be null"); + this.registry = registry; + this.fallbackRegistry = fallbackRegistry; + this.components = components; + } + + public void update(HealthStatusManager manager) { + update(manager::setStatus); + } + + public void update(BiConsumer updator) { + Cache cache = new Cache(); + HealthCheckedGrpcComponent serverComponent = this.components.getServer(); + if (serverComponent != null) { + updator.accept("", getServingStatus(cache, serverComponent)); + } + for (String serviceName : this.components.getServiceNames()) { + HealthCheckedGrpcComponent serviceComponent = this.components.getService(serviceName); + if (!serviceName.isEmpty() && serviceComponent != null) { + updator.accept(serviceName, getServingStatus(cache, serviceComponent)); + } + } + } + + private ServingStatus getServingStatus(Cache cache, HealthCheckedGrpcComponent component) { + Set statuses = new LinkedHashSet<>(); + collectStatuses(cache, component, statuses, this.registry, ""); + if (this.fallbackRegistry != null) { + collectStatuses(cache, component, statuses, this.fallbackRegistry.asHealthContributors(), ""); + } + Status status = component.getStatusAggregator().getAggregateStatus(statuses); + return component.getStatusMapper().getServingStatus(status); + } + + private void collectStatuses(Cache cache, HealthCheckedGrpcComponent component, Set statuses, + HealthContributors contributors, String prefix) { + for (HealthContributors.Entry entry : contributors) { + String name = (prefix.isEmpty()) ? entry.name() : prefix + "/" + entry.name(); + if (entry.contributor() instanceof CompositeHealthContributor composite) { + collectStatuses(cache, component, statuses, composite, name); + } + else if (component.isMember(name)) { + Health health = cache.getHealth(name, (HealthIndicator) entry.contributor()); + if (health != null) { + statuses.add(health.getStatus()); + } + } + } + } + + class Cache { + + private final Map health = new HashMap<>(); + + Health getHealth(String name, HealthIndicator indicator) { + return this.health.computeIfAbsent(name, (key) -> indicator.health(false)); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/HealthCheckedGrpcComponent.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/HealthCheckedGrpcComponent.java new file mode 100644 index 000000000000..fd71d19007e9 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/HealthCheckedGrpcComponent.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.health; + +/** + * A single gRPC component that can be checked for health. + * + * @author Phillip Webb + * @since 4.1.0 + */ +public interface HealthCheckedGrpcComponent { + + /** + * Returns {@code true} if the given health contributor is a member of this component. + * @param healthContributorName the contributor name + * @return {@code true} if the contributor is a member + */ + boolean isMember(String healthContributorName); + + /** + * Returns the status aggregator that should be used for this component. + * @return the status aggregator + */ + StatusAggregator getStatusAggregator(); + + /** + * Returns the {@link StatusMapper} that should be used for this component. + * @return the status mapper + */ + StatusMapper getStatusMapper(); + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/HealthCheckedGrpcComponents.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/HealthCheckedGrpcComponents.java new file mode 100644 index 000000000000..5673c6bbf663 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/HealthCheckedGrpcComponents.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.health; + +import java.util.Set; + +import org.jspecify.annotations.Nullable; + +/** + * A collection of {@link HealthCheckedGrpcComponent components} used to check the health + * of a gRPC server. + * + * @author Phillip Webb + * @since 4.1.0 + * @see GrpcServerHealth + */ +public interface HealthCheckedGrpcComponents { + + /** + * Return the component that represents the overall server health or {@code null} if + * no overall health should be reported. + * @return the server component or {@code null} + */ + @Nullable HealthCheckedGrpcComponent getServer(); + + /** + * Return the names of the services that contribute health checks. + * @return the service names + */ + Set getServiceNames(); + + /** + * Return the component for the service with the specified name or {@code null} if the + * name is not known. + * @param serviceName the name of the service + * @return the service component or {@code null} + */ + @Nullable HealthCheckedGrpcComponent getService(String serviceName); + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/SimpleServingStatusMapper.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/SimpleServingStatusMapper.java new file mode 100644 index 000000000000..769c4b0cce29 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/SimpleServingStatusMapper.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.health; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import io.grpc.health.v1.HealthCheckResponse.ServingStatus; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.health.contributor.Status; +import org.springframework.lang.Contract; + +/** + * Simple {@link StatusMapper} backed by map of {@link Status#getCode() status code} to + * {@link ServingStatus}. + * + * @author Phillip Webb + */ +class SimpleServingStatusMapper implements StatusMapper { + + static final SimpleServingStatusMapper DEFAULT_MAPPINGS; + static { + Map mappings = new HashMap<>(); + mappings.put(Status.DOWN.getCode(), ServingStatus.NOT_SERVING); + mappings.put(Status.OUT_OF_SERVICE.getCode(), ServingStatus.NOT_SERVING); + mappings.put(Status.UNKNOWN.getCode(), ServingStatus.UNKNOWN); + DEFAULT_MAPPINGS = new SimpleServingStatusMapper(mappings); + } + + private final Map mappings; + + SimpleServingStatusMapper(Map mappings) { + this.mappings = getUniformMappings(mappings); + } + + @Override + public ServingStatus getServingStatus(Status status) { + String code = getUniformCode(status.getCode()); + return this.mappings.getOrDefault(code, ServingStatus.SERVING); + } + + private static Map getUniformMappings(Map mappings) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : mappings.entrySet()) { + String code = getUniformCode(entry.getKey()); + if (code != null) { + result.putIfAbsent(code, entry.getValue()); + } + } + return Collections.unmodifiableMap(result); + } + + @Contract("!null -> !null") + private static @Nullable String getUniformCode(@Nullable String code) { + return (code != null) ? code.codePoints() + .filter(Character::isLetterOrDigit) + .map(Character::toLowerCase) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString() : null; + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/SimpleStatusAggregator.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/SimpleStatusAggregator.java new file mode 100644 index 000000000000..1c4f57a038eb --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/SimpleStatusAggregator.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.health; + +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.health.contributor.Status; +import org.springframework.lang.Contract; + +/** + * {@link StatusAggregator} backed by an ordered status list. + * + * @author Phillip Webb + */ +class SimpleStatusAggregator implements StatusAggregator { + + static final SimpleStatusAggregator DEFAULT_ORDER = new SimpleStatusAggregator( + Status.DEFAULT_ORDER.stream().map(Status::getCode)); + + private final List order; + + private final Comparator comparator = Comparator.comparingInt(this::orderIndex) + .thenComparing(Status::getCode); + + SimpleStatusAggregator(Stream order) { + this.order = order.map(SimpleStatusAggregator::getUniformCode).toList(); + } + + @Override + public Status getAggregateStatus(Set statuses) { + return statuses.stream().filter(this::contains).min(this.comparator).orElse(Status.UNKNOWN); + } + + private boolean contains(Status status) { + return this.order.contains(getUniformCode(status)); + } + + private int orderIndex(Status status) { + return this.order.indexOf(getUniformCode(status)); + } + + private static @Nullable String getUniformCode(Status status) { + return getUniformCode(status.getCode()); + } + + @Contract("!null -> !null") + private static @Nullable String getUniformCode(@Nullable String code) { + return (code != null) ? code.codePoints() + .filter(Character::isLetterOrDigit) + .map(Character::toLowerCase) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString() : null; + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/StatusAggregator.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/StatusAggregator.java new file mode 100644 index 000000000000..f8fe4e47b616 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/StatusAggregator.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.health; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.boot.health.contributor.Health; +import org.springframework.boot.health.contributor.Status; +import org.springframework.util.ObjectUtils; + +/** + * Strategy used to aggregate {@link Status} instances. + *

    + * This is required in order to combine subsystem states expressed through + * {@link Health#getStatus()} into one state for the gRPC health. + * + * @author Phillip Webb + * @since 4.1.0 + */ +@FunctionalInterface +public interface StatusAggregator { + + /** + * Return the aggregate status for the given set of statuses. + * @param statuses the statuses to aggregate + * @return the aggregate status + */ + default Status getAggregateStatus(Status... statuses) { + return getAggregateStatus(new LinkedHashSet<>(Arrays.asList(statuses))); + } + + /** + * Return the aggregate status for the given set of statuses. + * @param statuses the statuses to aggregate + * @return the aggregate status + */ + Status getAggregateStatus(Set statuses); + + /** + * Return a {@link StatusAggregator} backed by the given ordered status list. + * @param order the status order + * @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is + * provided + */ + static StatusAggregator of(Status... order) { + return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(Arrays.stream(order).map(Status::getCode)) + : getDefault(); + } + + /** + * Return a {@link StatusAggregator} backed by the given ordered status list. + * @param order the status order + * @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is + * provided + */ + static StatusAggregator of(String... order) { + return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(Arrays.stream(order)) : getDefault(); + } + + /** + * Return a {@link StatusAggregator} backed by the given ordered status list. + * @param order the status order + * @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is + * provided + */ + static StatusAggregator of(List order) { + return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(order.stream()) : getDefault(); + } + + /** + * Return {@link StatusAggregator} instance using default ordering rules. + * @return a {@code StatusAggregator} with default ordering rules. + */ + static StatusAggregator getDefault() { + return SimpleStatusAggregator.DEFAULT_ORDER; + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/StatusMapper.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/StatusMapper.java new file mode 100644 index 000000000000..837a1be208d9 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/StatusMapper.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.health; + +import java.util.Map; + +import io.grpc.health.v1.HealthCheckResponse.ServingStatus; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.health.contributor.Status; +import org.springframework.util.CollectionUtils; + +/** + * Strategy used to map a {@link Status health status} to a gRPC {@link ServingStatus}. + * + * @author Phillip Webb + * @since 4.1.0 + */ +@FunctionalInterface +public interface StatusMapper { + + /** + * Return the HTTP status code that corresponds to the given {@link Status health + * status}. + * @param status the health status to map + * @return the corresponding HTTP status code + */ + ServingStatus getServingStatus(Status status); + + /** + * Create a new {@link StatusMapper} with the specified mappings. + * @param mappings the mappings to use or {@code null} to use the default mappings + * @return a {@link StatusMapper} or {@link #getDefault()} + */ + static StatusMapper of(@Nullable Map mappings) { + return CollectionUtils.isEmpty(mappings) ? SimpleServingStatusMapper.DEFAULT_MAPPINGS + : new SimpleServingStatusMapper(mappings); + } + + /** + * Return an {@link StatusMapper} instance using default mappings. + * @return a mapper using default mappings + */ + static StatusMapper getDefault() { + return SimpleServingStatusMapper.DEFAULT_MAPPINGS; + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/package-info.java new file mode 100644 index 000000000000..cd575a4c15ab --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 gRPC server health checks. + */ +@NullMarked +package org.springframework.boot.grpc.server.health; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index b5ab23908422..c060e23ab8dc 100644 --- a/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,2 +1,4 @@ org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration org.springframework.boot.grpc.server.autoconfigure.GrpcServerServicesAutoConfiguration +org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration +org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthSchedulerAutoConfiguration \ No newline at end of file diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentTests.java new file mode 100644 index 000000000000..13e14d764761 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.health; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.boot.grpc.server.health.StatusAggregator; +import org.springframework.boot.grpc.server.health.StatusMapper; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutoConfiguredHealthCheckedGrpcComponent}. + * + * @author Phillip Webb + */ +@ExtendWith(MockitoExtension.class) +class AutoConfiguredHealthCheckedGrpcComponentTests { + + @Mock + @SuppressWarnings("NullAway.Init") + private StatusAggregator statusAggregator; + + @Mock + @SuppressWarnings("NullAway.Init") + private StatusMapper statusMapper; + + @Test + void isMemberWhenMembershipMatchesAcceptsTrue() { + AutoConfiguredHealthCheckedGrpcComponent component = new AutoConfiguredHealthCheckedGrpcComponent( + (name) -> name.startsWith("a"), this.statusAggregator, this.statusMapper); + assertThat(component.isMember("albert")).isTrue(); + assertThat(component.isMember("arnold")).isTrue(); + } + + @Test + void isMemberWhenMembershipRejectsReturnsTrue() { + AutoConfiguredHealthCheckedGrpcComponent component = new AutoConfiguredHealthCheckedGrpcComponent( + (name) -> name.startsWith("a"), this.statusAggregator, this.statusMapper); + assertThat(component.isMember("bert")).isFalse(); + assertThat(component.isMember("ernie")).isFalse(); + } + + @Test + void getStatusAggregatorReturnsStatusAggregator() { + AutoConfiguredHealthCheckedGrpcComponent component = new AutoConfiguredHealthCheckedGrpcComponent( + (name) -> true, this.statusAggregator, this.statusMapper); + assertThat(component.getStatusAggregator()).isSameAs(this.statusAggregator); + } + + @Test + void getStatusMapperReturnsHttpCodeStatusMapper() { + AutoConfiguredHealthCheckedGrpcComponent component = new AutoConfiguredHealthCheckedGrpcComponent( + (name) -> true, this.statusAggregator, this.statusMapper); + assertThat(component.getStatusMapper()).isSameAs(this.statusMapper); + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentsTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentsTests.java new file mode 100644 index 000000000000..e0ece6931d10 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentsTests.java @@ -0,0 +1,437 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.health; + +import java.util.Collections; + +import io.grpc.health.v1.HealthCheckResponse.ServingStatus; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponent; +import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponents; +import org.springframework.boot.grpc.server.health.StatusAggregator; +import org.springframework.boot.grpc.server.health.StatusMapper; +import org.springframework.boot.health.contributor.Status; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutoConfiguredHealthCheckedGrpcComponent}. + * + * @author Phillip Webb + */ +class AutoConfiguredHealthCheckedGrpcComponentsTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AutoConfiguredHealthCheckedGrpcComponentsTestConfiguration.class)); + + @Test + void getServerMatchesAllMembers() { + this.contextRunner.run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent server = components.getServer(); + assertThat(server).isNotNull(); + assertThat(server.isMember("a")).isTrue(); + assertThat(server.isMember("b")).isTrue(); + assertThat(server.isMember("C")).isTrue(); + }); + } + + @Test + void getServiceNamesReturnsServiceNames() { + this.contextRunner + .withPropertyValues("spring.grpc.server.health.service.a.include=*", + "spring.grpc.server.health.service.b.include=*") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + assertThat(components.getServiceNames()).containsExactlyInAnyOrder("a", "b"); + }); + } + + @Test + void getServiceWhenServiceExistsReturnsService() { + this.contextRunner.withPropertyValues("spring.grpc.server.health.service.a.include=*").run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent component = components.getService("a"); + assertThat(component).isNotNull(); + }); + } + + @Test + void getServiceWhenServiceDoesNotExistReturnsNull() { + this.contextRunner.withPropertyValues("spring.grpc.server.health.service.a.include=*").run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent component = components.getService("b"); + assertThat(component).isNull(); + }); + } + + @Test + void createWhenNoDefinedBeansAdaptsProperties() { + this.contextRunner + .withPropertyValues("spring.grpc.server.health.status.order=up,down", + "spring.grpc.server.health.status.mapping.down=serving") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent server = components.getServer(); + assertThat(server).isNotNull(); + assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN)) + .isEqualTo(Status.UP); + assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING); + }); + } + + @Test + void createWhenHasStatusAggregatorBeanReturnsInstanceWithAggregatorUsedForAllServices() { + this.contextRunner.withUserConfiguration(CustomStatusAggregatorConfiguration.class) + .withPropertyValues("spring.grpc.server.health.status.order=up,down", + "spring.grpc.server.health.service.a.include=*") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent server = components.getServer(); + HealthCheckedGrpcComponent serviceA = components.getService("a"); + assertThat(server).isNotNull(); + assertThat(serviceA).isNotNull(); + assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UNKNOWN); + assertThat(serviceA).isNotNull(); + assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UNKNOWN); + }); + } + + @Test + void createWhenHasStatusAggregatorBeanAndServiceSpecificPropertyReturnsInstanceThatUsesBeanOnlyForUnconfiguredServices() { + this.contextRunner.withUserConfiguration(CustomStatusAggregatorConfiguration.class) + .withPropertyValues("spring.grpc.server.health.service.a.include=*", + "spring.grpc.server.health.service.a.status.order=up,down", + "spring.grpc.server.health.service.b.include=*") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent server = components.getServer(); + HealthCheckedGrpcComponent serviceA = components.getService("a"); + HealthCheckedGrpcComponent serviceB = components.getService("b"); + assertThat(server).isNotNull(); + assertThat(serviceA).isNotNull(); + assertThat(serviceB).isNotNull(); + assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UNKNOWN); + assertThat(serviceA).isNotNull(); + assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UP); + assertThat(serviceB).isNotNull(); + assertThat(serviceB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UNKNOWN); + }); + } + + @Test + void createWhenHasStatusAggregatorPropertyReturnsInstanceWithPropertyUsedForAllServices() { + this.contextRunner + .withPropertyValues("spring.grpc.server.health.status.order=up,down", + "spring.grpc.server.health.service.a.include=*") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent server = components.getServer(); + HealthCheckedGrpcComponent serviceA = components.getService("a"); + assertThat(server).isNotNull(); + assertThat(serviceA).isNotNull(); + assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN)) + .isEqualTo(Status.UP); + assertThat(serviceA).isNotNull(); + assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN)) + .isEqualTo(Status.UP); + }); + } + + @Test + void createWhenHasStatusAggregatorPropertyAndServiceSpecificPropertyReturnsInstanceWithPropertyUsedForExpectedServices() { + this.contextRunner + .withPropertyValues("spring.grpc.server.health.status.order=up,down", + "spring.grpc.server.health.service.a.include=*", + "spring.grpc.server.health.service.a.status.order=unknown,up,down", + "spring.grpc.server.health.service.b.include=*") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent server = components.getServer(); + HealthCheckedGrpcComponent serviceA = components.getService("a"); + HealthCheckedGrpcComponent serviceB = components.getService("b"); + assertThat(server).isNotNull(); + assertThat(serviceA).isNotNull(); + assertThat(serviceB).isNotNull(); + assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UP); + assertThat(serviceA).isNotNull(); + assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UNKNOWN); + assertThat(serviceB).isNotNull(); + assertThat(serviceB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UP); + }); + } + + @Test + void createWhenHasStatusAggregatorPropertyAndServiceQualifiedBeanReturnsInstanceWithBeanUsedForExpectedServices() { + this.contextRunner.withUserConfiguration(CustomStatusAggregatorServiceAConfiguration.class) + .withPropertyValues("spring.grpc.server.health.status.order=up,down", + "spring.grpc.server.health.service.a.include=*", + "spring.grpc.server.health.service.a.status.order=up,down", + "spring.grpc.server.health.service.b.include=*") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent server = components.getServer(); + HealthCheckedGrpcComponent serviceA = components.getService("a"); + HealthCheckedGrpcComponent serviceB = components.getService("b"); + assertThat(server).isNotNull(); + assertThat(serviceA).isNotNull(); + assertThat(serviceB).isNotNull(); + assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UP); + assertThat(serviceA).isNotNull(); + assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UNKNOWN); + assertThat(serviceB).isNotNull(); + assertThat(serviceB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UP); + }); + } + + @Test + void createWhenHasServiceSpecificStatusAggregatorPropertyAndServiceQualifiedBeanReturnsInstanceWithBeanUsedForExpectedServices() { + this.contextRunner.withUserConfiguration(CustomStatusAggregatorServiceAConfiguration.class) + .withPropertyValues("spring.grpc.server.health.service.a.include=*", + "spring.grpc.server.health.service.a.status.order=up,down", + "spring.grpc.server.health.service.b.include=*", + "spring.grpc.server.health.service.b.status.order=up,down") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent server = components.getServer(); + HealthCheckedGrpcComponent serviceA = components.getService("a"); + HealthCheckedGrpcComponent serviceB = components.getService("b"); + assertThat(server).isNotNull(); + assertThat(serviceA).isNotNull(); + assertThat(serviceB).isNotNull(); + assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.DOWN); + assertThat(serviceA).isNotNull(); + assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UNKNOWN); + assertThat(serviceB).isNotNull(); + assertThat(serviceB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UP); + }); + } + + @Test + void createWhenHasStatusMapperBeanReturnsInstanceWithMapperUsedForAllServices() { + this.contextRunner.withUserConfiguration(CustomStatusMapperConfiguration.class) + .withPropertyValues("spring.grpc.server.health.status.mapping.down=service-unknown", + "spring.grpc.server.health.service.a.include=*") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent server = components.getServer(); + HealthCheckedGrpcComponent serviceA = components.getService("a"); + assertThat(server).isNotNull(); + assertThat(serviceA).isNotNull(); + assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING); + assertThat(serviceA).isNotNull(); + assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING); + }); + } + + @Test + void createWhenHasStatusMapperBeanAndServiceSpecificPropertyReturnsInstanceThatUsesBeanOnlyForUnconfiguredServices() { + this.contextRunner.withUserConfiguration(CustomStatusMapperConfiguration.class) + .withPropertyValues("spring.grpc.server.health.service.a.include=*", + "spring.grpc.server.health.service.a.status.mapping.down=service-unknown", + "spring.grpc.server.health.service.b.include=*") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent server = components.getServer(); + HealthCheckedGrpcComponent serviceA = components.getService("a"); + HealthCheckedGrpcComponent serviceB = components.getService("b"); + assertThat(server).isNotNull(); + assertThat(serviceA).isNotNull(); + assertThat(serviceB).isNotNull(); + assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING); + assertThat(serviceA).isNotNull(); + assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN)) + .isEqualTo(ServingStatus.SERVICE_UNKNOWN); + assertThat(serviceB).isNotNull(); + assertThat(serviceB.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING); + }); + } + + @Test + void createWhenHasStatusMapperPropertyReturnsInstanceWithPropertyUsedForAllServices() { + this.contextRunner + .withPropertyValues("spring.grpc.server.health.status.mapping.down=service-unknown", + "spring.grpc.server.health.service.a.include=*") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent server = components.getServer(); + HealthCheckedGrpcComponent serviceA = components.getService("a"); + assertThat(server).isNotNull(); + assertThat(serviceA).isNotNull(); + assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)) + .isEqualTo(ServingStatus.SERVICE_UNKNOWN); + assertThat(serviceA).isNotNull(); + assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN)) + .isEqualTo(ServingStatus.SERVICE_UNKNOWN); + }); + } + + @Test + void createWhenHasStatusMapperPropertyAndServiceSpecificPropertyReturnsInstanceWithPropertyUsedForExpectedServices() { + this.contextRunner + .withPropertyValues("spring.grpc.server.health.status.mapping.down=service-unknown", + "spring.grpc.server.health.service.a.include=*", + "spring.grpc.server.health.service.a.status.mapping.down=unrecognized", + "spring.grpc.server.health.service.b.include=*") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent server = components.getServer(); + HealthCheckedGrpcComponent serviceA = components.getService("a"); + HealthCheckedGrpcComponent serviceB = components.getService("b"); + assertThat(server).isNotNull(); + assertThat(serviceA).isNotNull(); + assertThat(serviceB).isNotNull(); + assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)) + .isEqualTo(ServingStatus.SERVICE_UNKNOWN); + assertThat(serviceA).isNotNull(); + assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN)) + .isEqualTo(ServingStatus.UNRECOGNIZED); + assertThat(serviceB).isNotNull(); + assertThat(serviceB.getStatusMapper().getServingStatus(Status.DOWN)) + .isEqualTo(ServingStatus.SERVICE_UNKNOWN); + }); + } + + @Test + void createWhenHasStatusMapperPropertyAndServiceQualifiedBeanReturnsInstanceWithBeanUsedForExpectedServices() { + this.contextRunner.withUserConfiguration(CustomStatusMapperServiceAConfiguration.class) + .withPropertyValues("spring.grpc.server.health.status.mapping.down=service-unknown", + "spring.grpc.server.health.service.a.include=*", + "spring.grpc.server.health.service.a.status.mapping.down=service-unknown", + "spring.grpc.server.health.service.b.include=*") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent server = components.getServer(); + HealthCheckedGrpcComponent serviceA = components.getService("a"); + HealthCheckedGrpcComponent serviceB = components.getService("b"); + assertThat(server).isNotNull(); + assertThat(serviceA).isNotNull(); + assertThat(serviceB).isNotNull(); + assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)) + .isEqualTo(ServingStatus.SERVICE_UNKNOWN); + assertThat(serviceA).isNotNull(); + assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING); + assertThat(serviceB).isNotNull(); + assertThat(serviceB.getStatusMapper().getServingStatus(Status.DOWN)) + .isEqualTo(ServingStatus.SERVICE_UNKNOWN); + }); + } + + @Test + void createWhenHasServiceSpecificStatusMapperPropertyAndServiceQualifiedBeanReturnsInstanceWithBeanUsedForExpectedServices() { + this.contextRunner.withUserConfiguration(CustomStatusMapperServiceAConfiguration.class) + .withPropertyValues("spring.grpc.server.health.service.a.include=*", + "spring.grpc.server.health.service.a.status.mapping.down=service-unknown", + "spring.grpc.server.health.service.b.include=*", + "spring.grpc.server.health.service.b.status.mapping.down=service-unknown") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + HealthCheckedGrpcComponent server = components.getServer(); + HealthCheckedGrpcComponent serviceA = components.getService("a"); + HealthCheckedGrpcComponent serviceB = components.getService("b"); + assertThat(server).isNotNull(); + assertThat(serviceA).isNotNull(); + assertThat(serviceB).isNotNull(); + assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.NOT_SERVING); + assertThat(serviceA).isNotNull(); + assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING); + assertThat(serviceB).isNotNull(); + assertThat(serviceB.getStatusMapper().getServingStatus(Status.DOWN)) + .isEqualTo(ServingStatus.SERVICE_UNKNOWN); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(GrpcServerHealthProperties.class) + static class AutoConfiguredHealthCheckedGrpcComponentsTestConfiguration { + + @Bean + AutoConfiguredHealthCheckedGrpcComponents healthCheckedGrpcComponents( + ConfigurableApplicationContext applicationContext, GrpcServerHealthProperties properties) { + return new AutoConfiguredHealthCheckedGrpcComponents(applicationContext, properties); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomStatusAggregatorConfiguration { + + @Bean + @Primary + StatusAggregator statusAggregator() { + return StatusAggregator.of(Status.UNKNOWN, Status.UP, Status.DOWN); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomStatusAggregatorServiceAConfiguration { + + @Bean + @Qualifier("a") + StatusAggregator statusAggregator() { + return StatusAggregator.of(Status.UNKNOWN, Status.UP, Status.DOWN); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomStatusMapperConfiguration { + + @Bean + @Primary + StatusMapper httpCodeStatusMapper() { + return StatusMapper.of(Collections.singletonMap(Status.DOWN.getCode(), ServingStatus.SERVING)); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomStatusMapperServiceAConfiguration { + + @Bean + @Qualifier("a") + StatusMapper httpCodeStatusMapper() { + return StatusMapper.of(Collections.singletonMap(Status.DOWN.getCode(), ServingStatus.SERVING)); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java new file mode 100644 index 000000000000..ad9a30bbeeb2 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java @@ -0,0 +1,458 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.health; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import io.grpc.BindableService; +import io.grpc.health.v1.HealthCheckResponse.ServingStatus; +import io.grpc.protobuf.services.HealthStatusManager; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration; +import org.springframework.boot.diagnostics.FailureAnalyzedException; +import org.springframework.boot.grpc.server.health.GrpcServerHealth; +import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponents; +import org.springframework.boot.grpc.server.health.StatusAggregator; +import org.springframework.boot.grpc.server.health.StatusMapper; +import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembershipValidator; +import org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration; +import org.springframework.boot.health.contributor.CompositeHealthContributor; +import org.springframework.boot.health.contributor.CompositeReactiveHealthContributor; +import org.springframework.boot.health.contributor.Health; +import org.springframework.boot.health.contributor.HealthIndicator; +import org.springframework.boot.health.contributor.ReactiveHealthIndicator; +import org.springframework.boot.health.contributor.Status; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.GrpcServerFactory; + +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; + +/** + * Tests for {@link GrpcServerHealthAutoConfiguration}. + * + * @author Phillip Webb + * @author Chris Bono + * @author Andrey Litvitski + */ +class GrpcServerHealthAutoConfigurationTests { + + private static final AutoConfigurations autoConfigurations = AutoConfigurations.of( + GrpcServerHealthAutoConfiguration.class, GrpcServerHealthSchedulerAutoConfiguration.class, + HealthContributorRegistryAutoConfiguration.class, TaskSchedulingAutoConfiguration.class); + + private final BindableService service = mock(); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(HealthIndicatorsConfiguration.class) + .withConfiguration(autoConfigurations) + .with(this::serviceBean); + + @Test + void autoConfiguresBeans() { + this.contextRunner.run(this::assertConfigured); + } + + @Test + void whenNoBindableServiceDefinedDoesNotAutoConfigureBeans() { + new ApplicationContextRunner().withConfiguration(autoConfigurations).run(this::assertNotConfigured); + } + + @Test + void whenNoBindableServiceDefinedButHealthEnabledPropertyIsTrueAutoConfiguresBeans() { + new ApplicationContextRunner().withConfiguration(autoConfigurations) + .withPropertyValues("spring.grpc.server.health.enabled=true") + .run(this::assertConfigured); + } + + @Test + void whenGrpcNotOnClasspathDoesNotAutoConfigureBeans() { + this.contextRunner.withClassLoader(new FilteredClassLoader(BindableService.class)) + .run(this::assertNotConfigured); + } + + @Test + void whenSpringGrpcNotOnClasspathDoesNotAutoConfigureBeans() { + this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)) + .run(this::assertNotConfigured); + } + + @Test + void whenHealthStatusManagerNotOnClasspathDoesNotAutoConfigureBeans() { + this.contextRunner.withClassLoader(new FilteredClassLoader(HealthStatusManager.class)) + .run(this::assertNotConfigured); + } + + @Test + void whenNoTaskSchedulerDoesNotAutoConfigureHealthScheduler() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcServerHealthAutoConfiguration.class, + HealthContributorRegistryAutoConfiguration.class)) + .with(this::serviceBean) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerHealthScheduler.class)); + } + + @Test + void whenHealthScheduleEnabledPropertyFalseDoesNotAutoConfigureHealthScheduler() { + this.contextRunner.withPropertyValues("spring.grpc.server.health.schedule.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerHealthScheduler.class)); + } + + @Test + void whenHealthScheduleEnabledPropertyTrueDoesAutoConfigureHealthScheduler() { + this.contextRunner.withPropertyValues("spring.grpc.server.health.schedule.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcServerHealthScheduler.class)); + } + + @Test + void whenHealthEnabledPropertyIsTrueAutoConfiguresBeans() { + this.contextRunner.withPropertyValues("spring.grpc.server.health.enabled=true").run(this::assertConfigured); + } + + @Test + void whenHealthEnabledPropertyIsFalseDoesNotAutoConfigureBeans() { + this.contextRunner.withPropertyValues("spring.grpc.server.health.enabled=false").run(this::assertNotConfigured); + } + + @Test + void whenServerEnabledPropertyIsFalseDoesNotAutoConfigureBeans() { + this.contextRunner.withPropertyValues("spring.grpc.server.enabled=false").run(this::assertNotConfigured); + } + + @Test + void whenServerEnabledPropertyIsTrueAutoConfiguresBeans() { + this.contextRunner.withPropertyValues("spring.grpc.server.enabled=true").run(this::assertConfigured); + } + + @Test + void enterTerminalStateIsCalledOnShutdown() { + AtomicReference manager = new AtomicReference<>(); + this.contextRunner.run((context) -> { + manager.set(context.getBean(HealthStatusManager.class)); + assertTerminalState(manager.get(), false); + }); + assertTerminalState(manager.get(), true); + } + + @Test + void whenHasUserDefinedHealthStatusManagerDoesNotAutoConfigureBean() { + HealthStatusManager customHealthStatusManager = mock(); + this.contextRunner + .withBean("customHealthStatusManager", HealthStatusManager.class, () -> customHealthStatusManager) + .run((context) -> assertThat(context).getBean(HealthStatusManager.class) + .isSameAs(customHealthStatusManager)); + } + + @Test + void whenHasNoHealthContributorRegistryOnlyAutoConfiguresBasicService() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcServerHealthAutoConfiguration.class, + TaskSchedulingAutoConfiguration.class)) + .with(this::serviceBean) + .run((context) -> { + assertThat(context).hasSingleBean(HealthStatusManager.class); + assertThat(context).hasBean("grpcServerHealthService"); + assertThat(context).doesNotHaveBean(GrpcServerHealth.class); + }); + } + + @Test + void createsStatusAggregatorFromProperties() { + this.contextRunner.withPropertyValues("spring.grpc.server.health.status.order=up,down").run((context) -> { + StatusAggregator aggregator = context.getBean(StatusAggregator.class); + assertThat(aggregator.getAggregateStatus(Status.UP, Status.DOWN)).isEqualTo(Status.UP); + }); + } + + @Test + void whenHasStatusAggregatorBeanIgnoresProperties() { + this.contextRunner.withUserConfiguration(StatusAggregatorConfiguration.class) + .withPropertyValues("spring.grpc.server.health.status.order=up,down") + .run((context) -> { + StatusAggregator aggregator = context.getBean(StatusAggregator.class); + assertThat(aggregator.getAggregateStatus(Status.UP, Status.DOWN)).isEqualTo(Status.UNKNOWN); + }); + } + + @Test + void createsStatusMapperFromProperties() { + this.contextRunner.withPropertyValues("spring.grpc.server.health.status.mapping.up=not-serving") + .run((context) -> { + StatusMapper mapper = context.getBean(StatusMapper.class); + assertThat(mapper.getServingStatus(Status.UP)).isEqualTo(ServingStatus.NOT_SERVING); + }); + } + + @Test + void whenHasHttpCodeStatusMapperBeanIgnoresProperties() { + this.contextRunner.withUserConfiguration(StatusMapperConfiguration.class) + .withPropertyValues("spring.grpc.server.health.status.mapping.up=not-serving") + .run((context) -> { + StatusMapper mapper = context.getBean(StatusMapper.class); + assertThat(mapper.getServingStatus(Status.UP)).isEqualTo(ServingStatus.UNRECOGNIZED); + }); + } + + @Test + void createsHealthCheckedGrpcComponents() { + this.contextRunner.withPropertyValues("spring.grpc.server.health.service.test.include=*").run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + assertThat(components).isInstanceOf(AutoConfiguredHealthCheckedGrpcComponents.class); + assertThat(components.getServiceNames()).containsOnly("test"); + }); + } + + @Test + void whenComponentsIncludesContributorThatExistsDoesNotFail() { + this.contextRunner.withUserConfiguration(CompositeHealthIndicatorConfiguration.class) + .withPropertyValues("spring.grpc.server.health.service.test.include=composite/b/c") + .run((context) -> assertThat(context).hasNotFailed()); + } + + @Test + void whenHealthCheckedGrpcComponentsIncludesReactiveContributorThatExists() { + this.contextRunner.withUserConfiguration(CompositeReactiveHealthIndicatorConfiguration.class) + .withPropertyValues("spring.grpc.server.health.service.test.include=composite/b/c") + .run((context) -> assertThat(context).hasNotFailed()); + } + + @Test + void whenHealthCheckedGrpcComponentsIncludesContributorThatDoesNotExistThrowsException() { + this.contextRunner.withUserConfiguration(CompositeHealthIndicatorConfiguration.class) + .withPropertyValues("spring.grpc.server.health.service.test.include=composite/b/c,nope") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).isInstanceOf(FailureAnalyzedException.class) + .hasMessage("Health contributor 'nope' defined in " + + "'spring.grpc.server.health.service.test.include' does not exist"); + }); + } + + @Test + void whenHealthCheckedGrpcComponentsExcludesContributorThatDoesNotExistThrowsException() { + this.contextRunner + .withPropertyValues("spring.grpc.server.health.service.test.exclude=composite/b/d", + "spring.grpc.server.health.service.test.include=*") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).isInstanceOf(FailureAnalyzedException.class) + .hasMessage("Health contributor 'composite/b/d' defined in " + + "'spring.grpc.server.health.service.test.exclude' does not exist"); + }); + } + + @Test + void whenHealthCheckedGrpcComponentsIncludesContributorThatDoesNotExistAndValidationDisabledCreatesComponents() { + this.contextRunner + .withPropertyValues("spring.grpc.server.health.services.validate-membership=false", + "spring.grpc.server.health.service.test.include=nope") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + assertThat(components).isInstanceOf(AutoConfiguredHealthCheckedGrpcComponents.class); + assertThat(components.getServiceNames()).containsOnly("test"); + }); + } + + @Test + void whenHasHealthCheckedGrpcComponentsBeanDoesNotCreateAdditional() { + this.contextRunner.withUserConfiguration(HealthCheckedGrpcComponentsConfiguration.class) + .withPropertyValues("spring.grpc.server.health.service.test.include=*") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + assertThat(components.getServiceNames()).containsOnly("mock"); + }); + } + + @Test + void createsGrpcServerHealth() { + this.contextRunner.run((context) -> { + GrpcServerHealth serverHealth = context.getBean(GrpcServerHealth.class); + Map result = new LinkedHashMap<>(); + serverHealth.update(result::put); + assertThat(result).containsExactly(entry("", ServingStatus.SERVING)); + }); + } + + @Test + void whenHasGrpcServerHealthBeanDoesNotCreateAdditional() { + this.contextRunner.withUserConfiguration(GrpcServerHealthConfiguration.class).run((context) -> { + GrpcServerHealth serverHealth = context.getBean(GrpcServerHealth.class); + Map result = new LinkedHashMap<>(); + serverHealth.update(result::put); + assertThat(result).isEmpty(); + }); + } + + @Test + void runWithIndicatorsInParentContextFindsIndicators() { + new ApplicationContextRunner().withUserConfiguration(DownHealthIndicatorConfiguration.class) + .run((parent) -> new ApplicationContextRunner().withConfiguration(autoConfigurations) + .withUserConfiguration(HealthIndicatorsConfiguration.class) + .with(this::serviceBean) + .withParent(parent) + .run((context) -> { + GrpcServerHealth serverHealth = context.getBean(GrpcServerHealth.class); + Map result = new LinkedHashMap<>(); + serverHealth.update(result::put); + assertThat(result).containsExactly(entry("", ServingStatus.NOT_SERVING)); + })); + } + + private void assertTerminalState(HealthStatusManager healthStatusManager, boolean expected) { + assertThat(healthStatusManager).extracting("healthService.terminal").isEqualTo(expected); + } + + private void assertConfigured(AssertableApplicationContext context) { + assertThat(context).hasSingleBean(HealthStatusManager.class); + assertThat(context).hasBean("grpcServerHealthService"); + assertThat(context).hasSingleBean(StatusAggregator.class); + assertThat(context).hasSingleBean(StatusMapper.class); + assertThat(context).hasSingleBean(HealthCheckedGrpcComponents.class); + assertThat(context).hasSingleBean(GrpcServerHealth.class); + assertThat(context).hasSingleBean(HealthContributorMembershipValidator.class); + assertThat(context).hasSingleBean(GrpcServerHealthScheduler.class); + } + + private void assertNotConfigured(AssertableApplicationContext context) { + assertThat(context).doesNotHaveBean(HealthStatusManager.class); + assertThat(context).doesNotHaveBean("grpcServerHealthService"); + assertThat(context).doesNotHaveBean(GrpcServerHealthAutoConfiguration.class); + } + + private , C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> R serviceBean( + R contextRunner) { + return contextRunner.withBean(BindableService.class, () -> this.service); + } + + @Configuration(proxyBeanMethods = false) + static class HealthIndicatorsConfiguration { + + @Bean + HealthIndicator simpleHealthIndicator() { + return () -> Health.up().withDetail("counter", 42).build(); + } + + @Bean + HealthIndicator additionalHealthIndicator() { + return () -> Health.up().build(); + } + + @Bean + ReactiveHealthIndicator reactiveHealthIndicator() { + return () -> Mono.just(Health.up().build()); + } + + } + + @Configuration(proxyBeanMethods = false) + static class DownHealthIndicatorConfiguration { + + @Bean + HealthIndicator downHealthIndicator() { + return () -> Health.down().build(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CompositeHealthIndicatorConfiguration { + + @Bean + CompositeHealthContributor compositeHealthIndicator() { + return CompositeHealthContributor.fromMap(Map.of("a", createHealthIndicator(), "b", + CompositeHealthContributor.fromMap(Map.of("c", createHealthIndicator())))); + } + + private HealthIndicator createHealthIndicator() { + return () -> Health.up().build(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CompositeReactiveHealthIndicatorConfiguration { + + @Bean + CompositeReactiveHealthContributor compositeHealthIndicator() { + return CompositeReactiveHealthContributor.fromMap(Map.of("a", createHealthIndicator(), "b", + CompositeReactiveHealthContributor.fromMap(Map.of("c", createHealthIndicator())))); + } + + private ReactiveHealthIndicator createHealthIndicator() { + return () -> Mono.just(Health.up().build()); + } + + } + + @Configuration(proxyBeanMethods = false) + static class StatusAggregatorConfiguration { + + @Bean + StatusAggregator statusAggregator() { + return (statuses) -> Status.UNKNOWN; + } + + } + + @Configuration(proxyBeanMethods = false) + static class StatusMapperConfiguration { + + @Bean + StatusMapper httpCodeStatusMapper() { + return (status) -> ServingStatus.UNRECOGNIZED; + } + + } + + @Configuration(proxyBeanMethods = false) + static class HealthCheckedGrpcComponentsConfiguration { + + @Bean + HealthCheckedGrpcComponents healthCheckedGrpcComponents() { + HealthCheckedGrpcComponents components = mock(); + given(components.getServiceNames()).willReturn(Collections.singleton("mock")); + return components; + } + + } + + @Configuration(proxyBeanMethods = false) + static class GrpcServerHealthConfiguration { + + @Bean + GrpcServerHealth grpcServerHealth() { + return mock(); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerTests.java new file mode 100644 index 000000000000..c054fc87663f --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.health; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; + +import io.grpc.protobuf.services.HealthStatusManager; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.grpc.server.health.GrpcServerHealth; +import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent; +import org.springframework.scheduling.TaskScheduler; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +/** + * Tests for {@link GrpcServerHealthScheduler}. + * + * @author Phillip Webb + */ +class GrpcServerHealthSchedulerTests { + + @Test + void onApplicationEventWhenEventIsGrpcStartStartsHealth() { + Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); + GrpcServerHealth serverHealth = mock(); + HealthStatusManager statusManager = mock(); + TaskScheduler taskScheduler = mock(); + Duration period = Duration.ofSeconds(10); + Duration delay = Duration.ofSeconds(30); + GrpcServerHealthScheduler healthScheduler = new GrpcServerHealthScheduler(clock, serverHealth, statusManager, + taskScheduler, period, delay); + then(serverHealth).should(never()).update(statusManager); + healthScheduler.onApplicationEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 123)); + Instant startTime = Instant.now(clock).plus(delay); + ArgumentCaptor runnable = ArgumentCaptor.captor(); + then(taskScheduler).should().scheduleAtFixedRate(runnable.capture(), eq(startTime), eq(period)); + then(serverHealth).should(never()).update(statusManager); + runnable.getValue().run(); + then(serverHealth).should().update(statusManager); + } + + @Test + void onApplicationEventWhenEventIsGrpcStartAndCalledTwiceStartsHealthOnlyOnce() { + GrpcServerHealth serverHealth = mock(); + HealthStatusManager statusManager = mock(); + TaskScheduler taskScheduler = mock(); + GrpcServerHealthScheduler healthScheduler = new GrpcServerHealthScheduler(serverHealth, statusManager, + taskScheduler, Duration.ofSeconds(10), Duration.ofSeconds(30)); + then(serverHealth).should(never()).update(statusManager); + healthScheduler.onApplicationEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 123)); + healthScheduler.onApplicationEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 345)); + then(taskScheduler).should(times(1)).scheduleAtFixedRate(any(), any(), any()); + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/GrpcServerHealthTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/GrpcServerHealthTests.java new file mode 100644 index 000000000000..9a57df1153a4 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/GrpcServerHealthTests.java @@ -0,0 +1,293 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.health; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import io.grpc.health.v1.HealthCheckResponse.ServingStatus; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.health.contributor.CompositeHealthContributor; +import org.springframework.boot.health.contributor.Health; +import org.springframework.boot.health.contributor.HealthContributor; +import org.springframework.boot.health.contributor.HealthIndicator; +import org.springframework.boot.health.contributor.ReactiveHealthIndicator; +import org.springframework.boot.health.contributor.Status; +import org.springframework.boot.health.registry.DefaultHealthContributorRegistry; +import org.springframework.boot.health.registry.DefaultReactiveHealthContributorRegistry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.entry; +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 GrpcServerHealth}. + * + * @author Phillip Webb + */ +class GrpcServerHealthTests { + + private static final HealthIndicator UP = () -> Health.up().build(); + + private static final HealthIndicator DOWN = () -> Health.down().build(); + + private static final ReactiveHealthIndicator REACTIVE_DOWN = () -> Mono.just(Health.down().build()); + + @Test + @SuppressWarnings("NullAway") // Test null check + void createWhenRegistryIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new GrpcServerHealth(null, null, mock())) + .withMessage("'registry' must not be null"); + } + + @Test + @SuppressWarnings("NullAway") // Test null check + void createWhenComponentsIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new GrpcServerHealth(mock(), null, null)) + .withMessage("'components' must not be null"); + } + + @Test + void updateWhenHasServerComponentAndUp() { + assertThat(updateWithServerComponent("test", UP)).containsExactly(entry("", ServingStatus.SERVING)); + } + + @Test + void updateWhenHasServerComponentAndDown() { + assertThat(updateWithServerComponent("test", DOWN)).containsExactly(entry("", ServingStatus.NOT_SERVING)); + } + + @Test + void updateWhenHasServerComponentAndMissing() { + assertThat(updateWithServerComponent("other", UP)).containsExactly(entry("", ServingStatus.UNKNOWN)); + } + + private Map updateWithServerComponent(String indicatorName, HealthIndicator indicator) { + HealthCheckedGrpcComponent server = new TestHealthCheckedGrpcComponent(Set.of("test")); + HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(server, Collections.emptyMap()); + DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry(); + registry.registerContributor(indicatorName, indicator); + GrpcServerHealth health = new GrpcServerHealth(registry, null, components); + Map result = new LinkedHashMap<>(); + health.update(result::put); + return result; + } + + @Test + void updateWhenHasServices() { + Map services = new LinkedHashMap<>(); + services.put("one", new TestHealthCheckedGrpcComponent(Set.of("up"))); + services.put("two", new TestHealthCheckedGrpcComponent(Set.of("down"))); + HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services); + DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry(); + registry.registerContributor("up", UP); + registry.registerContributor("down", DOWN); + GrpcServerHealth health = new GrpcServerHealth(registry, null, components); + Map result = new LinkedHashMap<>(); + health.update(result::put); + assertThat(result).containsExactly(entry("one", ServingStatus.SERVING), + entry("two", ServingStatus.NOT_SERVING)); + } + + @Test + void updateUsesCache() { + Map services = new LinkedHashMap<>(); + services.put("one", new TestHealthCheckedGrpcComponent(Set.of("test"))); + services.put("two", new TestHealthCheckedGrpcComponent(Set.of("test"))); + HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services); + DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry(); + HealthIndicator contributor = mock(); + given(contributor.health(false)).willReturn(Health.up().build(), Health.down().build()); + registry.registerContributor("test", contributor); + GrpcServerHealth health = new GrpcServerHealth(registry, null, components); + Map result = new LinkedHashMap<>(); + health.update(result::put); + assertThat(result).containsExactly(entry("one", ServingStatus.SERVING), entry("two", ServingStatus.SERVING)); + } + + @Test + void updateWhenHasEmptyNamedServicesDoesNotIncludeIt() { + Map services = new LinkedHashMap<>(); + services.put("", new TestHealthCheckedGrpcComponent(Set.of("up"))); + services.put("one", new TestHealthCheckedGrpcComponent(Set.of("up"))); + HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services); + DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry(); + registry.registerContributor("up", UP); + GrpcServerHealth health = new GrpcServerHealth(registry, null, components); + Map result = new LinkedHashMap<>(); + health.update(result::put); + assertThat(result).containsExactly(entry("one", ServingStatus.SERVING)); + } + + @Test + void updateWhenHasFallbackRegistry() { + Map services = new LinkedHashMap<>(); + services.put("one", new TestHealthCheckedGrpcComponent(Set.of("1"))); + services.put("two", new TestHealthCheckedGrpcComponent(Set.of("2"))); + services.put("three", new TestHealthCheckedGrpcComponent(Set.of("3"))); + HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services); + DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry(); + DefaultReactiveHealthContributorRegistry fallbackRegistry = new DefaultReactiveHealthContributorRegistry(); + registry.registerContributor("1", UP); + registry.registerContributor("2", UP); + fallbackRegistry.registerContributor("2", REACTIVE_DOWN); + fallbackRegistry.registerContributor("3", REACTIVE_DOWN); + GrpcServerHealth health = new GrpcServerHealth(registry, fallbackRegistry, components); + Map result = new LinkedHashMap<>(); + health.update(result::put); + assertThat(result).containsExactly(entry("one", ServingStatus.SERVING), entry("two", ServingStatus.SERVING), + entry("three", ServingStatus.NOT_SERVING)); + } + + @Test + void updateWhenHasCompositeContributor() { + Map services = new LinkedHashMap<>(); + services.put("one", new TestHealthCheckedGrpcComponent(Set.of("dbs", "dbs/db1", "dbs/db2"))); + HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services); + HealthContributor contributor = CompositeHealthContributor.fromMap(Map.of("db1", UP, "db2", DOWN)); + DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry(); + registry.registerContributor("dbs", contributor); + GrpcServerHealth health = new GrpcServerHealth(registry, null, components); + Map result = new LinkedHashMap<>(); + health.update(result::put); + assertThat(result).containsExactly(entry("one", ServingStatus.NOT_SERVING)); + } + + @Test + void updateFiltersIndicatorsAndCompositeParentByName() { + Map services = new LinkedHashMap<>(); + services.put("one", new TestHealthCheckedGrpcComponent(Set.of("dbs", "dbs/db1"))); + HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services); + HealthContributor contributor = CompositeHealthContributor.fromMap(Map.of("db1", UP, "db2", DOWN)); + DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry(); + registry.registerContributor("dbs", contributor); + GrpcServerHealth health = new GrpcServerHealth(registry, null, components); + Map result = new LinkedHashMap<>(); + health.update(result::put); + assertThat(result).containsExactly(entry("one", ServingStatus.SERVING)); + } + + @Test + @SuppressWarnings("unchecked") + void updateCallsStatusAggregator() { + StatusAggregator statusAggregator = mock(); + given(statusAggregator.getAggregateStatus((Set) any(Set.class))).willReturn(Status.DOWN); + Map services = new LinkedHashMap<>(); + services.put("one", + new TestHealthCheckedGrpcComponent(Set.of("up"), statusAggregator, StatusMapper.getDefault())); + HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services); + DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry(); + registry.registerContributor("up", UP); + GrpcServerHealth health = new GrpcServerHealth(registry, null, components); + Map result = new LinkedHashMap<>(); + health.update(result::put); + assertThat(result).containsExactly(entry("one", ServingStatus.NOT_SERVING)); + then(statusAggregator).should().getAggregateStatus(Set.of(Status.UP)); + } + + @Test + void updateCallsStatusMapper() { + StatusMapper statusMapper = mock(); + given(statusMapper.getServingStatus(Status.UP)).willReturn(ServingStatus.UNRECOGNIZED); + Map services = new LinkedHashMap<>(); + services.put("one", + new TestHealthCheckedGrpcComponent(Set.of("up"), StatusAggregator.getDefault(), statusMapper)); + HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services); + DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry(); + registry.registerContributor("up", UP); + GrpcServerHealth health = new GrpcServerHealth(registry, null, components); + Map result = new LinkedHashMap<>(); + health.update(result::put); + assertThat(result).containsExactly(entry("one", ServingStatus.UNRECOGNIZED)); + then(statusMapper).should().getServingStatus(Status.UP); + } + + static class TestHealthCheckedGrpcComponents implements HealthCheckedGrpcComponents { + + private final @Nullable HealthCheckedGrpcComponent server; + + private final Map services; + + TestHealthCheckedGrpcComponents(@Nullable HealthCheckedGrpcComponent server, + Map services) { + this.server = server; + this.services = services; + } + + @Override + public @Nullable HealthCheckedGrpcComponent getServer() { + return this.server; + } + + @Override + public Set getServiceNames() { + return this.services.keySet(); + } + + @Override + public @Nullable HealthCheckedGrpcComponent getService(String serviceName) { + return this.services.get(serviceName); + } + + } + + private static class TestHealthCheckedGrpcComponent implements HealthCheckedGrpcComponent { + + private final Set members; + + private final StatusAggregator statusAggregator; + + private final StatusMapper statusMapper; + + TestHealthCheckedGrpcComponent(Set memebers) { + this(memebers, StatusAggregator.getDefault(), StatusMapper.getDefault()); + } + + TestHealthCheckedGrpcComponent(Set memebers, StatusAggregator statusAggregator, + StatusMapper statusMapper) { + this.members = memebers; + this.statusAggregator = statusAggregator; + this.statusMapper = statusMapper; + } + + @Override + public boolean isMember(String healthContributorName) { + return this.members.contains(healthContributorName); + } + + @Override + public StatusAggregator getStatusAggregator() { + return this.statusAggregator; + } + + @Override + public StatusMapper getStatusMapper() { + return this.statusMapper; + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusAggregatorTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusAggregatorTests.java new file mode 100644 index 000000000000..bbc8a6abe62b --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusAggregatorTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.health; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.health.actuate.endpoint.StatusAggregator; +import org.springframework.boot.health.contributor.Status; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StatusAggregator}. + * + * @author Phillip Webb + */ +class StatusAggregatorTests { + + @Test + void ofAndGetAggregateStatusWhenUsingDefaultInstance() { + StatusAggregator aggregator = StatusAggregator.getDefault(); + Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE); + assertThat(status).isEqualTo(Status.DOWN); + } + + @Test + void ofAndGetAggregateStatusWhenUsingCustomOrder() { + StatusAggregator aggregator = StatusAggregator.of(Status.UNKNOWN, Status.UP, Status.OUT_OF_SERVICE, + Status.DOWN); + Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE); + assertThat(status).isEqualTo(Status.UNKNOWN); + } + + @Test + void ofAndGetAggregateStatusWhenHasCustomStatusAndUsingDefaultOrder() { + StatusAggregator aggregator = StatusAggregator.getDefault(); + Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE, + new Status("CUSTOM")); + assertThat(status).isEqualTo(Status.DOWN); + } + + @Test + void ofAndGetAggregateStatusWhenHasCustomStatusAndUsingCustomOrder() { + StatusAggregator aggregator = StatusAggregator.of("DOWN", "OUT_OF_SERVICE", "UP", "UNKNOWN", "CUSTOM"); + Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE, + new Status("CUSTOM")); + assertThat(status).isEqualTo(Status.DOWN); + } + + @Test + void ofWithNonUniformCodes() { + StatusAggregator aggregator = StatusAggregator.of("out-of-service", "up"); + Status status = aggregator.getAggregateStatus(Status.UP, Status.OUT_OF_SERVICE); + assertThat(status).isEqualTo(Status.OUT_OF_SERVICE); + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusMapperTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusMapperTests.java new file mode 100644 index 000000000000..af027f7200c1 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusMapperTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.health; + +import java.util.LinkedHashMap; +import java.util.Map; + +import io.grpc.health.v1.HealthCheckResponse.ServingStatus; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.health.contributor.Status; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StatusMapper}. + * + * @author Phillip Webb + */ +class StatusMapperTests { + + @Test + void createWhenMappingsAreNullUsesDefaultMappings() { + StatusMapper mapper = StatusMapper.of(null); + assertThat(mapper.getServingStatus(Status.UNKNOWN)).isEqualTo(ServingStatus.UNKNOWN); + assertThat(mapper.getServingStatus(Status.UP)).isEqualTo(ServingStatus.SERVING); + assertThat(mapper.getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.NOT_SERVING); + assertThat(mapper.getServingStatus(Status.OUT_OF_SERVICE)).isEqualTo(ServingStatus.NOT_SERVING); + } + + @Test + void getStatusCodeReturnsMappedStatus() { + Map map = new LinkedHashMap<>(); + map.put("up", ServingStatus.UNRECOGNIZED); + map.put("down", ServingStatus.UNKNOWN); + StatusMapper mapper = StatusMapper.of(map); + assertThat(mapper.getServingStatus(Status.UP)).isEqualTo(ServingStatus.UNRECOGNIZED); + assertThat(mapper.getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.UNKNOWN); + assertThat(mapper.getServingStatus(Status.OUT_OF_SERVICE)).isEqualTo(ServingStatus.SERVING); + } + + @Test + void getStatusCodeWhenMappingsAreNotUniformReturnsMappedStatus() { + Map map = new LinkedHashMap<>(); + map.put("out-of-service", ServingStatus.SERVING); + StatusMapper mapper = StatusMapper.of(map); + assertThat(mapper.getServingStatus(Status.OUT_OF_SERVICE)).isEqualTo(ServingStatus.SERVING); + } + +} From d6b28375c027331c99f1734a7232b72059acea42 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 24 Feb 2026 22:38:40 -0800 Subject: [PATCH 388/930] Validate 'server.http2.enabled' for gRPC Servlet applications Throw an error is a Spring gRPC Servlet based server application has forgotten to configure HTTP/2 support. There is also the option of suppressing the error if HTTP/2 has been configured directly and not using the configuration property. Closes gh-49050 --- .../autoconfigure/GrpcServerProperties.java | 43 +++++++++++++++++++ .../ServletGrpcServerConfiguration.java | 14 +++++- ...itional-spring-configuration-metadata.json | 6 --- .../GrpcServerAutoConfigurationTests.java | 30 ++++++++++++- 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java index c3f97ea2a507..0cd2266f1066 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java @@ -61,6 +61,8 @@ public class GrpcServerProperties { private final Netty netty = new Netty(); + private final Servlet servlet = new Servlet(); + public @Nullable Integer getPort() { return this.port; } @@ -101,6 +103,10 @@ public Netty getNetty() { return this.netty; } + public Servlet getServlet() { + return this.servlet; + } + /** * Server shutdown properties. */ @@ -449,4 +455,41 @@ public enum Transport { } + /** + * Servlet properties. + */ + public static class Servlet { + + /** + * Whether to use a servlet server in a servlet-based web application. When the + * value is false, a native gRPC server will be created as long as one is + * available, and it will listen on its own port. Should only be needed if the + * GrpcServlet is on the classpath. + */ + private boolean enabled; + + /** + * Whether to validate that HTTP/2 is enabled. Validation may need to be skipped + * if your servlet container is not configured using properties. + */ + private boolean validateHttp2 = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isValidateHttp2() { + return this.validateHttp2; + } + + public void setValidateHttp2(boolean validateHttp2) { + this.validateHttp2 = validateHttp2; + } + + } + } diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletGrpcServerConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletGrpcServerConfiguration.java index 95e6ab7e436d..5ac366e658a2 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletGrpcServerConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletGrpcServerConfiguration.java @@ -21,9 +21,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.diagnostics.FailureAnalyzedException; import org.springframework.boot.grpc.server.GrpcServletRegistration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; import org.springframework.grpc.server.service.GrpcServiceConfigurer; import org.springframework.grpc.server.service.GrpcServiceDiscoverer; @@ -43,8 +45,16 @@ class ServletGrpcServerConfiguration { @Bean - GrpcServletRegistration grpcServletRegistration(GrpcServiceDiscoverer serviceDiscoverer, - GrpcServiceConfigurer serviceConfigurer, GrpcServerBuilderCustomizers grpcServerBuilderCustomizers) { + GrpcServletRegistration grpcServletRegistration(Environment environment, GrpcServerProperties properties, + GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer, + GrpcServerBuilderCustomizers grpcServerBuilderCustomizers) { + if (properties.getServlet().isValidateHttp2() + && !Boolean.TRUE.equals(environment.getProperty("server.http2.enabled", Boolean.class))) { + throw new FailureAnalyzedException( + "Configuration property 'server.http2.enabled' should be set to true for gRPC support", + "Update your application to correct the invalid configuration.\n" + + "You can also set 'spring.grpc.server.servlet.validate-http2' to false to disable the validation."); + } return new GrpcServletRegistration(serviceDiscoverer, serviceConfigurer, grpcServerBuilderCustomizers::apply); } diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json index d99721c35be3..12640064b221 100644 --- a/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -18,12 +18,6 @@ "type": "java.lang.Boolean", "description": "Whether to enable Reflection on the gRPC server.", "defaultValue": true - }, - { - "name": "spring.grpc.server.servlet.enabled", - "type": "java.lang.Boolean", - "description": "Whether to use a servlet server in a servlet-based web application. When the value is false, a native gRPC server will be created as long as one is available, and it will listen on its own port. Should only be needed if the GrpcServlet is on the classpath", - "defaultValue": true } ] } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java index b0066cb0d19a..497715b3f445 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java @@ -374,7 +374,8 @@ class ServletServerAutoConfigurationTests { private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(autoConfigurations) - .with(GrpcServerAutoConfigurationTests.this::serviceBean); + .with(GrpcServerAutoConfigurationTests.this::serviceBean) + .withPropertyValues("server.http2.enabled=true"); @Test void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { @@ -439,6 +440,33 @@ void customizersAreAppliedToServletServer() { .run((context) -> assertThat(applied.get()).isInstanceOf(ServletServerBuilder.class)); } + @Test + void whenHttp2EnabledPropertyMissing() { + new WebApplicationContextRunner().withConfiguration(autoConfigurations) + .with(GrpcServerAutoConfigurationTests.this::serviceBean) + .run((context) -> assertThat(context).getFailure() + .hasMessageContaining( + "Configuration property 'server.http2.enabled' should be set to true for gRPC support")); + } + + @Test + void whenHttp2EnabledPropertyFalse() { + new WebApplicationContextRunner().withConfiguration(autoConfigurations) + .with(GrpcServerAutoConfigurationTests.this::serviceBean) + .withPropertyValues("server.http2.enabled=false") + .run((context) -> assertThat(context).getFailure() + .hasMessageContaining( + "Configuration property 'server.http2.enabled' should be set to true for gRPC support")); + } + + @Test + void whenHttp2EnabledPropertyMissingAndValidationDisabled() { + new WebApplicationContextRunner().withConfiguration(autoConfigurations) + .with(GrpcServerAutoConfigurationTests.this::serviceBean) + .withPropertyValues("spring.grpc.server.servlet.validate-http2=false") + .run((context) -> assertThat(context).hasNotFailed()); + } + } @Configuration(proxyBeanMethods = false) From 1b41cb20c18b6a7f8bb7a87c68d9bc1971792c2a Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Wed, 25 Feb 2026 20:50:52 -0800 Subject: [PATCH 389/930] Add Spring gRPC client and server observation support Update client and server code to support micrometer observations. Closes gh-49056 Co-authored-by: Phillip Webb --- module/spring-boot-grpc-client/build.gradle | 1 + ...rpcClientObservationAutoConfiguration.java | 62 ++++++++ ...itional-spring-configuration-metadata.json | 6 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...ientObservationAutoConfigurationTests.java | 112 +++++++++++++++ module/spring-boot-grpc-server/build.gradle | 3 + ...rpcServerObservationAutoConfiguration.java | 73 ++++++++++ ...itional-spring-configuration-metadata.json | 6 + ...ot.autoconfigure.AutoConfiguration.imports | 3 +- ...rverObservationAutoConfigurationTests.java | 133 ++++++++++++++++++ 10 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java diff --git a/module/spring-boot-grpc-client/build.gradle b/module/spring-boot-grpc-client/build.gradle index 4bf990aff43d..52bf78936283 100644 --- a/module/spring-boot-grpc-client/build.gradle +++ b/module/spring-boot-grpc-client/build.gradle @@ -40,6 +40,7 @@ dependencies { optional("io.grpc:grpc-xds") { exclude group: "javax.annotation", module: "javax.annotation-api" } + optional("io.micrometer:micrometer-core") testImplementation(project(":core:spring-boot-test")) testImplementation(project(":test-support:spring-boot-test-support")) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java new file mode 100644 index 000000000000..394b7aabe310 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import io.grpc.stub.AbstractStub; +import io.micrometer.core.instrument.binder.grpc.GrpcClientObservationConvention; +import io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor; +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.grpc.client.GlobalClientInterceptor; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC observation support. + * + * @author Chris Bono + * @author Phillip Webb + * @since 4.1.0 + */ + +@AutoConfiguration( + afterName = "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration") +@ConditionalOnClass({ AbstractStub.class, GrpcChannelBuilderCustomizer.class, ObservationRegistry.class, + ObservationGrpcClientInterceptor.class }) +@ConditionalOnProperty(name = "spring.grpc.client.enabled", matchIfMissing = true) +@ConditionalOnProperty(name = "spring.grpc.client.observation.enabled", havingValue = "true", matchIfMissing = true) +@ConditionalOnBean(ObservationRegistry.class) +public final class GrpcClientObservationAutoConfiguration { + + @Bean + @GlobalClientInterceptor + @ConditionalOnMissingBean + ObservationGrpcClientInterceptor grpcClientObservationInterceptor(ObservationRegistry observationRegistry, + ObjectProvider GrpcClientObservationConvention) { + ObservationGrpcClientInterceptor interceptor = new ObservationGrpcClientInterceptor(observationRegistry); + GrpcClientObservationConvention.ifAvailable(interceptor::setCustomConvention); + return interceptor; + } + +} diff --git a/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 0b2cfcc4bd2f..262cb1f229fe 100644 --- a/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -18,6 +18,12 @@ "type": "java.lang.Boolean", "description": "Whether to configure the in-process channel factory.", "defaultValue": true + }, + { + "name": "spring.grpc.client.observation.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Observations on the client.", + "defaultValue": true } ] } diff --git a/module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index e2d9c74b8c48..7d3249ab92fc 100644 --- a/module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,2 +1,3 @@ org.springframework.boot.grpc.client.autoconfigure.CompositeChannelFactoryAutoConfiguration org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration +org.springframework.boot.grpc.client.autoconfigure.GrpcClientObservationAutoConfiguration diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java new file mode 100644 index 000000000000..358814ab5bfa --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import java.util.Map; + +import io.grpc.stub.AbstractStub; +import io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor; +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.grpc.client.GlobalClientInterceptor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for the {@link GrpcClientObservationAutoConfiguration}. + * + * @author Chris Bono + */ +class GrpcClientObservationAutoConfigurationTests { + + private static final AutoConfigurations autoConfigurations = AutoConfigurations + .of(GrpcClientObservationAutoConfiguration.class); + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(autoConfigurations) + .withBean("observationRegistry", ObservationRegistry.class, Mockito::mock); + + @Test + void whenObservationRegistryNotOnClasspathAutoConfigSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(ObservationRegistry.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenObservationGrpcClientInterceptorNotOnClasspathAutoConfigSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(ObservationGrpcClientInterceptor.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenObservationRegistryNotProvidedThenAutoConfigSkipped() { + new ApplicationContextRunner().withConfiguration(autoConfigurations) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenObservationPropertyEnabledThenAutoConfigNotSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.client.observation.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenObservationPropertyDisabledThenAutoConfigIsSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.client.observation.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenClientEnabledPropertyNotSetThenAutoConfigNotSkipped() { + this.contextRunner + .run((context) -> assertThat(context).hasSingleBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenClientEnabledPropertySetTrueThenAutoConfigIsNotSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.client.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenClientEnabledPropertySetFalseThenAutoConfigIsSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.client.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenGrpcStubNotOnClasspathThenAutoConfigIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(AbstractStub.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenAllConditionsAreMetThenInterceptorConfiguredAsExpected() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(ObservationGrpcClientInterceptor.class); + Map annotated = context.getBeansWithAnnotation(GlobalClientInterceptor.class); + assertThat(annotated).hasEntrySatisfying("grpcClientObservationInterceptor", + ObservationGrpcClientInterceptor.class::isInstance); + }); + } + +} diff --git a/module/spring-boot-grpc-server/build.gradle b/module/spring-boot-grpc-server/build.gradle index 3f730227c6a4..581e145e6406 100644 --- a/module/spring-boot-grpc-server/build.gradle +++ b/module/spring-boot-grpc-server/build.gradle @@ -30,6 +30,7 @@ dependencies { optional(project(":core:spring-boot-autoconfigure")) optional(project(":module:spring-boot-health")) + optional(project(":module:spring-boot-micrometer-observation")) optional("com.fasterxml.jackson.core:jackson-annotations") optional("io.projectreactor:reactor-core") optional("io.grpc:grpc-servlet-jakarta") @@ -40,6 +41,8 @@ dependencies { optional("io.grpc:grpc-kotlin-stub") { exclude group: "javax.annotation", module: "javax.annotation-api" } + optional("io.micrometer:micrometer-core") + optional("io.micrometer:context-propagation") optional("jakarta.servlet:jakarta.servlet-api") testImplementation(project(":core:spring-boot-test")) diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java new file mode 100644 index 000000000000..19d2da8c2acf --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import io.grpc.BindableService; +import io.micrometer.core.instrument.binder.grpc.ObservationGrpcServerInterceptor; +import io.micrometer.core.instrument.kotlin.ObservationCoroutineContextServerInterceptor; +import io.micrometer.observation.ObservationRegistry; + +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.ConditionalOnBooleanProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.GrpcServerFactory; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side observations. + * + * @author Sunny Tang + * @author Chris Bono + * @author Dave Syer + * @since 1.0.0 + */ +@AutoConfiguration( + afterName = "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration") +@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, ObservationRegistry.class, + ObservationGrpcServerInterceptor.class }) +@ConditionalOnBean(ObservationRegistry.class) +@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true) +@ConditionalOnBooleanProperty(name = "spring.grpc.server.observation.enabled", matchIfMissing = true) +public final class GrpcServerObservationAutoConfiguration { + + @Bean + @Order(0) + @GlobalServerInterceptor + ObservationGrpcServerInterceptor grpcServerObservationInterceptor(ObservationRegistry observationRegistry) { + return new ObservationGrpcServerInterceptor(observationRegistry); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = { "io.grpc.kotlin.AbstractCoroutineStub", "io.micrometer.context.ContextRegistry" }) + static class GrpcServerCoroutineStubConfiguration { + + @Bean + @Order(10) + @GlobalServerInterceptor + ObservationCoroutineContextServerInterceptor grpcServerObservationCoroutineContextServerInterceptor( + ObservationRegistry observationRegistry) { + return new ObservationCoroutineContextServerInterceptor(observationRegistry); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 12640064b221..a49d405d6c24 100644 --- a/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -13,6 +13,12 @@ "description": "Whether to enable gRPC server factory bean auto-configuration.", "defaultValue": true }, + { + "name": "spring.grpc.server.observation.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Observations on the server.", + "defaultValue": true + }, { "name": "spring.grpc.server.reflection.enabled", "type": "java.lang.Boolean", diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index c060e23ab8dc..00f402214f4e 100644 --- a/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,4 +1,5 @@ org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration +org.springframework.boot.grpc.server.autoconfigure.GrpcServerObservationAutoConfiguration org.springframework.boot.grpc.server.autoconfigure.GrpcServerServicesAutoConfiguration org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration -org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthSchedulerAutoConfiguration \ No newline at end of file +org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthSchedulerAutoConfiguration diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java new file mode 100644 index 000000000000..7aee14b5a0d2 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure; + +import java.util.List; +import java.util.Map; + +import io.grpc.BindableService; +import io.grpc.ServerInterceptor; +import io.micrometer.context.ContextRegistry; +import io.micrometer.core.instrument.binder.grpc.ObservationGrpcServerInterceptor; +import io.micrometer.core.instrument.kotlin.ObservationCoroutineContextServerInterceptor; +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.GrpcServerFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for the {@link GrpcServerObservationAutoConfiguration}. + * + * @author Chris Bono + */ +class GrpcServerObservationAutoConfigurationTests { + + private static final AutoConfigurations autoConfigurations = AutoConfigurations + .of(GrpcServerObservationAutoConfiguration.class); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(autoConfigurations) + .withBean("observationRegistry", ObservationRegistry.class, Mockito::mock); + + @Test + void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(BindableService.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenObservationRegistryNotOnClasspathAutoConfigurationSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(ObservationRegistry.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenObservationGrpcServerInterceptorNotOnClasspathAutoConfigurationSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(ObservationGrpcServerInterceptor.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenObservationRegistryNotProvidedThenAutoConfigurationSkipped() { + new ApplicationContextRunner().withConfiguration(autoConfigurations) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenObservationPropertyEnabledThenAutoConfigurationNotSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.server.observation.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenObservationPropertyDisabledThenAutoConfigurationIsSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.server.observation.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetFalseThenAutoConfigurationIsSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.server.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() { + this.contextRunner + .run((context) -> assertThat(context).hasSingleBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.server.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenAllConditionsAreMetThenInterceptorConfiguredAsExpected() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(ObservationGrpcServerInterceptor.class); + Map annotated = context.getBeansWithAnnotation(GlobalServerInterceptor.class); + List interceptors = context.getBeanProvider(ServerInterceptor.class) + .orderedStream() + .toList(); + assertThat(annotated).hasSize(2); + assertThat(interceptors.get(0)).isInstanceOf(ObservationGrpcServerInterceptor.class); + }); + } + + @Test + void whenMicrometerContextPropagationIsNotOnClasspathCoroutineInterceptorIsNotCreated() { + this.contextRunner.withClassLoader(new FilteredClassLoader(ContextRegistry.class)) + .run((context) -> assertThat(context).hasSingleBean(ObservationGrpcServerInterceptor.class) + .doesNotHaveBean(ObservationCoroutineContextServerInterceptor.class)); + } + +} From 1cb8d02ed7d61308ec88925de9701474662aa2fa Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Sun, 15 Mar 2026 20:05:29 -0700 Subject: [PATCH 390/930] Add Spring gRPC server and client security support Add auto-configuration to integrate gRPC server applications with Spring Security. This commit provides both standard Spring Security support as well as OAuth support. Closes gh-49047 Co-authored-by: Phillip Webb --- module/spring-boot-grpc-server/build.gradle | 2 + .../GrpcDisableCsrfHttpConfigurer.java | 86 +++++++ ...OAuth2ResourceServerAutoConfiguration.java | 76 ++++++ .../GrpcServerSecurityAutoConfiguration.java | 127 ++++++++++ .../autoconfigure/security/package-info.java | 23 ++ .../security/web/reactive/GrpcRequest.java | 148 ++++++++++++ .../security/web/reactive/package-info.java | 23 ++ .../security/web/servlet/GrpcRequest.java | 148 ++++++++++++ .../security/web/servlet/package-info.java | 23 ++ ...ot.autoconfigure.AutoConfiguration.imports | 2 + .../GrpcDisableCsrfHttpConfigurerTests.java | 167 +++++++++++++ ...2ResourceServerAutoConfigurationTests.java | 170 ++++++++++++++ ...cServerSecurityAutoConfigurationTests.java | 152 ++++++++++++ .../web/reactive/GrpcRequestTests.java | 114 +++++++++ .../web/servlet/GrpcRequestTests.java | 93 ++++++++ settings.gradle | 2 + .../build.gradle | 68 ++++++ .../grpcserveroauth/HelloWorldService.java | 60 +++++ .../SampleGrpcServerOAuthApplication.java | 29 +++ .../SecurityConfiguration.java | 61 +++++ .../grpcserveroauth/package-info.java | 20 ++ .../src/main/proto/hello.proto | 18 ++ .../src/main/resources/application.yaml | 21 ++ ...SampleGrpcServerOAuthApplicationTests.java | 220 ++++++++++++++++++ .../build.gradle | 66 ++++++ .../grpcserversecure/HelloWorldService.java | 68 ++++++ .../SampleGrpcServerSecureApplication.java | 29 +++ .../SecurityConfiguration.java | 64 +++++ .../grpcserversecure/package-info.java | 20 ++ .../src/main/proto/hello.proto | 19 ++ ...ampleGrpcServerSecureApplicationTests.java | 192 +++++++++++++++ 31 files changed, 2311 insertions(+) create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequest.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/package-info.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequest.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/package-info.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequestTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequestTests.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/HelloWorldService.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SecurityConfiguration.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/proto/hello.proto create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/resources/application.yaml create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/test/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplicationTests.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/HelloWorldService.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SecurityConfiguration.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/proto/hello.proto create mode 100644 smoke-test/spring-boot-smoke-test-grpc-server-secure/src/test/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplicationTests.java diff --git a/module/spring-boot-grpc-server/build.gradle b/module/spring-boot-grpc-server/build.gradle index 581e145e6406..28f6200c30ee 100644 --- a/module/spring-boot-grpc-server/build.gradle +++ b/module/spring-boot-grpc-server/build.gradle @@ -31,6 +31,8 @@ dependencies { optional(project(":core:spring-boot-autoconfigure")) optional(project(":module:spring-boot-health")) optional(project(":module:spring-boot-micrometer-observation")) + optional(project(":module:spring-boot-security")) + optional(project(":module:spring-boot-security-oauth2-resource-server")) optional("com.fasterxml.jackson.core:jackson-annotations") optional("io.projectreactor:reactor-core") optional("io.grpc:grpc-servlet-jakarta") diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java new file mode 100644 index 000000000000..fa407365734f --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.security; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.boot.grpc.server.GrpcServletRegistration; +import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest; +import org.springframework.context.ApplicationContext; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.web.csrf.CsrfFilter; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.ObjectUtils; + +/** + * A custom {@link AbstractHttpConfigurer} that disables CSRF protection for gRPC + * requests. + *

    + * This configurer checks the application context to determine if CSRF protection should + * be disabled for gRPC requests based on the property + * {@code spring.grpc.server.security.csrf.enabled}. By default, CSRF protection is + * disabled unless explicitly enabled in the application properties. + *

    + * + * @author Dave Syer + * @see AbstractHttpConfigurer + * @see HttpSecurity + */ +class GrpcDisableCsrfHttpConfigurer extends AbstractHttpConfigurer { + + @Override + public void init(HttpSecurity http) { + ApplicationContext context = http.getSharedObject(ApplicationContext.class); + if (context != null && isCsrfConfigurerPresent(http) && hasBean(context, GrpcServiceDiscoverer.class) + && hasBean(context, GrpcServletRegistration.class) && isCsrfEnabled(context)) { + http.csrf(this::disable); + } + } + + @SuppressWarnings("unchecked") + private boolean isCsrfConfigurerPresent(HttpSecurity http) { + return http.getConfigurer(CsrfConfigurer.class) != null; + } + + private boolean hasBean(ApplicationContext context, Class type) { + return !ObjectUtils.isEmpty(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type)); + } + + private boolean isCsrfEnabled(ApplicationContext context) { + return context.getEnvironment().getProperty("spring.grpc.server.security.csrf.enabled", Boolean.class, true); + } + + private void disable(CsrfConfigurer csrf) { + csrf.requireCsrfProtectionMatcher(GrpcCsrfRequestMatcher.INSTANCE); + } + + static class GrpcCsrfRequestMatcher implements RequestMatcher { + + static GrpcCsrfRequestMatcher INSTANCE = new GrpcCsrfRequestMatcher(); + + @Override + public boolean matches(HttpServletRequest request) { + return CsrfFilter.DEFAULT_CSRF_MATCHER.matches(request) && !GrpcRequest.toAnyService().matches(request); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfiguration.java new file mode 100644 index 000000000000..263506708851 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfiguration.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.security; + +import io.grpc.BindableService; + +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.ConditionalOnBooleanProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; + +import static org.springframework.security.config.Customizer.withDefaults; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC OAuth2 resource server. + * + * @author Dave Syer + * @author Andrey Litvitski + * @author Phillip Webb + * @since 4.1.0 + */ +@AutoConfiguration(beforeName = "org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration", + afterName = { "org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration", + "org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration" }, + after = { GrpcServerSecurityAutoConfiguration.class, GrpcServerAutoConfiguration.class }) +@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true) +@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, ObjectPostProcessor.class }) +@ConditionalOnMissingBean(AuthenticationProcessInterceptor.class) +@ConditionalOnBean(GrpcSecurity.class) +public final class GrpcServerOAuth2ResourceServerAutoConfiguration { + + @Bean + @ConditionalOnBean(OpaqueTokenIntrospector.class) + @GlobalServerInterceptor + AuthenticationProcessInterceptor opaqueTokenAuthenticationProcessInterceptor(GrpcSecurity grpcSecurity) + throws Exception { + grpcSecurity.authorizeRequests((requests) -> requests.allRequests().authenticated()); + grpcSecurity.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults())); + return grpcSecurity.build(); + } + + @Bean + @ConditionalOnBean(JwtDecoder.class) + @GlobalServerInterceptor + AuthenticationProcessInterceptor jwtAuthenticationProcessInterceptor(GrpcSecurity grpcSecurity) throws Exception { + grpcSecurity.authorizeRequests((requests) -> requests.allRequests().authenticated()); + grpcSecurity.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults())); + return grpcSecurity.build(); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfiguration.java new file mode 100644 index 000000000000..07ef0e7afc0b --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfiguration.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.security; + +import io.grpc.BindableService; +import io.grpc.internal.GrpcUtil; + +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.ConditionalOnBooleanProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.grpc.server.GrpcServletRegistration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerExecutorProvider; +import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.ExceptionHandlerConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.GrpcNativeSecurityConfigurerConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.GrpcServletSecurityConfigurerConfiguration; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.security.CoroutineSecurityContextInterceptor; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.grpc.server.security.SecurityContextServerInterceptor; +import org.springframework.grpc.server.security.SecurityGrpcExceptionHandler; +import org.springframework.security.concurrent.DelegatingSecurityContextExecutor; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; +import org.springframework.security.web.SecurityFilterChain; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side security. + * + * @author Dave Syer + * @author Chris Bono + * @author Andrey Litvitski + * @author Phillip Webb + * @since 4.1.0 + */ +@AutoConfiguration(after = GrpcServerAutoConfiguration.class, + afterName = "org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration") +@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true) +@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, ObjectPostProcessor.class }) +@Import({ ExceptionHandlerConfiguration.class, GrpcNativeSecurityConfigurerConfiguration.class, + GrpcServletSecurityConfigurerConfiguration.class }) +public final class GrpcServerSecurityAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + @Import(AuthenticationConfiguration.class) + static class ExceptionHandlerConfiguration { + + @Bean + SecurityGrpcExceptionHandler accessExceptionHandler() { + return new SecurityGrpcExceptionHandler(); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(GrpcServerFactory.class) + @EnableGlobalAuthentication + static class GrpcNativeSecurityConfigurerConfiguration { + + @Bean + GrpcSecurity grpcSecurity(ApplicationContext context, ObjectPostProcessor objectPostProcessor, + AuthenticationConfiguration authenticationConfiguration) { + AuthenticationManagerBuilder authenticationManagerBuilder = authenticationConfiguration + .authenticationManagerBuilder(objectPostProcessor, context); + authenticationManagerBuilder + .parentAuthenticationManager(authenticationConfiguration.getAuthenticationManager()); + return new GrpcSecurity(objectPostProcessor, authenticationManagerBuilder, context); + } + + } + + @ConditionalOnBean({ GrpcServletRegistration.class, SecurityFilterChain.class }) + @Configuration(proxyBeanMethods = false) + static class GrpcServletSecurityConfigurerConfiguration { + + @Bean + @GlobalServerInterceptor + SecurityContextServerInterceptor securityContextInterceptor() { + return new SecurityContextServerInterceptor(); + } + + @Bean + @ConditionalOnMissingBean + GrpcServerExecutorProvider grpcServerExecutorProvider() { + return () -> new DelegatingSecurityContextExecutor(GrpcUtil.SHARED_CHANNEL_EXECUTOR.create()); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = "io.grpc.kotlin.CoroutineContextServerInterceptor") + static class GrpcClientCoroutineStubConfiguration { + + @Bean + @GlobalServerInterceptor + @ConditionalOnMissingBean + CoroutineSecurityContextInterceptor coroutineSecurityContextInterceptor() { + return new CoroutineSecurityContextInterceptor(); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java new file mode 100644 index 000000000000..264c92d6e9a6 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 gRPC server security. + */ +@NullMarked +package org.springframework.boot.grpc.server.autoconfigure.security; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequest.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequest.java new file mode 100644 index 000000000000..c298429d7677 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.security.web.reactive; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; +import reactor.core.publisher.Mono; + +import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest.GrpcServletRequestMatcher; +import org.springframework.boot.security.web.reactive.ApplicationContextServerWebExchangeMatcher; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.web.server.ServerWebExchange; + +/** + * Factory that can be used to create a {@link ServerWebExchangeMatcher} to match against + * gRPC service locations. + * + * @author Dave Syer + * @author Phillip Webb + * @since 4.1.0 + */ +public final class GrpcRequest { + + private GrpcRequest() { + } + + /** + * Returns a matcher that includes all gRPC services. The + * {@link GrpcReactiveRequestMatcher#excluding(String...) excluding} method can be + * used to remove specific services by name if required. For example: + * + *
    +	 * GrpcReactiveRequest.toAnyService().excluding("my-service")
    +	 * 
    + * @return the configured {@link ServerWebExchangeMatcher} + */ + public static GrpcReactiveRequestMatcher toAnyService() { + return new GrpcReactiveRequestMatcher(Collections.emptySet()); + } + + /** + * The matcher used to match against service locations. + */ + public static final class GrpcReactiveRequestMatcher + extends ApplicationContextServerWebExchangeMatcher { + + private static final ServerWebExchangeMatcher EMPTY_MATCHER = (exchange) -> MatchResult.notMatch(); + + private final Set excludes; + + private volatile @Nullable ServerWebExchangeMatcher delegate; + + private GrpcReactiveRequestMatcher(Set excludes) { + super(GrpcServiceDiscoverer.class); + this.excludes = excludes; + } + + /** + * Return a new {@link GrpcServletRequestMatcher} based on this one but excluding + * the specified services. + * @param services additional services to exclude + * @return a new {@link GrpcServletRequestMatcher} + */ + public GrpcReactiveRequestMatcher excluding(String... services) { + return excluding(Set.of(services)); + } + + /** + * Return a new {@link GrpcServletRequestMatcher} based on this one but excluding + * the specified services. + * @param services additional service names to exclude + * @return a new {@link GrpcServletRequestMatcher} + */ + public GrpcReactiveRequestMatcher excluding(Collection services) { + Assert.notNull(services, "'services' must not be null"); + Set excludes = new LinkedHashSet<>(this.excludes); + excludes.addAll(services); + return new GrpcReactiveRequestMatcher(excludes); + } + + @Override + protected void initialized(Supplier context) { + this.delegate = createDelegate(context.get()); + + } + + private ServerWebExchangeMatcher createDelegate(GrpcServiceDiscoverer serviceDiscoverer) { + List delegateMatchers = getDelegateMatchers(serviceDiscoverer); + return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrServerWebExchangeMatcher(delegateMatchers) + : EMPTY_MATCHER; + } + + private List getDelegateMatchers(GrpcServiceDiscoverer serviceDiscoverer) { + return getPatterns(serviceDiscoverer).map(this::getDelegateMatcher).toList(); + } + + private Stream getPatterns(GrpcServiceDiscoverer serviceDiscoverer) { + return serviceDiscoverer.listServiceNames().stream().filter(this::isExcluded).map(this::getPath); + } + + private boolean isExcluded(String service) { + return !this.excludes.stream().anyMatch((candidate) -> candidate.equals(service)); + } + + private String getPath(String service) { + return "/" + service + "/**"; + } + + private ServerWebExchangeMatcher getDelegateMatcher(String path) { + Assert.hasText(path, "'path' must not be empty"); + return new PathPatternParserServerWebExchangeMatcher(path); + } + + @Override + protected Mono matches(ServerWebExchange exchange, Supplier context) { + Assert.state(this.delegate != null, "'delegate' must not be null"); + return this.delegate.matches(exchange); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/package-info.java new file mode 100644 index 000000000000..51f413b9a251 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 gRPC web security when using a servlet stack. + */ +@NullMarked +package org.springframework.boot.grpc.server.autoconfigure.security.web.reactive; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequest.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequest.java new file mode 100644 index 000000000000..f2eb644214fb --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.security.web.servlet; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.security.web.servlet.ApplicationContextRequestMatcher; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Factory that can be used to create a {@link RequestMatcher} to match against gRPC + * service locations. + * + * @author Dave Syer + * @author Phillip Webb + * @since 4.1.0 + */ +public final class GrpcRequest { + + private static final RequestMatcher EMPTY_MATCHER = (request) -> false; + + private static final GrpcServletRequestMatcher TO_ANY_SERVICE = new GrpcServletRequestMatcher( + Collections.emptySet()); + + private GrpcRequest() { + } + + /** + * Returns a matcher that includes all gRPC services. The + * {@link GrpcServletRequestMatcher#excluding(String...) excluding} method can be used + * to remove specific services by name if required. For example: + * + *
    +	 * GrpcServletRequest.toAnyService().excluding("my-service")
    +	 * 
    + * @return the configured {@link RequestMatcher} + */ + public static GrpcServletRequestMatcher toAnyService() { + return TO_ANY_SERVICE; + } + + /** + * The matcher used to match against service locations. + */ + public static final class GrpcServletRequestMatcher + extends ApplicationContextRequestMatcher { + + private final Set excludes; + + private volatile @Nullable RequestMatcher delegate; + + private GrpcServletRequestMatcher(Set exclusions) { + super(GrpcServiceDiscoverer.class); + this.excludes = exclusions; + } + + /** + * Return a new {@link GrpcServletRequestMatcher} based on this one but excluding + * the specified services. + * @param services additional services to exclude + * @return a new {@link GrpcServletRequestMatcher} + */ + public GrpcServletRequestMatcher excluding(String... services) { + return excluding(Set.of(services)); + } + + /** + * Return a new {@link GrpcServletRequestMatcher} based on this one but excluding + * the specified services. + * @param services additional service names to exclude + * @return a new {@link GrpcServletRequestMatcher} + */ + public GrpcServletRequestMatcher excluding(Collection services) { + Assert.notNull(services, "'services' must not be null"); + Set excludes = new LinkedHashSet<>(this.excludes); + excludes.addAll(services); + return new GrpcServletRequestMatcher(excludes); + } + + @Override + protected void initialized(Supplier context) { + this.delegate = createDelegate(context.get()); + } + + private @Nullable RequestMatcher createDelegate(GrpcServiceDiscoverer grpcServiceDiscoverer) { + List delegateMatchers = getDelegateMatchers(grpcServiceDiscoverer); + return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrRequestMatcher(delegateMatchers) + : EMPTY_MATCHER; + } + + private List getDelegateMatchers(GrpcServiceDiscoverer serviceDiscoverer) { + return getPatterns(serviceDiscoverer).map(this::getDelegateMatcher).toList(); + } + + private Stream getPatterns(GrpcServiceDiscoverer serviceDiscoverer) { + return serviceDiscoverer.listServiceNames().stream().filter(this::isExcluded).map(this::getPath); + } + + private boolean isExcluded(String service) { + return !this.excludes.stream().anyMatch((candidate) -> candidate.equals(service)); + } + + private String getPath(String service) { + return "/" + service + "/**"; + } + + private RequestMatcher getDelegateMatcher(String path) { + Assert.hasText(path, "'path' must not be empty"); + return PathPatternRequestMatcher.withDefaults().matcher(path); + } + + @Override + protected boolean matches(HttpServletRequest request, Supplier context) { + Assert.state(this.delegate != null, "'delegate' must not be null"); + return this.delegate.matches(request); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/package-info.java new file mode 100644 index 000000000000..441fff7c423a --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 gRPC web security when using a reactive stack. + */ +@NullMarked +package org.springframework.boot.grpc.server.autoconfigure.security.web.servlet; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 00f402214f4e..1b4dd8385233 100644 --- a/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -3,3 +3,5 @@ org.springframework.boot.grpc.server.autoconfigure.GrpcServerObservationAutoConf org.springframework.boot.grpc.server.autoconfigure.GrpcServerServicesAutoConfiguration org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthSchedulerAutoConfiguration +org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerOAuth2ResourceServerAutoConfiguration +org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java new file mode 100644 index 000000000000..418b257a7658 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.security; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.grpc.server.GrpcServletRegistration; +import org.springframework.boot.grpc.server.autoconfigure.security.GrpcDisableCsrfHttpConfigurer.GrpcCsrfRequestMatcher; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.web.util.matcher.RequestMatcher; + +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; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link GrpcDisableCsrfHttpConfigurer}. + * + * @author Phillip Webb + */ +class GrpcDisableCsrfHttpConfigurerTests { + + private GrpcDisableCsrfHttpConfigurer configurer = new GrpcDisableCsrfHttpConfigurer(); + + @Test + void initDisablesCsrf() { + ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity(); + AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); + HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); + StaticApplicationContext applicationContext = addApplicationContext(http); + addServiceDiscover(applicationContext); + addGrpcServletRegistration(applicationContext); + CsrfConfigurer csrf = addCsrf(http); + this.configurer.init(http); + ArgumentCaptor matcher = ArgumentCaptor.captor(); + then(csrf).should().requireCsrfProtectionMatcher(matcher.capture()); + assertThat(matcher.getValue()).isSameAs(GrpcCsrfRequestMatcher.INSTANCE); + } + + @Test + void initWhenNoApplicationContextDoesNothing() { + ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity(); + AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); + HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); + CsrfConfigurer csrf = addCsrf(http); + this.configurer.init(http); + then(csrf).should(never()).requireCsrfProtectionMatcher(any()); + } + + @Test + @SuppressWarnings("unchecked") + void initWhenNoCsrfConfigurerDoesNothing() { + ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity(); + AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); + HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); + StaticApplicationContext applicationContext = addApplicationContext(http); + addServiceDiscover(applicationContext); + addGrpcServletRegistration(applicationContext); + this.configurer.init(http); + CsrfConfigurer csrfConfigurer = http.getConfigurer(CsrfConfigurer.class); + assertThat(csrfConfigurer).isNull(); + } + + @Test + void initWhenNoGrpcServiceDiscovererBeanDoesNothing() { + ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity(); + AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); + HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); + StaticApplicationContext applicationContext = addApplicationContext(http); + addGrpcServletRegistration(applicationContext); + CsrfConfigurer csrf = addCsrf(http); + this.configurer.init(http); + then(csrf).should(never()).requireCsrfProtectionMatcher(any()); + } + + @Test + void initWhenNoGrpcServletRegistrationBeanDoesNothing() { + ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity(); + AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); + HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); + StaticApplicationContext applicationContext = addApplicationContext(http); + addServiceDiscover(applicationContext); + CsrfConfigurer csrf = addCsrf(http); + this.configurer.init(http); + then(csrf).should(never()).requireCsrfProtectionMatcher(any()); + } + + @Test + void initWhenEnabledPropertyFalseDoesNothing() { + ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity(); + AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); + HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); + StaticApplicationContext applicationContext = addApplicationContext(http); + TestPropertyValues.of("spring.grpc.server.security.csrf.enabled=false").applyTo(applicationContext); + addServiceDiscover(applicationContext); + addGrpcServletRegistration(applicationContext); + CsrfConfigurer csrf = addCsrf(http); + this.configurer.init(http); + then(csrf).should(never()).requireCsrfProtectionMatcher(any()); + } + + @Test + void initWhenEnabledPropertyTrueDisablesCsrf() { + ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity(); + AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); + HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); + StaticApplicationContext applicationContext = addApplicationContext(http); + TestPropertyValues.of("spring.grpc.server.security.csrf.enabled=true").applyTo(applicationContext); + addServiceDiscover(applicationContext); + addGrpcServletRegistration(applicationContext); + CsrfConfigurer csrf = addCsrf(http); + this.configurer.init(http); + ArgumentCaptor matcher = ArgumentCaptor.captor(); + then(csrf).should().requireCsrfProtectionMatcher(matcher.capture()); + assertThat(matcher.getValue()).isSameAs(GrpcCsrfRequestMatcher.INSTANCE); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private CsrfConfigurer addCsrf(HttpSecurity http) { + CsrfConfigurer csrf = mock(); + http.with((CsrfConfigurer) csrf); + return csrf; + } + + private StaticApplicationContext addApplicationContext(HttpSecurity http) { + StaticApplicationContext applicationContext = new StaticApplicationContext(); + http.setSharedObject(ApplicationContext.class, applicationContext); + return applicationContext; + } + + private void addServiceDiscover(StaticApplicationContext applicationContext) { + GrpcServiceDiscoverer serviceDiscoverer = mock(); + applicationContext.registerBean(GrpcServiceDiscoverer.class, serviceDiscoverer); + } + + private void addGrpcServletRegistration(StaticApplicationContext applicationContext) { + GrpcServletRegistration servletRegistration = mock(); + applicationContext.registerBean(GrpcServletRegistration.class, servletRegistration); + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java new file mode 100644 index 000000000000..f20be2bf4ed7 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java @@ -0,0 +1,170 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.security; + +import io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; +import org.springframework.boot.context.annotation.UserConfigurations; +import org.springframework.boot.context.event.ApplicationFailedEvent; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider; +import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.context.servlet.AnnotationConfigServletWebApplicationContext; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link GrpcServerOAuth2ResourceServerAutoConfiguration}. + * + * @author Chris Bono + */ +class GrpcServerOAuth2ResourceServerAutoConfigurationTests { + + private static final AutoConfigurations autoConfigurations = AutoConfigurations + .of(OAuth2ResourceServerAutoConfiguration.class, GrpcServerOAuth2ResourceServerAutoConfiguration.class); + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(autoConfigurations) + .withUserConfiguration(GrpcSecurityConfiguration.class) + .with(this::serviceBean) + .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock); + + @Test + void jwtConfiguredWhenIssuerIsProvided() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000") + .run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class)); + } + + @Test + void jwtConfiguredWhenJwkSetIsProvided() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000") + .run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class)); + } + + @Test + void customInterceptorWhenJwkSetIsProvided() { + this.contextRunner.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .withConfiguration(UserConfigurations.of(CustomInterceptorConfiguration.class)) + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000") + .run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class)); + } + + @Test + void notConfiguredWhenIssuerNotProvided() { + this.contextRunner + .run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class)); + } + + @Test + void notConfiguredInWebApplication() { + new WebApplicationContextRunner().withConfiguration(autoConfigurations) + .withConfiguration(AutoConfigurations.of(ServletWebSecurityAutoConfiguration.class, + OAuth2ResourceServerAutoConfiguration.class)) + .with(this::serviceBean) + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000") + .run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class)); + } + + @Test + void notConfiguredInWebApplicationWithNoBindableService() { + new WebApplicationContextRunner().withConfiguration(autoConfigurations) + .withConfiguration(AutoConfigurations.of(ServletWebSecurityAutoConfiguration.class, + OAuth2ResourceServerAutoConfiguration.class)) + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000") + .run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class)); + } + + private , C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> R serviceBean( + R contextRunner) { + BindableService service = mock(); + ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); + given(service.bindService()).willReturn(serviceDefinition); + return contextRunner.withBean(BindableService.class, () -> service); + } + + static class FailingApplicationFailedEventContext extends AnnotationConfigServletWebApplicationContext { + + @Override + public void refresh() { + try { + super.refresh(); + } + catch (Throwable ex) { + publishEvent(new ApplicationFailedEvent(new SpringApplication(this), new String[0], this, ex)); + throw ex; + } + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomInterceptorConfiguration { + + @Bean + @GlobalServerInterceptor + AuthenticationProcessInterceptor jwtSecurityFilterChain(GrpcSecurity grpc) throws Exception { + return grpc.authorizeRequests((requests) -> requests.allRequests().authenticated()) + .oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults())) + .build(); + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableWebSecurity + static class GrpcSecurityConfiguration { + + @Bean + GrpcSecurity grpcSecurity(ApplicationContext context, ObjectPostProcessor objectPostProcessor, + AuthenticationConfiguration authenticationConfiguration) { + AuthenticationManagerBuilder authenticationManagerBuilder = authenticationConfiguration + .authenticationManagerBuilder(objectPostProcessor, context); + authenticationManagerBuilder + .parentAuthenticationManager(authenticationConfiguration.getAuthenticationManager()); + return new GrpcSecurity(objectPostProcessor, authenticationManagerBuilder, context); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfigurationTests.java new file mode 100644 index 000000000000..f3085d633a2c --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfigurationTests.java @@ -0,0 +1,152 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.security; + +import io.grpc.BindableService; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; +import org.springframework.boot.grpc.server.GrpcServletRegistration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerExecutorProvider; +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.context.runner.WebApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.exception.GrpcExceptionHandler; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.grpc.server.security.SecurityContextServerInterceptor; +import org.springframework.grpc.server.security.SecurityGrpcExceptionHandler; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link GrpcServerSecurityAutoConfiguration}. + * + * @author Chris Bono + */ +class GrpcServerSecurityAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcServerSecurityAutoConfiguration.class)) + .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock); + + @Test + void whenSpringSecurityNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(ObjectPostProcessor.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class)); + } + + @Test + void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(BindableService.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class)); + } + + @Test + void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class)); + } + + @Test + void whenSpringGrpcAndSpringSecurityPresentAndUsingGrpcServletCreatesGrpcSecurity() { + new WebApplicationContextRunner() + .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .withConfiguration(AutoConfigurations.of(GrpcServerSecurityAutoConfiguration.class)) + .withUserConfiguration(ServletConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(SecurityContextServerInterceptor.class); + assertThat(context).hasSingleBean(GrpcServerExecutorProvider.class); + }); + } + + @Test + void whenSpringGrpcAndSpringSecurityPresentAndUsingGrpcNativeCreatesGrpcSecurity() { + new ApplicationContextRunner() + .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .withConfiguration(AutoConfigurations.of(GrpcServerSecurityAutoConfiguration.class)) + .withUserConfiguration(NativeConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(GrpcSecurity.class)); + } + + @Test + void whenServerEnabledPropertySetFalseThenAutoConfigurationIsSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.server.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() { + this.contextRunner + .run((context) -> assertThat(context).hasSingleBean(GrpcServerSecurityAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() { + this.contextRunner.withPropertyValues("spring.grpc.server.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcServerSecurityAutoConfiguration.class)); + } + + @Test + void grpcSecurityAutoConfiguredAsExpected() { + this.contextRunner.run((context) -> { + assertThat(context).getBean(GrpcExceptionHandler.class).isInstanceOf(SecurityGrpcExceptionHandler.class); + assertThat(context).getBean(AuthenticationProcessInterceptor.class).isNull(); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableWebSecurity + static class ServletConfiguration { + + @Bean + GrpcServletRegistration grpcServletRegistration() { + return new GrpcServletRegistration(mock(), mock()); + } + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) { + return http.authorizeHttpRequests((requests) -> requests.anyRequest().permitAll()).build(); + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableMethodSecurity + static class NativeConfiguration { + + @Bean + GrpcServerFactory grpcServerFactory() { + return mock(); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequestTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequestTests.java new file mode 100644 index 000000000000..860e086745c3 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequestTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.security.web.reactive; + +import io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.grpc.server.autoconfigure.security.web.reactive.GrpcRequest.GrpcReactiveRequestMatcher; +import org.springframework.boot.web.context.reactive.GenericReactiveWebApplicationContext; +import org.springframework.context.ApplicationContext; +import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.MockServerHttpResponse; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; +import org.springframework.web.server.session.DefaultWebSessionManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link GrpcRequest}. + * + * @author Dave Syer + * @author Phillip Webb + */ +class GrpcRequestTests { + + private GenericReactiveWebApplicationContext context = new GenericReactiveWebApplicationContext(); + + @BeforeEach + void setup() { + MockService service1 = mock(); + given(service1.bindService()).willReturn(ServerServiceDefinition.builder("my-service").build()); + MockService service2 = mock(); + given(service2.bindService()).willReturn(ServerServiceDefinition.builder("my-other-service").build()); + this.context.registerBean("s1", BindableService.class, () -> service1); + this.context.registerBean("s2", BindableService.class, () -> service2); + this.context.registerBean(GrpcServiceDiscoverer.class, () -> new DefaultGrpcServiceDiscoverer(this.context)); + this.context.refresh(); + } + + @Test + void whenToAnyService() { + GrpcReactiveRequestMatcher matcher = GrpcRequest.toAnyService(); + assertThat(isMatch(matcher, "/my-service/Method")).isTrue(); + assertThat(isMatch(matcher, "/my-service/Other")).isTrue(); + assertThat(isMatch(matcher, "/my-other-service/Other")).isTrue(); + assertThat(isMatch(matcher, "/notaservice")).isFalse(); + } + + @Test + void whenToAnyServiceWithExclude() { + GrpcReactiveRequestMatcher matcher = GrpcRequest.toAnyService().excluding("my-other-service"); + assertThat(isMatch(matcher, "/my-service/Method")).isTrue(); + assertThat(isMatch(matcher, "/my-service/Other")).isTrue(); + assertThat(isMatch(matcher, "/my-other-service/Other")).isFalse(); + assertThat(isMatch(matcher, "/notaservice")).isFalse(); + } + + private boolean isMatch(GrpcReactiveRequestMatcher matcher, String path) { + MockExchange request = mockRequest(path); + MatchResult result = matcher.matches(request).block(); + return (result != null) && result.isMatch(); + } + + private MockExchange mockRequest(String path) { + MockServerHttpRequest servletContext = MockServerHttpRequest.get(path).build(); + MockExchange request = new MockExchange(servletContext, this.context); + return request; + } + + interface MockService extends BindableService { + + } + + static class MockExchange extends DefaultServerWebExchange { + + private ApplicationContext context; + + MockExchange(MockServerHttpRequest request, ApplicationContext context) { + super(request, new MockServerHttpResponse(), new DefaultWebSessionManager(), ServerCodecConfigurer.create(), + new AcceptHeaderLocaleContextResolver()); + this.context = context; + } + + @Override + public ApplicationContext getApplicationContext() { + return this.context; + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequestTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequestTests.java new file mode 100644 index 000000000000..87763de2a49f --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequestTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.server.autoconfigure.security.web.servlet; + +import io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest.GrpcServletRequestMatcher; +import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.GenericWebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Test for {@link GrpcRequest}. + * + * @author Dave Syer + * @author Phillip Webb + */ +class GrpcRequestTests { + + private GenericWebApplicationContext context = new GenericWebApplicationContext(); + + @BeforeEach + void setup() { + MockService service1 = mock(); + given(service1.bindService()).willReturn(ServerServiceDefinition.builder("my-service").build()); + MockService service2 = mock(); + given(service2.bindService()).willReturn(ServerServiceDefinition.builder("my-other-service").build()); + this.context.registerBean("s1", BindableService.class, () -> service1); + this.context.registerBean("s2", BindableService.class, () -> service2); + this.context.registerBean(GrpcServiceDiscoverer.class, () -> new DefaultGrpcServiceDiscoverer(this.context)); + this.context.refresh(); + } + + @Test + void whenToAnyService() { + GrpcServletRequestMatcher matcher = GrpcRequest.toAnyService(); + assertThat(isMatch(matcher, "/my-service/Method")).isTrue(); + assertThat(isMatch(matcher, "/my-service/Other")).isTrue(); + assertThat(isMatch(matcher, "/my-other-service/Other")).isTrue(); + assertThat(isMatch(matcher, "/notaservice")).isFalse(); + } + + @Test + void whenToAnyServiceWithExclude() { + GrpcServletRequestMatcher matcher = GrpcRequest.toAnyService().excluding("my-other-service"); + assertThat(isMatch(matcher, "/my-service/Method")).isTrue(); + assertThat(isMatch(matcher, "/my-service/Other")).isTrue(); + assertThat(isMatch(matcher, "/my-other-service/Other")).isFalse(); + assertThat(isMatch(matcher, "/notaservice")).isFalse(); + } + + private boolean isMatch(GrpcServletRequestMatcher matcher, String path) { + MockHttpServletRequest request = mockRequest(path); + return matcher.matches(request); + } + + private MockHttpServletRequest mockRequest(String path) { + MockServletContext servletContext = new MockServletContext(); + servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); + MockHttpServletRequest request = new MockHttpServletRequest(servletContext); + request.setRequestURI(path); + return request; + } + + interface MockService extends BindableService { + + } + +} diff --git a/settings.gradle b/settings.gradle index 28dffcc3a6a0..94d968336d5a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -431,7 +431,9 @@ include ":smoke-test:spring-boot-smoke-test-grpc-client" include ":smoke-test:spring-boot-smoke-test-grpc-client-test" include ":smoke-test:spring-boot-smoke-test-grpc-server" include ":smoke-test:spring-boot-smoke-test-grpc-server-netty-shaded" +include ":smoke-test:spring-boot-smoke-test-grpc-server-oauth" include ":smoke-test:spring-boot-smoke-test-grpc-server-servlet" +include ":smoke-test:spring-boot-smoke-test-grpc-server-secure" include ":smoke-test:spring-boot-smoke-test-grpc-server-test" include ":smoke-test:spring-boot-smoke-test-hateoas" include ":smoke-test:spring-boot-smoke-test-hibernate" diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle new file mode 100644 index 000000000000..758cc967819d --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle @@ -0,0 +1,68 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java" + id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "org.springframework.boot.docker-test" +} + +description = "Spring Boot gRPC server oauth smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-oauth2-authorization-server")) + implementation(project(":starter:spring-boot-starter-oauth2-resource-server")) + implementation(project(":starter:spring-boot-starter-grpc-server")) + + testImplementation(project(":starter:spring-boot-starter-grpc-client")) + testImplementation(project(":starter:spring-boot-starter-grpc-test")) + testImplementation(project(":starter:spring-boot-starter-oauth2-client")) + testImplementation(project(":starter:spring-boot-starter-test")) +} + +def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom") +def grpcJava = dependenciesBom.getLibrary("Grpc Java") +def protobufJava = dependenciesBom.getLibrary("Protobuf Java") + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +nullability { + requireExplicitNullMarking = false +} + +configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all { + extendsFrom(configurations.dependencyManagement) +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protobufJava.version}" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}" + } + } + generateProtoTasks { + all()*.plugins { + grpc { + option '@generated=omit' + } + } + } +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/HelloWorldService.java b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/HelloWorldService.java new file mode 100644 index 000000000000..e7f8ca8ab905 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/HelloWorldService.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcserveroauth; + +import io.grpc.stub.StreamObserver; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import smoketest.grpcserveroauth.proto.HelloReply; +import smoketest.grpcserveroauth.proto.HelloRequest; +import smoketest.grpcserveroauth.proto.HelloWorldGrpc; + +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +@Service +public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase { + + private static Log logger = LogFactory.getLog(HelloWorldService.class); + + @Override + public void sayHelloProfileScope(HelloRequest request, StreamObserver responseObserver) { + sayHello("sayHelloProfileScope", request, responseObserver); + } + + @Override + public void sayHelloEmailScope(HelloRequest request, StreamObserver responseObserver) { + sayHello("sayHelloEmailScope", request, responseObserver); + } + + @Override + public void sayHelloAuthenticated(HelloRequest request, StreamObserver responseObserver) { + sayHello("sayHelloAuthenticated", request, responseObserver); + } + + public void sayHello(String methodName, HelloRequest request, StreamObserver responseObserver) { + String name = request.getName(); + logger.info(methodName + " " + name); + Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name); + Assert.state(!name.startsWith("internal"), "Internal error"); + String message = "%s '%s'".formatted(methodName, name); + HelloReply reply = HelloReply.newBuilder().setMessage(message).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplication.java b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplication.java new file mode 100644 index 000000000000..66cce5e13103 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcserveroauth; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleGrpcServerOAuthApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleGrpcServerOAuthApplication.class, args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SecurityConfiguration.java b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SecurityConfiguration.java new file mode 100644 index 000000000000..6f7405ed52fa --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SecurityConfiguration.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcserveroauth; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.grpc.server.security.RequestMapperConfigurer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration(proxyBeanMethods = false) +public class SecurityConfiguration { + + @Bean + @GlobalServerInterceptor + AuthenticationProcessInterceptor securityInterceptor(GrpcSecurity grpcSecurity) throws Exception { + return grpcSecurity.authorizeRequests(this::authorizeGrpcRequests) + .oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults())) + .build(); + } + + private void authorizeGrpcRequests(RequestMapperConfigurer requests) { + requests.methods("HelloWorld/SayHelloProfileScope").hasAuthority("SCOPE_profile"); + requests.methods("HelloWorld/SayHelloEmailScope").hasAuthority("SCOPE_email"); + requests.methods("HelloWorld/SayHelloAuthenticated").authenticated(); + requests.methods("grpc.*/*").permitAll(); + requests.allRequests().denyAll(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) { + http.oauth2AuthorizationServer((authorizationServer) -> { + http.securityMatcher(authorizationServer.getEndpointsMatcher()); + authorizationServer.oidc(withDefaults()); + }); + http.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults())); + http.authorizeHttpRequests((requests) -> requests.anyRequest().permitAll()); + http.csrf((csrf) -> csrf.disable()); + return http.build(); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/package-info.java b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/package-info.java new file mode 100644 index 000000000000..ab81b93206fb --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NullMarked +package smoketest.grpcserveroauth; + +import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/proto/hello.proto b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/proto/hello.proto new file mode 100644 index 000000000000..e443b360171a --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/proto/hello.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +option java_package = "smoketest.grpcserveroauth.proto"; +option java_multiple_files = true; + +service HelloWorld { + rpc SayHelloProfileScope (HelloRequest) returns (HelloReply) {} + rpc SayHelloEmailScope (HelloRequest) returns (HelloReply) {} + rpc SayHelloAuthenticated (HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/resources/application.yaml b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/resources/application.yaml new file mode 100644 index 000000000000..c9257e42c1ed --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/resources/application.yaml @@ -0,0 +1,21 @@ +logging.level.org.springframework.security: TRACE +spring: + security: + oauth2: + authorizationserver: + client: + oidc-client: + registration: + client-id: "spring" + client-secret: "{noop}secret" + client-authentication-methods: + - "client_secret_basic" + authorization-grant-types: + - "client_credentials" + - "refresh_token" + scopes: + - "openid" + - "profile" + resourceserver: + jwt: + jwk-set-uri: http://localhost:8080/oauth2/jwks diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/test/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/test/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplicationTests.java new file mode 100644 index 000000000000..9bfeb8bab906 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/test/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplicationTests.java @@ -0,0 +1,220 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcserveroauth; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import io.grpc.ManagedChannelBuilder; +import io.grpc.Status.Code; +import io.grpc.StatusRuntimeException; +import io.grpc.reflection.v1.ServerReflectionGrpc.ServerReflectionStub; +import io.grpc.reflection.v1.ServerReflectionRequest; +import io.grpc.reflection.v1.ServerReflectionResponse; +import io.grpc.stub.StreamObserver; +import org.awaitility.Awaitility; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; +import smoketest.grpcserveroauth.proto.HelloReply; +import smoketest.grpcserveroauth.proto.HelloRequest; +import smoketest.grpcserveroauth.proto.HelloWorldGrpc.HelloWorldBlockingStub; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; +import org.springframework.grpc.client.ImportGrpcClients; +import org.springframework.grpc.client.interceptor.security.BearerTokenAuthenticationInterceptor; +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; +import org.springframework.security.oauth2.client.endpoint.RestClientClientCredentialsTokenResponseClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.SupplierClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.JwtDecoder; +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.test.annotation.DirtiesContext; +import org.springframework.util.function.SingletonSupplier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = { "spring.grpc.server.port=0", + "spring.grpc.client.channel.default.target=static://localhost:${local.grpc.server.port}", + "spring.grpc.client.channel.oauth.target=static://localhost:${local.grpc.server.port}" }) +@DirtiesContext +class SampleGrpcServerOAuthApplicationTests { + + @Autowired + @Qualifier("unauthenticatedHelloWorldBlockingStub") + private HelloWorldBlockingStub unuathenticated; + + @Autowired + @Qualifier("oauthHelloWorldBlockingStub") + private HelloWorldBlockingStub oauth; + + @Autowired + private ServerReflectionStub reflection; + + @Test + void whenUnauthenticatedStub() { + HelloWorldBlockingStub stub = this.unuathenticated; + assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloProfileScope)) + .satisfies(statusCode(Code.UNAUTHENTICATED)); + assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloEmailScope)) + .satisfies(statusCode(Code.UNAUTHENTICATED)); + assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAuthenticated)) + .satisfies(statusCode(Code.UNAUTHENTICATED)); + assertCanInvokeReflection(); + } + + @Test + void whenOAuth() { + HelloWorldBlockingStub stub = this.oauth; + assertThat(invoke(stub::sayHelloProfileScope)).isEqualTo("sayHelloProfileScope 'Spring'"); + assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloEmailScope)) + .satisfies(statusCode(Code.PERMISSION_DENIED)); + assertThat(invoke(stub::sayHelloAuthenticated)).isEqualTo("sayHelloAuthenticated 'Spring'"); + assertCanInvokeReflection(); + } + + private String invoke(Function method) { + HelloRequest request = HelloRequest.newBuilder().setName("Spring").build(); + return method.apply(request).getMessage(); + } + + private void assertCanInvokeReflection() { + ObservedResponse response = invokeReflection(); + assertThat(response.getValue()).isNotNull(); + assertThat(response.getError()).isNull(); + } + + private ObservedResponse invokeReflection() { + ObservedResponse response = new ObservedResponse<>(); + StreamObserver request = this.reflection.serverReflectionInfo(response); + request.onNext(ServerReflectionRequest.newBuilder().setListServices("").build()); + request.onCompleted(); + response.await(); + return response; + } + + private Consumer statusCode(Code expected) { + return (ex) -> assertThat(ex).extracting("status.code").isEqualTo(expected); + } + + @TestConfiguration(proxyBeanMethods = false) + @ImportGrpcClients(types = ServerReflectionStub.class) + @ImportGrpcClients(prefix = "unauthenticated", types = HelloWorldBlockingStub.class) + @ImportGrpcClients(target = "oauth", prefix = "oauth", types = HelloWorldBlockingStub.class) + static class GrpcClientTestConfiguration { + + @Bean + > GrpcChannelBuilderCustomizer channelSecurityCustomizer( + ObjectProvider clientRegistrationRepository) { + return GrpcChannelBuilderCustomizer.matching("oauth", (builder) -> { + Supplier tokenSupplier = SingletonSupplier + .of(() -> token(clientRegistrationRepository.getObject())); + builder.intercept(new BearerTokenAuthenticationInterceptor(tokenSupplier)); + }); + } + + private String token(ClientRegistrationRepository clientRegistrationRepository) { + RestClientClientCredentialsTokenResponseClient client = new RestClientClientCredentialsTokenResponseClient(); + ClientRegistration registration = clientRegistrationRepository.findByRegistrationId("spring"); + OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(registration); + return client.getTokenResponse(request).getAccessToken().getTokenValue(); + } + + @Bean + ClientRegistrationRepository lazyClientRegistrationRepository(Environment environment) { + return new SupplierClientRegistrationRepository(() -> getClientRegistrationRepository(environment)); + } + + private InMemoryClientRegistrationRepository getClientRegistrationRepository(Environment environment) { + return new InMemoryClientRegistrationRepository(ClientRegistration.withRegistrationId("spring") + .clientId("spring") + .clientSecret("secret") + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .scope("profile") + .tokenUri(environment.resolvePlaceholders("http://localhost:${local.server.port}/oauth2/token")) + .build()); + } + + @Bean + SupplierJwtDecoder lazyJwtDecoder(Environment environment) { + return new SupplierJwtDecoder(() -> getJwtDecoder(environment)); + } + + private JwtDecoder getJwtDecoder(Environment environment) { + JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder + .withJwkSetUri(environment.resolvePlaceholders("http://localhost:${local.server.port}/oauth2/jwks")); + builder.jwsAlgorithms((algorithms) -> algorithms.add(SignatureAlgorithm.from("RS256"))); + return builder.build(); + } + + } + + static class ObservedResponse implements StreamObserver { + + private volatile @Nullable T value; + + private volatile @Nullable Throwable error; + + @Override + public synchronized void onNext(T value) { + this.value = value; + } + + @Override + public synchronized void onError(Throwable error) { + this.error = error; + } + + @Override + public void onCompleted() { + } + + void await() { + Awaitility.await().until(this::hasResponse); + } + + private synchronized boolean hasResponse() { + return this.value != null || this.error != null; + } + + @Nullable T getValue() { + return this.value; + } + + @Nullable Throwable getError() { + return this.error; + } + + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle new file mode 100644 index 000000000000..a4ec289d2f2a --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle @@ -0,0 +1,66 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java" + id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "org.springframework.boot.docker-test" +} + +description = "Spring Boot gRPC server secure smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-grpc-server")) + implementation(project(":starter:spring-boot-starter-security")) + + testImplementation(project(":starter:spring-boot-starter-grpc-client")) + testImplementation(project(":starter:spring-boot-starter-grpc-test")) + testImplementation(project(":starter:spring-boot-starter-test")) +} + +def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom") +def grpcJava = dependenciesBom.getLibrary("Grpc Java") +def protobufJava = dependenciesBom.getLibrary("Protobuf Java") + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +nullability { + requireExplicitNullMarking = false +} + +configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all { + extendsFrom(configurations.dependencyManagement) +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protobufJava.version}" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}" + } + } + generateProtoTasks { + all()*.plugins { + grpc { + option '@generated=omit' + } + } + } +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/HelloWorldService.java b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/HelloWorldService.java new file mode 100644 index 000000000000..3330d1a15551 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/HelloWorldService.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcserversecure; + +import io.grpc.stub.StreamObserver; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import smoketest.grpcserversecure.proto.HelloReply; +import smoketest.grpcserversecure.proto.HelloRequest; +import smoketest.grpcserversecure.proto.HelloWorldGrpc; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +@Service +public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase { + + private static Log logger = LogFactory.getLog(HelloWorldService.class); + + @Override + public void sayHelloAdmin(HelloRequest request, StreamObserver responseObserver) { + sayHello("sayHelloAdmin", request, responseObserver); + } + + @Override + @PreAuthorize("hasAuthority('ROLE_ADMIN')") + public void sayHelloAdminAnnotated(HelloRequest request, StreamObserver responseObserver) { + sayHello("sayHelloAdminAnnotated", request, responseObserver); + } + + @Override + public void sayHelloUser(HelloRequest request, StreamObserver responseObserver) { + sayHello("sayHelloUser", request, responseObserver); + } + + @Override + @PreAuthorize("hasAuthority('ROLE_USER')") + public void sayHelloUserAnnotated(HelloRequest request, StreamObserver responseObserver) { + sayHello("sayHelloUserAnnotated", request, responseObserver); + } + + public void sayHello(String methodName, HelloRequest request, StreamObserver responseObserver) { + String name = request.getName(); + logger.info(methodName + " " + name); + Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name); + Assert.state(!name.startsWith("internal"), "Internal error"); + String message = "%s '%s'".formatted(methodName, name); + HelloReply reply = HelloReply.newBuilder().setMessage(message).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplication.java b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplication.java new file mode 100644 index 000000000000..9f92bc748834 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcserversecure; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleGrpcServerSecureApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleGrpcServerSecureApplication.class, args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SecurityConfiguration.java b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SecurityConfiguration.java new file mode 100644 index 000000000000..7beaa3c3dd8f --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SecurityConfiguration.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcserversecure; + +import io.grpc.ServerInterceptor; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.grpc.server.security.RequestMapperConfigurer; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; + +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration(proxyBeanMethods = false) +public class SecurityConfiguration { + + @Bean + InMemoryUserDetailsManager inMemoryUserDetailsManager() { + UserDetails user = user("user", "{noop}userpassword", "ROLE_USER"); + UserDetails admin = user("admin", "{noop}adminpassword", "ROLE_ADMIN"); + return new InMemoryUserDetailsManager(user, admin); + } + + private UserDetails user(String username, String password, String authority) { + return User.withUsername(username).password(password).authorities(authority).build(); + } + + @Bean + @GlobalServerInterceptor + ServerInterceptor securityInterceptor(GrpcSecurity security) throws Exception { + return security.authorizeRequests(this::authorizeRequests) + .httpBasic(withDefaults()) + .preauth(withDefaults()) + .build(); + } + + private void authorizeRequests(RequestMapperConfigurer requests) { + requests.methods("HelloWorld/SayHelloAdmin").hasAuthority("ROLE_ADMIN"); + requests.methods("HelloWorld/SayHelloAdminAnnotated").hasAuthority("ROLE_ADMIN"); + requests.methods("HelloWorld/SayHelloUser").hasAuthority("ROLE_USER"); + requests.methods("HelloWorld/SayHelloUserAnnotated").hasAuthority("ROLE_USER"); + requests.methods("grpc.*/*").permitAll(); + requests.allRequests().denyAll(); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/package-info.java b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/package-info.java new file mode 100644 index 000000000000..7f0195487428 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NullMarked +package smoketest.grpcserversecure; + +import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/proto/hello.proto b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/proto/hello.proto new file mode 100644 index 000000000000..700ee4ac930f --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/proto/hello.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +option java_package = "smoketest.grpcserversecure.proto"; +option java_multiple_files = true; + +service HelloWorld { + rpc SayHelloAdmin (HelloRequest) returns (HelloReply) {} + rpc SayHelloAdminAnnotated (HelloRequest) returns (HelloReply) {} + rpc SayHelloUser (HelloRequest) returns (HelloReply) {} + rpc SayHelloUserAnnotated (HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/test/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/test/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplicationTests.java new file mode 100644 index 000000000000..4c7dab0e33dd --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/test/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplicationTests.java @@ -0,0 +1,192 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.grpcserversecure; + +import java.util.function.Consumer; +import java.util.function.Function; + +import io.grpc.ManagedChannelBuilder; +import io.grpc.Status.Code; +import io.grpc.StatusRuntimeException; +import io.grpc.reflection.v1.ServerReflectionGrpc.ServerReflectionStub; +import io.grpc.reflection.v1.ServerReflectionRequest; +import io.grpc.reflection.v1.ServerReflectionResponse; +import io.grpc.stub.StreamObserver; +import org.awaitility.Awaitility; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; +import smoketest.grpcserversecure.proto.HelloReply; +import smoketest.grpcserversecure.proto.HelloRequest; +import smoketest.grpcserversecure.proto.HelloWorldGrpc.HelloWorldBlockingStub; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; +import org.springframework.grpc.client.ImportGrpcClients; +import org.springframework.grpc.client.interceptor.security.BasicAuthenticationInterceptor; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +@SpringBootTest(properties = { "spring.grpc.server.port=0", + "spring.grpc.client.channel.default.target=static://localhost:${local.grpc.server.port}", + "spring.grpc.client.channel.user.target=static://localhost:${local.grpc.server.port}", + "spring.grpc.client.channel.admin.target=static://localhost:${local.grpc.server.port}" }) +@DirtiesContext +class SampleGrpcServerSecureApplicationTests { + + @Autowired + @Qualifier("unauthenticatedHelloWorldBlockingStub") + private HelloWorldBlockingStub unuathenticated; + + @Autowired + @Qualifier("userAuthenticatedHelloWorldBlockingStub") + private HelloWorldBlockingStub userAuthenticated; + + @Autowired + @Qualifier("adminAuthenticatedHelloWorldBlockingStub") + private HelloWorldBlockingStub adminAuthenticated; + + @Autowired + private ServerReflectionStub reflection; + + @Test + void whenUnauthenticatedStub() { + HelloWorldBlockingStub stub = this.unuathenticated; + assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUser)) + .satisfies(statusCode(Code.UNAUTHENTICATED)); + assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUserAnnotated)) + .satisfies(statusCode(Code.UNAUTHENTICATED)); + assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdmin)) + .satisfies(statusCode(Code.UNAUTHENTICATED)); + assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdminAnnotated)) + .satisfies(statusCode(Code.UNAUTHENTICATED)); + assertCanInvokeReflection(); + } + + @Test + void whenUserAuthenticatedStub() { + HelloWorldBlockingStub stub = this.userAuthenticated; + assertThat(invoke(stub::sayHelloUser)).isEqualTo("sayHelloUser 'Spring'"); + assertThat(invoke(stub::sayHelloUserAnnotated)).isEqualTo("sayHelloUserAnnotated 'Spring'"); + assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdmin)) + .satisfies(statusCode(Code.PERMISSION_DENIED)); + assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdminAnnotated)) + .satisfies(statusCode(Code.PERMISSION_DENIED)); + assertCanInvokeReflection(); + } + + @Test + void whenAdminAuthenticatedStub() { + HelloWorldBlockingStub stub = this.adminAuthenticated; + assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUser)) + .satisfies(statusCode(Code.PERMISSION_DENIED)); + assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUserAnnotated)) + .satisfies(statusCode(Code.PERMISSION_DENIED)); + assertThat(invoke(stub::sayHelloAdmin)).isEqualTo("sayHelloAdmin 'Spring'"); + assertThat(invoke(stub::sayHelloAdminAnnotated)).isEqualTo("sayHelloAdminAnnotated 'Spring'"); + assertCanInvokeReflection(); + } + + private String invoke(Function method) { + HelloRequest request = HelloRequest.newBuilder().setName("Spring").build(); + return method.apply(request).getMessage(); + } + + private void assertCanInvokeReflection() { + ObservedResponse response = invokeReflection(); + assertThat(response.getValue()).isNotNull(); + assertThat(response.getError()).isNull(); + } + + private ObservedResponse invokeReflection() { + ObservedResponse response = new ObservedResponse<>(); + StreamObserver request = this.reflection.serverReflectionInfo(response); + request.onNext(ServerReflectionRequest.newBuilder().setListServices("").build()); + request.onCompleted(); + response.await(); + return response; + } + + private Consumer statusCode(Code expected) { + return (ex) -> assertThat(ex).extracting("status.code").isEqualTo(expected); + } + + @TestConfiguration(proxyBeanMethods = false) + @ImportGrpcClients(types = ServerReflectionStub.class) + @ImportGrpcClients(prefix = "unauthenticated", types = HelloWorldBlockingStub.class) + @ImportGrpcClients(target = "user", prefix = "userAuthenticated", types = HelloWorldBlockingStub.class) + @ImportGrpcClients(target = "admin", prefix = "adminAuthenticated", types = HelloWorldBlockingStub.class) + static class GrpcClientTestConfiguration { + + @Bean + > GrpcChannelBuilderCustomizer channelSecurityCustomizer() { + return (target, builder) -> { + if ("user".equals(target)) { + builder.intercept(new BasicAuthenticationInterceptor("user", "userpassword")); + } + if ("admin".equals(target)) { + builder.intercept(new BasicAuthenticationInterceptor("admin", "adminpassword")); + } + }; + } + + } + + static class ObservedResponse implements StreamObserver { + + private volatile @Nullable T value; + + private volatile @Nullable Throwable error; + + @Override + public synchronized void onNext(T value) { + this.value = value; + } + + @Override + public synchronized void onError(Throwable error) { + this.error = error; + } + + @Override + public void onCompleted() { + } + + void await() { + Awaitility.await().until(this::hasResponse); + } + + private synchronized boolean hasResponse() { + return this.value != null || this.error != null; + } + + @Nullable T getValue() { + return this.value; + } + + @Nullable Throwable getError() { + return this.error; + } + + } + +} From f75fea3601ec2113922160b9ba13baae87271df3 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 19 Mar 2026 11:27:15 -0700 Subject: [PATCH 391/930] Add type-safe gRPC service config properties Update `GrpcClientProperties` to include type-safe service config properties commonly configured items. Closes gh-49540 --- module/spring-boot-grpc-client/build.gradle | 1 + .../autoconfigure/GrpcClientProperties.java | 13 + ...cClientDefaultServiceConfigCustomizer.java | 41 +- .../client/autoconfigure/ServiceConfig.java | 430 ++++++++++++++++++ .../GrpcChannelBuilderCustomizersTests.java | 34 ++ .../autoconfigure/ServiceConfigTests.java | 393 ++++++++++++++++ 6 files changed, 908 insertions(+), 4 deletions(-) create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfigTests.java diff --git a/module/spring-boot-grpc-client/build.gradle b/module/spring-boot-grpc-client/build.gradle index 52bf78936283..6a2d64dd9aa0 100644 --- a/module/spring-boot-grpc-client/build.gradle +++ b/module/spring-boot-grpc-client/build.gradle @@ -30,6 +30,7 @@ dependencies { optional(project(":core:spring-boot-autoconfigure")) optional(project(":module:spring-boot-micrometer-observation")) + optional("io.grpc:grpc-grpclb") optional("io.grpc:grpc-stub") optional("io.grpc:grpc-netty") optional("io.grpc:grpc-netty-shaded") diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java index f015938d1bef..e57ecece11b2 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java @@ -70,6 +70,11 @@ public static class Channel { */ private boolean bypassCertificateValidation; + /** + * Service config for the channel. + */ + private @Nullable ServiceConfig serviceConfig; + private final Inbound inbound = new Inbound(); @Name("default") @@ -107,6 +112,14 @@ public void setBypassCertificateValidation(boolean bypassCertificateValidation) this.bypassCertificateValidation = bypassCertificateValidation; } + public @Nullable ServiceConfig getServiceConfig() { + return this.serviceConfig; + } + + public void setServiceConfig(@Nullable ServiceConfig serviceConfig) { + this.serviceConfig = serviceConfig; + } + public Inbound getInbound() { return this.inbound; } diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcClientDefaultServiceConfigCustomizer.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcClientDefaultServiceConfigCustomizer.java index 58026ae3e93b..c33f9f5782e4 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcClientDefaultServiceConfigCustomizer.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesGrpcClientDefaultServiceConfigCustomizer.java @@ -16,9 +16,15 @@ package org.springframework.boot.grpc.client.autoconfigure; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel; +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel.Health; +import org.springframework.util.Assert; /** * {@link GrpcClientDefaultServiceConfigCustomizer} to apply {@link GrpcClientProperties}. @@ -34,11 +40,38 @@ record PropertiesGrpcClientDefaultServiceConfigCustomizer( public void customize(String target, Map defaultServiceConfig) { Channel channel = this.properties.getChannel().get(target); channel = (channel != null) ? channel : this.properties.getChannel().get("default"); - if (channel != null && channel.getHealth().isEnabled()) { - String serviceName = channel.getHealth().getServiceName(); - Map healthCheckConfig = Map.of("serviceName", (serviceName != null) ? serviceName : ""); - defaultServiceConfig.put("healthCheckConfig", healthCheckConfig); + if (channel == null) { + return; } + applyServiceConfig(channel.getServiceConfig(), defaultServiceConfig); + applyHealth(channel.getHealth(), defaultServiceConfig); + } + + private void applyServiceConfig(@Nullable ServiceConfig serviceConfig, Map defaultServiceConfig) { + if (serviceConfig != null) { + serviceConfig.applyTo(defaultServiceConfig); + } + } + + private void applyHealth(Health health, Map defaultServiceConfig) { + if (!health.isEnabled()) { + return; + } + String serviceName = (health.getServiceName() != null) ? health.getServiceName() : ""; + Map healthCheckConfig = cloneOrCreateHealthCheckConfig(defaultServiceConfig); + String existingServiceName = (String) healthCheckConfig.get(ServiceConfig.HEALTH_CHECK_SERVICE_NAME_KEY); + Assert.state(existingServiceName == null || serviceName.equals(existingServiceName), + () -> "Unable to change health check config service name from '%s' to '%s'" + .formatted(existingServiceName, serviceName)); + healthCheckConfig.put(ServiceConfig.HEALTH_CHECK_SERVICE_NAME_KEY, serviceName); + defaultServiceConfig.put(ServiceConfig.HEALTH_CHECK_CONFIG_KEY, healthCheckConfig); + } + + @SuppressWarnings("unchecked") + private Map cloneOrCreateHealthCheckConfig(Map defaultServiceConfig) { + Map healthCheckConfig = (Map) defaultServiceConfig + .get(ServiceConfig.HEALTH_CHECK_CONFIG_KEY); + return new LinkedHashMap<>((healthCheckConfig != null) ? healthCheckConfig : Collections.emptyMap()); } } diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java new file mode 100644 index 000000000000..1ddd11a3a62e --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java @@ -0,0 +1,430 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import java.time.Duration; +import java.util.Collections; +import java.util.LinkedHashMap; +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 io.grpc.Status; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.context.properties.PropertyMapper.Source.Adapter; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; +import org.springframework.util.CollectionUtils; +import org.springframework.util.unit.DataSize; + +/** + * Bindable service configuration for gRPC channel. Allows type safe binding of common + * service configuration options which can ultimately be applied to the {@link Map} + * provided by a {@link GrpcClientDefaultServiceConfigCustomizer}. + *

    + * The configuration provided here is a subset of the canonical service_config.proto + * protocol definition. For advanced or experimental service configurations, use the + * {@link GrpcClientDefaultServiceConfigCustomizer} to directly add any entries supported + * by {@code grpc-java}. + * + * @author Phillip Webb + * @param loadbalancing load balancing configurations in the order that they should be + * applied + * @param method method configuration + * @param retrythrottling retry throttling policy + * @param healthcheck health check configuration + * @since 4.1.0 + * @see GrpcClientDefaultServiceConfigCustomizer + * @see io.grpc.internal.ServiceConfigUtil + */ +public record ServiceConfig(@Nullable List loadbalancing, @Nullable List method, + @Nullable RetryThrottlingPolicy retrythrottling, @Nullable HealthCheckConfig healthcheck) { + + static final String HEALTH_CHECK_CONFIG_KEY = "healthCheckConfig"; + + static final String HEALTH_CHECK_SERVICE_NAME_KEY = "serviceName"; + + /** + * Apply this service config to the given gRPC Java config Map. + * @param grpcJavaConfig the gRPC Java config map + */ + public void applyTo(Map grpcJavaConfig) { + applyTo(new GrpcJavaConfig(grpcJavaConfig)); + } + + private void applyTo(GrpcJavaConfig config) { + PropertyMapper map = PropertyMapper.get(); + map.from(this::loadbalancing) + .as(listOf(LoadBalancingConfig::grpcJavaConfig)) + .to(config.in("loadBalancingConfig")); + map.from(this::method).as(listOf(MethodConfig::grpcJavaConfig)).to(config.in("methodConfig")); + map.from(this::retrythrottling).as(RetryThrottlingPolicy::grpcJavaConfig).to(config.in("retryThrottling")); + map.from(this::healthcheck).as(HealthCheckConfig::grpcJavaConfig).to(config.in(HEALTH_CHECK_CONFIG_KEY)); + } + + static Adapter, @Nullable List>> listOf(Function> adapter) { + return (list) -> (!CollectionUtils.isEmpty(list)) ? list.stream().map(adapter).toList() : null; + } + + static String durationString(Duration duration) { + return duration.getSeconds() + "." + duration.getNano() + "s"; + } + + static String bytesString(DataSize dataSize) { + return Long.toString(dataSize.toBytes()); + } + + /** + * Load balancing config. + * + * @param pickfirst 'pick first' load balancing + * @param roundrobin 'round robin' load balancing + * @param weightedroundrobin 'weighted round robin' load balancing + * @param grpc 'grpc' load balancing + */ + public record LoadBalancingConfig(@Nullable PickFirstLoadBalancingConfig pickfirst, + @Nullable RoundRobinLoadBalancingConfig roundrobin, + @Nullable WeightedRoundRobinLoadBalancingConfig weightedroundrobin, + @Nullable GrpcLoadBalancingConfig grpc) { + + public LoadBalancingConfig { + if (pickfirst == null && roundrobin == null && weightedroundrobin == null && grpc == null) { + throw new InvalidConfigurationPropertyValueException("loadbalancing", null, + "Missing load balancing strategy"); + } + MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> { + entries.put("loadbalancing.pickfirst", pickfirst); + entries.put("loadbalancing.roundrobin", roundrobin); + entries.put("loadbalancing.weightedroundrobin", weightedroundrobin); + entries.put("loadbalancing.grpc", grpc); + }); + } + + Map grpcJavaConfig() { + LinkedHashMap grpcJavaConfig = new LinkedHashMap<>(); + PropertyMapper map = PropertyMapper.get(); + map.from(this::pickfirst) + .as(PickFirstLoadBalancingConfig::grpcJavaConfig) + .to((loadBalancingConfig) -> grpcJavaConfig.put("pick_first", loadBalancingConfig)); + map.from(this::roundrobin) + .as(RoundRobinLoadBalancingConfig::grpcJavaConfig) + .to((loadBalancingConfig) -> grpcJavaConfig.put("round_robin", loadBalancingConfig)); + map.from(this::weightedroundrobin) + .as(WeightedRoundRobinLoadBalancingConfig::grpcJavaConfig) + .to((loadBalancingConfig) -> grpcJavaConfig.put("weighted_round_robin", loadBalancingConfig)); + map.from(this::grpc) + .as(GrpcLoadBalancingConfig::grpcJavaConfig) + .to((loadBalancingConfig) -> grpcJavaConfig.put("grpclb", loadBalancingConfig)); + return grpcJavaConfig; + } + + /** + * 'pick first' load balancing. + * + * @param shuffleAddressList randomly shuffle the list of addresses received from + * the name resolver before attempting to connect to them. + */ + public record PickFirstLoadBalancingConfig(Boolean shuffleAddressList) { + + Map grpcJavaConfig() { + // Aligned with PickFirstLoadBalancerProvider + GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig(); + PropertyMapper map = PropertyMapper.get(); + map.from(this::shuffleAddressList).to(grpcJavaConfig.in("shuffleAddressList")); + return grpcJavaConfig.asMap(); + } + + } + + /** + * 'round robin' load balancing. + */ + public record RoundRobinLoadBalancingConfig() { + + /** + * Return the gRPC java config as supported by the + * {@code SecretRoundRobinLoadBalancerProvider}. + * @return the config + */ + Map grpcJavaConfig() { + return Collections.emptyMap(); + } + + } + + /** + * 'weighted round robin' load balancing. + * + * @param blackoutPeriod must report load metrics continuously for at least this + * long before the endpoint weight will be used + * @param weightExpirationPeriod if has not reported load metrics in this long, + * then we stop using the reported weight + * @param outOfBandReportingPeriod load reporting interval to request from the + * server + * @param enableOutOfBandLoadReport whether to enable out-of-band utilization + * reporting collection from the endpoints + * @param weightUpdatePeriod how often endpoint weights are recalculated + * @param errorUtilizationPenalty multiplier used to adjust endpoint weights with + * the error rate calculated as eps/qps + */ + public record WeightedRoundRobinLoadBalancingConfig(Duration blackoutPeriod, Duration weightExpirationPeriod, + Duration outOfBandReportingPeriod, Boolean enableOutOfBandLoadReport, Duration weightUpdatePeriod, + Float errorUtilizationPenalty) { + + Map grpcJavaConfig() { + // Aligned with WeightedRoundRobinLoadBalancerProvider + GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig(); + PropertyMapper map = PropertyMapper.get(); + map.from(this::blackoutPeriod) + .as(ServiceConfig::durationString) + .to(grpcJavaConfig.in("blackoutPeriod")); + map.from(this::weightExpirationPeriod) + .as(ServiceConfig::durationString) + .to(grpcJavaConfig.in("weightExpirationPeriod")); + map.from(this::outOfBandReportingPeriod) + .as(ServiceConfig::durationString) + .to(grpcJavaConfig.in("oobReportingPeriod")); + map.from(this::enableOutOfBandLoadReport).to(grpcJavaConfig.in("enableOobLoadReport")); + map.from(this::weightUpdatePeriod) + .as(ServiceConfig::durationString) + .to(grpcJavaConfig.in("weightUpdatePeriod")); + map.from(this::errorUtilizationPenalty).to(grpcJavaConfig.in("errorUtilizationPenalty")); + return grpcJavaConfig.asMap(); + } + + } + + /** + * 'grpc' load balancing. + * + * @param child what load balancer policies to use for routing between the backend + * addresses + * @param serviceName override of the service name to be sent to the balancer + * @param initialFallbackTimeout timeout in seconds for receiving the server list + */ + public record GrpcLoadBalancingConfig(List child, String serviceName, + Duration initialFallbackTimeout) { + + public GrpcLoadBalancingConfig { + child.forEach(this::assertChild); + } + + private void assertChild(LoadBalancingConfig child) { + if (child.pickfirst() == null && child.roundrobin() == null) { + throw new InvalidConfigurationPropertyValueException("loadbalancing.grpc.child", null, + "Only 'pickfirst' or 'roundrobin' child load balancer strategies can be used"); + } + } + + Map grpcJavaConfig() { + // Aligned with GrpclbLoadBalancerProvider + GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig(); + PropertyMapper map = PropertyMapper.get(); + map.from(this::child) + .as(listOf(LoadBalancingConfig::grpcJavaConfig)) + .to(grpcJavaConfig.in("childPolicy")); + map.from(this::serviceName).to(grpcJavaConfig.in("serviceName")); + map.from(this::initialFallbackTimeout) + .as(ServiceConfig::durationString) + .to(grpcJavaConfig.in("initialFallbackTimeout")); + return grpcJavaConfig.asMap(); + } + + } + + } + + /** + * Method configuration. + * + * @param name Names of the methods to which this configuration applies + * @param waitForReady Whether RPCs sent to this method should wait until the + * connection is ready by default + * @param maxRequestMessage maximum allowed payload size for an individual request or + * object in a stream + * @param maxResponseMessage maximum allowed payload size for an individual response + * or object in a stream + * @param timeout default timeout for RPCs sent to this method + * @param retry retry policy for outgoing RPCs + * @param hedging hedging policy for outgoing RPCs + */ + public record MethodConfig(List name, Boolean waitForReady, DataSize maxRequestMessage, + DataSize maxResponseMessage, Duration timeout, RetryPolicy retry, HedgingPolicy hedging) { + + public MethodConfig { + MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> { + entries.put("method.retry", retry); + entries.put("method.hedging", hedging); + }); + } + + static @Nullable List> grpcJavaConfigs(List methodConfigs) { + return (!CollectionUtils.isEmpty(methodConfigs)) + ? methodConfigs.stream().map(MethodConfig::grpcJavaConfig).toList() : null; + } + + Map grpcJavaConfig() { + GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig(); + PropertyMapper map = PropertyMapper.get(); + map.from(this::name).as(listOf(Name::grpcJavaConfig)).to(grpcJavaConfig.in("name")); + map.from(this::waitForReady).to(grpcJavaConfig.in("waitForReady")); + map.from(this::maxRequestMessage) + .as(ServiceConfig::bytesString) + .to(grpcJavaConfig.in("maxRequestMessageBytes")); + map.from(this::maxResponseMessage) + .as(ServiceConfig::bytesString) + .to(grpcJavaConfig.in("maxResponseMessageBytes")); + map.from(this::timeout).as(ServiceConfig::durationString).to(grpcJavaConfig.in("timeout")); + map.from(this::retry).as(RetryPolicy::grpcJavaConfig).to(grpcJavaConfig.in("retryPolicy")); + map.from(this::hedging).as(HedgingPolicy::grpcJavaConfig).to(grpcJavaConfig.in("hedgingPolicy")); + return grpcJavaConfig.asMap(); + } + + /** + * The name of a gRPC method. + * + * @param service service name + * @param method method name + */ + public record Name(String service, String method) { + + Map grpcJavaConfig() { + GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig(); + PropertyMapper map = PropertyMapper.get(); + map.from(this::service).to(grpcJavaConfig.in("service")); + map.from(this::method).to(grpcJavaConfig.in("method")); + return grpcJavaConfig.asMap(); + } + + } + + /** + * Retry policy for outgoing RPCs. + * + * @param maxAttempts maximum number of RPC attempts, including the original + * attempt + * @param initialBackoff initial exponential backoff + * @param maxBackoff maximum exponential backoff + * @param backoffMultiplier exponential backoff multiplier + * @param perAttemptReceiveTimeout per-attempt receive timeout + * @param retryableStatusCodes status codes which may be retried + */ + public record RetryPolicy(Integer maxAttempts, Duration initialBackoff, Duration maxBackoff, + Double backoffMultiplier, Duration perAttemptReceiveTimeout, Set retryableStatusCodes) { + + Map grpcJavaConfig() { + GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig(); + PropertyMapper map = PropertyMapper.get(); + map.from(this::maxAttempts).as(Objects::toString).to(grpcJavaConfig.in("maxAttempts")); + map.from(this::initialBackoff) + .as(ServiceConfig::durationString) + .to(grpcJavaConfig.in("initialBackoff")); + map.from(this::maxBackoff).as(ServiceConfig::durationString).to(grpcJavaConfig.in("maxBackoff")); + map.from(this::backoffMultiplier).to(grpcJavaConfig.in("backoffMultiplier")); + map.from(this::perAttemptReceiveTimeout) + .as(ServiceConfig::durationString) + .to(grpcJavaConfig.in("perAttemptRecvTimeout")); + map.from(this::retryableStatusCodes) + .as((codes) -> codes.stream().map(Objects::toString).toList()) + .to(grpcJavaConfig.in("retryableStatusCodes")); + return grpcJavaConfig.asMap(); + } + + } + + /** + * Hedging policy for outgoing RPCs. + * + * @param maxAttempts maximum number of send attempts + * @param delay delay for subsequent RPCs + * @param nonFatalStatusCodes status codes which indicate other hedged RPCs may + * still succeed + */ + public record HedgingPolicy(Integer maxAttempts, Duration delay, Set nonFatalStatusCodes) { + + Map grpcJavaConfig() { + GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig(); + PropertyMapper map = PropertyMapper.get(); + map.from(this::maxAttempts).as(Objects::toString).to(grpcJavaConfig.in("maxAttempts")); + map.from(this::delay).as(ServiceConfig::durationString).to(grpcJavaConfig.in("hedgingDelay")); + map.from(this::nonFatalStatusCodes) + .as((codes) -> codes.stream().map(Objects::toString).toList()) + .to(grpcJavaConfig.in("nonFatalStatusCodes")); + return grpcJavaConfig.asMap(); + } + + } + } + + /** + * Retry throttling policy. + * + * @param maxTokens maximum number of tokens + * @param tokenRatio the token ratio + */ + public record RetryThrottlingPolicy(Float maxTokens, Float tokenRatio) { + + Map grpcJavaConfig() { + GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig(); + PropertyMapper map = PropertyMapper.get(); + map.from(this::maxTokens).as(Objects::toString).to(grpcJavaConfig.in("maxTokens")); + map.from(this::tokenRatio).as(Objects::toString).to(grpcJavaConfig.in("tokenRatio")); + return grpcJavaConfig.asMap(); + } + + } + + /** + * Health check configuration. + * + * @param serviceName service name to use in the health-checking request. + */ + public record HealthCheckConfig(String serviceName) { + + Map grpcJavaConfig() { + GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig(); + PropertyMapper map = PropertyMapper.get(); + map.from(this::serviceName).to(grpcJavaConfig.in(HEALTH_CHECK_SERVICE_NAME_KEY)); + return grpcJavaConfig.asMap(); + } + + } + + /** + * Internal helper to collection gRPC java config. + * + * @param asMap the underling data as a map + */ + record GrpcJavaConfig(Map asMap) { + + GrpcJavaConfig() { + this(new LinkedHashMap<>()); + } + + Consumer in(String key) { + return (value) -> this.asMap.put(key, value); + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizersTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizersTests.java index d49b4bfc5857..ac59391bca28 100644 --- a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizersTests.java +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizersTests.java @@ -29,9 +29,11 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel; +import org.springframework.boot.grpc.client.autoconfigure.ServiceConfig.HealthCheckConfig; import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; 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.then; import static org.mockito.Mockito.mock; @@ -169,6 +171,38 @@ void applyWhenHasDefaultHealthAddsHealthServiceConfig() { then(builder).should().defaultServiceConfig(expected); } + @Test + void applyWhenHasServiceConfig() { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channel = new Channel(); + ServiceConfig serviceConfig = new ServiceConfig(null, null, null, new HealthCheckConfig("test")); + channel.setServiceConfig(serviceConfig); + properties.getChannel().put("default", channel); + GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null, + Collections.emptyList(), Collections.emptyList()); + NettyChannelBuilder builder = mock(NettyChannelBuilder.class); + customizers.apply("target", builder); + Map expected = new LinkedHashMap<>(); + expected.put("healthCheckConfig", Map.of("serviceName", "test")); + then(builder).should().defaultServiceConfig(expected); + } + + @Test + void applyWhenHasClashingServiceConfigAndHealth() { + GrpcClientProperties properties = new GrpcClientProperties(); + Channel channel = new Channel(); + channel.getHealth().setEnabled(true); + channel.getHealth().setServiceName("fromhealth"); + ServiceConfig serviceConfig = new ServiceConfig(null, null, null, new HealthCheckConfig("fromservice")); + channel.setServiceConfig(serviceConfig); + properties.getChannel().put("default", channel); + GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null, + Collections.emptyList(), Collections.emptyList()); + NettyChannelBuilder builder = mock(NettyChannelBuilder.class); + assertThatIllegalStateException().isThrownBy(() -> customizers.apply("target", builder)) + .withMessage("Unable to change health check config service name from 'fromservice' to 'fromhealth'"); + } + @Test void applyWhenHealthEnabledAndNoServiceNameAddsHealthConfig() { GrpcClientProperties properties = new GrpcClientProperties(); diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfigTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfigTests.java new file mode 100644 index 000000000000..f7b4e56174ed --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfigTests.java @@ -0,0 +1,393 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.grpc.client.autoconfigure; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import io.grpc.LoadBalancerRegistry; +import io.grpc.NameResolver.ConfigOrError; +import io.grpc.Status.Code; +import io.grpc.internal.AutoConfiguredLoadBalancerFactory; +import io.grpc.internal.ScParser; +import io.grpc.internal.ServiceConfigUtil; +import io.grpc.internal.ServiceConfigUtil.LbConfig; +import io.grpc.internal.ServiceConfigUtil.PolicySelection; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; +import org.springframework.boot.env.YamlPropertySourceLoader; +import org.springframework.boot.testsupport.classpath.resources.WithResource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link ServiceConfig}. + * + * @author Phillip Webb + */ +class ServiceConfigTests { + + @Test + @WithResource(name = "config.yaml", content = """ + config: + load-balancing: + - pickfirst: {} + """) + void pickFirstLoadBalancing() throws Exception { + Map map = bindAndGetAsMap(); + assertThat(map).containsKey("loadBalancingConfig"); + List> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map); + assertThat(loadBalancingConfigs).hasSize(1); + assertThat(loadBalancingConfigs.get(0)).containsKey("pick_first"); + PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs); + assertThat(loadBalancingPolicySelection.toString()).contains("PickFirstLoadBalancer"); + assertThat(loadBalancingPolicySelection.getConfig()).extracting("shuffleAddressList").isNull(); + } + + @Test + @WithResource(name = "config.yaml", content = """ + config: + load-balancing: + - pickfirst: + shuffle-address-list: true + """) + void pickFirstLoadBalancingWithProperties() throws Exception { + Map map = bindAndGetAsMap(); + assertThat(map).containsKey("loadBalancingConfig"); + List> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map); + assertThat(loadBalancingConfigs).hasSize(1); + assertThat(loadBalancingConfigs.get(0)).containsKey("pick_first"); + PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs); + assertThat(loadBalancingPolicySelection.toString()).contains("PickFirstLoadBalancer"); + assertThat(loadBalancingPolicySelection.getConfig()).extracting("shuffleAddressList").isEqualTo(Boolean.TRUE); + } + + @Test + @WithResource(name = "config.yaml", content = """ + config: + load-balancing: + - roundrobin: {} + """) + void roundRobinLoadBalancing() throws Exception { + Map map = bindAndGetAsMap(); + assertThat(map).containsKey("loadBalancingConfig"); + List> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map); + assertThat(loadBalancingConfigs).hasSize(1); + assertThat(loadBalancingConfigs.get(0)).containsKey("round_robin"); + PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs); + assertThat(loadBalancingPolicySelection.toString()).contains("policy=round_robin") + .contains("no service config"); + } + + @Test + @WithResource(name = "config.yaml", content = """ + config: + load-balancing: + - weightedroundrobin: {} + """) + void weightedRoundRobinLoadBalancing() throws Exception { + Map map = bindAndGetAsMap(); + assertThat(map).containsKey("loadBalancingConfig"); + List> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map); + assertThat(loadBalancingConfigs).hasSize(1); + assertThat(loadBalancingConfigs.get(0)).containsKey("weighted_round_robin"); + PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs); + assertThat(loadBalancingPolicySelection.toString()).contains("WeightedRoundRobinLoadBalancerProvider"); + } + + @Test + @WithResource(name = "config.yaml", content = """ + config: + load-balancing: + - weightedroundrobin: + blackout-period: 1m + weight-expiration-period: 500ms + out-of-band-reporting-period: 1s + enable-out-of-band-load-report: true + weight-update-period: 2s + error-utilization-penalty: 0.5 + """) + void weightedRoundRobinLoadBalancingWithProperties() throws Exception { + Map map = bindAndGetAsMap(); + assertThat(map).containsKey("loadBalancingConfig"); + List> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map); + assertThat(loadBalancingConfigs).hasSize(1); + assertThat(loadBalancingConfigs.get(0)).containsKey("weighted_round_robin"); + PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs); + assertThat(loadBalancingPolicySelection.toString()).contains("WeightedRoundRobinLoadBalancerProvider"); + assertThat(loadBalancingPolicySelection.getConfig()).extracting("blackoutPeriodNanos") + .isEqualTo(Duration.ofMinutes(1).toNanos()); + assertThat(loadBalancingPolicySelection.getConfig()).extracting("weightExpirationPeriodNanos") + .isEqualTo(Duration.ofMillis(500).toNanos()); + assertThat(loadBalancingPolicySelection.getConfig()).extracting("enableOobLoadReport").isEqualTo(true); + assertThat(loadBalancingPolicySelection.getConfig()).extracting("oobReportingPeriodNanos") + .isEqualTo(Duration.ofSeconds(1).toNanos()); + assertThat(loadBalancingPolicySelection.getConfig()).extracting("weightUpdatePeriodNanos") + .isEqualTo(Duration.ofSeconds(2).toNanos()); + assertThat(loadBalancingPolicySelection.getConfig()).extracting("errorUtilizationPenalty").isEqualTo(0.5f); + } + + @Test + @WithResource(name = "config.yaml", content = """ + config: + load-balancing: + - grpc: + child: + - roundrobin: {} + - pickfirst: {} + service-name: test + initial-fallback-timeout: 10s + """) + void grpcLoadBalancingWithProperties() throws Exception { + Map map = bindAndGetAsMap(); + assertThat(map).containsKey("loadBalancingConfig"); + List> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map); + assertThat(loadBalancingConfigs).hasSize(1); + assertThat(loadBalancingConfigs.get(0)).containsKey("grpclb"); + PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs); + assertThat(loadBalancingPolicySelection.toString()).contains("GrpclbLoadBalancerProvider"); + assertThat(loadBalancingPolicySelection.getConfig()).extracting("mode").hasToString("ROUND_ROBIN"); + assertThat(loadBalancingPolicySelection.getConfig()).extracting("serviceName").isEqualTo("test"); + assertThat(loadBalancingPolicySelection.getConfig()).extracting("fallbackTimeoutMs") + .isEqualTo(Duration.ofSeconds(10).toMillis()); + } + + @Test + @WithResource(name = "config.yaml", content = """ + config: + load-balancing: + - pickfirst: {} + - weightedroundrobin: {} + """) + void multipleLoadBalancerPolicies() throws Exception { + Map map = bindAndGetAsMap(); + assertThat(map).containsKey("loadBalancingConfig"); + List> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map); + assertThat(loadBalancingConfigs).hasSize(2); + assertThat(loadBalancingConfigs.get(0)).containsKey("pick_first"); + assertThat(loadBalancingConfigs.get(1)).containsKey("weighted_round_robin"); + } + + @Test + @WithResource(name = "config.yaml", content = """ + config: + load-balancing: + - pickfirst: {} + weightedroundrobin: {} + """) + void whenMultileLoadBalancingPoliciesInListItemThrowsException() { + assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindAndGetAsMap()) + .havingRootCause() + .isInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class); + } + + @Test + @WithResource(name = "config.yaml", content = """ + config: + load-balancing: + - {} + """) + void whenNoLoadBalancingPoliciesInListItemThrowsException() { + assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindAndGetAsMap()) + .havingRootCause() + .isInstanceOf(InvalidConfigurationPropertyValueException.class); + } + + @Test + @WithResource(name = "config.yaml", content = """ + config: + method: + - name: + - service: s-one + method: m-one + - service: s-two + method: m-two + wait-for-ready: true + max-request-message: 10KB + max-response-message: 20KB + timeout: 30s + """) + @SuppressWarnings("unchecked") + void methodConfig() throws Exception { + Map map = bindAndGetAsMap(); + assertThat(map).containsKey("methodConfig"); + Map serviceMethodMap = getServiceMethodMap(map, false); + assertThat(serviceMethodMap).containsOnlyKeys("s-one/m-one", "s-two/m-two"); + Object methodInfo = serviceMethodMap.get("s-one/m-one"); + assertThat(methodInfo).extracting("timeoutNanos").isEqualTo(Duration.ofSeconds(30).toNanos()); + assertThat(methodInfo).extracting("waitForReady").isEqualTo(Boolean.TRUE); + assertThat(methodInfo).extracting("maxOutboundMessageSize").isEqualTo(10240); + assertThat(methodInfo).extracting("maxInboundMessageSize").isEqualTo(20480); + } + + @Test + @WithResource(name = "config.yaml", content = """ + config: + method: + - name: + - service: s-one + method: m-one + retry: + max-attempts: 2 + initial-backoff: 1m + max-backoff: 1h + backoff-multiplier: 2.5 + per-attempt-receive-timeout: 2s + retryable-status-codes: + - cancelled + - already-exists + """) + void methodConfigRetryPolicy() throws Exception { + Map map = bindAndGetAsMap(); + Map serviceMethodMap = getServiceMethodMap(map, true); + Object methodInfo = serviceMethodMap.get("s-one/m-one"); + assertThat(methodInfo).extracting("retryPolicy.maxAttempts").isEqualTo(2); + assertThat(methodInfo).extracting("retryPolicy.initialBackoffNanos").isEqualTo(Duration.ofMinutes(1).toNanos()); + assertThat(methodInfo).extracting("retryPolicy.maxBackoffNanos").isEqualTo(Duration.ofHours(1).toNanos()); + assertThat(methodInfo).extracting("retryPolicy.backoffMultiplier").isEqualTo(2.5); + assertThat(methodInfo).extracting("retryPolicy.perAttemptRecvTimeoutNanos") + .isEqualTo(Duration.ofSeconds(2).toNanos()); + assertThat(methodInfo).extracting("retryPolicy.retryableStatusCodes") + .asInstanceOf(InstanceOfAssertFactories.SET) + .containsExactlyInAnyOrder(Code.CANCELLED, Code.ALREADY_EXISTS); + } + + @Test + @WithResource(name = "config.yaml", content = """ + config: + method: + - name: + - service: s-one + method: m-one + hedging: + max-attempts: 4 + delay: 6s + non-fatal-status-codes: + - invalid-argument + - deadline-exceeded + """) + void methodConfigHedgingPolicy() throws Exception { + Map map = bindAndGetAsMap(); + Map serviceMethodMap = getServiceMethodMap(map, true); + Object methodInfo = serviceMethodMap.get("s-one/m-one"); + assertThat(methodInfo).extracting("hedgingPolicy.maxAttempts").isEqualTo(4); + assertThat(methodInfo).extracting("hedgingPolicy.hedgingDelayNanos").isEqualTo(Duration.ofSeconds(6).toNanos()); + assertThat(methodInfo).extracting("hedgingPolicy.nonFatalStatusCodes") + .asInstanceOf(InstanceOfAssertFactories.SET) + .containsExactlyInAnyOrder(Code.INVALID_ARGUMENT, Code.DEADLINE_EXCEEDED); + } + + @Test + @WithResource(name = "config.yaml", content = """ + config: + method: + - name: + - service: s-one + method: m-one + retry: {} + hedging: {} + """) + void whenMultiplePoliciesInMethodConfigThrowsException() { + assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindAndGetAsMap()) + .havingRootCause() + .isInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class); + } + + @Test + @WithResource(name = "config.yaml", content = """ + config: + retrythrottling: + max-tokens: 2.5 + token-ratio: 1.5 + """) + void retryThrottling() throws Exception { + Map map = bindAndGetAsMap(); + assertThat(map).containsKey("retryThrottling"); + Object throttle = ReflectionTestUtils.invokeMethod(ServiceConfigUtil.class, "getThrottlePolicy", map); + assertThat(throttle).extracting("maxTokens").isEqualTo(2500); + assertThat(throttle).extracting("tokenRatio").isEqualTo(1500); + } + + @Test + @WithResource(name = "config.yaml", content = """ + config: + healthcheck: + service-name: test + """) + @SuppressWarnings("unchecked") + void healthCheck() throws Exception { + Map map = bindAndGetAsMap(); + assertThat(map).containsKey("healthCheckConfig"); + Map healthCheckedService = (Map) ServiceConfigUtil.getHealthCheckedService(map); + assertThat(healthCheckedService).hasSize(1).containsEntry("serviceName", "test"); + } + + private PolicySelection getLoadBalancingPolicySelection(List> rawConfigs) { + List unwrappedConfigs = ServiceConfigUtil.unwrapLoadBalancingConfigList(rawConfigs); + LoadBalancerRegistry registry = LoadBalancerRegistry.getDefaultRegistry(); + ConfigOrError selected = ServiceConfigUtil.selectLbPolicyFromList(unwrappedConfigs, registry); + assertThat(selected).isNotNull(); + PolicySelection policySelection = (PolicySelection) selected.getConfig(); + if (policySelection == null) { + System.err.println(selected); + System.err.println(selected.getError()); + if (selected.getError() != null && selected.getError().asException() != null) { + selected.getError().asException().printStackTrace(); + } + } + assertThat(policySelection).isNotNull(); + return policySelection; + } + + @SuppressWarnings("unchecked") + private Map getServiceMethodMap(Map map, boolean retryEnabled) { + ScParser scParser = new ScParser(retryEnabled, 100, 100, new AutoConfiguredLoadBalancerFactory("pick_first")); + Object config = scParser.parseServiceConfig(map).getConfig(); + assertThat(config).isNotNull(); + Object serviceMethodMap = ReflectionTestUtils.getField(config, "serviceMethodMap"); + assertThat(serviceMethodMap).isNotNull(); + return (Map) serviceMethodMap; + } + + private Map bindAndGetAsMap() throws Exception { + Map map = new LinkedHashMap<>(); + bind().applyTo(map); + return map; + } + + private ServiceConfig bind() throws Exception { + YamlPropertySourceLoader loader = new YamlPropertySourceLoader(); + PropertySource propertySource = loader.load("config.yaml", new ClassPathResource("config.yaml")).get(0); + MockEnvironment environment = new MockEnvironment(); + environment.getPropertySources().addLast(propertySource); + Binder binder = Binder.get(environment); + return binder.bind("config", ServiceConfig.class).get(); + } + +} From a72db5b670313a551b45dc7e3c0cfe721c4657cf Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 19 Mar 2026 15:55:39 -0700 Subject: [PATCH 392/930] Use cleaned path when checking for generated source Update `JavaConventions` to use a cleaned path when checking for generated sources to ensure that files are also detected on Windows. See gh-49153 --- .../java/org/springframework/boot/build/JavaConventions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java index eff1d527d829..4c552e876aa8 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java @@ -309,7 +309,7 @@ private SourceTask excludeGeneratedSources(SourceTask task) { } private boolean isGeneratedSource(FileTreeElement candidate) { - String path = candidate.getFile().getPath(); + String path = StringUtils.cleanPath(candidate.getFile().getPath()); return path.contains("/generated/sources/") || path.contains("/generated-source/"); } From 328e91b4921c0c705319bf7e46149130c4815e9e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 19 Mar 2026 16:47:06 -0700 Subject: [PATCH 393/930] Use testcontains mechanism to find access local gRPC address See gh-49540 --- .../SampleGrpcServerNettyShadedApplicationTests.java | 4 +++- .../SampleGrpcServerServletApplicationTests.java | 6 ++++-- .../grpcserver/SampleGrpcServerApplicationTests.java | 5 ++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/dockerTest/java/smoketest/grpcservernettyshaded/SampleGrpcServerNettyShadedApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/dockerTest/java/smoketest/grpcservernettyshaded/SampleGrpcServerNettyShadedApplicationTests.java index e058762d2dc5..63af451767b1 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/dockerTest/java/smoketest/grpcservernettyshaded/SampleGrpcServerNettyShadedApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/dockerTest/java/smoketest/grpcservernettyshaded/SampleGrpcServerNettyShadedApplicationTests.java @@ -47,7 +47,9 @@ class SampleGrpcServerNettyShadedApplicationTests { @Test @SuppressWarnings("resource") void test() { - String address = "host.docker.internal:" + this.startedEventListener.getPort(); + int port = this.startedEventListener.getPort(); + String address = "host.testcontainers.internal:" + port; + org.testcontainers.Testcontainers.exposeHostPorts(port); try (GenericContainer container = new GenericContainer<>( DockerImageName.parse("fullstorydev/grpcurl:v1.9.3")) .withCommand("-d", "{\"name\": \"spring\"}", "--plaintext", address, "HelloWorld/SayHello") diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/dockerTest/java/smoketest/grpcserverservlet/SampleGrpcServerServletApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/dockerTest/java/smoketest/grpcserverservlet/SampleGrpcServerServletApplicationTests.java index b2aaa1a2ab3b..41cff7716dc0 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/dockerTest/java/smoketest/grpcserverservlet/SampleGrpcServerServletApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/src/dockerTest/java/smoketest/grpcserverservlet/SampleGrpcServerServletApplicationTests.java @@ -38,12 +38,14 @@ class SampleGrpcServerServletApplicationTests { @LocalServerPort - private String localServerPort; + private int localServerPort; @Test @SuppressWarnings("resource") void test() { - String address = "host.docker.internal:" + this.localServerPort; + int port = this.localServerPort; + String address = "host.testcontainers.internal:" + port; + org.testcontainers.Testcontainers.exposeHostPorts(port); try (GenericContainer container = new GenericContainer<>( DockerImageName.parse("fullstorydev/grpcurl:v1.9.3")) .withCommand("-d", "{\"name\": \"spring\"}", "--plaintext", address, "HelloWorld/SayHello") diff --git a/smoke-test/spring-boot-smoke-test-grpc-server/src/dockerTest/java/smoketest/grpcserver/SampleGrpcServerApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-server/src/dockerTest/java/smoketest/grpcserver/SampleGrpcServerApplicationTests.java index 0e34103552bb..ca79ef5cf5dc 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server/src/dockerTest/java/smoketest/grpcserver/SampleGrpcServerApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-grpc-server/src/dockerTest/java/smoketest/grpcserver/SampleGrpcServerApplicationTests.java @@ -47,9 +47,12 @@ class SampleGrpcServerApplicationTests { @Test @SuppressWarnings("resource") void test() { - String address = "host.docker.internal:" + this.startedEventListener.getPort(); + int port = this.startedEventListener.getPort(); + String address = "host.testcontainers.internal:" + port; + org.testcontainers.Testcontainers.exposeHostPorts(port); try (GenericContainer container = new GenericContainer<>( DockerImageName.parse("fullstorydev/grpcurl:v1.9.3")) + .withNetworkAliases("") .withCommand("-d", "{\"name\": \"spring\"}", "--plaintext", address, "HelloWorld/SayHello") .withStartupCheckStrategy(new IndefiniteWaitOneShotStartupCheckStrategy())) { container.start(); From 57080f9ef58d16d5675c70b919009baa4f17274d Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 19 Mar 2026 17:54:34 -0700 Subject: [PATCH 394/930] Switch to Spring gRPC 1.1.0-M1 See gh-49670 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4f5925d72a9d..2f510354c2ed 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2587,7 +2587,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-graphql/releases/tag/v{version}") } } - library("Spring gRPC", "1.1.0-SNAPSHOT") { + library("Spring gRPC", "1.1.0-M1") { considerSnapshots() group("org.springframework.grpc") { modules = [ From f728235906473900edfc02e3e544a911b5c0d00d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 20 Mar 2026 10:53:28 +0100 Subject: [PATCH 395/930] Revert "Introduce Generic AMQP 1.0 client support" This reverts commit 8dde82b557d0a3020f89b088d9ba19f5b4b1e9ec, c388dc92 5b7f0f726b9be082b09b0714cadb1b00bff79280, and 2d71ef94ed2b5b82070720984baa2ffeb4cc9e7d. See gh-49621 --- .../DocumentConfigurationProperties.java | 1 - documentation/spring-boot-docs/build.gradle | 1 - .../antora/modules/ROOT/pages/redirect.adoc | 14 +- .../pages/features/dev-services.adoc | 3 - .../reference/pages/messaging/amqp.adoc | 64 +--- .../pages/testing/testcontainers.adoc | 3 - .../amqp/generic/sending/MyBean.java | 37 --- .../amqp/{rabbitmq => }/receiving/MyBean.java | 2 +- .../receiving/custom/MyBean.java | 2 +- .../receiving/custom/MyMessageConverter.java | 2 +- .../custom/MyRabbitConfiguration.java | 2 +- .../amqp/{rabbitmq => }/sending/MyBean.java | 2 +- .../messaging/amqp/generic/sending/MyBean.kt | 32 -- .../amqp/{rabbitmq => }/receiving/MyBean.kt | 2 +- .../{rabbitmq => }/receiving/custom/MyBean.kt | 2 +- .../receiving/custom/MyMessageConverter.kt | 2 +- .../receiving/custom/MyRabbitConfiguration.kt | 2 +- .../amqp/{rabbitmq => }/sending/MyBean.kt | 2 +- module/spring-boot-amqp/build.gradle | 61 ---- ...AmqpAutoConfigurationIntegrationTests.java | 74 ----- ...nectionDetailsFactoryIntegrationTests.java | 42 --- ...nectionDetailsFactoryIntegrationTests.java | 71 ----- .../src/dockerTest/resources/logback-test.xml | 4 - .../dockerTest/resources/spring.properties | 1 - .../autoconfigure/AmqpAutoConfiguration.java | 132 --------- .../autoconfigure/AmqpClientCustomizer.java | 37 --- .../autoconfigure/AmqpConnectionDetails.java | 62 ---- .../amqp/autoconfigure/AmqpProperties.java | 125 -------- .../ConnectionOptionsCustomizer.java | 39 --- .../PropertiesAmqpConnectionDetails.java | 49 ---- .../boot/amqp/autoconfigure/package-info.java | 23 -- ...DockerComposeConnectionDetailsFactory.java | 84 ------ .../docker/compose/RabbitMqEnvironment.java | 50 ---- .../amqp/docker/compose/package-info.java | 23 -- ...itMqContainerConnectionDetailsFactory.java | 71 ----- .../amqp/testcontainers/package-info.java | 23 -- .../main/resources/META-INF/spring.factories | 5 - ...ot.autoconfigure.AutoConfiguration.imports | 1 - .../AmqpAutoConfigurationTests.java | 276 ------------------ .../compose/RabbitMqEnvironmentTests.java | 72 ----- .../build.gradle | 3 - ...ectionDetailsFactoryIntegrationTests.java} | 11 +- ...ectionDetailsFactoryIntegrationTests.java} | 8 +- ...ectionDetailsFactoryIntegrationTests.java} | 8 +- ...ectionDetailsFactoryIntegrationTests.java} | 9 +- ...nectionDetailsFactoryIntegrationTests.java | 2 +- ...ectionDetailsFactoryIntegrationTests.java} | 4 +- ...ectionDetailsFactoryIntegrationTests.java} | 10 +- ...ectionDetailsFactoryIntegrationTests.java} | 8 +- ...ectionDetailsFactoryIntegrationTests.java} | 8 +- ...ectionDetailsFactoryIntegrationTests.java} | 10 +- ...bit-stream-separate-services-compose.yaml} | 0 ...rabbit-stream-single-service-compose.yaml} | 0 .../docker/compose/rabbit-compose.yaml} | 0 ...l-compose.yaml => rabbit-ssl-compose.yaml} | 0 ...ompose.yaml => rabbit-stream-compose.yaml} | 0 ...se.yaml => rabbit-stream-ssl-compose.yaml} | 0 .../docker/compose/rabbitmq-compose.yaml | 8 - ...ockerComposeConnectionDetailsFactory.java} | 8 +- ...nvironment.java => RabbitEnvironment.java} | 4 +- ...ockerComposeConnectionDetailsFactory.java} | 8 +- ...bbitContainerConnectionDetailsFactory.java | 2 +- ...bitContainerConnectionDetailsFactory.java} | 2 +- ...eamContainerConnectionDetailsFactory.java} | 4 +- .../main/resources/META-INF/spring.factories | 8 +- ...Tests.java => RabbitEnvironmentTests.java} | 28 +- .../spring-boot-dependencies/build.gradle | 7 +- settings.gradle | 3 - .../build.gradle | 26 -- starter/spring-boot-starter-amqp/build.gradle | 27 -- .../RabbitMqManagementContainer.java | 57 ---- .../boot/testsupport/container/TestImage.java | 5 +- 72 files changed, 100 insertions(+), 1678 deletions(-) delete mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{rabbitmq => }/receiving/MyBean.java (92%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{rabbitmq => }/receiving/custom/MyBean.java (91%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{rabbitmq => }/receiving/custom/MyMessageConverter.java (93%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{rabbitmq => }/receiving/custom/MyRabbitConfiguration.java (95%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{rabbitmq => }/sending/MyBean.java (94%) delete mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{rabbitmq => }/receiving/MyBean.kt (92%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{rabbitmq => }/receiving/custom/MyBean.kt (92%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{rabbitmq => }/receiving/custom/MyMessageConverter.kt (92%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{rabbitmq => }/receiving/custom/MyRabbitConfiguration.kt (95%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{rabbitmq => }/sending/MyBean.kt (93%) delete mode 100644 module/spring-boot-amqp/build.gradle delete mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java delete mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java delete mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java delete mode 100644 module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml delete mode 100644 module/spring-boot-amqp/src/dockerTest/resources/spring.properties delete mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java delete mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java delete mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java delete mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java delete mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java delete mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java delete mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java delete mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java delete mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironment.java delete mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java delete mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactory.java delete mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java delete mode 100644 module/spring-boot-amqp/src/main/resources/META-INF/spring.factories delete mode 100644 module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports delete mode 100644 module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java delete mode 100644 module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironmentTests.java rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/{RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java => RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java} (84%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/{RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java => RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java} (87%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/{RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java => RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java} (85%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/{SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java => SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java} (85%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/{RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java => RabbitContainerConnectionDetailsFactoryIntegrationTests.java} (95%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/{RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java => RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java} (90%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/{RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java => RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java} (92%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/{SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java => SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java} (92%) rename module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/{SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java => SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java} (91%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/{rabbitmq-and-rabbitmq-stream-separate-services-compose.yaml => rabbit-and-rabbit-stream-separate-services-compose.yaml} (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/{rabbitmq-and-rabbitmq-stream-single-service-compose.yaml => rabbit-and-rabbit-stream-single-service-compose.yaml} (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-compose.yaml} (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/{rabbitmq-ssl-compose.yaml => rabbit-ssl-compose.yaml} (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/{rabbitmq-stream-compose.yaml => rabbit-stream-compose.yaml} (100%) rename module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/{rabbitmq-stream-ssl-compose.yaml => rabbit-stream-ssl-compose.yaml} (100%) delete mode 100644 module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-compose.yaml rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/{RabbitMqDockerComposeConnectionDetailsFactory.java => RabbitDockerComposeConnectionDetailsFactory.java} (92%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/{RabbitMqEnvironment.java => RabbitEnvironment.java} (93%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/{RabbitMqStreamDockerComposeConnectionDetailsFactory.java => RabbitStreamDockerComposeConnectionDetailsFactory.java} (92%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/{RabbitMqContainerConnectionDetailsFactory.java => RabbitContainerConnectionDetailsFactory.java} (98%) rename module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/{RabbitMqStreamContainerConnectionDetailsFactory.java => RabbitStreamContainerConnectionDetailsFactory.java} (96%) rename module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/{RabbitMqEnvironmentTests.java => RabbitEnvironmentTests.java} (59%) delete mode 100644 starter/spring-boot-starter-amqp-test/build.gradle delete mode 100644 starter/spring-boot-starter-amqp/build.gradle delete mode 100644 test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.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 5eb829029896..5051fdcbfa35 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,7 +171,6 @@ private void dataMigrationPrefixes(Config prefix) { private void integrationPrefixes(Config prefix) { prefix.accept("spring.activemq"); - prefix.accept("spring.amqp"); prefix.accept("spring.artemis"); prefix.accept("spring.batch"); prefix.accept("spring.integration"); diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index fe01de87d6fb..0a31d6f6f402 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -94,7 +94,6 @@ dependencies { implementation(project(path: ":loader:spring-boot-loader-tools")) implementation(project(path: ":module:spring-boot-actuator")) implementation(project(path: ":module:spring-boot-actuator-autoconfigure")) - implementation(project(path: ":module:spring-boot-amqp")) implementation(project(path: ":module:spring-boot-cache")) implementation(project(path: ":module:spring-boot-cache-test")) implementation(project(path: ":module:spring-boot-data-cassandra")) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc index 42d3b57c37fd..72414df0faae 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc @@ -1716,13 +1716,13 @@ * xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#features.messaging.amqp.rabbit] * xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#messaging.amqp.rabbit] * xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#messaging.amqp.rabbitmq] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#boot-features-using-amqp-receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#features.messaging.amqp.receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#messaging.amqp.receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending-stream[#messaging.amqp.sending-stream] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#boot-features-using-amqp-sending] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#features.messaging.amqp.sending] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#messaging.amqp.sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.receiving[#boot-features-using-amqp-receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.receiving[#features.messaging.amqp.receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.receiving[#messaging.amqp.receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.sending-stream[#messaging.amqp.sending-stream] +* xref:reference:messaging/amqp.adoc#messaging.amqp.sending[#boot-features-using-amqp-sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.sending[#features.messaging.amqp.sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.sending[#messaging.amqp.sending] * xref:reference:messaging/amqp.adoc#messaging.amqp[#boot-features-amqp] * xref:reference:messaging/amqp.adoc#messaging.amqp[#features.messaging.amqp] * xref:reference:messaging/amqp.adoc#messaging.amqp[#messaging.amqp] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc index f816ca64d25c..36e57ff478db 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc @@ -86,9 +86,6 @@ The following service connections are currently supported: | javadoc:org.springframework.boot.activemq.autoconfigure.ActiveMQConnectionDetails[] | Containers named "symptoma/activemq" or "apache/activemq-classic" -| javadoc:org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails[] -| Containers named "rabbitmq" - | javadoc:org.springframework.boot.artemis.autoconfigure.ArtemisConnectionDetails[] | Containers named "apache/activemq-artemis" diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index 15cf865bd868..3864f7ebe9ea 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc @@ -3,53 +3,7 @@ The Advanced Message Queuing Protocol (AMQP) is a platform-neutral, wire-level protocol for message-oriented middleware. The Spring AMQP project applies core Spring concepts to the development of AMQP-based messaging solutions. -Spring Boot offers several conveniences for working with AMQP: generic AMQP 1.0 support is provided by the `spring-boot-starter-amqp` starter while specific RabbitMQ support is available via the `spring-boot-starter-rabbitmq` starter. - - - -[[messaging.amqp.generic]] -== Generic AMQP 1.0 Support - -AMQP 1.0 is supported by several brokers and messaging services beyond RabbitMQ, including ActiveMQ, Azure Service Bus, and others. -Spring AMQP provides {url-spring-amqp-docs}/amqp10-client.html[generic support for AMQP 1.0] via `org.springframework.amqp:spring-amqp-client` that is based on the https://github.com/apache/qpid-protonj2/blob/main/protonj2-client/README.md[Qpid ProtonJ2 Client Library]. - -AMQP configuration is controlled by external configuration properties in `+spring.amqp.*+`. -For example, you might declare the following in your configuration: - -[configprops,yaml] ----- -spring: - amqp: - host: "localhost" - port: 5672 - username: "admin" - password: "secret" ----- - -To configure javadoc:org.apache.qpid.protonj2.client.ConnectionOptions[connection options] of the auto-configured javadoc:org.springframework.amqp.client.AmqpConnectionFactory[], define a javadoc:org.springframework.boot.amqp.autoconfigure.ConnectionOptionsCustomizer[] bean. - -[[messaging.amqp.generic.sending]] -=== Sending a Message - -Spring's javadoc:org.springframework.amqp.client.AmqpClient[] is auto-configured, and you can autowire it directly into your own beans, as shown in the following example: - -include-code::MyBean[] - -If a javadoc:org.springframework.amqp.support.converter.MessageConverter[] bean is defined, it is associated automatically with the auto-configured javadoc:org.springframework.amqp.client.AmqpClient[]. -If no such converter is defined and Jackson is available, javadoc:org.springframework.amqp.support.converter.JacksonJsonMessageConverter[] is used. - -Client-specific settings can be configured as follows: - -[configprops,yaml] ----- -spring: - amqp: - client: - default-to-address: "/queues/default_queue" - completion-timeout: "500ms" ----- - -To further configure the auto-configured javadoc:org.springframework.amqp.client.AmqpClient[], define a javadoc:org.springframework.boot.amqp.autoconfigure.AmqpClientCustomizer[] bean. +Spring Boot offers several conveniences for working with AMQP through RabbitMQ, including the `spring-boot-starter-rabbitmq` starter. @@ -95,8 +49,8 @@ TIP: See https://spring.io/blog/2010/06/14/understanding-amqp-the-protocol-used- -[[messaging.amqp.rabbitmq.sending]] -=== Sending a Message +[[messaging.amqp.sending]] +== Sending a Message Spring's javadoc:org.springframework.amqp.core.AmqpTemplate[] and javadoc:org.springframework.amqp.core.AmqpAdmin[] are auto-configured, and you can autowire them directly into your own beans, as shown in the following example: @@ -128,8 +82,8 @@ If there's a bean of type javadoc:org.springframework.amqp.rabbit.support.microm -[[messaging.amqp.rabbitmq.sending-stream]] -=== Sending a Message To A Stream +[[messaging.amqp.sending-stream]] +== Sending a Message To A Stream To send a message to a particular stream, specify the name of the stream, as shown in the following example: @@ -147,14 +101,14 @@ If you need to create more javadoc:org.springframework.rabbit.stream.producer.Ra -[[messaging.amqp.rabbitmq.sending-stream.ssl]] -==== SSL +[[messaging.amqp.sending-stream.ssl]] +=== SSL To use SSL with RabbitMQ Streams, set configprop:spring.rabbitmq.stream.ssl.enabled[] to `true` or set configprop:spring.rabbitmq.stream.ssl.bundle[] to configure the xref:features/ssl.adoc#features.ssl.bundles[SSL bundle] to use. -[[messaging.amqp.rabbitmq.receiving]] -=== Receiving a Message +[[messaging.amqp.receiving]] +== Receiving a Message When the Rabbit infrastructure is present, any bean can be annotated with javadoc:org.springframework.amqp.rabbit.annotation.RabbitListener[format=annotation] to create a listener endpoint. If no javadoc:org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory[] has been defined, a default javadoc:org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory[] is automatically configured and you can switch to a direct container using the configprop:spring.rabbitmq.listener.type[] property. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc index c925920a077d..72240022f6ad 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc @@ -122,9 +122,6 @@ The following service connection factories are provided in the `spring-boot-test | javadoc:org.springframework.boot.activemq.autoconfigure.ActiveMQConnectionDetails[] | Containers named "symptoma/activemq" or javadoc:org.testcontainers.activemq.ActiveMQContainer[] -| javadoc:org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails[] -| Containers of type javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] - | javadoc:org.springframework.boot.artemis.autoconfigure.ArtemisConnectionDetails[] | Containers of type javadoc:org.testcontainers.activemq.ArtemisContainer[] diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java deleted file mode 100644 index 8f7ffc09ff5c..000000000000 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.generic.sending; - -import org.springframework.amqp.client.AmqpClient; -import org.springframework.stereotype.Component; - -@Component -public class MyBean { - - private final AmqpClient amqpClient; - - public MyBean(AmqpClient amqpClient) { - this.amqpClient = amqpClient; - } - - // @fold:on // ... - public void sendMessage(String msg) { - this.amqpClient.to("/queues/test").body(msg).send(); - } - // @fold:off - -} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.java similarity index 92% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.java index 56064bfb94ce..d5fd3014a058 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving; +package org.springframework.boot.docs.messaging.amqp.receiving; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.java similarity index 91% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.java index 671de83f0e3d..0f0c59f87c99 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom; +package org.springframework.boot.docs.messaging.amqp.receiving.custom; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.java similarity index 93% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.java index 18169c7e7aba..ba8d391a53f3 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom; +package org.springframework.boot.docs.messaging.amqp.receiving.custom; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java similarity index 95% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java index 318428b08be0..172c9cbba0d1 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom; +package org.springframework.boot.docs.messaging.amqp.receiving.custom; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/sending/MyBean.java similarity index 94% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/sending/MyBean.java index 51b91d35769b..f2adb052cecb 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/sending/MyBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmq.sending; +package org.springframework.boot.docs.messaging.amqp.sending; import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.AmqpTemplate; diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt deleted file mode 100644 index 64c5174f4177..000000000000 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.generic.sending - -import org.springframework.amqp.client.AmqpClient -import org.springframework.stereotype.Component - -@Component -class MyBean(private val amqpClient: AmqpClient) { - - // @fold:on // ... - fun someOtherMethod(msg: String) { - amqpClient.to("/queues/test").body(msg).send() - } - // @fold:off - -} - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.kt similarity index 92% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.kt index 9bac053496d2..a3241ddb95b8 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving +package org.springframework.boot.docs.messaging.amqp.receiving import org.springframework.amqp.rabbit.annotation.RabbitListener import org.springframework.stereotype.Component diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.kt similarity index 92% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.kt index 3b90bf3dc42d..f92ffc012497 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom +package org.springframework.boot.docs.messaging.amqp.receiving.custom import org.springframework.amqp.rabbit.annotation.RabbitListener import org.springframework.stereotype.Component diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.kt similarity index 92% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.kt index 03b165448e08..7b6265875ccb 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom +package org.springframework.boot.docs.messaging.amqp.receiving.custom import org.springframework.amqp.core.Message import org.springframework.amqp.core.MessageProperties diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt similarity index 95% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt index 900866bf2464..95ca6f610039 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom +package org.springframework.boot.docs.messaging.amqp.receiving.custom import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory import org.springframework.amqp.rabbit.connection.CachingConnectionFactory diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/sending/MyBean.kt similarity index 93% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/sending/MyBean.kt index 6d21de9bce0a..e8ec5274920e 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/sending/MyBean.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmq.sending +package org.springframework.boot.docs.messaging.amqp.sending import org.springframework.amqp.core.AmqpAdmin import org.springframework.amqp.core.AmqpTemplate diff --git a/module/spring-boot-amqp/build.gradle b/module/spring-boot-amqp/build.gradle deleted file mode 100644 index 7aebee0bdcb8..000000000000 --- a/module/spring-boot-amqp/build.gradle +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -plugins { - id "java-library" - id "org.springframework.boot.docker-test" - id "org.springframework.boot.auto-configuration" - id "org.springframework.boot.configuration-properties" - id "org.springframework.boot.deployed" - id "org.springframework.boot.optional-dependencies" -} - -description = "Spring Boot support for AMQP 1.0" - -dependencies { - api(project(":core:spring-boot")) - api("org.apache.qpid:protonj2-client") - api("org.springframework:spring-messaging") - api("org.springframework.amqp:spring-amqp-client") - - implementation(project(":module:spring-boot-transaction")) - - optional(project(":core:spring-boot-autoconfigure")) - optional(project(":core:spring-boot-docker-compose")) - optional(project(":module:spring-boot-jackson")) - optional(project(":core:spring-boot-testcontainers")) - optional("org.testcontainers:testcontainers-rabbitmq") - - dockerTestImplementation(project(":core:spring-boot-test")) - dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) - dockerTestImplementation(testFixtures(project(":core:spring-boot-docker-compose"))) - dockerTestImplementation("ch.qos.logback:logback-classic") - dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") - - - testImplementation(project(":core:spring-boot-test")) - testImplementation(project(":test-support:spring-boot-test-support")) - - testRuntimeOnly("ch.qos.logback:logback-classic") -} - -tasks.named("compileTestJava") { - options.nullability.checking = "tests" -} - -tasks.named("compileDockerTestJava") { - options.nullability.checking = "tests" -} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java deleted file mode 100644 index d08d29a55980..000000000000 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.autoconfigure; - -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.Test; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import org.springframework.amqp.client.AmqpClient; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.boot.testsupport.container.RabbitMqManagementContainer; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration tests for {@link AmqpAutoConfiguration}. - * - * @author Stephane Nicoll - */ -@Testcontainers(disabledWithoutDocker = true) -class AmqpAutoConfigurationIntegrationTests { - - @Container - static final RabbitMqManagementContainer container = new RabbitMqManagementContainer(); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(AmqpAutoConfiguration.class)) - .withPropertyValues("spring.amqp.host=" + container.getHost(), "spring.amqp.port=" + container.getAmqpPort()); - - @Test - void sendAndReceiveUsingAmqpClient() { - String queue = container.createRandomQueue(); - this.contextRunner.run((context) -> { - AmqpClient amqpClient = context.getBean(AmqpClient.class); - assertThat(amqpClient.to(queue).body("Hello World").send().get(1, TimeUnit.SECONDS)).isTrue(); - assertThat(amqpClient.from(queue).receiveAndConvert().get(1, TimeUnit.SECONDS)).isEqualTo("Hello World"); - }); - } - - @Test - void sendAndReceiveUsingJson() { - String queue = container.createRandomQueue(); - this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)).run((context) -> { - AmqpClient amqpClient = context.getBean(AmqpClient.class); - assertThat(amqpClient.to(queue).body(new TestMessage("hello", 42)).send().get(1, TimeUnit.SECONDS)) - .isTrue(); - assertThat(amqpClient.from(queue).receiveAndConvert().get(1, TimeUnit.SECONDS)) - .isEqualTo(new TestMessage("hello", 42)); - }); - } - - record TestMessage(String value, int counter) { - - } - -} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java deleted file mode 100644 index 04198e1e32bd..000000000000 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.docker.compose; - -import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails.Address; -import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; -import org.springframework.boot.testsupport.container.TestImage; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration tests for {@link RabbitMqDockerComposeConnectionDetailsFactory}. - * - * @author Stephane Nicoll - */ -class RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests { - - @DockerComposeTest(composeFile = "rabbitmq-compose.yaml", image = TestImage.RABBITMQ) - void runCreatesConnectionDetails(AmqpConnectionDetails connectionDetails) { - assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); - assertThat(connectionDetails.getPassword()).isEqualTo("secret"); - Address address = connectionDetails.getAddress(); - assertThat(address.host()).isNotNull(); - assertThat(address.port()).isGreaterThan(0); - } - -} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java deleted file mode 100644 index 483ec6e90ad3..000000000000 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.testcontainers; - -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.Test; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import org.springframework.amqp.client.AmqpClient; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.amqp.autoconfigure.AmqpAutoConfiguration; -import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.testsupport.container.RabbitMqManagementContainer; -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 RabbitMqContainerConnectionDetailsFactory}. - * - * @author Stephane Nicoll - */ -@SpringJUnitConfig -@Testcontainers(disabledWithoutDocker = true) -class RabbitMqContainerConnectionDetailsFactoryIntegrationTests { - - @Container - @ServiceConnection - static final RabbitMqManagementContainer container = new RabbitMqManagementContainer(); - - @Autowired(required = false) - private AmqpConnectionDetails connectionDetails; - - @Autowired - private AmqpClient amqpClient; - - @Test - void connectionCanBeMadeToRabbitMqContainer() throws Exception { - assertThat(this.connectionDetails).isNotNull(); - container.createQueue("test"); - this.amqpClient.to("/queues/test").body("test message").send(); - Object message = this.amqpClient.from("/queues/test").receiveAndConvert().get(4, TimeUnit.MINUTES); - assertThat(message).isEqualTo("test message"); - } - - @Configuration(proxyBeanMethods = false) - @ImportAutoConfiguration(AmqpAutoConfiguration.class) - static class TestConfiguration { - - } - -} diff --git a/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml b/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml deleted file mode 100644 index b8a41480d7d6..000000000000 --- a/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/module/spring-boot-amqp/src/dockerTest/resources/spring.properties b/module/spring-boot-amqp/src/dockerTest/resources/spring.properties deleted file mode 100644 index 47dff33f0bb5..000000000000 --- a/module/spring-boot-amqp/src/dockerTest/resources/spring.properties +++ /dev/null @@ -1 +0,0 @@ -spring.test.context.cache.maxSize=1 \ No newline at end of file diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java deleted file mode 100644 index 24babf2b4883..000000000000 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.autoconfigure; - -import java.util.stream.Stream; - -import org.apache.qpid.protonj2.client.Client; -import org.apache.qpid.protonj2.client.ClientOptions; -import org.apache.qpid.protonj2.client.ConnectionOptions; -import tools.jackson.databind.json.JsonMapper; - -import org.springframework.amqp.client.AmqpClient; -import org.springframework.amqp.client.AmqpConnectionFactory; -import org.springframework.amqp.client.SingleAmqpConnectionFactory; -import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; -import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails.Address; -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.ConditionalOnSingleCandidate; -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.Configuration; - -/** - * Auto-configuration for generic AMQP 1.0 support. - * - * @author Stephane Nicoll - * @since 4.1.0 - */ -@AutoConfiguration(afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration") -@ConditionalOnClass({ Client.class, AmqpConnectionFactory.class }) -@EnableConfigurationProperties(AmqpProperties.class) -public final class AmqpAutoConfiguration { - - @Configuration(proxyBeanMethods = false) - static class AmqpConnectionFactoryConfiguration { - - @Bean - @ConditionalOnMissingBean - Client protonClient() { - return Client.create(new ClientOptions()); - } - - @Bean - @ConditionalOnMissingBean - AmqpConnectionDetails amqpConnectionDetails(AmqpProperties properties) { - return new PropertiesAmqpConnectionDetails(properties); - } - - @Bean - @ConditionalOnMissingBean - AmqpConnectionFactory amqpConnectionFactory(AmqpConnectionDetails connectionDetails, Client protonClient, - ObjectProvider connectionOptionsCustomizers) { - ConnectionOptions connectionOptions = createConnectionOptions(connectionDetails, - connectionOptionsCustomizers.orderedStream()); - return createAmqpConnectionFactory(connectionDetails, protonClient).setConnectionOptions(connectionOptions); - } - - private SingleAmqpConnectionFactory createAmqpConnectionFactory(AmqpConnectionDetails connectionDetails, - Client protonClient) { - SingleAmqpConnectionFactory connectionFactory = new SingleAmqpConnectionFactory(protonClient); - PropertyMapper map = PropertyMapper.get(); - Address address = connectionDetails.getAddress(); - map.from(address::host).to(connectionFactory::setHost); - map.from(address::port).to(connectionFactory::setPort); - return connectionFactory; - } - - private ConnectionOptions createConnectionOptions(AmqpConnectionDetails connectionDetails, - Stream customizers) { - ConnectionOptions connectionOptions = new ConnectionOptions(); - PropertyMapper map = PropertyMapper.get(); - map.from(connectionDetails::getUsername).to(connectionOptions::user); - map.from(connectionDetails::getPassword).to(connectionOptions::password); - customizers.forEach((customizer) -> customizer.customize(connectionOptions)); - return connectionOptions; - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnBean(JsonMapper.class) - @ConditionalOnSingleCandidate(JsonMapper.class) - static class JacksonMessageConverterConfiguration { - - @Bean - @ConditionalOnMissingBean(MessageConverter.class) - JacksonJsonMessageConverter jacksonJsonMessageConverter(JsonMapper jsonMapper) { - return new JacksonJsonMessageConverter(jsonMapper); - } - - } - - @Configuration(proxyBeanMethods = false) - static class AmqpClientConfiguration { - - @Bean - @ConditionalOnMissingBean - AmqpClient amqpClient(AmqpConnectionFactory amqpConnectionFactory, AmqpProperties properties, - ObjectProvider messageConverter, ObjectProvider customizers) { - AmqpClient.Builder builder = AmqpClient.builder(amqpConnectionFactory); - AmqpProperties.Client client = properties.getClient(); - PropertyMapper map = PropertyMapper.get(); - map.from(client.getDefaultToAddress()).to(builder::defaultToAddress); - map.from(client.getCompletionTimeout()).to(builder::completionTimeout); - messageConverter.ifAvailable(builder::messageConverter); - customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - return builder.build(); - } - - } - -} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java deleted file mode 100644 index b2ab3c2ed766..000000000000 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.autoconfigure; - -import org.springframework.amqp.client.AmqpClient; - -/** - * Callback interface that can be used to customize the auto-configured - * {@link AmqpClient.Builder}. - * - * @author Stephane Nicoll - * @since 4.1.0 - */ -@FunctionalInterface -public interface AmqpClientCustomizer { - - /** - * Callback to customize a {@link AmqpClient.Builder} instance. - * @param amqpClientBuilder the client builder to customize - */ - void customize(AmqpClient.Builder amqpClientBuilder); - -} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java deleted file mode 100644 index 246183800aea..000000000000 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.autoconfigure; - -import org.jspecify.annotations.Nullable; - -import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; - -/** - * Details required to establish a connection to an AMQP broker. - * - * @author Stephane Nicoll - * @since 4.1.0 - */ -public interface AmqpConnectionDetails extends ConnectionDetails { - - /** - * Return the {@link Address} of the broker. - * @return the address - */ - Address getAddress(); - - /** - * Login user to authenticate to the broker. - * @return the login user to authenticate to the broker or {@code null} - */ - default @Nullable String getUsername() { - return null; - } - - /** - * Password used to authenticate to the broker. - * @return the password to authenticate to the broker or {@code null} - */ - default @Nullable String getPassword() { - return null; - } - - /** - * An AMQP broker address. - * - * @param host the host - * @param port the port - */ - record Address(String host, int port) { - } - -} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java deleted file mode 100644 index 7091fc292252..000000000000 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.autoconfigure; - -import java.time.Duration; - -import org.jspecify.annotations.Nullable; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for AMQP 1.0 connectivity. - * - * @author Stephane Nicoll - * @since 4.1.0 - */ -@ConfigurationProperties("spring.amqp") -public class AmqpProperties { - - /** - * AMQP broker host. - */ - private String host = "localhost"; - - /** - * AMQP broker port. - */ - private int port = 5672; - - /** - * Login user to authenticate to the broker. - */ - private @Nullable String username; - - /** - * Password used to authenticate to the broker. - */ - private @Nullable String password; - - private final Client client = new Client(); - - public String getHost() { - return this.host; - } - - public void setHost(String host) { - this.host = host; - } - - public int getPort() { - return this.port; - } - - public void setPort(int port) { - this.port = port; - } - - public @Nullable String getUsername() { - return this.username; - } - - public void setUsername(@Nullable String username) { - this.username = username; - } - - public @Nullable String getPassword() { - return this.password; - } - - public void setPassword(@Nullable String password) { - this.password = password; - } - - public Client getClient() { - return this.client; - } - - /** - * Client-level settings. - */ - public static class Client { - - /** - * Default destination address for send operations when none is specified. - */ - private @Nullable String defaultToAddress; - - /** - * Maximum time to wait for request operations to complete. - */ - private Duration completionTimeout = Duration.ofSeconds(60); - - public @Nullable String getDefaultToAddress() { - return this.defaultToAddress; - } - - public void setDefaultToAddress(@Nullable String defaultToAddress) { - this.defaultToAddress = defaultToAddress; - } - - public Duration getCompletionTimeout() { - return this.completionTimeout; - } - - public void setCompletionTimeout(Duration completionTimeout) { - this.completionTimeout = completionTimeout; - } - - } - -} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java deleted file mode 100644 index 9d3c54f1cdb5..000000000000 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.autoconfigure; - -import org.apache.qpid.protonj2.client.ConnectionOptions; - -import org.springframework.amqp.client.AmqpConnectionFactory; - -/** - * Callback interface for customizing {@link ConnectionOptions} on the auto-configured - * {@link AmqpConnectionFactory}. - * - * @author Stephane Nicoll - * @since 4.1.0 - */ -@FunctionalInterface -public interface ConnectionOptionsCustomizer { - - /** - * Customize the {@link ConnectionOptions}. - * @param connectionOptions the connection options to customize - */ - void customize(ConnectionOptions connectionOptions); - -} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java deleted file mode 100644 index 13e47d047a5f..000000000000 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.autoconfigure; - -import org.jspecify.annotations.Nullable; - -/** - * Adapts {@link AmqpProperties} to {@link AmqpConnectionDetails}. - * - * @author Stephane Nicoll - */ -class PropertiesAmqpConnectionDetails implements AmqpConnectionDetails { - - private final AmqpProperties properties; - - PropertiesAmqpConnectionDetails(AmqpProperties properties) { - this.properties = properties; - } - - @Override - public Address getAddress() { - return new Address(this.properties.getHost(), this.properties.getPort()); - } - - @Override - public @Nullable String getUsername() { - return this.properties.getUsername(); - } - - @Override - public @Nullable String getPassword() { - return this.properties.getPassword(); - } - -} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java deleted file mode 100644 index fef2b40e8d3f..000000000000 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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 generic AMQP 1.0 support. - */ -@NullMarked -package org.springframework.boot.amqp.autoconfigure; - -import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java deleted file mode 100644 index 09a45147c4f2..000000000000 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.docker.compose; - -import org.jspecify.annotations.Nullable; - -import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; -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 AmqpConnectionDetails} - * for a {@code rabbitmq} service. - * - * @author Stephane Nicoll - */ -class RabbitMqDockerComposeConnectionDetailsFactory - extends DockerComposeConnectionDetailsFactory { - - private static final int RABBITMQ_PORT = 5672; - - protected RabbitMqDockerComposeConnectionDetailsFactory() { - super("rabbitmq"); - } - - @Override - protected @Nullable AmqpConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { - try { - return new RabbitMqDockerComposeConnectionDetails(source.getRunningService()); - } - catch (IllegalStateException ex) { - return null; - } - } - - /** - * {@link AmqpConnectionDetails} backed by a {@code rabbitmq} {@link RunningService}. - */ - static class RabbitMqDockerComposeConnectionDetails extends DockerComposeConnectionDetails - implements AmqpConnectionDetails { - - private final RabbitMqEnvironment environment; - - private final Address address; - - protected RabbitMqDockerComposeConnectionDetails(RunningService service) { - super(service); - this.environment = new RabbitMqEnvironment(service.env()); - this.address = new Address(service.host(), service.ports().get(RABBITMQ_PORT)); - } - - @Override - public Address getAddress() { - return this.address; - } - - @Override - public @Nullable String getUsername() { - return this.environment.getUsername(); - } - - @Override - public @Nullable String getPassword() { - return this.environment.getPassword(); - } - - } - -} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironment.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironment.java deleted file mode 100644 index 0da210981ae9..000000000000 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironment.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.docker.compose; - -import java.util.Map; - -import org.jspecify.annotations.Nullable; - -/** - * RabbitMQ environment details. - * - * @author Moritz Halbritter - * @author Andy Wilkinson - * @author Phillip Webb - * @author Scott Frederick - */ -class RabbitMqEnvironment { - - private final @Nullable String username; - - private final @Nullable String password; - - RabbitMqEnvironment(Map env) { - this.username = env.getOrDefault("RABBITMQ_DEFAULT_USER", env.getOrDefault("RABBITMQ_USERNAME", "guest")); - this.password = env.getOrDefault("RABBITMQ_DEFAULT_PASS", env.getOrDefault("RABBITMQ_PASSWORD", "guest")); - } - - @Nullable String getUsername() { - return this.username; - } - - @Nullable String getPassword() { - return this.password; - } - -} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java deleted file mode 100644 index 6408a2ff0030..000000000000 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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 generic AMQP service connections. - */ -@NullMarked -package org.springframework.boot.amqp.docker.compose; - -import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactory.java deleted file mode 100644 index a36da1c19bee..000000000000 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactory.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.testcontainers; - -import java.net.URI; - -import org.testcontainers.rabbitmq.RabbitMQContainer; - -import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; -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 AmqpConnectionDetails} from - * a {@link ServiceConnection @ServiceConnection}-annotated {@link RabbitMQContainer}. - * - * @author Stephane Nicoll - */ -class RabbitMqContainerConnectionDetailsFactory - extends ContainerConnectionDetailsFactory { - - @Override - protected AmqpConnectionDetails getContainerConnectionDetails(ContainerConnectionSource source) { - return new RabbitMqContainerConnectionDetails(source); - } - - /** - * {@link AmqpConnectionDetails} for RabbitMQ backed by a - * {@link ContainerConnectionSource}. - */ - static final class RabbitMqContainerConnectionDetails extends ContainerConnectionDetails - implements AmqpConnectionDetails { - - private RabbitMqContainerConnectionDetails(ContainerConnectionSource source) { - super(source); - } - - @Override - public Address getAddress() { - URI uri = URI.create(getContainer().getAmqpUrl()); - return new Address(uri.getHost(), uri.getPort()); - } - - @Override - public String getUsername() { - return getContainer().getAdminUsername(); - } - - @Override - public String getPassword() { - return getContainer().getAdminPassword(); - } - - } - -} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java deleted file mode 100644 index 398ec731847a..000000000000 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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 generic AMQP service connections. - */ -@NullMarked -package org.springframework.boot.amqp.testcontainers; - -import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories b/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories deleted file mode 100644 index d3f0d273fc15..000000000000 --- a/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,5 +0,0 @@ -# Connection Details Factories -org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ -org.springframework.boot.amqp.docker.compose.RabbitMqDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.amqp.testcontainers.RabbitMqContainerConnectionDetailsFactory - diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 1bbaad209b67..000000000000 --- a/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1 +0,0 @@ -org.springframework.boot.amqp.autoconfigure.AmqpAutoConfiguration diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java deleted file mode 100644 index 42fe2fa2cef4..000000000000 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.autoconfigure; - -import java.time.Duration; - -import org.apache.qpid.protonj2.client.Client; -import org.apache.qpid.protonj2.client.ConnectionOptions; -import org.assertj.core.api.InstanceOfAssertFactories; -import org.junit.jupiter.api.Test; -import org.mockito.InOrder; -import tools.jackson.databind.json.JsonMapper; - -import org.springframework.amqp.client.AmqpClient; -import org.springframework.amqp.client.AmqpConnectionFactory; -import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; -import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails.Address; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; -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 static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link AmqpAutoConfiguration}. - * - * @author Stephane Nicoll - */ -class AmqpAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(AmqpAutoConfiguration.class)); - - @Test - void autoConfigurationConfiguresConnectionFactoryWithDefaultSettings() { - this.contextRunner.run((context) -> { - AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); - assertThat(connectionFactory).hasFieldOrPropertyWithValue("host", "localhost"); - assertThat(connectionFactory).hasFieldOrPropertyWithValue("port", 5672); - assertThat(connectionFactory) - .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) - .satisfies((connectOptions) -> { - assertThat(connectOptions).hasFieldOrPropertyWithValue("user", null); - assertThat(connectOptions).hasFieldOrPropertyWithValue("password", null); - }); - }); - } - - @Test - void autoConfigurationConfiguresConnectionFactoryWithOverrides() { - this.contextRunner - .withPropertyValues("spring.amqp.host=amqp.example.com", "spring.amqp.port=1234", - "spring.amqp.username=user", "spring.amqp.password=secret") - .run((context) -> { - AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); - assertThat(connectionFactory).hasFieldOrPropertyWithValue("host", "amqp.example.com"); - assertThat(connectionFactory).hasFieldOrPropertyWithValue("port", 1234); - assertThat(connectionFactory) - .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) - .satisfies((connectOptions) -> { - assertThat(connectOptions).hasFieldOrPropertyWithValue("user", "user"); - assertThat(connectOptions).hasFieldOrPropertyWithValue("password", "secret"); - }); - }); - } - - @Test - void autoConfigurationUsesAmqpConnectionDetailsForConnectionFactory() { - AmqpConnectionDetails connectionDetails = mock(); - given(connectionDetails.getAddress()).willReturn(new Address("details.example.com", 9999)); - given(connectionDetails.getUsername()).willReturn("from-details"); - given(connectionDetails.getPassword()).willReturn("details-secret"); - this.contextRunner.withBean(AmqpConnectionDetails.class, () -> connectionDetails) - .withPropertyValues("spring.amqp.host=ignored.example.com", "spring.amqp.port=1111", - "spring.amqp.username=ignored", "spring.amqp.password=ignored") - .run((context) -> { - AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); - assertThat(connectionFactory).hasFieldOrPropertyWithValue("host", "details.example.com"); - assertThat(connectionFactory).hasFieldOrPropertyWithValue("port", 9999); - assertThat(connectionFactory) - .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) - .satisfies((connectOptions) -> { - assertThat(connectOptions).hasFieldOrPropertyWithValue("user", "from-details"); - assertThat(connectOptions).hasFieldOrPropertyWithValue("password", "details-secret"); - }); - }); - } - - @Test - void autoConfigurationAppliesConnectionOptionsCustomizer() { - this.contextRunner - .withBean(ConnectionOptionsCustomizer.class, () -> (connectOptions) -> connectOptions.user("custom-user")) - .withPropertyValues("spring.amqp.username=user") - .run((context) -> { - AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); - assertThat(connectionFactory) - .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) - .satisfies((connectOptions) -> assertThat(connectOptions).hasFieldOrPropertyWithValue("user", - "custom-user")); - }); - } - - @Test - void autoConfigurationAppliesConnectionOptionsCustomizersInOrder() { - this.contextRunner.withUserConfiguration(ConnectionOptionsCustomizersConfiguration.class).run((context) -> { - ConnectionOptionsCustomizer first = context.getBean("first", ConnectionOptionsCustomizer.class); - ConnectionOptionsCustomizer second = context.getBean("second", ConnectionOptionsCustomizer.class); - InOrder ordered = inOrder(first, second); - ordered.verify(first).customize(any()); - ordered.verify(second).customize(any()); - }); - } - - @Test - void autoConfigurationBacksOffWhenUserProvidesProtonClient() { - Client protonClient = mock(); - this.contextRunner.withBean(Client.class, () -> protonClient).run((context) -> { - assertThat(context).hasSingleBean(Client.class); - assertThat(context.getBean(Client.class)).isSameAs(protonClient); - }); - } - - @Test - void autoConfigurationBacksOffWhenUserProvidesAmqpConnectionDetails() { - AmqpConnectionDetails connectionDetails = mock(); - given(connectionDetails.getAddress()).willReturn(new Address("details.example.com", 9999)); - this.contextRunner.withBean(AmqpConnectionDetails.class, () -> connectionDetails).run((context) -> { - assertThat(context).hasSingleBean(AmqpConnectionDetails.class); - assertThat(context.getBean(AmqpConnectionDetails.class)).isSameAs(connectionDetails); - }); - } - - @Test - void autoConfigurationBacksOffWhenUserProvidesAmqpConnectionFactory() { - AmqpConnectionFactory amqpConnectionFactory = mock(); - this.contextRunner.withBean(AmqpConnectionFactory.class, () -> amqpConnectionFactory).run((context) -> { - assertThat(context).hasSingleBean(AmqpConnectionFactory.class); - assertThat(context.getBean(AmqpConnectionFactory.class)).isSameAs(amqpConnectionFactory); - }); - } - - @Test - void autoConfigurationConfiguresAmqpClientWithOverrides() { - this.contextRunner - .withPropertyValues("spring.amqp.client.default-to-address=/queues/default_queue", - "spring.amqp.client.completion-timeout=20s") - .run((context) -> { - AmqpClient amqpClient = context.getBean(AmqpClient.class); - assertThat(amqpClient).hasFieldOrPropertyWithValue("defaultToAddress", "/queues/default_queue"); - assertThat(amqpClient).hasFieldOrPropertyWithValue("completionTimeout", Duration.ofSeconds(20)); - }); - } - - @Test - void autoConfigurationConfiguresAmqpClientWithJacksonMessageConverter() { - this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)).run((context) -> { - assertThat(context).hasSingleBean(JsonMapper.class); - AmqpClient amqpClient = context.getBean(AmqpClient.class); - assertThat(amqpClient) - .extracting("messageConverter", InstanceOfAssertFactories.type(MessageConverter.class)) - .satisfies((messageConverter) -> assertThat(messageConverter) - .isInstanceOf(JacksonJsonMessageConverter.class) - .hasFieldOrPropertyWithValue("objectMapper", context.getBean(JsonMapper.class))); - }); - } - - @Test - void autoConfigurationConfiguresAmqpClientWithMessageConverter() { - MessageConverter messageConverter = mock(MessageConverter.class); - this.contextRunner.withBean(MessageConverter.class, () -> messageConverter).run((context) -> { - AmqpClient amqpClient = context.getBean(AmqpClient.class); - assertThat(amqpClient).hasFieldOrPropertyWithValue("messageConverter", messageConverter); - }); - } - - @Test - void autoConfigurationBacksOffWhenUserProvidesAmqpClient() { - AmqpClient amqpClient = mock(); - this.contextRunner.withBean(AmqpClient.class, () -> amqpClient).run((context) -> { - assertThat(context).hasSingleBean(AmqpClient.class); - assertThat(context.getBean(AmqpClient.class)).isSameAs(amqpClient); - }); - } - - @Test - void autoConfigurationUsesUserMessageConverterWhenDefinedAlongsideJackson() { - MessageConverter messageConverter = mock(MessageConverter.class); - this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) - .withBean(MessageConverter.class, () -> messageConverter) - .run((context) -> { - assertThat(context).doesNotHaveBean(JacksonJsonMessageConverter.class); - AmqpClient amqpClient = context.getBean(AmqpClient.class); - assertThat(amqpClient).hasFieldOrPropertyWithValue("messageConverter", messageConverter); - }); - } - - @Test - void autoConfigurationAppliesAmqpClientCustomizer() { - this.contextRunner - .withBean(AmqpClientCustomizer.class, - () -> (client) -> client.defaultToAddress("/queues/custom_default_queue")) - .withPropertyValues("spring.amqp.client.default-to-address=/queues/default_queue") - .run((context) -> { - AmqpClient amqpClient = context.getBean(AmqpClient.class); - assertThat(amqpClient).hasFieldOrPropertyWithValue("defaultToAddress", "/queues/custom_default_queue"); - }); - } - - @Test - void autoConfigurationAppliesAmqpClientCustomizersInOrder() { - this.contextRunner.withUserConfiguration(AmqpClientCustomizersConfiguration.class).run((context) -> { - AmqpClientCustomizer first = context.getBean("first", AmqpClientCustomizer.class); - AmqpClientCustomizer second = context.getBean("second", AmqpClientCustomizer.class); - InOrder ordered = inOrder(first, second); - ordered.verify(first).customize(any()); - ordered.verify(second).customize(any()); - }); - } - - @Configuration(proxyBeanMethods = false) - static class ConnectionOptionsCustomizersConfiguration { - - @Bean - @Order(2) - ConnectionOptionsCustomizer second() { - return mock(); - } - - @Bean - @Order(1) - ConnectionOptionsCustomizer first() { - return mock(); - } - - } - - @Configuration(proxyBeanMethods = false) - static class AmqpClientCustomizersConfiguration { - - @Bean - @Order(2) - AmqpClientCustomizer second() { - return mock(); - } - - @Bean - @Order(1) - AmqpClientCustomizer first() { - return mock(); - } - - } - -} diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironmentTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironmentTests.java deleted file mode 100644 index 01decb6785cb..000000000000 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironmentTests.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.docker.compose; - -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 RabbitMqEnvironment}. - * - * @author Moritz Halbritter - * @author Andy Wilkinson - * @author Phillip Webb - * @author Scott Frederick - */ -class RabbitMqEnvironmentTests { - - @Test - void getUsernameWhenNoRabbitMqDefaultUser() { - RabbitMqEnvironment environment = new RabbitMqEnvironment(Collections.emptyMap()); - assertThat(environment.getUsername()).isEqualTo("guest"); - } - - @Test - void getUsernameWhenHasRabbitMqDefaultUser() { - RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_DEFAULT_USER", "me")); - assertThat(environment.getUsername()).isEqualTo("me"); - } - - @Test - void getUsernameWhenHasRabbitMqUsername() { - RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_USERNAME", "me")); - assertThat(environment.getUsername()).isEqualTo("me"); - } - - @Test - void getPasswordWhenNoRabbitMqDefaultPass() { - RabbitMqEnvironment environment = new RabbitMqEnvironment(Collections.emptyMap()); - assertThat(environment.getPassword()).isEqualTo("guest"); - } - - @Test - void getPasswordWhenHasRabbitMqDefaultPass() { - RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); - assertThat(environment.getPassword()).isEqualTo("secret"); - } - - @Test - void getPasswordWhenHasRabbitMqPassword() { - RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_PASSWORD", "secret")); - assertThat(environment.getPassword()).isEqualTo("secret"); - } - -} diff --git a/module/spring-boot-autoconfigure-classic-modules/build.gradle b/module/spring-boot-autoconfigure-classic-modules/build.gradle index 2b45c0d9ec76..06e1e54df35c 100644 --- a/module/spring-boot-autoconfigure-classic-modules/build.gradle +++ b/module/spring-boot-autoconfigure-classic-modules/build.gradle @@ -28,9 +28,6 @@ dependencies { api(project(":module:spring-boot-activemq")) { transitive = false } - api(project(":module:spring-boot-amqp")) { - transitive = false - } api(project(":module:spring-boot-artemis")) { transitive = false } diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 84% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index 0c4902328afb..dcef4737265a 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -25,14 +25,14 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link RabbitMqDockerComposeConnectionDetailsFactory} and - * {@link RabbitMqStreamDockerComposeConnectionDetailsFactory} when used in combination. + * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory} and + * {@link RabbitStreamDockerComposeConnectionDetailsFactory} when used in combination. * * @author Andy Wilkinson */ -class RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests { +class RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests { - @DockerComposeTest(composeFile = "rabbitmq-and-rabbitmq-stream-separate-services-compose.yaml", + @DockerComposeTest(composeFile = "rabbit-and-rabbit-stream-separate-services-compose.yaml", image = TestImage.RABBITMQ) void runCreatesConnectionDetailsFromSeparateServices(RabbitConnectionDetails connectionDetails, RabbitStreamConnectionDetails streamConnectionDetails) { @@ -40,8 +40,7 @@ void runCreatesConnectionDetailsFromSeparateServices(RabbitConnectionDetails con assertConnectionDetails(streamConnectionDetails); } - @DockerComposeTest(composeFile = "rabbitmq-and-rabbitmq-stream-single-service-compose.yaml", - image = TestImage.RABBITMQ) + @DockerComposeTest(composeFile = "rabbit-and-rabbit-stream-single-service-compose.yaml", image = TestImage.RABBITMQ) void runCreatesConnectionDetailsFromSingleService(RabbitConnectionDetails connectionDetails, RabbitStreamConnectionDetails streamConnectionDetails) { assertConnectionDetails(connectionDetails); diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 87% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java index fb482a6cbb83..fb1e627f60e4 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -25,22 +25,22 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link RabbitMqDockerComposeConnectionDetailsFactory}. + * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory}. * * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb * @author Scott Frederick */ -class RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests { +class RabbitDockerComposeConnectionDetailsFactoryIntegrationTests { - @DockerComposeTest(composeFile = "rabbitmq-compose.yaml", image = TestImage.RABBITMQ) + @DockerComposeTest(composeFile = "rabbit-compose.yaml", image = TestImage.RABBITMQ) void runCreatesConnectionDetails(RabbitConnectionDetails connectionDetails) { assertConnectionDetails(connectionDetails); assertThat(connectionDetails.getSslBundle()).isNull(); } - @DockerComposeTest(composeFile = "rabbitmq-ssl-compose.yaml", image = TestImage.RABBITMQ, + @DockerComposeTest(composeFile = "rabbit-ssl-compose.yaml", image = TestImage.RABBITMQ, additionalResources = { "../../ca.crt", "../../server.crt", "../../server.key", "../../client.crt", "../../client.key", "rabbitmq-ssl.conf" }) void runWithSslCreatesConnectionDetails(RabbitConnectionDetails connectionDetails) { diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 85% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index 1a4d0fdf9817..b54ab7270fb4 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -24,18 +24,18 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link RabbitMqStreamDockerComposeConnectionDetailsFactory}. + * Integration tests for {@link RabbitStreamDockerComposeConnectionDetailsFactory}. * * @author Andy Wilkinson */ -class RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests { +class RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests { - @DockerComposeTest(composeFile = "rabbitmq-stream-compose.yaml", image = TestImage.RABBITMQ) + @DockerComposeTest(composeFile = "rabbit-stream-compose.yaml", image = TestImage.RABBITMQ) void runCreatesConnectionDetails(RabbitStreamConnectionDetails connectionDetails) { assertConnectionDetails(connectionDetails); } - @DockerComposeTest(composeFile = "rabbitmq-stream-ssl-compose.yaml", image = TestImage.RABBITMQ, + @DockerComposeTest(composeFile = "rabbit-stream-ssl-compose.yaml", image = TestImage.RABBITMQ, additionalResources = { "../../ca.crt", "../../server.crt", "../../server.key", "../../client.crt", "../../client.key", "rabbitmq-stream-ssl.conf" }) void runWithSslCreatesConnectionDetails(RabbitStreamConnectionDetails connectionDetails) { diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 85% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index af67b52c44b0..ae9d61597f63 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -25,16 +25,15 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link RabbitMqDockerComposeConnectionDetailsFactory} and - * {@link RabbitMqStreamDockerComposeConnectionDetailsFactory} when using a single service + * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory} and + * {@link RabbitStreamDockerComposeConnectionDetailsFactory} when using a single service * for standard and stream-based messaging. * * @author Andy Wilkinson */ -class SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests { +class SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests { - @DockerComposeTest(composeFile = "rabbitmq-and-rabbitmq-stream-single-service-compose.yaml", - image = TestImage.RABBITMQ) + @DockerComposeTest(composeFile = "rabbit-and-rabbit-stream-single-service-compose.yaml", image = TestImage.RABBITMQ) void runCreatesConnectionDetails(RabbitConnectionDetails connectionDetails, RabbitStreamConnectionDetails streamConnectionDetails) { assertConnectionDetails(connectionDetails); diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java index 61b0a43c5311..91aed53b74b5 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java @@ -48,7 +48,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link RabbitMqContainerConnectionDetailsFactoryIntegrationTests}. + * {@link RabbitContainerConnectionDetailsFactoryIntegrationTests}. */ @SpringJUnitConfig @Testcontainers(disabledWithoutDocker = true) diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java similarity index 95% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java index 0de6be2fad4a..f371b8562284 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java @@ -42,7 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitMqContainerConnectionDetailsFactory}. + * Tests for {@link RabbitContainerConnectionDetailsFactory}. * * @author Moritz Halbritter * @author Andy Wilkinson @@ -50,7 +50,7 @@ */ @SpringJUnitConfig @Testcontainers(disabledWithoutDocker = true) -class RabbitMqContainerConnectionDetailsFactoryIntegrationTests { +class RabbitContainerConnectionDetailsFactoryIntegrationTests { @Container @ServiceConnection diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 90% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java index 3f3981b727d9..8820db4606a9 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -36,8 +36,8 @@ import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; @@ -50,8 +50,8 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitMqStreamContainerConnectionDetailsFactory} with a single - * container that's only used for streams. + * Tests for {@link RabbitStreamContainerConnectionDetailsFactory} with a single container + * that's only used for streams. * * @author Eddú Meléndez * @author Andy Wilkinson @@ -60,7 +60,7 @@ @TestPropertySource( properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) @Testcontainers(disabledWithoutDocker = true) -class RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests { +class RabbitStreamContainerConnectionDetailsFactoryIntegrationTests { private static final int RABBITMQ_STREAMS_PORT = 5552; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java similarity index 92% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java index c13ddc3eae7a..6eab916aef27 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java @@ -37,8 +37,8 @@ import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.PemKeyStore; import org.springframework.boot.testcontainers.service.connection.PemTrustStore; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -53,7 +53,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitMqStreamContainerConnectionDetailsFactory} with SSL. + * Tests for {@link RabbitStreamContainerConnectionDetailsFactory} with SSL. * * @author Eddú Meléndez * @author Andy Wilkinson @@ -62,7 +62,7 @@ @TestPropertySource( properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) @Testcontainers(disabledWithoutDocker = true) -class RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests { +class RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests { private static final int RABBITMQ_STREAMS_TLS_PORT = 5551; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 92% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java index 099cd3816864..e28c2657a7c8 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -40,8 +40,8 @@ import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; @@ -54,7 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitMqStreamContainerConnectionDetailsFactory} with two containers, + * Tests for {@link RabbitStreamContainerConnectionDetailsFactory} with two containers, * one for streams and one for standard messaging. * * @author Eddú Meléndez @@ -64,7 +64,7 @@ @TestPropertySource( properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) @Testcontainers(disabledWithoutDocker = true) -class SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests { +class SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests { private static final int RABBITMQ_STREAMS_PORT = 5552; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 91% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java index 686a447bc2e5..3d306284fa4f 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -40,8 +40,8 @@ import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; @@ -54,8 +54,8 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitMqStreamContainerConnectionDetailsFactory} with a single - * container used for both streams and standard messaging. + * Tests for {@link RabbitStreamContainerConnectionDetailsFactory} with a single container + * used for both streams and standard messaging. * * @author Eddú Meléndez * @author Andy Wilkinson @@ -64,7 +64,7 @@ @TestPropertySource( properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) @Testcontainers(disabledWithoutDocker = true) -class SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests { +class SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests { private static final int RABBITMQ_STREAMS_PORT = 5552; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-and-rabbitmq-stream-separate-services-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-and-rabbitmq-stream-separate-services-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-and-rabbitmq-stream-single-service-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-and-rabbitmq-stream-single-service-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-compose.yaml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-ssl-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-ssl-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-ssl-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-ssl-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-ssl-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-ssl-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-ssl-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-ssl-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-compose.yaml deleted file mode 100644 index 1951fba4bb08..000000000000 --- a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-compose.yaml +++ /dev/null @@ -1,8 +0,0 @@ -services: - rabbitmq: - image: '{imageName}' - environment: - - 'RABBITMQ_DEFAULT_USER=myuser' - - 'RABBITMQ_DEFAULT_PASS=secret' - ports: - - '5672' diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java similarity index 92% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java index 30b7ab878cb1..bd662a242aac 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java @@ -33,12 +33,12 @@ * * @author Andy Wilkinson */ -class RabbitMqDockerComposeConnectionDetailsFactory +class RabbitDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { private static final int RABBITMQ_PORT = 5672; - protected RabbitMqDockerComposeConnectionDetailsFactory() { + protected RabbitDockerComposeConnectionDetailsFactory() { super("rabbitmq"); } @@ -60,7 +60,7 @@ protected RabbitMqDockerComposeConnectionDetailsFactory() { static class RabbitDockerComposeConnectionDetails extends DockerComposeConnectionDetails implements RabbitConnectionDetails { - private final RabbitMqEnvironment environment; + private final RabbitEnvironment environment; private final List

    addresses; @@ -68,7 +68,7 @@ static class RabbitDockerComposeConnectionDetails extends DockerComposeConnectio protected RabbitDockerComposeConnectionDetails(RunningService service) { super(service); - this.environment = new RabbitMqEnvironment(service.env()); + this.environment = new RabbitEnvironment(service.env()); this.sslBundle = getSslBundle(service); this.addresses = List.of(new Address(service.host(), service.ports().get(RABBITMQ_PORT))); } diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironment.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironment.java similarity index 93% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironment.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironment.java index a71a8207ec71..1d37579e1a84 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironment.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironment.java @@ -28,13 +28,13 @@ * @author Phillip Webb * @author Scott Frederick */ -class RabbitMqEnvironment { +class RabbitEnvironment { private final @Nullable String username; private final @Nullable String password; - RabbitMqEnvironment(Map env) { + RabbitEnvironment(Map env) { this.username = env.getOrDefault("RABBITMQ_DEFAULT_USER", env.getOrDefault("RABBITMQ_USERNAME", "guest")); this.password = env.getOrDefault("RABBITMQ_DEFAULT_PASS", env.getOrDefault("RABBITMQ_PASSWORD", "guest")); } diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java similarity index 92% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java index 9a0b8f22bc31..5b1fbbb87941 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java @@ -35,10 +35,10 @@ * @author Scott Frederick * @author Jay Choi */ -class RabbitMqStreamDockerComposeConnectionDetailsFactory +class RabbitStreamDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { - protected RabbitMqStreamDockerComposeConnectionDetailsFactory() { + protected RabbitStreamDockerComposeConnectionDetailsFactory() { super("rabbitmq"); } @@ -64,7 +64,7 @@ static class RabbitStreamDockerComposeConnectionDetails extends DockerComposeCon private static final int STREAMS_TLS_PORT = 5551; - private final RabbitMqEnvironment environment; + private final RabbitEnvironment environment; private final String host; @@ -74,7 +74,7 @@ static class RabbitStreamDockerComposeConnectionDetails extends DockerComposeCon protected RabbitStreamDockerComposeConnectionDetails(RunningService service) { super(service); - this.environment = new RabbitMqEnvironment(service.env()); + this.environment = new RabbitEnvironment(service.env()); this.host = service.host(); this.sslBundle = getSslBundle(service); this.port = service.ports().get((this.sslBundle != null) ? STREAMS_TLS_PORT : STREAMS_PORT); diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java index 750d611deb67..445d8410611a 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java @@ -37,7 +37,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link RabbitMqContainerConnectionDetailsFactory}. + * {@link RabbitContainerConnectionDetailsFactory}. */ @Deprecated(since = "4.0.0", forRemoval = true) class DeprecatedRabbitContainerConnectionDetailsFactory diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactory.java similarity index 98% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactory.java index 5e13fc5d999c..32a89a3e23ac 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactory.java @@ -37,7 +37,7 @@ * @author Andy Wilkinson * @author Phillip Webb */ -class RabbitMqContainerConnectionDetailsFactory +class RabbitContainerConnectionDetailsFactory extends ContainerConnectionDetailsFactory { @Override diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java index 17171157b33f..dccd1d441417 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java @@ -33,10 +33,10 @@ * @author Eddú Meléndez * @author Jay Choi */ -class RabbitMqStreamContainerConnectionDetailsFactory +class RabbitStreamContainerConnectionDetailsFactory extends ContainerConnectionDetailsFactory { - RabbitMqStreamContainerConnectionDetailsFactory() { + RabbitStreamContainerConnectionDetailsFactory() { super(ANY_CONNECTION_NAME, "org.springframework.rabbit.stream.producer.RabbitStreamTemplate"); } diff --git a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories index a7a1d458e73e..e3667de2bbcf 100644 --- a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories @@ -1,8 +1,8 @@ # Connection Details Factories org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ -org.springframework.boot.rabbitmq.docker.compose.RabbitMqDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.rabbitmq.docker.compose.RabbitMqStreamDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.rabbitmq.docker.compose.RabbitDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.rabbitmq.docker.compose.RabbitStreamDockerComposeConnectionDetailsFactory,\ org.springframework.boot.rabbitmq.testcontainers.DeprecatedRabbitContainerConnectionDetailsFactory,\ -org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory,\ -org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory +org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory,\ +org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironmentTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironmentTests.java similarity index 59% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironmentTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironmentTests.java index 533ee69f8c24..356d93ad535e 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironmentTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironmentTests.java @@ -24,48 +24,48 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitMqEnvironment}. + * Tests for {@link RabbitEnvironment}. * * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb * @author Scott Frederick */ -class RabbitMqEnvironmentTests { +class RabbitEnvironmentTests { @Test - void getUsernameWhenNoRabbitMqDefaultUser() { - RabbitMqEnvironment environment = new RabbitMqEnvironment(Collections.emptyMap()); + void getUsernameWhenNoRabbitmqDefaultUser() { + RabbitEnvironment environment = new RabbitEnvironment(Collections.emptyMap()); assertThat(environment.getUsername()).isEqualTo("guest"); } @Test - void getUsernameWhenHasRabbitMqDefaultUser() { - RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_DEFAULT_USER", "me")); + void getUsernameWhenHasRabbitmqDefaultUser() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_DEFAULT_USER", "me")); assertThat(environment.getUsername()).isEqualTo("me"); } @Test - void getUsernameWhenHasRabbitMqUsername() { - RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_USERNAME", "me")); + void getUsernameWhenHasRabbitmqUsername() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_USERNAME", "me")); assertThat(environment.getUsername()).isEqualTo("me"); } @Test - void getUsernameWhenNoRabbitMqDefaultPass() { - RabbitMqEnvironment environment = new RabbitMqEnvironment(Collections.emptyMap()); + void getUsernameWhenNoRabbitmqDefaultPass() { + RabbitEnvironment environment = new RabbitEnvironment(Collections.emptyMap()); assertThat(environment.getPassword()).isEqualTo("guest"); } @Test - void getUsernameWhenHasRabbitMqDefaultPass() { - RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); + void getUsernameWhenHasRabbitmqDefaultPass() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); assertThat(environment.getPassword()).isEqualTo("secret"); } @Test - void getUsernameWhenHasRabbitMqPassword() { - RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_PASSWORD", "secret")); + void getUsernameWhenHasRabbitmqPassword() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_PASSWORD", "secret")); assertThat(environment.getPassword()).isEqualTo("secret"); } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 2f510354c2ed..20e4cb262a9e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2118,7 +2118,6 @@ bom { "spring-boot-activemq", "spring-boot-actuator", "spring-boot-actuator-autoconfigure", - "spring-boot-amqp", "spring-boot-artemis", "spring-boot-autoconfigure", "spring-boot-autoconfigure-classic", @@ -2239,8 +2238,6 @@ bom { "spring-boot-starter-activemq-test", "spring-boot-starter-actuator", "spring-boot-starter-actuator-test", - "spring-boot-starter-amqp", - "spring-boot-starter-amqp-test", "spring-boot-starter-artemis", "spring-boot-starter-artemis-test", "spring-boot-starter-aspectj", @@ -2356,8 +2353,8 @@ bom { "spring-boot-starter-quartz-test", "spring-boot-starter-r2dbc", "spring-boot-starter-r2dbc-test", - "spring-boot-starter-rabbitmq", - "spring-boot-starter-rabbitmq-test", + "spring-boot-starter-rabbitmqmq", + "spring-boot-starter-rabbitmqmq-test", "spring-boot-starter-reactor-netty", "spring-boot-starter-restclient", "spring-boot-starter-restclient-test", diff --git a/settings.gradle b/settings.gradle index 94d968336d5a..e9181fe88526 100644 --- a/settings.gradle +++ b/settings.gradle @@ -80,7 +80,6 @@ include "core:spring-boot-testcontainers" include "module:spring-boot-activemq" include "module:spring-boot-actuator" include "module:spring-boot-actuator-autoconfigure" -include "module:spring-boot-amqp" include "module:spring-boot-artemis" include "module:spring-boot-autoconfigure-classic" include "module:spring-boot-autoconfigure-classic-modules" @@ -212,8 +211,6 @@ include "starter:spring-boot-starter-activemq" include "starter:spring-boot-starter-activemq-test" include "starter:spring-boot-starter-actuator" include "starter:spring-boot-starter-actuator-test" -include "starter:spring-boot-starter-amqp" -include "starter:spring-boot-starter-amqp-test" include "starter:spring-boot-starter-artemis" include "starter:spring-boot-starter-artemis-test" include "starter:spring-boot-starter-aspectj" diff --git a/starter/spring-boot-starter-amqp-test/build.gradle b/starter/spring-boot-starter-amqp-test/build.gradle deleted file mode 100644 index 6991f36fc910..000000000000 --- a/starter/spring-boot-starter-amqp-test/build.gradle +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -plugins { - id "org.springframework.boot.starter" -} - -description = "Starter for testing generic AMQP 1.0 support" - -dependencies { - api(project(":starter:spring-boot-starter-amqp")) - api(project(":starter:spring-boot-starter-test")) -} diff --git a/starter/spring-boot-starter-amqp/build.gradle b/starter/spring-boot-starter-amqp/build.gradle deleted file mode 100644 index 39de12fb1139..000000000000 --- a/starter/spring-boot-starter-amqp/build.gradle +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -plugins { - id "org.springframework.boot.starter" -} - -description = "Starter for generic AMQP 1.0 support" - -dependencies { - api(project(":starter:spring-boot-starter")) - - api(project(":module:spring-boot-amqp")) -} diff --git a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.java b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.java deleted file mode 100644 index baea0dcddd5f..000000000000 --- a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.container; - -import java.util.UUID; - -import org.testcontainers.rabbitmq.RabbitMQContainer; - -/** - * A {@link RabbitMQContainer} with management plugin enabled. - * - * @author Stephane Nicoll - */ -public class RabbitMqManagementContainer extends RabbitMQContainer { - - public RabbitMqManagementContainer() { - super(TestImage.RABBITMQ.toString()); - } - - /** - * Create a queue with the given name. - * @param name the name of the queue - * @return the AMQP target of the queue - */ - public String createQueue(String name) { - try { - execInContainer("rabbitmqadmin", "queues", "declare", "--name", name); - return "/queues/" + name; - } - catch (Exception ex) { - throw new RuntimeException("Could not create queue", ex); - } - } - - /** - * Create a queue with a random, unique, name. - * @return the AMQP target of the queue - */ - public String createRandomQueue() { - return createQueue(UUID.randomUUID().toString()); - } - -} diff --git a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java index 5f2da85c8b55..e64fc32615cf 100644 --- a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java +++ b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java @@ -245,10 +245,9 @@ public enum TestImage { .withStartupTimeout(Duration.ofMinutes(3))), /** - * A container image suitable for testing RabbitMQ with support for both AMQP 0.9 and - * 1.0. Queues can be declared using {@code rabbitmqadmin}. + * A container image suitable for testing RabbitMQ. */ - RABBITMQ("rabbitmq", "4.2-management", () -> RabbitMQContainer.class, + RABBITMQ("rabbitmq", "3.11-alpine", () -> RabbitMQContainer.class, (container) -> ((RabbitMQContainer) container).withStartupTimeout(Duration.ofMinutes(4))), /** From 5296a5bbfdf7c12f3bccadf3f1abb3ff098c370b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 20 Mar 2026 10:54:15 +0100 Subject: [PATCH 396/930] Revert "Rename spring-boot-amqp to spring-boot-rabbitmq" This reverts commit 72b8a6ca057ea9bfb9260f46f59e08e560d99600, b820d9feac55cc4dcdb623210c9737cb7d4c4c19, and edadf816abcda532048324696336014778e75d9a. See gh-49620 --- documentation/spring-boot-docs/build.gradle | 2 +- .../reference/pages/actuator/endpoints.adoc | 2 +- .../pages/features/dev-services.adoc | 4 +-- .../reference/pages/messaging/amqp.adoc | 18 +++++------ .../pages/testing/testcontainers.adoc | 10 +++--- .../custom/MyRabbitConfiguration.java | 2 +- .../receiving/custom/MyRabbitConfiguration.kt | 2 +- .../build.gradle | 2 +- ...nectionDetailsFactoryIntegrationTests.java | 8 ++--- ...nectionDetailsFactoryIntegrationTests.java | 6 ++-- ...nectionDetailsFactoryIntegrationTests.java | 4 +-- ...nectionDetailsFactoryIntegrationTests.java | 8 ++--- ...nectionDetailsFactoryIntegrationTests.java | 6 ++-- ...nectionDetailsFactoryIntegrationTests.java | 6 ++-- ...nectionDetailsFactoryIntegrationTests.java | 14 ++++---- ...nectionDetailsFactoryIntegrationTests.java | 30 +++++++++--------- ...nectionDetailsFactoryIntegrationTests.java | 14 ++++---- ...nectionDetailsFactoryIntegrationTests.java | 14 ++++---- .../src/dockerTest/resources/logback-test.xml | 0 .../org/springframework/boot/amqp}/ca.crt | 0 .../org/springframework/boot/amqp}/client.crt | 0 .../org/springframework/boot/amqp}/client.key | 0 ...bbit-stream-separate-services-compose.yaml | 0 ...-rabbit-stream-single-service-compose.yaml | 0 .../amqp}/docker/compose/rabbit-compose.yaml | 0 .../docker/compose/rabbit-ssl-compose.yaml | 0 .../docker/compose/rabbit-stream-compose.yaml | 0 .../compose/rabbit-stream-ssl-compose.yaml | 0 .../amqp}/docker/compose/rabbitmq-ssl.conf | 0 .../docker/compose/rabbitmq-stream-ssl.conf | 0 .../org/springframework/boot/amqp}/server.crt | 0 .../org/springframework/boot/amqp}/server.key | 0 .../testcontainers/rabbitmq-stream-ssl.conf | 0 .../dockerTest/resources/spring.properties | 0 .../AbstractConnectionFactoryConfigurer.java | 4 +-- ...bitListenerContainerFactoryConfigurer.java | 8 ++--- .../CachingConnectionFactoryConfigurer.java | 4 +-- .../ConnectionFactoryCustomizer.java | 4 +-- ...bitListenerContainerFactoryConfigurer.java | 4 +-- .../EnvironmentBuilderCustomizer.java | 4 +-- .../PropertiesRabbitConnectionDetails.java | 4 +-- .../RabbitAnnotationDrivenConfiguration.java | 2 +- .../RabbitAutoConfiguration.java | 4 +-- .../RabbitConnectionDetails.java | 4 +-- ...RabbitConnectionFactoryBeanConfigurer.java | 6 ++-- ...RabbitListenerRetrySettingsCustomizer.java | 4 +-- .../amqp}/autoconfigure/RabbitProperties.java | 4 +-- .../RabbitRetryTemplateSettings.java | 4 +-- .../RabbitStreamConfiguration.java | 8 ++--- .../RabbitStreamConnectionDetails.java | 2 +- .../RabbitStreamTemplateConfigurer.java | 4 +-- .../RabbitTemplateConfigurer.java | 4 +-- .../RabbitTemplateCustomizer.java | 4 +-- ...RabbitTemplateRetrySettingsCustomizer.java | 4 +-- ...bitListenerContainerFactoryConfigurer.java | 4 +-- .../SslBundleRabbitConnectionFactoryBean.java | 2 +- ...bitHealthContributorAutoConfiguration.java | 8 ++--- .../autoconfigure/health/package-info.java | 2 +- ...ConnectionFactoryMetricsPostProcessor.java | 4 +-- .../RabbitMetricsAutoConfiguration.java | 6 ++-- .../autoconfigure/metrics/package-info.java | 2 +- .../amqp}/autoconfigure/package-info.java | 2 +- ...DockerComposeConnectionDetailsFactory.java | 6 ++-- .../docker/compose/RabbitEnvironment.java | 2 +- ...DockerComposeConnectionDetailsFactory.java | 6 ++-- .../amqp}/docker/compose/package-info.java | 2 +- .../amqp}/health/RabbitHealthIndicator.java | 4 +-- .../boot/amqp}/health/package-info.java | 2 +- .../boot/amqp}/metrics/RabbitMetrics.java | 4 +-- .../boot/amqp}/metrics/package-info.java | 2 +- ...bbitContainerConnectionDetailsFactory.java | 4 +-- ...bbitContainerConnectionDetailsFactory.java | 4 +-- ...reamContainerConnectionDetailsFactory.java | 4 +-- .../amqp}/testcontainers/package-info.java | 2 +- ...itional-spring-configuration-metadata.json | 0 .../main/resources/META-INF/spring.factories | 8 +++++ ...ot.autoconfigure.AutoConfiguration.imports | 3 ++ ...ropertiesRabbitConnectionDetailsTests.java | 4 +-- .../RabbitAutoConfigurationTests.java | 16 +++++----- .../autoconfigure/RabbitPropertiesTests.java | 2 +- .../RabbitStreamConfigurationTests.java | 8 ++--- ...althContributorAutoConfigurationTests.java | 6 ++-- ...ationMeterBinderCycleIntegrationTests.java | 4 +-- .../RabbitMetricsAutoConfigurationTests.java | 4 +-- .../compose/RabbitEnvironmentTests.java | 2 +- .../health/RabbitHealthIndicatorTests.java | 2 +- .../amqp}/metrics/RabbitMetricsTests.java | 2 +- .../src/test/resources/logback-test.xml | 0 .../boot/amqp}/autoconfigure/test.jks | Bin .../build.gradle | 6 ++-- .../main/resources/META-INF/spring.factories | 8 ----- ...ot.autoconfigure.AutoConfiguration.imports | 3 -- .../spring-boot-dependencies/build.gradle | 6 ++-- settings.gradle | 8 ++--- .../build.gradle | 6 ++-- .../SampleAmqpSimpleApplicationSslTests.java} | 4 +-- .../SampleAmqpSimpleApplicationTests.java} | 4 +-- .../amqp}/SecureRabbitMqContainer.java | 2 +- .../amqp/SampleAmqpSimpleApplication.java} | 8 ++--- .../src/main/java/smoketest/amqp}/Sender.java | 2 +- .../java/smoketest/amqp}/package-info.java | 2 +- .../src/test/resources/ssl/rabbitmq.conf | 0 .../src/test/resources/ssl/test-ca.crt | 0 .../src/test/resources/ssl/test-ca.key | 0 .../src/test/resources/ssl/test-client.crt | 0 .../src/test/resources/ssl/test-client.key | 0 .../src/test/resources/ssl/test-server.crt | 0 .../src/test/resources/ssl/test-server.key | 0 .../build.gradle | 4 +-- .../build.gradle | 4 +-- 110 files changed, 219 insertions(+), 219 deletions(-) rename module/{spring-boot-rabbitmq => spring-boot-amqp}/build.gradle (98%) rename module/{spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp}/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java (89%) rename module/{spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp}/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java (91%) rename module/{spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp}/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java (93%) rename module/{spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp}/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java (88%) rename module/{spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp}/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java (93%) rename module/{spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp}/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java (93%) rename module/{spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp}/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java (86%) rename module/{spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp}/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java (82%) rename module/{spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp}/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java (89%) rename module/{spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp}/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java (88%) rename module/{spring-boot-rabbitmq => spring-boot-amqp}/src/dockerTest/resources/logback-test.xml (100%) rename module/{spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp}/ca.crt (100%) rename module/{spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp}/client.crt (100%) rename module/{spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp}/client.key (100%) rename module/{spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp}/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml (100%) rename module/{spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp}/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml (100%) rename module/{spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp}/docker/compose/rabbit-compose.yaml (100%) rename module/{spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp}/docker/compose/rabbit-ssl-compose.yaml (100%) rename module/{spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp}/docker/compose/rabbit-stream-compose.yaml (100%) rename module/{spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp}/docker/compose/rabbit-stream-ssl-compose.yaml (100%) rename module/{spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp}/docker/compose/rabbitmq-ssl.conf (100%) rename module/{spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp}/docker/compose/rabbitmq-stream-ssl.conf (100%) rename module/{spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp}/server.crt (100%) rename module/{spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp}/server.key (100%) rename module/{spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp}/testcontainers/rabbitmq-stream-ssl.conf (100%) rename module/{spring-boot-rabbitmq => spring-boot-amqp}/src/dockerTest/resources/spring.properties (100%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/AbstractConnectionFactoryConfigurer.java (98%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java (96%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/CachingConnectionFactoryConfigurer.java (97%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/ConnectionFactoryCustomizer.java (93%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java (96%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/EnvironmentBuilderCustomizer.java (94%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/PropertiesRabbitConnectionDetails.java (94%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/RabbitAnnotationDrivenConfiguration.java (99%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/RabbitAutoConfiguration.java (99%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/RabbitConnectionDetails.java (97%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java (97%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/RabbitListenerRetrySettingsCustomizer.java (94%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/RabbitProperties.java (99%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/RabbitRetryTemplateSettings.java (95%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/RabbitStreamConfiguration.java (96%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/RabbitStreamConnectionDetails.java (97%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/RabbitStreamTemplateConfigurer.java (98%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/RabbitTemplateConfigurer.java (98%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/RabbitTemplateCustomizer.java (93%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java (94%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java (96%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/SslBundleRabbitConnectionFactoryBean.java (96%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java (91%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/health/package-info.java (91%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java (96%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java (92%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/metrics/package-info.java (91%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/autoconfigure/package-info.java (92%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java (92%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/docker/compose/RabbitEnvironment.java (95%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java (93%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/docker/compose/package-info.java (92%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/health/RabbitHealthIndicator.java (96%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/health/package-info.java (93%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/metrics/RabbitMetrics.java (96%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/metrics/package-info.java (93%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java (95%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/testcontainers/RabbitContainerConnectionDetailsFactory.java (94%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java (95%) rename module/{spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/main/java/org/springframework/boot/amqp}/testcontainers/package-info.java (92%) rename module/{spring-boot-rabbitmq => spring-boot-amqp}/src/main/resources/META-INF/additional-spring-configuration-metadata.json (100%) create mode 100644 module/spring-boot-amqp/src/main/resources/META-INF/spring.factories create mode 100644 module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename module/{spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/test/java/org/springframework/boot/amqp}/autoconfigure/PropertiesRabbitConnectionDetailsTests.java (95%) rename module/{spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/test/java/org/springframework/boot/amqp}/autoconfigure/RabbitAutoConfigurationTests.java (99%) rename module/{spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/test/java/org/springframework/boot/amqp}/autoconfigure/RabbitPropertiesTests.java (99%) rename module/{spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/test/java/org/springframework/boot/amqp}/autoconfigure/RabbitStreamConfigurationTests.java (98%) rename module/{spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/test/java/org/springframework/boot/amqp}/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java (89%) rename module/{spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/test/java/org/springframework/boot/amqp}/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java (94%) rename module/{spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/test/java/org/springframework/boot/amqp}/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java (95%) rename module/{spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/test/java/org/springframework/boot/amqp}/docker/compose/RabbitEnvironmentTests.java (97%) rename module/{spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/test/java/org/springframework/boot/amqp}/health/RabbitHealthIndicatorTests.java (98%) rename module/{spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq => spring-boot-amqp/src/test/java/org/springframework/boot/amqp}/metrics/RabbitMetricsTests.java (97%) rename module/{spring-boot-rabbitmq => spring-boot-amqp}/src/test/resources/logback-test.xml (100%) rename module/{spring-boot-rabbitmq/src/test/resources/org/springframework/boot/rabbitmq => spring-boot-amqp/src/test/resources/org/springframework/boot/amqp}/autoconfigure/test.jks (100%) delete mode 100644 module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories delete mode 100644 module/spring-boot-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename smoke-test/{spring-boot-smoke-test-rabbitmq => spring-boot-smoke-test-amqp}/build.gradle (90%) rename smoke-test/{spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationSslTests.java => spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java} (97%) rename smoke-test/{spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationTests.java => spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java} (96%) rename smoke-test/{spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq => spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp}/SecureRabbitMqContainer.java (98%) rename smoke-test/{spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/SampleRabbitApplication.java => spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java} (87%) rename smoke-test/{spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq => spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp}/Sender.java (97%) rename smoke-test/{spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq => spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp}/package-info.java (96%) rename smoke-test/{spring-boot-smoke-test-rabbitmq => spring-boot-smoke-test-amqp}/src/test/resources/ssl/rabbitmq.conf (100%) rename smoke-test/{spring-boot-smoke-test-rabbitmq => spring-boot-smoke-test-amqp}/src/test/resources/ssl/test-ca.crt (100%) rename smoke-test/{spring-boot-smoke-test-rabbitmq => spring-boot-smoke-test-amqp}/src/test/resources/ssl/test-ca.key (100%) rename smoke-test/{spring-boot-smoke-test-rabbitmq => spring-boot-smoke-test-amqp}/src/test/resources/ssl/test-client.crt (100%) rename smoke-test/{spring-boot-smoke-test-rabbitmq => spring-boot-smoke-test-amqp}/src/test/resources/ssl/test-client.key (100%) rename smoke-test/{spring-boot-smoke-test-rabbitmq => spring-boot-smoke-test-amqp}/src/test/resources/ssl/test-server.crt (100%) rename smoke-test/{spring-boot-smoke-test-rabbitmq => spring-boot-smoke-test-amqp}/src/test/resources/ssl/test-server.key (100%) rename starter/{spring-boot-starter-rabbitmq-test => spring-boot-starter-amqp-test}/build.gradle (85%) rename starter/{spring-boot-starter-rabbitmq => spring-boot-starter-amqp}/build.gradle (86%) diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index 0a31d6f6f402..2d739b45907c 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -94,6 +94,7 @@ dependencies { implementation(project(path: ":loader:spring-boot-loader-tools")) implementation(project(path: ":module:spring-boot-actuator")) implementation(project(path: ":module:spring-boot-actuator-autoconfigure")) + implementation(project(path: ":module:spring-boot-amqp")) implementation(project(path: ":module:spring-boot-cache")) implementation(project(path: ":module:spring-boot-cache-test")) implementation(project(path: ":module:spring-boot-data-cassandra")) @@ -128,7 +129,6 @@ dependencies { implementation(project(path: ":module:spring-boot-micrometer-metrics")) implementation(project(path: ":module:spring-boot-persistence")) implementation(project(path: ":module:spring-boot-r2dbc")) - implementation(project(path: ":module:spring-boot-rabbitmq")) implementation(project(path: ":module:spring-boot-restdocs")) implementation(project(path: ":module:spring-boot-reactor-netty")) implementation(project(path: ":module:spring-boot-restclient")) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc index dbc310ac5564..c231b03da80f 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc @@ -673,7 +673,7 @@ with the `key` listed in the following table: | Always responds with `UP`. | `rabbit` -| javadoc:org.springframework.boot.rabbitmq.health.RabbitHealthIndicator[] +| javadoc:org.springframework.boot.amqp.health.RabbitHealthIndicator[] | Checks that a Rabbit server is up. | `redis` diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc index 36e57ff478db..dac48926bed9 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc @@ -125,10 +125,10 @@ The following service connections are currently supported: | javadoc:org.springframework.boot.r2dbc.autoconfigure.R2dbcConnectionDetails[] | Containers named "clickhouse/clickhouse-server", "gvenzl/oracle-free", "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres" -| javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails[] +| javadoc:org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails[] | Containers named "rabbitmq" with container port 5672 mapped -| javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[] +| javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] | Containers named "rabbitmq" with container port 5552 mapped | javadoc:org.springframework.boot.data.redis.autoconfigure.DataRedisConnectionDetails[] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index 3864f7ebe9ea..aaa67268da6f 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc @@ -3,7 +3,7 @@ The Advanced Message Queuing Protocol (AMQP) is a platform-neutral, wire-level protocol for message-oriented middleware. The Spring AMQP project applies core Spring concepts to the development of AMQP-based messaging solutions. -Spring Boot offers several conveniences for working with AMQP through RabbitMQ, including the `spring-boot-starter-rabbitmq` starter. +Spring Boot offers several conveniences for working with AMQP through RabbitMQ, including the `spring-boot-starter-amqp` starter. @@ -38,12 +38,12 @@ spring: NOTE: When specifying addresses that way, the `host` and `port` properties are ignored. If the address uses the `amqps` protocol, SSL support is enabled automatically. -See javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties[] for more of the supported property-based configuration options. -To configure lower-level details of the RabbitMQ javadoc:com.rabbitmq.client.ConnectionFactory[] that is used by Spring AMQP, define a javadoc:org.springframework.boot.rabbitmq.autoconfigure.ConnectionFactoryCustomizer[] bean. +See javadoc:org.springframework.boot.amqp.autoconfigure.RabbitProperties[] for more of the supported property-based configuration options. +To configure lower-level details of the RabbitMQ javadoc:com.rabbitmq.client.ConnectionFactory[] that is used by Spring AMQP, define a javadoc:org.springframework.boot.amqp.autoconfigure.ConnectionFactoryCustomizer[] bean. If a javadoc:org.springframework.amqp.rabbit.connection.ConnectionNameStrategy[] bean exists in the context, it will be automatically used to name connections created by the auto-configured javadoc:org.springframework.amqp.rabbit.connection.CachingConnectionFactory[]. -To make an application-wide, additive customization to the javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[], use a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitTemplateCustomizer[] bean. +To make an application-wide, additive customization to the javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[], use a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitTemplateCustomizer[] bean. TIP: See https://spring.io/blog/2010/06/14/understanding-amqp-the-protocol-used-by-rabbitmq/[Understanding AMQP, the protocol used by RabbitMQ] for more details. @@ -74,9 +74,9 @@ spring: ---- Retries are disabled by default. -You can also customize the javadoc:org.springframework.core.retry.RetryTemplate[] programmatically by declaring a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitTemplateRetrySettingsCustomizer[] bean. +You can also customize the javadoc:org.springframework.core.retry.RetryTemplate[] programmatically by declaring a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitTemplateRetrySettingsCustomizer[] bean. -If you need to create more javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] with the same settings as the factories used by the auto-configuration. +If you need to create more javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] with the same settings as the factories used by the auto-configuration. If there's a bean of type javadoc:org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservationConvention[] in the context, it will automatically be configured on the javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[]. @@ -97,7 +97,7 @@ spring: If a javadoc:org.springframework.amqp.support.converter.MessageConverter[], javadoc:org.springframework.rabbit.stream.support.converter.StreamMessageConverter[], javadoc:org.springframework.rabbit.stream.producer.ProducerCustomizer[] or javadoc:org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservationConvention[] bean is defined, it is associated automatically to the auto-configured javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[]. -If you need to create more javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[] with the same settings as the factories used by the auto-configuration. +If you need to create more javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[] with the same settings as the factories used by the auto-configuration. @@ -120,7 +120,7 @@ include-code::MyBean[] TIP: See javadoc:org.springframework.amqp.rabbit.annotation.EnableRabbit[format=annotation] for more details. -If you need to create more javadoc:org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.rabbitmq.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer[] and a javadoc:org.springframework.boot.rabbitmq.autoconfigure.DirectRabbitListenerContainerFactoryConfigurer[] that you can use to initialize a javadoc:org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory[] and a javadoc:org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory[] with the same settings as the factories used by the auto-configuration. +If you need to create more javadoc:org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.amqp.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer[] and a javadoc:org.springframework.boot.amqp.autoconfigure.DirectRabbitListenerContainerFactoryConfigurer[] that you can use to initialize a javadoc:org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory[] and a javadoc:org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory[] with the same settings as the factories used by the auto-configuration. TIP: It does not matter which container type you chose. Those two beans are exposed by the auto-configuration. @@ -137,7 +137,7 @@ You can enable retries to handle situations where your listener throws an except By default, javadoc:org.springframework.amqp.rabbit.retry.RejectAndDontRequeueRecoverer[] is used, but you can define a javadoc:org.springframework.amqp.rabbit.retry.MessageRecoverer[] of your own. When retries are exhausted, the message is rejected and either dropped or routed to a dead-letter exchange if the broker is configured to do so. By default, retries are disabled. -You can also customize the javadoc:org.springframework.core.retry.RetryPolicy[] programmatically by declaring a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitListenerRetrySettingsCustomizer[] bean. +You can also customize the javadoc:org.springframework.core.retry.RetryPolicy[] programmatically by declaring a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitListenerRetrySettingsCustomizer[] bean. IMPORTANT: By default, if retries are disabled and the listener throws an exception, the delivery is retried indefinitely. You can modify this behavior in two ways: Set the `defaultRequeueRejected` property to `false` so that zero re-deliveries are attempted or throw an javadoc:org.springframework.amqp.AmqpRejectAndDontRequeueException[] to signal the message should be rejected. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc index 72240022f6ad..d54476a6a809 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc @@ -173,11 +173,11 @@ javadoc:org.testcontainers.clickhouse.ClickHouseContainer[], javadoc:{url-testcontainers-mariadb-javadoc}/org.testcontainers.mariadb.MariaDBContainer[], javadoc:{url-testcontainers-mssqlserver-javadoc}/org.testcontainers.mssqlserver.MSSQLServerContainer[], javadoc:{url-testcontainers-mysql-javadoc}/org.testcontainers.mysql.MySQLContainer[], javadoc:org.testcontainers.oracle.OracleContainer[OracleContainer (free)], javadoc:{url-testcontainers-oracle-xe-javadoc}/org.testcontainers.containers.OracleContainer[OracleContainer (XE)] or javadoc:{url-testcontainers-postgresql-javadoc}/org.testcontainers.postgresql.PostgreSQLContainer[] -| javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails[] +| javadoc:org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails[] | Containers of type javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] -| javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[] -| Containers of type javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] when the `@ServiceConnection` `type` attribute includes javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[] +| javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] +| Containers of type javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] when the `@ServiceConnection` `type` attribute includes javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] | javadoc:org.springframework.boot.data.redis.autoconfigure.DataRedisConnectionDetails[] | Containers of type javadoc:com.redis.testcontainers.RedisContainer[] or javadoc:com.redis.testcontainers.RedisStackContainer[], or containers named "redis", "redis/redis-stack" or "redis/redis-stack-server" @@ -188,12 +188,12 @@ javadoc:org.testcontainers.oracle.OracleContainer[OracleContainer (free)], javad [TIP] ==== -By default, with the exception of javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[], all applicable connection details beans will be created for a given javadoc:org.testcontainers.containers.Container[]. +By default, with the exception of javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[], all applicable connection details beans will be created for a given javadoc:org.testcontainers.containers.Container[]. For example, a javadoc:{url-testcontainers-postgresql-javadoc}/org.testcontainers.postgresql.PostgreSQLContainer[] will create both javadoc:org.springframework.boot.jdbc.autoconfigure.JdbcConnectionDetails[] and javadoc:org.springframework.boot.r2dbc.autoconfigure.R2dbcConnectionDetails[]. If you want to create only a subset of the applicable types, you can use the `type` attribute of javadoc:org.springframework.boot.testcontainers.service.connection.ServiceConnection[format=annotation]. -To create a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[] bean from a javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[], you must opt in using the `type` attribute of javadoc:org.springframework.boot.testcontainers.service.connection.ServiceConnection[format=annotation]. +To create a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] bean from a javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[], you must opt in using the `type` attribute of javadoc:org.springframework.boot.testcontainers.service.connection.ServiceConnection[format=annotation]. The container must also expose port 5552, the RabbitMQ streams port. ==== diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java index 172c9cbba0d1..e109d5de244a 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java @@ -18,7 +18,7 @@ import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.boot.rabbitmq.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer; +import org.springframework.boot.amqp.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt index 95ca6f610039..4f1c80f38666 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt @@ -19,7 +19,7 @@ package org.springframework.boot.docs.messaging.amqp.receiving.custom import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory import org.springframework.amqp.rabbit.connection.CachingConnectionFactory import org.springframework.amqp.rabbit.connection.ConnectionFactory -import org.springframework.boot.rabbitmq.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer +import org.springframework.boot.amqp.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration diff --git a/module/spring-boot-rabbitmq/build.gradle b/module/spring-boot-amqp/build.gradle similarity index 98% rename from module/spring-boot-rabbitmq/build.gradle rename to module/spring-boot-amqp/build.gradle index 4942083bfa4f..0152cb160b86 100644 --- a/module/spring-boot-rabbitmq/build.gradle +++ b/module/spring-boot-amqp/build.gradle @@ -23,7 +23,7 @@ plugins { id "org.springframework.boot.optional-dependencies" } -description = "Spring Boot RabbitMQ" +description = "Spring Boot AMQP" dependencies { api(project(":core:spring-boot")) diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 89% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index dcef4737265a..5b1c7797bbac 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.docker.compose; +package org.springframework.boot.amqp.docker.compose; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 91% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java index fb1e627f60e4..4d644b742e48 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.docker.compose; +package org.springframework.boot.amqp.docker.compose; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 93% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index b54ab7270fb4..67f8fad358e6 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.docker.compose; +package org.springframework.boot.amqp.docker.compose; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 88% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index ae9d61597f63..d9f077bc21cd 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.docker.compose; +package org.springframework.boot.amqp.docker.compose; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java similarity index 93% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java index 91aed53b74b5..51e32711184d 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.testcontainers; +package org.springframework.boot.amqp.testcontainers; import java.time.Duration; import java.util.ArrayList; @@ -30,9 +30,9 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java similarity index 93% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java index f371b8562284..ea80da8d9bf3 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.testcontainers; +package org.springframework.boot.amqp.testcontainers; import java.time.Duration; import java.util.ArrayList; @@ -30,9 +30,9 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 86% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java index 8820db4606a9..1e65d213f2ac 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.testcontainers; +package org.springframework.boot.amqp.testcontainers; import java.time.Duration; import java.util.ArrayList; @@ -31,13 +31,13 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.EnvironmentBuilderCustomizer; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java similarity index 82% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java index 6eab916aef27..d439b0fdbf48 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.testcontainers; +package org.springframework.boot.amqp.testcontainers; import java.time.Duration; import java.util.ArrayList; @@ -32,13 +32,13 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.EnvironmentBuilderCustomizer; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.PemKeyStore; import org.springframework.boot.testcontainers.service.connection.PemTrustStore; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -68,9 +68,9 @@ class RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests { @Container @ServiceConnection(type = RabbitStreamConnectionDetails.class) - @PemTrustStore(certificate = "classpath:org/springframework/boot/rabbitmq/ca.crt") - @PemKeyStore(certificate = "classpath:org/springframework/boot/rabbitmq/client.crt", - privateKey = "classpath:org/springframework/boot/rabbitmq/client.key") + @PemTrustStore(certificate = "classpath:org/springframework/boot/amqp/ca.crt") + @PemKeyStore(certificate = "classpath:org/springframework/boot/amqp/client.crt", + privateKey = "classpath:org/springframework/boot/amqp/client.key") static final RabbitMQContainer rabbit = getRabbitMqStreamContainer(); private static RabbitMQContainer getRabbitMqStreamContainer() { @@ -80,15 +80,15 @@ private static RabbitMQContainer getRabbitMqStreamContainer() { container.withCopyToContainer(Transferable.of(enabledPlugins), "/etc/rabbitmq/enabled_plugins"); container.withCopyFileToContainer( MountableFile - .forClasspathResource("org/springframework/boot/rabbitmq/testcontainers/rabbitmq-stream-ssl.conf"), + .forClasspathResource("org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf"), "/etc/rabbitmq/rabbitmq.conf"); + container.withCopyFileToContainer(MountableFile.forClasspathResource("org/springframework/boot/amqp/ca.crt"), + "/etc/rabbitmq/ca.crt"); container.withCopyFileToContainer( - MountableFile.forClasspathResource("org/springframework/boot/rabbitmq/ca.crt"), "/etc/rabbitmq/ca.crt"); - container.withCopyFileToContainer( - MountableFile.forClasspathResource("org/springframework/boot/rabbitmq/server.key"), + MountableFile.forClasspathResource("org/springframework/boot/amqp/server.key"), "/etc/rabbitmq/server.key"); container.withCopyFileToContainer( - MountableFile.forClasspathResource("org/springframework/boot/rabbitmq/server.crt"), + MountableFile.forClasspathResource("org/springframework/boot/amqp/server.crt"), "/etc/rabbitmq/server.crt"); return container; } diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 89% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java index e28c2657a7c8..778f6536bd57 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.testcontainers; +package org.springframework.boot.amqp.testcontainers; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -35,13 +35,13 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.EnvironmentBuilderCustomizer; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 88% rename from module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java index 3d306284fa4f..6dc8b12291c3 100644 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.testcontainers; +package org.springframework.boot.amqp.testcontainers; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -35,13 +35,13 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.EnvironmentBuilderCustomizer; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/logback-test.xml b/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/logback-test.xml rename to module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/ca.crt b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/ca.crt similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/ca.crt rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/ca.crt diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/client.crt b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.crt similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/client.crt rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.crt diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/client.key b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.key similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/client.key rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.key diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-compose.yaml rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-ssl-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-ssl-compose.yaml rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-compose.yaml rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-ssl-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbit-stream-ssl-compose.yaml rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-ssl.conf b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-ssl.conf similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-ssl.conf rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-ssl.conf diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-ssl.conf b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-ssl.conf rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/server.crt b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.crt similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/server.crt rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.crt diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/server.key b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.key similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/server.key rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.key diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/testcontainers/rabbitmq-stream-ssl.conf b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/testcontainers/rabbitmq-stream-ssl.conf rename to module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf diff --git a/module/spring-boot-rabbitmq/src/dockerTest/resources/spring.properties b/module/spring-boot-amqp/src/dockerTest/resources/spring.properties similarity index 100% rename from module/spring-boot-rabbitmq/src/dockerTest/resources/spring.properties rename to module/spring-boot-amqp/src/dockerTest/resources/spring.properties diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractConnectionFactoryConfigurer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractConnectionFactoryConfigurer.java similarity index 98% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractConnectionFactoryConfigurer.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractConnectionFactoryConfigurer.java index f1667d8130ba..07b17ce5a9a1 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractConnectionFactoryConfigurer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractConnectionFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import java.util.stream.Collectors; @@ -33,7 +33,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb - * @since 4.1.0 + * @since 4.0.0 */ public abstract class AbstractConnectionFactoryConfigurer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java index ce4591f5a2eb..ffed69eda93e 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import java.util.List; import java.util.concurrent.Executor; @@ -28,8 +28,8 @@ import org.springframework.amqp.rabbit.retry.RejectAndDontRequeueRecoverer; import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention; import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.ListenerRetry; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.Retry; +import org.springframework.boot.amqp.autoconfigure.RabbitProperties.ListenerRetry; +import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Retry; import org.springframework.boot.retry.RetryPolicySettings; import org.springframework.core.retry.RetryPolicy; import org.springframework.util.Assert; @@ -41,7 +41,7 @@ * @param the container factory type. * @author Gary Russell * @author Stephane Nicoll - * @since 4.1.0 + * @since 4.0.0 */ public abstract class AbstractRabbitListenerContainerFactoryConfigurer> { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/CachingConnectionFactoryConfigurer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/CachingConnectionFactoryConfigurer.java similarity index 97% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/CachingConnectionFactoryConfigurer.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/CachingConnectionFactoryConfigurer.java index 735e5fffa16a..2221595f78d1 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/CachingConnectionFactoryConfigurer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/CachingConnectionFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import java.time.Duration; @@ -33,7 +33,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb - * @since 4.1.0 + * @since 4.0.0 */ public class CachingConnectionFactoryConfigurer extends AbstractConnectionFactoryConfigurer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/ConnectionFactoryCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionFactoryCustomizer.java similarity index 93% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/ConnectionFactoryCustomizer.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionFactoryCustomizer.java index 119e40b34c9d..15f2e14dfff8 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/ConnectionFactoryCustomizer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionFactoryCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import com.rabbitmq.client.ConnectionFactory; @@ -23,7 +23,7 @@ * {@link ConnectionFactory} to fine-tune its auto-configuration. * * @author Andy Wilkinson - * @since 4.1.0 + * @since 4.0.0 */ @FunctionalInterface public interface ConnectionFactoryCustomizer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java index e87c3aec7b28..e4057b5c2ecf 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -30,7 +30,7 @@ * * @author Gary Russell * @author Stephane Nicoll - * @since 4.1.0 + * @since 4.0.0 */ public final class DirectRabbitListenerContainerFactoryConfigurer extends AbstractRabbitListenerContainerFactoryConfigurer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/EnvironmentBuilderCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/EnvironmentBuilderCustomizer.java similarity index 94% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/EnvironmentBuilderCustomizer.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/EnvironmentBuilderCustomizer.java index fce0f7c94182..8d5f2373cd8e 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/EnvironmentBuilderCustomizer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/EnvironmentBuilderCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import com.rabbitmq.stream.Environment; import com.rabbitmq.stream.EnvironmentBuilder; @@ -25,7 +25,7 @@ * {@link Environment}. * * @author Andy Wilkinson - * @since 4.1.0 + * @since 4.0.0 */ @FunctionalInterface public interface EnvironmentBuilderCustomizer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetails.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetails.java similarity index 94% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetails.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetails.java index 3e3900f8a475..e15052bf3303 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetails.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetails.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import java.util.ArrayList; import java.util.List; import org.jspecify.annotations.Nullable; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.Ssl; +import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Ssl; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.util.Assert; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAnnotationDrivenConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java similarity index 99% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAnnotationDrivenConfiguration.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java index a2e01ef3942e..2c1b260abe0a 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAnnotationDrivenConfiguration.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.amqp.rabbit.config.ContainerCustomizer; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java similarity index 99% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfiguration.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java index ddb31437b55d..e6f98bcbd2a8 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfiguration.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import com.rabbitmq.client.Channel; import com.rabbitmq.client.impl.CredentialsProvider; @@ -72,7 +72,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Scott Frederick - * @since 4.1.0 + * @since 4.0.0 */ @AutoConfiguration @ConditionalOnClass({ RabbitTemplate.class, Channel.class }) diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionDetails.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionDetails.java similarity index 97% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionDetails.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionDetails.java index 86a7cbeb5c7f..d6cb1be0512f 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionDetails.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionDetails.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import java.util.List; @@ -30,7 +30,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb - * @since 4.1.0 + * @since 4.0.0 */ public interface RabbitConnectionDetails extends ConnectionDetails { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java similarity index 97% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java index 70c6067dd7dd..de03b57613ca 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import java.time.Duration; @@ -23,8 +23,8 @@ import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.core.io.ResourceLoader; @@ -44,7 +44,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @author Scott Frederick - * @since 4.1.0 + * @since 4.0.0 */ public class RabbitConnectionFactoryBeanConfigurer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitListenerRetrySettingsCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitListenerRetrySettingsCustomizer.java similarity index 94% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitListenerRetrySettingsCustomizer.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitListenerRetrySettingsCustomizer.java index eb577d4c5641..8a3bc26dd23e 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitListenerRetrySettingsCustomizer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitListenerRetrySettingsCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import org.springframework.boot.retry.RetryPolicySettings; import org.springframework.core.retry.RetryPolicy; @@ -24,7 +24,7 @@ * settings} used by message listeners. * * @author Stephane Nicoll - * @since 4.1.0 + * @since 4.0.0 * @see RetryPolicy */ @FunctionalInterface diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitProperties.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java similarity index 99% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitProperties.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java index 33001a04e512..e9fdff70acfa 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitProperties.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import java.time.Duration; import java.time.temporal.ChronoUnit; @@ -53,7 +53,7 @@ * @author Lasse Wulff * @author Yanming Zhou * @author Jay Choi - * @since 4.1.0 + * @since 4.0.0 */ @ConfigurationProperties("spring.rabbitmq") public class RabbitProperties { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitRetryTemplateSettings.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitRetryTemplateSettings.java similarity index 95% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitRetryTemplateSettings.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitRetryTemplateSettings.java index db1cd3a84600..8e29c1d440e0 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitRetryTemplateSettings.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitRetryTemplateSettings.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import java.util.LinkedList; @@ -26,7 +26,7 @@ * Define the settings of a {@link RetryTemplate}. * * @author Stephane Nicoll - * @since 4.1.0 + * @since 4.0.0 */ public class RabbitRetryTemplateSettings { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfiguration.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java index e2545e8f7d68..416a23d8196f 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfiguration.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import javax.net.ssl.SSLException; @@ -27,13 +27,13 @@ import org.springframework.amqp.rabbit.config.ContainerCustomizer; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Stream; +import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Stream.Ssl; +import org.springframework.boot.amqp.autoconfigure.RabbitProperties.StreamContainer; 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.PropertyMapper; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.Stream; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.Stream.Ssl; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.StreamContainer; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.ssl.SslManagerBundle; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConnectionDetails.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java similarity index 97% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConnectionDetails.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java index e37462a6f866..5de8fc8301cd 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConnectionDetails.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import org.jspecify.annotations.Nullable; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamTemplateConfigurer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java similarity index 98% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamTemplateConfigurer.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java index 724ad4a992a5..fe7306d3e473 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamTemplateConfigurer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import org.jspecify.annotations.Nullable; @@ -32,7 +32,7 @@ * auto-configuration. * * @author Eddú Meléndez - * @since 4.1.0 + * @since 4.0.0 */ public class RabbitStreamTemplateConfigurer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateConfigurer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java similarity index 98% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateConfigurer.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java index 2dda0397ff92..52823cb48d9a 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateConfigurer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import java.time.Duration; import java.util.List; @@ -45,7 +45,7 @@ * * @author Stephane Nicoll * @author Yanming Zhou - * @since 4.1.0 + * @since 4.0.0 */ public class RabbitTemplateConfigurer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateCustomizer.java similarity index 93% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateCustomizer.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateCustomizer.java index 8de9c689e3a6..9015fa8ca49d 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateCustomizer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -22,7 +22,7 @@ * Callback interface that can be used to customize a {@link RabbitTemplate}. * * @author dang zhicairang - * @since 4.1.0 + * @since 4.0.0 */ @FunctionalInterface public interface RabbitTemplateCustomizer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java similarity index 94% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java index 9e521ef6cb4c..099b510132bb 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.core.retry.RetryTemplate; @@ -24,7 +24,7 @@ * {@linkplain RabbitRetryTemplateSettings retry settings} used by {@link RabbitTemplate}. * * @author Stephane Nicoll - * @since 4.1.0 + * @since 4.0.0 * @see RabbitTemplateConfigurer * @see RetryTemplate */ diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java index 69ff1953f26f..10fac5395c1d 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -30,7 +30,7 @@ * * @author Stephane Nicoll * @author Gary Russell - * @since 4.1.0 + * @since 4.0.0 */ public final class SimpleRabbitListenerContainerFactoryConfigurer extends AbstractRabbitListenerContainerFactoryConfigurer { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SslBundleRabbitConnectionFactoryBean.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SslBundleRabbitConnectionFactoryBean.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SslBundleRabbitConnectionFactoryBean.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SslBundleRabbitConnectionFactoryBean.java index da53a2cbe28b..ae2345fb06bb 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SslBundleRabbitConnectionFactoryBean.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SslBundleRabbitConnectionFactoryBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import org.jspecify.annotations.Nullable; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java similarity index 91% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java index 2dd0cc3c7a40..0abccb7e34cf 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java @@ -14,10 +14,12 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure.health; +package org.springframework.boot.amqp.autoconfigure.health; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.amqp.health.RabbitHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -26,15 +28,13 @@ import org.springframework.boot.health.autoconfigure.contributor.CompositeHealthContributorConfiguration; import org.springframework.boot.health.autoconfigure.contributor.ConditionalOnEnabledHealthIndicator; import org.springframework.boot.health.contributor.HealthContributor; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.rabbitmq.health.RabbitHealthIndicator; import org.springframework.context.annotation.Bean; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link RabbitHealthIndicator}. * * @author Christian Dupuis - * @since 4.1.0 + * @since 4.0.0 */ @AutoConfiguration(after = RabbitAutoConfiguration.class) @ConditionalOnClass({ RabbitHealthIndicator.class, RabbitTemplate.class, ConditionalOnEnabledHealthIndicator.class }) diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/package-info.java similarity index 91% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/package-info.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/package-info.java index 27f98c583ad9..bbd0d5e86d39 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/package-info.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/package-info.java @@ -18,6 +18,6 @@ * Auto-configuration for RabbitMQ health. */ @NullMarked -package org.springframework.boot.rabbitmq.autoconfigure.health; +package org.springframework.boot.amqp.autoconfigure.health; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java index 2f9f18802c76..9841b953e937 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure.metrics; +package org.springframework.boot.amqp.autoconfigure.metrics; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.MetricsCollector; @@ -24,7 +24,7 @@ import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory; import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.boot.rabbitmq.metrics.RabbitMetrics; +import org.springframework.boot.amqp.metrics.RabbitMetrics; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; import org.springframework.util.StringUtils; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java similarity index 92% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java index 53ff9e680bb6..27b5b6ad36ed 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure.metrics; +package org.springframework.boot.amqp.autoconfigure.metrics; import com.rabbitmq.client.ConnectionFactory; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; 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.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -33,7 +33,7 @@ * {@link ConnectionFactory connection factories}. * * @author Stephane Nicoll - * @since 4.1.0 + * @since 4.0.0 */ @AutoConfiguration( afterName = "org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration", diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/package-info.java similarity index 91% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/package-info.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/package-info.java index 94b2c885530b..ab0814c6f728 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/package-info.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/package-info.java @@ -18,6 +18,6 @@ * Auto-configuration for RabbitMQ metrics. */ @NullMarked -package org.springframework.boot.rabbitmq.autoconfigure.metrics; +package org.springframework.boot.amqp.autoconfigure.metrics; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java similarity index 92% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/package-info.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java index 8b021e23bb05..b519c35a2cae 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/package-info.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java @@ -18,6 +18,6 @@ * Auto-configuration for RabbitMQ. */ @NullMarked -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java similarity index 92% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java index bd662a242aac..daa79344ae75 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.docker.compose; +package org.springframework.boot.amqp.docker.compose; import java.util.List; import org.jspecify.annotations.Nullable; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; 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; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.ssl.SslBundle; /** diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironment.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java similarity index 95% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironment.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java index 1d37579e1a84..bc776b015dbf 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironment.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.docker.compose; +package org.springframework.boot.amqp.docker.compose; import java.util.Map; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java similarity index 93% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java index 5b1fbbb87941..6b19483ff4a9 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.docker.compose; +package org.springframework.boot.amqp.docker.compose; import org.jspecify.annotations.Nullable; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; 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; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.ssl.SslBundle; /** diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java similarity index 92% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/package-info.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java index 7fffb63a7c4c..c2d355c0797e 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/package-info.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java @@ -18,6 +18,6 @@ * Support for Docker Compose RabbitMQ service connections. */ @NullMarked -package org.springframework.boot.rabbitmq.docker.compose; +package org.springframework.boot.amqp.docker.compose; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicator.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/RabbitHealthIndicator.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicator.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/RabbitHealthIndicator.java index c985139e521e..045ab2f89c27 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicator.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/RabbitHealthIndicator.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.health; +package org.springframework.boot.amqp.health; import org.jspecify.annotations.Nullable; @@ -29,7 +29,7 @@ * RabbitMQ messaging system. * * @author Christian Dupuis - * @since 4.1.0 + * @since 4.0.0 */ public class RabbitHealthIndicator extends AbstractHealthIndicator { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/package-info.java similarity index 93% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/package-info.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/package-info.java index 83f913732e91..ecb6ea0b1649 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/package-info.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/package-info.java @@ -18,6 +18,6 @@ * Health integration for AMQP and RabbitMQ. */ @NullMarked -package org.springframework.boot.rabbitmq.health; +package org.springframework.boot.amqp.health; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/RabbitMetrics.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/RabbitMetrics.java similarity index 96% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/RabbitMetrics.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/RabbitMetrics.java index 1c32dfd28c65..20ae4111b162 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/RabbitMetrics.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/RabbitMetrics.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.metrics; +package org.springframework.boot.amqp.metrics; import java.util.Collections; @@ -32,7 +32,7 @@ * * @author Arnaud Cogoluègnes * @author Stephane Nicoll - * @since 4.1.0 + * @since 4.0.0 */ public class RabbitMetrics implements MeterBinder { diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/package-info.java similarity index 93% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/package-info.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/package-info.java index c40e5aed68fc..e63026906cd8 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/package-info.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/package-info.java @@ -18,6 +18,6 @@ * Metrics for AMQP and RabbitMQ. */ @NullMarked -package org.springframework.boot.rabbitmq.metrics; +package org.springframework.boot.amqp.metrics; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java similarity index 95% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java index 445d8410611a..b641c37e4395 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.testcontainers; +package org.springframework.boot.amqp.testcontainers; import java.net.URI; import java.util.List; @@ -22,7 +22,7 @@ import org.jspecify.annotations.Nullable; import org.testcontainers.containers.RabbitMQContainer; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java similarity index 94% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactory.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java index 32a89a3e23ac..fd47f98fd478 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitContainerConnectionDetailsFactory.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.testcontainers; +package org.springframework.boot.amqp.testcontainers; import java.net.URI; import java.util.List; @@ -22,7 +22,7 @@ import org.jspecify.annotations.Nullable; import org.testcontainers.rabbitmq.RabbitMQContainer; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java similarity index 95% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java index dccd1d441417..5fc1321b7427 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.testcontainers; +package org.springframework.boot.amqp.testcontainers; import org.jspecify.annotations.Nullable; import org.testcontainers.rabbitmq.RabbitMQContainer; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java similarity index 92% rename from module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/package-info.java rename to module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java index 272d19a6b982..69c6364ef32f 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/package-info.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java @@ -18,6 +18,6 @@ * Support for testcontainers RabbitMQ service connections. */ @NullMarked -package org.springframework.boot.rabbitmq.testcontainers; +package org.springframework.boot.amqp.testcontainers; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-rabbitmq/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-amqp/src/main/resources/META-INF/additional-spring-configuration-metadata.json similarity index 100% rename from module/spring-boot-rabbitmq/src/main/resources/META-INF/additional-spring-configuration-metadata.json rename to module/spring-boot-amqp/src/main/resources/META-INF/additional-spring-configuration-metadata.json diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories b/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..17fb2095415a --- /dev/null +++ b/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories @@ -0,0 +1,8 @@ +# Connection Details Factories +org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ +org.springframework.boot.amqp.docker.compose.RabbitDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.amqp.docker.compose.RabbitStreamDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.amqp.testcontainers.DeprecatedRabbitContainerConnectionDetailsFactory,\ +org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory,\ +org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory + diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000000..daf8ae3ba45f --- /dev/null +++ b/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration +org.springframework.boot.amqp.autoconfigure.health.RabbitHealthContributorAutoConfiguration +org.springframework.boot.amqp.autoconfigure.metrics.RabbitMetricsAutoConfiguration \ No newline at end of file diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetailsTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetailsTests.java similarity index 95% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetailsTests.java rename to module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetailsTests.java index aed57edc4e04..fb37bd6f2712 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetailsTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetailsTests.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.ssl.DefaultSslBundleRegistry; import org.springframework.boot.ssl.SslBundle; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfigurationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java similarity index 99% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfigurationTests.java rename to module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java index b25f9cd3c5f9..7db86941280d 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfigurationTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import java.security.NoSuchAlgorithmException; import java.util.Collection; @@ -942,7 +942,7 @@ void enableSslWithInvalidTrustStoreTypeShouldFail() { void enableSslWithBundle() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.bundle=test-bundle", - "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/rabbitmq/autoconfigure/test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/amqp/autoconfigure/test.jks", "spring.ssl.bundle.jks.test-bundle.keystore.password=secret") .run((context) -> { com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); @@ -954,9 +954,9 @@ void enableSslWithBundle() { void enableSslWithKeystoreTypeAndTrustStoreTypeShouldWork() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.enabled:true", - "spring.rabbitmq.ssl.key-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", + "spring.rabbitmq.ssl.key-store=/org/springframework/boot/amqp/autoconfigure/test.jks", "spring.rabbitmq.ssl.key-store-type=jks", "spring.rabbitmq.ssl.key-store-password=secret", - "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", + "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/amqp/autoconfigure/test.jks", "spring.rabbitmq.ssl.trust-store-type=jks", "spring.rabbitmq.ssl.trust-store-password=secret") .run((context) -> assertThat(context).hasNotFailed()); } @@ -988,10 +988,10 @@ void enableSslWithValidateServerCertificateDefault(CapturedOutput output) { void enableSslWithValidStoreAlgorithmShouldWork() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.enabled:true", - "spring.rabbitmq.ssl.key-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", + "spring.rabbitmq.ssl.key-store=/org/springframework/boot/amqp/autoconfigure/test.jks", "spring.rabbitmq.ssl.key-store-type=jks", "spring.rabbitmq.ssl.key-store-password=secret", "spring.rabbitmq.ssl.key-store-algorithm=PKIX", - "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", + "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/amqp/autoconfigure/test.jks", "spring.rabbitmq.ssl.trust-store-type=jks", "spring.rabbitmq.ssl.trust-store-password=secret", "spring.rabbitmq.ssl.trust-store-algorithm=PKIX") .run((context) -> assertThat(context).hasNotFailed()); @@ -1001,7 +1001,7 @@ void enableSslWithValidStoreAlgorithmShouldWork() { void enableSslWithInvalidKeyStoreAlgorithmShouldFail() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.enabled:true", - "spring.rabbitmq.ssl.key-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", + "spring.rabbitmq.ssl.key-store=/org/springframework/boot/amqp/autoconfigure/test.jks", "spring.rabbitmq.ssl.key-store-type=jks", "spring.rabbitmq.ssl.key-store-password=secret", "spring.rabbitmq.ssl.key-store-algorithm=test-invalid-algo") .run((context) -> { @@ -1015,7 +1015,7 @@ void enableSslWithInvalidKeyStoreAlgorithmShouldFail() { void enableSslWithInvalidTrustStoreAlgorithmShouldFail() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.enabled:true", - "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", + "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/amqp/autoconfigure/test.jks", "spring.rabbitmq.ssl.trust-store-type=jks", "spring.rabbitmq.ssl.trust-store-password=secret", "spring.rabbitmq.ssl.trust-store-algorithm=test-invalid-algo") .run((context) -> { diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitPropertiesTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java similarity index 99% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitPropertiesTests.java rename to module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java index 345ac33ebe53..93548c283a41 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitPropertiesTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import java.util.List; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfigurationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java similarity index 98% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfigurationTests.java rename to module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java index 6ca687352abf..87230b0618c9 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfigurationTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure; +package org.springframework.boot.amqp.autoconfigure; import java.time.Duration; import java.util.List; @@ -34,9 +34,9 @@ import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.amqp.autoconfigure.RabbitStreamConfiguration.PropertiesRabbitStreamConnectionDetails; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConfiguration.PropertiesRabbitStreamConnectionDetails; import org.springframework.boot.ssl.NoSuchSslBundleException; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; @@ -352,7 +352,7 @@ void whenStreamSslIsEnabledThenTlsIsUsed() { @Test void whenStreamSslBundleIsConfiguredThenTlsIsUsed() { this.contextRunner.withPropertyValues("spring.rabbitmq.stream.ssl.bundle=test-bundle", - "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/rabbitmq/autoconfigure/test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/amqp/autoconfigure/test.jks", "spring.ssl.bundle.jks.test-bundle.keystore.password=secret") .run((context) -> { assertThat(context).hasNotFailed(); @@ -388,7 +388,7 @@ void whenStreamSslIsDisabledWithBundleThenTlsIsStillUsed() { void whenConnectionDetailsSslBundleIsProvidedThenTlsIsUsedWithoutProperties() { this.contextRunner.withUserConfiguration(CustomConnectionDetails.class) .withPropertyValues( - "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/rabbitmq/autoconfigure/test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/amqp/autoconfigure/test.jks", "spring.ssl.bundle.jks.test-bundle.keystore.password=secret") .run((context) -> { assertThat(context).hasNotFailed(); diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java similarity index 89% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java rename to module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java index 4b3571c2c8c6..8739399d0aae 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure.health; +package org.springframework.boot.amqp.autoconfigure.health; import org.junit.jupiter.api.Test; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.amqp.health.RabbitHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.health.autoconfigure.contributor.HealthContributorAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.rabbitmq.health.RabbitHealthIndicator; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java similarity index 94% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java rename to module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java index 81928ab80091..8c8c31a499e0 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure.metrics; +package org.springframework.boot.amqp.autoconfigure.metrics; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.MeterBinder; @@ -22,8 +22,8 @@ import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java similarity index 95% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java rename to module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java index 937a1203a096..b75b0e1857d9 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.autoconfigure.metrics; +package org.springframework.boot.amqp.autoconfigure.metrics; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; @@ -22,9 +22,9 @@ import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironmentTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java similarity index 97% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironmentTests.java rename to module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java index 356d93ad535e..1ecb0b4d6ff4 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitEnvironmentTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.docker.compose; +package org.springframework.boot.amqp.docker.compose; import java.util.Collections; import java.util.Map; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicatorTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/health/RabbitHealthIndicatorTests.java similarity index 98% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicatorTests.java rename to module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/health/RabbitHealthIndicatorTests.java index 161cb2fa6e94..5ce2f8e64a20 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicatorTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/health/RabbitHealthIndicatorTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.health; +package org.springframework.boot.amqp.health; import java.util.Collections; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/metrics/RabbitMetricsTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/metrics/RabbitMetricsTests.java similarity index 97% rename from module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/metrics/RabbitMetricsTests.java rename to module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/metrics/RabbitMetricsTests.java index a8b3f5b3c1d3..780de803e4f6 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/metrics/RabbitMetricsTests.java +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/metrics/RabbitMetricsTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.rabbitmq.metrics; +package org.springframework.boot.amqp.metrics; import com.rabbitmq.client.ConnectionFactory; import io.micrometer.core.instrument.Tags; diff --git a/module/spring-boot-rabbitmq/src/test/resources/logback-test.xml b/module/spring-boot-amqp/src/test/resources/logback-test.xml similarity index 100% rename from module/spring-boot-rabbitmq/src/test/resources/logback-test.xml rename to module/spring-boot-amqp/src/test/resources/logback-test.xml diff --git a/module/spring-boot-rabbitmq/src/test/resources/org/springframework/boot/rabbitmq/autoconfigure/test.jks b/module/spring-boot-amqp/src/test/resources/org/springframework/boot/amqp/autoconfigure/test.jks similarity index 100% rename from module/spring-boot-rabbitmq/src/test/resources/org/springframework/boot/rabbitmq/autoconfigure/test.jks rename to module/spring-boot-amqp/src/test/resources/org/springframework/boot/amqp/autoconfigure/test.jks diff --git a/module/spring-boot-autoconfigure-classic-modules/build.gradle b/module/spring-boot-autoconfigure-classic-modules/build.gradle index 06e1e54df35c..e8bf564e147d 100644 --- a/module/spring-boot-autoconfigure-classic-modules/build.gradle +++ b/module/spring-boot-autoconfigure-classic-modules/build.gradle @@ -28,6 +28,9 @@ dependencies { api(project(":module:spring-boot-activemq")) { transitive = false } + api(project(":module:spring-boot-amqp")) { + transitive = false + } api(project(":module:spring-boot-artemis")) { transitive = false } @@ -211,9 +214,6 @@ dependencies { api(project(":module:spring-boot-r2dbc")) { transitive = false } - api(project(":module:spring-boot-rabbitmq")) { - transitive = false - } api(project(":module:spring-boot-reactor")) { transitive = false } diff --git a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories deleted file mode 100644 index e3667de2bbcf..000000000000 --- a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,8 +0,0 @@ -# Connection Details Factories -org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ -org.springframework.boot.rabbitmq.docker.compose.RabbitDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.rabbitmq.docker.compose.RabbitStreamDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.rabbitmq.testcontainers.DeprecatedRabbitContainerConnectionDetailsFactory,\ -org.springframework.boot.rabbitmq.testcontainers.RabbitContainerConnectionDetailsFactory,\ -org.springframework.boot.rabbitmq.testcontainers.RabbitStreamContainerConnectionDetailsFactory - diff --git a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 9ec393f97b32..000000000000 --- a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1,3 +0,0 @@ -org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration -org.springframework.boot.rabbitmq.autoconfigure.health.RabbitHealthContributorAutoConfiguration -org.springframework.boot.rabbitmq.autoconfigure.metrics.RabbitMetricsAutoConfiguration \ No newline at end of file diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 20e4cb262a9e..f8e6ae8da672 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2118,6 +2118,7 @@ bom { "spring-boot-activemq", "spring-boot-actuator", "spring-boot-actuator-autoconfigure", + "spring-boot-amqp", "spring-boot-artemis", "spring-boot-autoconfigure", "spring-boot-autoconfigure-classic", @@ -2212,7 +2213,6 @@ bom { "spring-boot-pulsar", "spring-boot-quartz", "spring-boot-r2dbc", - "spring-boot-rabbitmq", "spring-boot-reactor", "spring-boot-reactor-netty", "spring-boot-restclient", @@ -2238,6 +2238,8 @@ bom { "spring-boot-starter-activemq-test", "spring-boot-starter-actuator", "spring-boot-starter-actuator-test", + "spring-boot-starter-amqp", + "spring-boot-starter-amqp-test", "spring-boot-starter-artemis", "spring-boot-starter-artemis-test", "spring-boot-starter-aspectj", @@ -2353,8 +2355,6 @@ bom { "spring-boot-starter-quartz-test", "spring-boot-starter-r2dbc", "spring-boot-starter-r2dbc-test", - "spring-boot-starter-rabbitmqmq", - "spring-boot-starter-rabbitmqmq-test", "spring-boot-starter-reactor-netty", "spring-boot-starter-restclient", "spring-boot-starter-restclient-test", diff --git a/settings.gradle b/settings.gradle index e9181fe88526..50e69eb92640 100644 --- a/settings.gradle +++ b/settings.gradle @@ -80,6 +80,7 @@ include "core:spring-boot-testcontainers" include "module:spring-boot-activemq" include "module:spring-boot-actuator" include "module:spring-boot-actuator-autoconfigure" +include "module:spring-boot-amqp" include "module:spring-boot-artemis" include "module:spring-boot-autoconfigure-classic" include "module:spring-boot-autoconfigure-classic-modules" @@ -166,7 +167,6 @@ include "module:spring-boot-persistence" include "module:spring-boot-pulsar" include "module:spring-boot-quartz" include "module:spring-boot-r2dbc" -include "module:spring-boot-rabbitmq" include "module:spring-boot-reactor" include "module:spring-boot-reactor-netty" include "module:spring-boot-restclient" @@ -211,6 +211,8 @@ include "starter:spring-boot-starter-activemq" include "starter:spring-boot-starter-activemq-test" include "starter:spring-boot-starter-actuator" include "starter:spring-boot-starter-actuator-test" +include "starter:spring-boot-starter-amqp" +include "starter:spring-boot-starter-amqp-test" include "starter:spring-boot-starter-artemis" include "starter:spring-boot-starter-artemis-test" include "starter:spring-boot-starter-aspectj" @@ -329,8 +331,6 @@ include "starter:spring-boot-starter-quartz" include "starter:spring-boot-starter-quartz-test" include "starter:spring-boot-starter-r2dbc" include "starter:spring-boot-starter-r2dbc-test" -include "starter:spring-boot-starter-rabbitmq" -include "starter:spring-boot-starter-rabbitmq-test" include "starter:spring-boot-starter-reactor-netty" include "starter:spring-boot-starter-restclient" include "starter:spring-boot-starter-restclient-test" @@ -399,6 +399,7 @@ include ":smoke-test:spring-boot-smoke-test-actuator-extension" include ":smoke-test:spring-boot-smoke-test-actuator-log4j2" include ":smoke-test:spring-boot-smoke-test-actuator-noweb" include ":smoke-test:spring-boot-smoke-test-actuator-ui" +include ":smoke-test:spring-boot-smoke-test-amqp" include ":smoke-test:spring-boot-smoke-test-ant" include ":smoke-test:spring-boot-smoke-test-artemis" include ":smoke-test:spring-boot-smoke-test-aspectj" @@ -454,7 +455,6 @@ include ":smoke-test:spring-boot-smoke-test-prometheus" include ":smoke-test:spring-boot-smoke-test-property-validation" include ":smoke-test:spring-boot-smoke-test-pulsar" include ":smoke-test:spring-boot-smoke-test-quartz" -include ":smoke-test:spring-boot-smoke-test-rabbitmq" include ":smoke-test:spring-boot-smoke-test-reactive-oauth2-client" include ":smoke-test:spring-boot-smoke-test-reactive-oauth2-resource-server" include ":smoke-test:spring-boot-smoke-test-restclient" diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/build.gradle b/smoke-test/spring-boot-smoke-test-amqp/build.gradle similarity index 90% rename from smoke-test/spring-boot-smoke-test-rabbitmq/build.gradle rename to smoke-test/spring-boot-smoke-test-amqp/build.gradle index 48fe3815c2bd..cadb8379f2ef 100644 --- a/smoke-test/spring-boot-smoke-test-rabbitmq/build.gradle +++ b/smoke-test/spring-boot-smoke-test-amqp/build.gradle @@ -19,13 +19,13 @@ plugins { id "org.springframework.boot.docker-test" } -description = "Spring Boot RabbitMQ smoke test" +description = "Spring Boot AMQP smoke test" dependencies { - implementation(project(":starter:spring-boot-starter-rabbitmq")) + implementation(project(":starter:spring-boot-starter-amqp")) dockerTestImplementation(project(":core:spring-boot-testcontainers")) - dockerTestImplementation(project(":starter:spring-boot-starter-rabbitmq-test")) + dockerTestImplementation(project(":starter:spring-boot-starter-amqp-test")) dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) dockerTestImplementation("org.awaitility:awaitility") dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationSslTests.java b/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java similarity index 97% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationSslTests.java rename to smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java index f846f70ae495..e1acf6319d04 100644 --- a/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationSslTests.java +++ b/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.rabbitmq; +package smoketest.amqp; import java.time.Duration; @@ -43,7 +43,7 @@ @SpringBootTest @Testcontainers(disabledWithoutDocker = true) @ExtendWith(OutputCaptureExtension.class) -class SampleRabbitApplicationSslTests { +class SampleAmqpSimpleApplicationSslTests { @Container @ServiceConnection diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationTests.java b/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java similarity index 96% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationTests.java rename to smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java index 626348198713..e763ade7aa09 100644 --- a/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.rabbitmq; +package smoketest.amqp; import java.time.Duration; @@ -37,7 +37,7 @@ @SpringBootTest @Testcontainers(disabledWithoutDocker = true) @ExtendWith(OutputCaptureExtension.class) -class SampleRabbitApplicationTests { +class SampleAmqpSimpleApplicationTests { @Container @ServiceConnection diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SecureRabbitMqContainer.java b/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SecureRabbitMqContainer.java similarity index 98% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SecureRabbitMqContainer.java rename to smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SecureRabbitMqContainer.java index 5dfb9e776c39..7599d2d4ff21 100644 --- a/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SecureRabbitMqContainer.java +++ b/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SecureRabbitMqContainer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.rabbitmq; +package smoketest.amqp; import org.testcontainers.rabbitmq.RabbitMQContainer; import org.testcontainers.utility.DockerImageName; diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/SampleRabbitApplication.java b/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java similarity index 87% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/SampleRabbitApplication.java rename to smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java index da492b1fe4f5..7c01337d3cba 100644 --- a/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/SampleRabbitApplication.java +++ b/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.rabbitmq; +package smoketest.amqp; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -30,9 +30,9 @@ @SpringBootApplication @RabbitListener(queues = "foo") -public class SampleRabbitApplication { +public class SampleAmqpSimpleApplication { - private static final Log logger = LogFactory.getLog(SampleRabbitApplication.class); + private static final Log logger = LogFactory.getLog(SampleAmqpSimpleApplication.class); @Bean public Sender mySender() { @@ -55,7 +55,7 @@ public ApplicationRunner runner(Sender sender) { } public static void main(String[] args) { - SpringApplication.run(SampleRabbitApplication.class, args); + SpringApplication.run(SampleAmqpSimpleApplication.class, args); } } diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/Sender.java b/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Sender.java similarity index 97% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/Sender.java rename to smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Sender.java index c3fd88bbed28..078f2416966c 100644 --- a/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/Sender.java +++ b/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Sender.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.rabbitmq; +package smoketest.amqp; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/package-info.java b/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/package-info.java similarity index 96% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/package-info.java rename to smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/package-info.java index a078a228474f..bc9e41a079d7 100644 --- a/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/package-info.java +++ b/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/package-info.java @@ -15,6 +15,6 @@ */ @NullMarked -package smoketest.rabbitmq; +package smoketest.amqp; import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/rabbitmq.conf b/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/rabbitmq.conf similarity index 100% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/rabbitmq.conf rename to smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/rabbitmq.conf diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-ca.crt b/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.crt similarity index 100% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-ca.crt rename to smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.crt diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-ca.key b/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.key similarity index 100% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-ca.key rename to smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.key diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-client.crt b/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.crt similarity index 100% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-client.crt rename to smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.crt diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-client.key b/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.key similarity index 100% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-client.key rename to smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.key diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-server.crt b/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.crt similarity index 100% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-server.crt rename to smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.crt diff --git a/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-server.key b/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.key similarity index 100% rename from smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-server.key rename to smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.key diff --git a/starter/spring-boot-starter-rabbitmq-test/build.gradle b/starter/spring-boot-starter-amqp-test/build.gradle similarity index 85% rename from starter/spring-boot-starter-rabbitmq-test/build.gradle rename to starter/spring-boot-starter-amqp-test/build.gradle index 217f6ee71891..efe35c54451b 100644 --- a/starter/spring-boot-starter-rabbitmq-test/build.gradle +++ b/starter/spring-boot-starter-amqp-test/build.gradle @@ -18,10 +18,10 @@ plugins { id "org.springframework.boot.starter" } -description = "Starter for testing RabbitMQ messaging and streaming broker" +description = "Starter for testing Spring AMQP and Rabbit MQ" dependencies { - api(project(":starter:spring-boot-starter-rabbitmq")) + api(project(":starter:spring-boot-starter-amqp")) api(project(":starter:spring-boot-starter-test")) api("org.springframework.amqp:spring-rabbit-test") diff --git a/starter/spring-boot-starter-rabbitmq/build.gradle b/starter/spring-boot-starter-amqp/build.gradle similarity index 86% rename from starter/spring-boot-starter-rabbitmq/build.gradle rename to starter/spring-boot-starter-amqp/build.gradle index 64879f96866f..bd8d7c906e19 100644 --- a/starter/spring-boot-starter-rabbitmq/build.gradle +++ b/starter/spring-boot-starter-amqp/build.gradle @@ -18,10 +18,10 @@ plugins { id "org.springframework.boot.starter" } -description = "Starter for using RabbitMQ messaging and streaming broker" +description = "Starter for using Spring AMQP and Rabbit MQ" dependencies { api(project(":starter:spring-boot-starter")) - api(project(":module:spring-boot-rabbitmq")) + api(project(":module:spring-boot-amqp")) } From d804749c2e3944f4480cfb0497039927e384ef6d Mon Sep 17 00:00:00 2001 From: Lee Jiwon Date: Sun, 22 Mar 2026 12:14:36 +0900 Subject: [PATCH 397/930] Fix duplicated words in external-config documentation See gh-49700 Signed-off-by: Lee Jiwon --- .../modules/reference/pages/features/external-config.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc index 5915325fd6b0..99d59dcf3c14 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc @@ -940,7 +940,7 @@ my.service.security= Will be bound as `new MyProperties(true, null, new Security(null, null, List.of("USER")))` ==== -If you want to always bind a a non-null instance of `Security`, even when properties are missing, you can use use an empty javadoc:org.springframework.boot.context.properties.bind.DefaultValue[format=annotation] annotation: +If you want to always bind a non-null instance of `Security`, even when properties are missing, you can use an empty javadoc:org.springframework.boot.context.properties.bind.DefaultValue[format=annotation] annotation: include-code::nonnull/MyProperties[tag=*] From 5b7a6a9cc645e5e19986ac4c71bec4902510126c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 24 Mar 2026 10:48:43 +0100 Subject: [PATCH 398/930] Consistently document default duration unit in gRPC support Closes gh-49722 --- .../autoconfigure/GrpcClientProperties.java | 13 ++++++++---- .../autoconfigure/GrpcServerProperties.java | 20 ++++++++++++------- .../health/GrpcServerHealthProperties.java | 10 ++++++++-- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java index e57ecece11b2..5dcb51031291 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java @@ -211,8 +211,10 @@ public void setMaxSize(DataSize maxSize) { public static class Default { /** - * The default deadline for RPCs performed on this channel. + * The default deadline for RPCs performed on this channel. If a duration + * suffix is not specified, seconds will be used. */ + @DurationUnit(ChronoUnit.SECONDS) private @Nullable Duration deadline; /** @@ -244,7 +246,8 @@ public void setLoadBalancingPolicy(String loadBalancingPolicy) { public static class Idle { /** - * The duration without ongoing RPCs before going to idle mode. + * The duration without ongoing RPCs before going to idle mode. If a duration + * suffix is not specified, seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) private Duration timeout = Duration.ofSeconds(20); @@ -267,13 +270,15 @@ public static class Keepalive { /** * The delay before sending a keepAlive. Note that shorter intervals increase * the network burden for the server and this value can not be lower than - * 'permitKeepAliveTime' on the server. + * 'permitKeepAliveTime' on the server. If a duration suffix is not specified, + * seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) private Duration time = Duration.ofMinutes(5); /** - * The default timeout for a keepAlives ping request. + * The default timeout for a keepAlives ping request. If a duration suffix is + * not specified, seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) private Duration timeout = Duration.ofSeconds(20); diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java index 0cd2266f1066..885af74ae68f 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java @@ -115,7 +115,8 @@ public static class Shutdown { /** * Maximum time to wait for the server to gracefully shutdown. When the value is * negative, the server waits forever. When the value is 0, the server will force - * shutdown immediately. The default is 30 seconds. + * shutdown immediately. The default is 30 seconds. If a duration suffix is not + * specified, seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) private Duration gracePeriod = Duration.ofSeconds(30); @@ -217,7 +218,8 @@ public void setName(@Nullable String name) { public static class Keepalive { /** - * Duration without read activity before sending a keep alive ping (default 2h). + * Duration without read activity before sending a keep alive ping. If a duration + * suffix is not specified, seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) private @Nullable Duration time = Duration.ofHours(2); @@ -225,7 +227,7 @@ public static class Keepalive { /** * Maximum time to wait for read activity after sending a keep alive ping. If * sender does not receive an acknowledgment within this time, it will close the - * connection (default 20s). + * connection. If a duration suffix is not specified, seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) private @Nullable Duration timeout = Duration.ofSeconds(20); @@ -264,7 +266,8 @@ public Connection getConnection() { public static class Permit { /** - * Maximum keep-alive time clients are permitted to configure (default 5m). + * Maximum keep-alive time clients are permitted to configure. If a duration + * suffix is not specified, seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) private @Nullable Duration time = Duration.ofMinutes(5); @@ -300,20 +303,23 @@ public static class Connection { /** * Maximum time a connection can remain idle before being gracefully - * terminated (default infinite). + * terminated (default infinite). If a duration suffix is not specified, + * seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) private @Nullable Duration maxIdleTime; /** * Maximum time a connection may exist before being gracefully terminated - * (default infinite). + * (default infinite). If a duration suffix is not specified, seconds will be + * used. */ @DurationUnit(ChronoUnit.SECONDS) private @Nullable Duration maxAge; /** - * Maximum time for graceful connection termination (default infinite). + * Maximum time for graceful connection termination (default infinite). If a + * duration suffix is not specified, seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) private @Nullable Duration gracePeriod = Duration.ofSeconds(30); diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java index fc80154becc0..7d60a4cc4479 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java @@ -17,6 +17,7 @@ package org.springframework.boot.grpc.server.autoconfigure.health; import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; @@ -29,6 +30,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.boot.convert.DurationUnit; import org.springframework.util.CollectionUtils; /** @@ -210,13 +212,17 @@ public static class Schedule { private boolean enabled = true; /** - * How often to update the health status. + * How often to update the health status. If a duration suffix is not specified, + * seconds will be used. */ + @DurationUnit(ChronoUnit.SECONDS) private Duration period = Duration.ofSeconds(5); /** - * The initial delay before updating the health status the very first time. + * The initial delay before updating the health status the very first time. If a + * duration suffix is not specified, seconds will be used. */ + @DurationUnit(ChronoUnit.SECONDS) private Duration delay = Duration.ofSeconds(5); public boolean isEnabled() { From bf5562359d763dbafc99147ac373f696ba0ec25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 24 Mar 2026 11:11:02 +0100 Subject: [PATCH 399/930] Polish configuration properties for gRPC support This commit aligns convention for configuration properties description: - Do not start with 'The' or 'A'. - Do not document default value when it is specified. - Document default change to unit --- .../autoconfigure/GrpcClientProperties.java | 28 ++++++------- .../autoconfigure/GrpcServerProperties.java | 40 ++++++++++--------- .../health/GrpcServerHealthProperties.java | 14 ++----- 3 files changed, 39 insertions(+), 43 deletions(-) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java index 5dcb51031291..164580bd8232 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java @@ -39,7 +39,7 @@ public class GrpcClientProperties { /** - * Map of channel configured by name. + * Map of channels configured by name. */ private final Map channel = new LinkedHashMap<>(); @@ -55,18 +55,18 @@ public static class Channel { static final String DEFAULT_TARGET = "static://localhost:9090"; /** - * The channel target address. + * Channel target address. */ private String target = DEFAULT_TARGET; /** - * The custom User-Agent for the channel. + * Custom User-Agent for the channel. */ private @Nullable String userAgent; /** - * Bypass certificate validation for easier testing (so the remote certificate - * could be anonymous). Should not be set in production. + * Whether to bypass certificate validation for easier testing (so the remote + * certificate could be anonymous). Should not be enabled in production. */ private boolean bypassCertificateValidation; @@ -211,14 +211,14 @@ public void setMaxSize(DataSize maxSize) { public static class Default { /** - * The default deadline for RPCs performed on this channel. If a duration - * suffix is not specified, seconds will be used. + * Default deadline for RPCs performed on this channel. If a duration suffix + * is not specified, seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) private @Nullable Duration deadline; /** - * The load balancing policy the channel should use. + * Load balancing policy the channel should use. */ private String loadBalancingPolicy = "round_robin"; @@ -246,8 +246,8 @@ public void setLoadBalancingPolicy(String loadBalancingPolicy) { public static class Idle { /** - * The duration without ongoing RPCs before going to idle mode. If a duration - * suffix is not specified, seconds will be used. + * Time without ongoing RPCs before going to idle mode. If a duration suffix + * is not specified, seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) private Duration timeout = Duration.ofSeconds(20); @@ -268,8 +268,8 @@ public void setTimeout(Duration timeout) { public static class Keepalive { /** - * The delay before sending a keepAlive. Note that shorter intervals increase - * the network burden for the server and this value can not be lower than + * Delay before sending a keepAlive. Note that shorter intervals increase the + * network burden for the server, and this value cannot be lower than * 'permitKeepAliveTime' on the server. If a duration suffix is not specified, * seconds will be used. */ @@ -277,8 +277,8 @@ public static class Keepalive { private Duration time = Duration.ofMinutes(5); /** - * The default timeout for a keepAlives ping request. If a duration suffix is - * not specified, seconds will be used. + * Default timeout for a keepAlives ping request. If a duration suffix is not + * specified, seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) private Duration timeout = Duration.ofSeconds(20); diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java index 885af74ae68f..6cc7dfe9e68a 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java @@ -115,8 +115,8 @@ public static class Shutdown { /** * Maximum time to wait for the server to gracefully shutdown. When the value is * negative, the server waits forever. When the value is 0, the server will force - * shutdown immediately. The default is 30 seconds. If a duration suffix is not - * specified, seconds will be used. + * shutdown immediately. If a duration suffix is not specified, seconds will be + * used. */ @DurationUnit(ChronoUnit.SECONDS) private Duration gracePeriod = Duration.ofSeconds(30); @@ -154,7 +154,8 @@ public Metadata getMetadata() { public static class Message { /** - * Maximum message size allowed to be received by the server (default 4MiB). + * Maximum message size allowed to be received by the server. If a data size + * suffix is not specified, bytes will be used. */ @DataSizeUnit(DataUnit.BYTES) private DataSize maxSize = DataSize.ofBytes(4194304); @@ -175,7 +176,8 @@ public void setMaxSize(DataSize maxSize) { public static class Metadata { /** - * Maximum metadata size allowed to be received by the server (default 8KiB). + * Maximum metadata size allowed to be received by the server. If a data size + * suffix is not specified, bytes will be used. */ @DataSizeUnit(DataUnit.BYTES) private DataSize maxSize = DataSize.ofBytes(8192); @@ -198,7 +200,7 @@ public void setMaxSize(DataSize maxSize) { public static class Inprocess { /** - * The name of the in-process server or null to not start the in-process server. + * Name of the in-process server or null to not start the in-process server. */ private @Nullable String name; @@ -225,7 +227,7 @@ public static class Keepalive { private @Nullable Duration time = Duration.ofHours(2); /** - * Maximum time to wait for read activity after sending a keep alive ping. If + * Maximum time to wait for read activity after sending a keep alive ping. If the * sender does not receive an acknowledgment within this time, it will close the * connection. If a duration suffix is not specified, seconds will be used. */ @@ -274,7 +276,7 @@ public static class Permit { /** * Whether clients are permitted to send keep alive pings when there are no - * outstanding RPCs on the connection (default false). + * outstanding RPCs on the connection. */ private boolean withoutCalls; @@ -303,23 +305,23 @@ public static class Connection { /** * Maximum time a connection can remain idle before being gracefully - * terminated (default infinite). If a duration suffix is not specified, - * seconds will be used. + * terminated (infinite if not specified). If a duration suffix is not + * specified, seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) private @Nullable Duration maxIdleTime; /** * Maximum time a connection may exist before being gracefully terminated - * (default infinite). If a duration suffix is not specified, seconds will be - * used. + * (infinite if not specified). If a duration suffix is not specified, seconds + * will be used. */ @DurationUnit(ChronoUnit.SECONDS) private @Nullable Duration maxAge; /** - * Maximum time for graceful connection termination (default infinite). If a - * duration suffix is not specified, seconds will be used. + * Maximum time for graceful connection termination. If a duration suffix is + * not specified, seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) private @Nullable Duration gracePeriod = Duration.ofSeconds(30); @@ -368,13 +370,13 @@ public static class Ssl { private ClientAuth clientAuth = ClientAuth.NONE; /** - * SSL bundle name. Should match a bundle configured in spring.ssl.bundle. + * SSL bundle name. */ private @Nullable String bundle; /** - * Flag to indicate that client authentication is secure (i.e. certificates are - * checked). Do not set this to false in production. + * Whether client authentication is secure (that is certificates are checked). Do + * not set this to false in production. */ private boolean secure = true; @@ -418,9 +420,9 @@ public boolean isSecure() { public static class Netty { /** - * Transport mechanism used for Netty and Netty Shaded servers. If not specified - * will the appropriate transport will be picked based on the - * 'deomain-socket-path' or 'address/port'. + * Transport mechanism used for Netty and Netty Shaded servers. If not specified, + * the appropriate transport will be picked based on the 'domain-socket-path' or + * 'address/port'. */ private @Nullable Transport transport; diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java index 7d60a4cc4479..2912cad3669b 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java @@ -44,7 +44,7 @@ public class GrpcServerHealthProperties { /** - * Whether to auto-configure Health feature on the gRPC server. + * Whether to auto-configure the Health feature on the gRPC server. */ private @Nullable Boolean enabled; @@ -53,13 +53,10 @@ public class GrpcServerHealthProperties { */ private boolean includeOverallHealth = true; - /** - * Properties that apply to all services. - */ private final Services services = new Services(); /** - * Service specific health reporting. + * Service-specific health reporting. */ private final Map service = new LinkedHashMap<>(); @@ -127,7 +124,7 @@ public void setValidateMembership(boolean validateMembership) { } /** - * A health for a specific service. + * Health for a specific service. */ public static class Service { @@ -141,9 +138,6 @@ public static class Service { */ private @Nullable Set exclude; - /** - * Status configuration. - */ @NestedConfigurationProperty private final Status status = new Status(); @@ -219,7 +213,7 @@ public static class Schedule { private Duration period = Duration.ofSeconds(5); /** - * The initial delay before updating the health status the very first time. If a + * Initial delay before updating the health status the very first time. If a * duration suffix is not specified, seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) From fe8fcaeaff3a330d0d04a8d666dcd2cc9cf8d68c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 24 Mar 2026 11:28:37 +0100 Subject: [PATCH 400/930] Polish --- .../grpc/client/autoconfigure/ServiceConfig.java | 4 ++-- .../grpc/server/GrpcServletRegistration.java | 2 +- .../grpc/server/autoconfigure/NettyAddress.java | 2 +- ...utoConfiguredHealthCheckedGrpcComponents.java | 2 +- .../GrpcServerHealthAutoConfiguration.java | 4 ++-- .../security/GrpcDisableCsrfHttpConfigurer.java | 2 +- .../security/web/reactive/GrpcRequest.java | 2 +- .../security/web/servlet/GrpcRequest.java | 4 ++-- .../grpc/server/health/GrpcServerHealth.java | 16 ++++++++-------- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java index 1ddd11a3a62e..8491084289a7 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java @@ -182,7 +182,7 @@ Map grpcJavaConfig() { * @param outOfBandReportingPeriod load reporting interval to request from the * server * @param enableOutOfBandLoadReport whether to enable out-of-band utilization - * reporting collection from the endpoints + * reporting collections from the endpoints * @param weightUpdatePeriod how often endpoint weights are recalculated * @param errorUtilizationPenalty multiplier used to adjust endpoint weights with * the error rate calculated as eps/qps @@ -327,7 +327,7 @@ Map grpcJavaConfig() { * @param maxBackoff maximum exponential backoff * @param backoffMultiplier exponential backoff multiplier * @param perAttemptReceiveTimeout per-attempt receive timeout - * @param retryableStatusCodes status codes which may be retried + * @param retryableStatusCodes status codes that may be retried */ public record RetryPolicy(Integer maxAttempts, Duration initialBackoff, Duration maxBackoff, Double backoffMultiplier, Duration perAttemptReceiveTimeout, Set retryableStatusCodes) { diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/GrpcServletRegistration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/GrpcServletRegistration.java index 1ddbbaf9d9b3..2b9185db55fa 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/GrpcServletRegistration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/GrpcServletRegistration.java @@ -49,7 +49,7 @@ */ public class GrpcServletRegistration extends DynamicRegistrationBean { - private static Log logger = LogFactory.getLog(GrpcServletRegistration.class); + private static final Log logger = LogFactory.getLog(GrpcServletRegistration.class); private final GrpcServlet servlet; diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddress.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddress.java index 4e66805002a4..077881fa1c4b 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddress.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddress.java @@ -40,7 +40,7 @@ record NettyAddress(@Nullable Transport transport, @Nullable InetAddress address @Nullable String domainSocketPath) { @Override - public final String toString() { + public String toString() { Transport transport = (this.transport != null) ? this.transport : deduceTransport(); return switch (transport) { case TCP -> tcpAddress(); diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java index 98248c2b885b..bdee841ba665 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java @@ -58,7 +58,7 @@ class AutoConfiguredHealthCheckedGrpcComponents implements HealthCheckedGrpcComp /** * Create a new {@link AutoConfiguredHealthCheckedGrpcComponents} instance. * @param applicationContext the application context used to check for override beans - * @param properties the grpc server health properties + * @param properties the gRPC server health properties */ AutoConfiguredHealthCheckedGrpcComponents(ApplicationContext applicationContext, GrpcServerHealthProperties properties) { diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java index f525a6e77b61..18b6c12cbabb 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java @@ -113,8 +113,8 @@ HealthContributorMembershipValidator grpcServerHealthServiceMembershipValidator( reactiveHealthContributorRegistry.getIfAvailable(), VALIDATE_MEMBERSHIP_PROPERTY, (members) -> properties.getService().forEach((serviceName, service) -> { String property = "spring.grpc.server.health.service." + serviceName; - members.member(property + ".include".formatted(serviceName), service.getInclude()); - members.member(property + ".exclude".formatted(serviceName), service.getExclude()); + members.member(property + ".include", service.getInclude()); + members.member(property + ".exclude", service.getExclude()); })); } diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java index fa407365734f..1b889389bd24 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java @@ -74,7 +74,7 @@ private void disable(CsrfConfigurer csrf) { static class GrpcCsrfRequestMatcher implements RequestMatcher { - static GrpcCsrfRequestMatcher INSTANCE = new GrpcCsrfRequestMatcher(); + static final GrpcCsrfRequestMatcher INSTANCE = new GrpcCsrfRequestMatcher(); @Override public boolean matches(HttpServletRequest request) { diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequest.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequest.java index c298429d7677..bc389a5da4fb 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequest.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequest.java @@ -125,7 +125,7 @@ private Stream getPatterns(GrpcServiceDiscoverer serviceDiscoverer) { } private boolean isExcluded(String service) { - return !this.excludes.stream().anyMatch((candidate) -> candidate.equals(service)); + return this.excludes.stream().noneMatch((candidate) -> candidate.equals(service)); } private String getPath(String service) { diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequest.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequest.java index f2eb644214fb..2641c6883a2c 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequest.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequest.java @@ -110,7 +110,7 @@ protected void initialized(Supplier context) { this.delegate = createDelegate(context.get()); } - private @Nullable RequestMatcher createDelegate(GrpcServiceDiscoverer grpcServiceDiscoverer) { + private RequestMatcher createDelegate(GrpcServiceDiscoverer grpcServiceDiscoverer) { List delegateMatchers = getDelegateMatchers(grpcServiceDiscoverer); return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrRequestMatcher(delegateMatchers) : EMPTY_MATCHER; @@ -125,7 +125,7 @@ private Stream getPatterns(GrpcServiceDiscoverer serviceDiscoverer) { } private boolean isExcluded(String service) { - return !this.excludes.stream().anyMatch((candidate) -> candidate.equals(service)); + return this.excludes.stream().noneMatch((candidate) -> candidate.equals(service)); } private String getPath(String service) { diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/GrpcServerHealth.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/GrpcServerHealth.java index 0ff80ca4cc69..b4c2bb056fb8 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/GrpcServerHealth.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/GrpcServerHealth.java @@ -44,11 +44,11 @@ */ public class GrpcServerHealth { - private HealthContributorRegistry registry; + private final HealthContributorRegistry registry; - private @Nullable ReactiveHealthContributorRegistry fallbackRegistry; + private final @Nullable ReactiveHealthContributorRegistry fallbackRegistry; - private HealthCheckedGrpcComponents components; + private final HealthCheckedGrpcComponents components; /** * Create a new {@link GrpcServerHealth} instance. @@ -69,16 +69,16 @@ public void update(HealthStatusManager manager) { update(manager::setStatus); } - public void update(BiConsumer updator) { + public void update(BiConsumer updater) { Cache cache = new Cache(); HealthCheckedGrpcComponent serverComponent = this.components.getServer(); if (serverComponent != null) { - updator.accept("", getServingStatus(cache, serverComponent)); + updater.accept("", getServingStatus(cache, serverComponent)); } for (String serviceName : this.components.getServiceNames()) { HealthCheckedGrpcComponent serviceComponent = this.components.getService(serviceName); if (!serviceName.isEmpty() && serviceComponent != null) { - updator.accept(serviceName, getServingStatus(cache, serviceComponent)); + updater.accept(serviceName, getServingStatus(cache, serviceComponent)); } } } @@ -109,11 +109,11 @@ else if (component.isMember(name)) { } } - class Cache { + static class Cache { private final Map health = new HashMap<>(); - Health getHealth(String name, HealthIndicator indicator) { + @Nullable Health getHealth(String name, HealthIndicator indicator) { return this.health.computeIfAbsent(name, (key) -> indicator.health(false)); } From d3b6fa73ad23b2a12606e9e35a20b79ba9c4eb7e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 23 Mar 2026 16:11:24 -0700 Subject: [PATCH 401/930] Polish EntityManagerFactoryBuilder --- .../boot/jpa/EntityManagerFactoryBuilder.java | 70 ++++++++----------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java index c23760d5e42a..44a8024b63d9 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java @@ -31,6 +31,7 @@ import org.jspecify.annotations.Nullable; +import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; @@ -261,48 +262,37 @@ public Builder jta(boolean jta) { return this; } + /** + * Build a new {@link LocalContainerEntityManagerFactoryBean} instance from this + * builder. + * @return the built {@link LocalContainerEntityManagerFactoryBean} + */ public LocalContainerEntityManagerFactoryBean build() { - LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); - if (EntityManagerFactoryBuilder.this.persistenceUnitManager != null) { - entityManagerFactoryBean - .setPersistenceUnitManager(EntityManagerFactoryBuilder.this.persistenceUnitManager); - } - if (this.persistenceUnit != null) { - entityManagerFactoryBean.setPersistenceUnitName(this.persistenceUnit); - } - entityManagerFactoryBean.setJpaVendorAdapter(EntityManagerFactoryBuilder.this.jpaVendorAdapter); + PropertyMapper map = PropertyMapper.get(); + LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); + map.from(EntityManagerFactoryBuilder.this.persistenceUnitManager).to(factory::setPersistenceUnitManager); + map.from(this.persistenceUnit).to(factory::setPersistenceUnitName); + map.from(EntityManagerFactoryBuilder.this.jpaVendorAdapter).to(factory::setJpaVendorAdapter); + map.from(this.dataSource).to((!this.jta) ? factory::setDataSource : factory::setJtaDataSource); + map.from(this.managedTypes).to(factory::setManagedTypes); + map.from(this.packagesToScan).to(factory::setPackagesToScan); + map.from(this::jpaPropertyMap).to(factory.getJpaPropertyMap()::putAll); + map.from(this.mappingResources).whenNot(ObjectUtils::isEmpty).to(factory::setMappingResources); + map.from(EntityManagerFactoryBuilder.this.persistenceUnitRootLocation) + .as(Object::toString) + .to(factory::setPersistenceUnitRootLocation); + map.from(EntityManagerFactoryBuilder.this.bootstrapExecutor).to(factory::setBootstrapExecutor); + map.from(EntityManagerFactoryBuilder.this.persistenceUnitPostProcessors) + .as((postProcessors) -> postProcessors.toArray(PersistenceUnitPostProcessor[]::new)) + .to(factory::setPersistenceUnitPostProcessors); + return factory; + } - if (this.jta) { - entityManagerFactoryBean.setJtaDataSource(this.dataSource); - } - else { - entityManagerFactoryBean.setDataSource(this.dataSource); - } - if (this.managedTypes != null) { - entityManagerFactoryBean.setManagedTypes(this.managedTypes); - } - else if (this.packagesToScan != null) { - entityManagerFactoryBean.setPackagesToScan(this.packagesToScan); - } - Map jpaProperties = EntityManagerFactoryBuilder.this.jpaPropertiesFactory.apply(this.dataSource); - entityManagerFactoryBean.getJpaPropertyMap().putAll(new LinkedHashMap<>(jpaProperties)); - entityManagerFactoryBean.getJpaPropertyMap().putAll(this.properties); - if (!ObjectUtils.isEmpty(this.mappingResources)) { - entityManagerFactoryBean.setMappingResources(this.mappingResources); - } - URL rootLocation = EntityManagerFactoryBuilder.this.persistenceUnitRootLocation; - if (rootLocation != null) { - entityManagerFactoryBean.setPersistenceUnitRootLocation(rootLocation.toString()); - } - if (EntityManagerFactoryBuilder.this.bootstrapExecutor != null) { - entityManagerFactoryBean.setBootstrapExecutor(EntityManagerFactoryBuilder.this.bootstrapExecutor); - } - if (EntityManagerFactoryBuilder.this.persistenceUnitPostProcessors != null) { - entityManagerFactoryBean - .setPersistenceUnitPostProcessors(EntityManagerFactoryBuilder.this.persistenceUnitPostProcessors - .toArray(PersistenceUnitPostProcessor[]::new)); - } - return entityManagerFactoryBean; + private Map jpaPropertyMap() { + Map jpaPropertyMap = new LinkedHashMap<>(); + jpaPropertyMap.putAll(EntityManagerFactoryBuilder.this.jpaPropertiesFactory.apply(this.dataSource)); + jpaPropertyMap.putAll(this.properties); + return jpaPropertyMap; } } From e76c6c5488fc1fa4e01030620db044f3f93eb0a7 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 23 Mar 2026 15:58:03 -0700 Subject: [PATCH 402/930] Refine Spring Data JPA background bootstrapping mode support Update `spring.data.jpa.repositories.bootstrap-mode` logic so that: - A value of `lazy` no longer sets a bootstrap executor. - A value of `deferred` requires that a bootstrap executor is ultimately set, and fails with a meaningful message if it is not. The logic used to find the AsyncTaskExecutor has now moved to the `JpaBaseConfiguration` and is triggered by the `EntityManagerFactoryBuilder` only when background bootstrapping is required. Closes gh-49688 --- ...DataJpaTestAttributesIntegrationTests.java | 3 + .../DataJpaRepositoriesAutoConfiguration.java | 31 ++-------- ...JpaRepositoriesAutoConfigurationTests.java | 15 +---- .../HibernateJpaAutoConfigurationTests.java | 2 +- .../boot/jpa/EntityManagerFactoryBuilder.java | 59 +++++++++++++++++- .../autoconfigure/JpaBaseConfiguration.java | 13 +++- ...yBasedRequiredBackgroundBootstrapping.java | 60 +++++++++++++++++++ .../jpa/EntityManagerFactoryBuilderTests.java | 46 +++++++++++++- ...dRequiredBackgroundBootstrappingTests.java | 40 +++++++++++++ 9 files changed, 224 insertions(+), 45 deletions(-) create mode 100644 module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrapping.java create mode 100644 module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrappingTests.java diff --git a/module/spring-boot-data-jpa-test/src/test/java/org/springframework/boot/data/jpa/test/autoconfigure/DataJpaTestAttributesIntegrationTests.java b/module/spring-boot-data-jpa-test/src/test/java/org/springframework/boot/data/jpa/test/autoconfigure/DataJpaTestAttributesIntegrationTests.java index 9fbec8b4e141..218388b805a7 100644 --- a/module/spring-boot-data-jpa-test/src/test/java/org/springframework/boot/data/jpa/test/autoconfigure/DataJpaTestAttributesIntegrationTests.java +++ b/module/spring-boot-data-jpa-test/src/test/java/org/springframework/boot/data/jpa/test/autoconfigure/DataJpaTestAttributesIntegrationTests.java @@ -19,6 +19,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.core.env.Environment; import org.springframework.data.repository.config.BootstrapMode; @@ -31,6 +33,7 @@ * @author Scott Frederick */ @DataJpaTest(properties = "spring.profiles.active=test", bootstrapMode = BootstrapMode.DEFERRED) +@ImportAutoConfiguration(TaskExecutionAutoConfiguration.class) class DataJpaTestAttributesIntegrationTests { @Autowired diff --git a/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java b/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java index 1cd398afad0d..a66405b5ac98 100644 --- a/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java +++ b/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java @@ -25,7 +25,6 @@ import org.springframework.boot.LazyInitializationExcludeFilter; 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.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -35,8 +34,8 @@ import org.springframework.boot.data.jpa.autoconfigure.DataJpaRepositoriesAutoConfiguration.JpaRepositoriesImportSelector; import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration; import org.springframework.boot.jpa.autoconfigure.EntityManagerFactoryBuilderCustomizer; +import org.springframework.boot.jpa.autoconfigure.PropertyBasedRequiredBackgroundBootstrapping; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.task.AsyncTaskExecutor; @@ -83,15 +82,11 @@ public final class DataJpaRepositoriesAutoConfiguration { @Bean - @Conditional(BootstrapExecutorCondition.class) + @ConditionalOnProperty(name = "spring.data.jpa.repositories.bootstrap-mode", havingValue = "deferred") EntityManagerFactoryBuilderCustomizer entityManagerFactoryBootstrapExecutorCustomizer( Map taskExecutors) { - return (builder) -> { - AsyncTaskExecutor bootstrapExecutor = determineBootstrapExecutor(taskExecutors); - if (bootstrapExecutor != null) { - builder.setBootstrapExecutor(bootstrapExecutor); - } - }; + return (builder) -> builder.requireBootstrapExecutor(new PropertyBasedRequiredBackgroundBootstrapping( + "spring.data.jpa.repositories.bootstrap-mode", "deferred")); } @Bean @@ -106,24 +101,6 @@ static LazyInitializationExcludeFilter eagerJpaMetamodelCacheCleanup() { return taskExecutors.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME); } - private static final class BootstrapExecutorCondition extends AnyNestedCondition { - - BootstrapExecutorCondition() { - super(ConfigurationPhase.REGISTER_BEAN); - } - - @ConditionalOnProperty(name = "spring.data.jpa.repositories.bootstrap-mode", havingValue = "deferred") - static class DeferredBootstrapMode { - - } - - @ConditionalOnProperty(name = "spring.data.jpa.repositories.bootstrap-mode", havingValue = "lazy") - static class LazyBootstrapMode { - - } - - } - static class JpaRepositoriesImportSelector implements ImportSelector { private static final boolean ENVERS_AVAILABLE = ClassUtils.isPresent( diff --git a/module/spring-boot-data-jpa/src/test/java/org/springframework/boot/data/jpa/autoconfigure/AbstractDataJpaRepositoriesAutoConfigurationTests.java b/module/spring-boot-data-jpa/src/test/java/org/springframework/boot/data/jpa/autoconfigure/AbstractDataJpaRepositoriesAutoConfigurationTests.java index 6765133aa6f5..7d7a219961d8 100644 --- a/module/spring-boot-data-jpa/src/test/java/org/springframework/boot/data/jpa/autoconfigure/AbstractDataJpaRepositoriesAutoConfigurationTests.java +++ b/module/spring-boot-data-jpa/src/test/java/org/springframework/boot/data/jpa/autoconfigure/AbstractDataJpaRepositoriesAutoConfigurationTests.java @@ -88,23 +88,12 @@ void autoConfigurationShouldNotKickInEvenIfManualConfigDidNotCreateAnyRepositori } @Test - void whenBootstrapModeIsLazyWithMultipleAsyncExecutorBootstrapExecutorIsConfigured() { - this.contextRunner.withUserConfiguration(MultipleAsyncTaskExecutorConfiguration.class) - .withConfiguration( - AutoConfigurations.of(TaskExecutionAutoConfiguration.class, TaskSchedulingAutoConfiguration.class)) - .withPropertyValues("spring.data.jpa.repositories.bootstrap-mode=lazy") - .run((context) -> assertThat( - context.getBean(LocalContainerEntityManagerFactoryBean.class).getBootstrapExecutor()) - .isEqualTo(context.getBean("applicationTaskExecutor"))); - } - - @Test - void whenBootstrapModeIsLazyWithSingleAsyncExecutorBootstrapExecutorIsConfigured() { + void whenBootstrapModeDoesNotUseFallbackBootstrapExecutor() { this.contextRunner.withUserConfiguration(SingleAsyncTaskExecutorConfiguration.class) .withPropertyValues("spring.data.jpa.repositories.bootstrap-mode=lazy") .run((context) -> assertThat( context.getBean(LocalContainerEntityManagerFactoryBean.class).getBootstrapExecutor()) - .isEqualTo(context.getBean("testAsyncTaskExecutor"))); + .isNull()); } @Test diff --git a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java index 406bee31d408..1a38445a3219 100644 --- a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java +++ b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java @@ -126,7 +126,7 @@ import static org.mockito.Mockito.mock; /** - * Base for JPA tests and tests for {@link JpaBaseConfiguration}. + * Test for {@link HibernateJpaAutoConfiguration} and {@link JpaBaseConfiguration}. * * @author Phillip Webb * @author Dave Syer diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java index 44a8024b63d9..d9251fb96350 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; +import java.util.function.Supplier; import javax.sql.DataSource; @@ -35,9 +36,11 @@ import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.LocalEntityManagerFactoryBean; import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -65,10 +68,14 @@ public class EntityManagerFactoryBuilder { private final @Nullable URL persistenceUnitRootLocation; + private final @Nullable AsyncTaskExecutor fallbackBootstrapExecutor; + private @Nullable AsyncTaskExecutor bootstrapExecutor; private @Nullable List persistenceUnitPostProcessors; + private @Nullable Supplier requireBootstrapExecutorExceptionSupplier; + /** * Create a new instance passing in the common pieces that will be shared if multiple * EntityManagerFactory instances are created. @@ -98,10 +105,32 @@ public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, Function> jpaPropertiesFactory, @Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation) { + this(jpaVendorAdapter, jpaPropertiesFactory, persistenceUnitManager, persistenceUnitRootLocation, null); + } + + /** + * Create a new instance passing in the common pieces that will be shared if multiple + * EntityManagerFactory instances are created. + * @param jpaVendorAdapter a vendor adapter + * @param jpaPropertiesFactory the JPA properties to be passed to the persistence + * provider, based on the {@linkplain #dataSource(DataSource) configured data source} + * @param persistenceUnitManager optional source of persistence unit information (can + * be null) + * @param persistenceUnitRootLocation the persistence unit root location to use as a + * fallback or {@code null} + * @param fallbackBootstrapExecutor the fallback executor to use when background + * bootstrapping is required but no explicit executor has been set + * @since 4.1.0 + */ + public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, + Function> jpaPropertiesFactory, + @Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation, + @Nullable AsyncTaskExecutor fallbackBootstrapExecutor) { this.jpaVendorAdapter = jpaVendorAdapter; this.persistenceUnitManager = persistenceUnitManager; this.jpaPropertiesFactory = jpaPropertiesFactory; this.persistenceUnitRootLocation = persistenceUnitRootLocation; + this.fallbackBootstrapExecutor = fallbackBootstrapExecutor; } /** @@ -123,6 +152,20 @@ public void setBootstrapExecutor(AsyncTaskExecutor bootstrapExecutor) { this.bootstrapExecutor = bootstrapExecutor; } + /** + * Require a bootstrap executor to be configured when the + * {@link LocalEntityManagerFactoryBean} is built. If no bootstrap executor is + * {@link #setBootstrapExecutor(AsyncTaskExecutor) explicitly set} and no + * {@link #EntityManagerFactoryBuilder(JpaVendorAdapter, Function, PersistenceUnitManager, URL, AsyncTaskExecutor) + * fallbackBootstrapExecutor} is available then the supplied exception is thrown. + * @param exceptionSupplier a supplier providing the exception to throw + * @since 4.1.0 + */ + public void requireBootstrapExecutor(Supplier exceptionSupplier) { + Assert.notNull(exceptionSupplier, "'exceptionSupplier' must not be null"); + this.requireBootstrapExecutorExceptionSupplier = exceptionSupplier; + } + /** * Set the {@linkplain PersistenceUnitPostProcessor persistence unit post processors} * to be applied to the PersistenceUnitInfo used for creating the @@ -281,7 +324,7 @@ public LocalContainerEntityManagerFactoryBean build() { map.from(EntityManagerFactoryBuilder.this.persistenceUnitRootLocation) .as(Object::toString) .to(factory::setPersistenceUnitRootLocation); - map.from(EntityManagerFactoryBuilder.this.bootstrapExecutor).to(factory::setBootstrapExecutor); + map.from(this::bootstrapExecutor).to(factory::setBootstrapExecutor); map.from(EntityManagerFactoryBuilder.this.persistenceUnitPostProcessors) .as((postProcessors) -> postProcessors.toArray(PersistenceUnitPostProcessor[]::new)) .to(factory::setPersistenceUnitPostProcessors); @@ -295,6 +338,20 @@ private Map jpaPropertyMap() { return jpaPropertyMap; } + private @Nullable AsyncTaskExecutor bootstrapExecutor() { + if (EntityManagerFactoryBuilder.this.bootstrapExecutor != null) { + return EntityManagerFactoryBuilder.this.bootstrapExecutor; + } + if (EntityManagerFactoryBuilder.this.requireBootstrapExecutorExceptionSupplier != null) { + if (EntityManagerFactoryBuilder.this.fallbackBootstrapExecutor != null) { + return EntityManagerFactoryBuilder.this.fallbackBootstrapExecutor; + } + RuntimeException ex = EntityManagerFactoryBuilder.this.requireBootstrapExecutorExceptionSupplier.get(); + throw (ex != null) ? ex : new IllegalStateException("A bootstrap executor is required"); + } + return null; + } + } } diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java index 47bbe2e9a8a5..032d26361d79 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingFilterBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jpa.EntityManagerFactoryBuilder; import org.springframework.boot.persistence.autoconfigure.EntityScanPackages; @@ -45,6 +46,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; @@ -120,13 +122,20 @@ public JpaVendorAdapter jpaVendorAdapter() { @ConditionalOnMissingBean public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, ObjectProvider persistenceUnitManager, - ObjectProvider customizers) { + ObjectProvider customizers, + Map taskExecutors) { + @Nullable AsyncTaskExecutor bootstrapExecutor = determineBootstrapExecutor(taskExecutors); EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(jpaVendorAdapter, - this::buildJpaProperties, persistenceUnitManager.getIfAvailable()); + this::buildJpaProperties, persistenceUnitManager.getIfAvailable(), null, bootstrapExecutor); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder; } + private @Nullable AsyncTaskExecutor determineBootstrapExecutor(Map taskExecutors) { + return (taskExecutors.size() == 1) ? taskExecutors.values().iterator().next() + : taskExecutors.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME); + } + private Map buildJpaProperties(DataSource dataSource) { Map properties = new HashMap<>(this.properties.getProperties()); Map vendorProperties = getVendorProperties(dataSource); diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrapping.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrapping.java new file mode 100644 index 000000000000..1f139a2a8c6f --- /dev/null +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrapping.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jpa.autoconfigure; + +import java.util.function.Supplier; + +import org.springframework.boot.diagnostics.FailureAnalyzedException; +import org.springframework.boot.jpa.EntityManagerFactoryBuilder; +import org.springframework.util.Assert; + +/** + * {@link Supplier} to use with + * {@link EntityManagerFactoryBuilder#requireBootstrapExecutor} when a property indicates + * background bootstrapping is required. + * + * @author Phillip Webb + * @since 4.1.0 + */ +public class PropertyBasedRequiredBackgroundBootstrapping implements Supplier { + + private final String propertyName; + + private final String propertyValue; + + public PropertyBasedRequiredBackgroundBootstrapping(String propertyName, String propertyValue) { + Assert.notNull(propertyName, "'propertyName' must not be null"); + Assert.notNull(propertyValue, "'propertyValue' must not be null"); + this.propertyName = propertyName; + this.propertyValue = propertyValue; + } + + @Override + public RuntimeException get() { + String description = "A LocalContainerEntityManagerFactoryBean bootstrap executor is required when '%s' is set to '%s'" + .formatted(this.propertyName, this.propertyValue); + StringBuilder action = new StringBuilder(); + action.append("Use a different '%s' or provide a bootstrap executor using one of the following methods:\n" + .formatted(this.propertyName)); + action.append("\tWith an auto-configured task executor " + + "(you may need to set 'spring.task.execution.mode' to 'force')."); + action.append("\tWith an AsyncTaskExecutor bean named 'applicationTaskExecutor.'"); + action.append("\tUsing a EntityManagerFactoryBuilderCustomizer."); + return new FailureAnalyzedException(description, action.toString()); + } + +} diff --git a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java index 5925dd6bac43..58b584f27951 100644 --- a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java +++ b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java @@ -24,12 +24,17 @@ import jakarta.persistence.spi.PersistenceProvider; import org.assertj.core.api.InstanceOfAssertFactories; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter; 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.Mockito.mock; /** @@ -86,9 +91,48 @@ void addPersistenceUnitPostProcessorsWhenNotEmpty() { .containsExactly(postProcessor, postProcessor2); } + @Test + void requireBootstrapExecutorWhenExecutorProvidedDoesNotThrow() { + EntityManagerFactoryBuilder builder = createEmptyBuilder(); + builder.requireBootstrapExecutor(() -> new IllegalStateException("BAD")); + builder.setBootstrapExecutor(new SimpleAsyncTaskExecutor()); + DataSource dataSource = mock(); + assertThatNoException().isThrownBy(builder.dataSource(dataSource)::build); + } + + @Test + void requireBootstrapExecutorWhenFallbackExecutorProvidesExecutorDoesNotThrow() { + EntityManagerFactoryBuilder builder = createEmptyBuilder(new SimpleAsyncTaskExecutor()); + builder.requireBootstrapExecutor(() -> new IllegalStateException("BAD")); + DataSource dataSource = mock(); + assertThatNoException().isThrownBy(builder.dataSource(dataSource)::build); + } + + @Test + void requireBootstrapExecutorWhenExecutorAndNoFallbackExecutorThrowsException() { + EntityManagerFactoryBuilder builder = createEmptyBuilder(); + builder.requireBootstrapExecutor(() -> new IllegalStateException("BAD")); + DataSource dataSource = mock(); + assertThatIllegalStateException().isThrownBy(builder.dataSource(dataSource)::build).withMessage("BAD"); + } + + @Test + void requireBootstrapExecutorWhenSupplierReturnsNullExecutorAndNoFallbackExecutorThrowsException() { + EntityManagerFactoryBuilder builder = createEmptyBuilder(); + builder.requireBootstrapExecutor(() -> null); + DataSource dataSource = mock(); + assertThatIllegalStateException().isThrownBy(builder.dataSource(dataSource)::build) + .withMessage("A bootstrap executor is required"); + } + private EntityManagerFactoryBuilder createEmptyBuilder() { + return createEmptyBuilder(null); + } + + private EntityManagerFactoryBuilder createEmptyBuilder(@Nullable AsyncTaskExecutor fallbackBootstrapExecutor) { Function> jpaPropertiesFactory = (dataSource) -> Collections.emptyMap(); - return new EntityManagerFactoryBuilder(new TestJpaVendorAdapter(), jpaPropertiesFactory, null); + return new EntityManagerFactoryBuilder(new TestJpaVendorAdapter(), jpaPropertiesFactory, null, null, + fallbackBootstrapExecutor); } static class TestJpaVendorAdapter extends AbstractJpaVendorAdapter { diff --git a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrappingTests.java b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrappingTests.java new file mode 100644 index 000000000000..765b0f1fa633 --- /dev/null +++ b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrappingTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jpa.autoconfigure; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.diagnostics.FailureAnalyzedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PropertyBasedRequiredBackgroundBootstrapping}. + * + * @author Phillip Webb + */ +class PropertyBasedRequiredBackgroundBootstrappingTests { + + @Test + void getReturnsFailureAnalyzableException() { + RuntimeException exception = new PropertyBasedRequiredBackgroundBootstrapping("test.bootstrap", "true").get(); + assertThat(exception).isInstanceOf(FailureAnalyzedException.class) + .hasMessage("A LocalContainerEntityManagerFactoryBean bootstrap executor is required " + + "when 'test.bootstrap' is set to 'true'"); + } + +} From 91f4bc94558e161dd9d100fdd9db209a8f55fa8b Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 23 Mar 2026 21:22:21 -0700 Subject: [PATCH 403/930] Add 'spring.jpa.bootstrap' property Add a `spring.jpa.bootstrap` property that can be set to `async` to switch on background bootstrapping in classic JPA setup. Closes gh-49733 --- .../HibernateJpaAutoConfigurationTests.java | 91 +++++++++++++++++++ .../autoconfigure/JpaBaseConfiguration.java | 5 + .../boot/jpa/autoconfigure/JpaProperties.java | 32 +++++++ 3 files changed, 128 insertions(+) diff --git a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java index 1a38445a3219..5e0ab0717235 100644 --- a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java +++ b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java @@ -67,6 +67,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; +import org.springframework.boot.diagnostics.FailureAnalyzedException; import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration; import org.springframework.boot.hibernate.SpringImplicitNamingStrategy; import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfigurationTests.JpaUsingApplicationListenerConfiguration.EventCapturingApplicationListener; @@ -102,6 +104,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.SQLExceptionTranslator; import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; @@ -247,6 +250,59 @@ void openEntityManagerInViewInterceptorIsNotRegisteredWhenExplicitlyOff() { .run((context) -> assertThat(context).doesNotHaveBean(OpenEntityManagerInViewInterceptor.class)); } + @Test + void whenBackgroundBootstrapingAndSingleAsyncTaksExecutorConfiguresBackgroundExecutor() { + this.contextRunner.withPropertyValues("spring.jpa.bootstrap=async") + .withUserConfiguration(SingleAsyncTaskExecutorConfiguration.class) + .run((context) -> assertThat( + context.getBean(LocalContainerEntityManagerFactoryBean.class).getBootstrapExecutor()) + .isInstanceOf(SimpleAsyncTaskExecutor.class)); + } + + @Test + void whenBackgroundBootstrapingAndApplicationTaksExecutorConfiguresBackgroundExecutor() { + this.contextRunner.withPropertyValues("spring.jpa.bootstrap=async") + .withUserConfiguration(MultipleAsyncTaskExecutorsConfiguration.class, + ApplicationTaskExecutorConfiguration.class) + .run((context) -> assertThat( + context.getBean(LocalContainerEntityManagerFactoryBean.class).getBootstrapExecutor()) + .isSameAs(context.getBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME))); + } + + @Test + void whenBackgroundBootstrapingAndMissingTaksExecutorThrowsException() { + this.contextRunner.withPropertyValues("spring.jpa.bootstrap=async") + .run((context) -> assertThat(context).getFailure() + .rootCause() + .isInstanceOf(FailureAnalyzedException.class) + .message() + .contains("bootstrap executor is required when 'spring.jpa.bootstrap' is set to 'async'")); + } + + @Test + void whenBackgroundBootstrapingAndMultipleTaksExecutorThrowsException() { + this.contextRunner.withPropertyValues("spring.jpa.bootstrap=async") + .withUserConfiguration(MultipleAsyncTaskExecutorsConfiguration.class) + .run((context) -> assertThat(context).getFailure() + .rootCause() + .isInstanceOf(FailureAnalyzedException.class) + .message() + .contains("bootstrap executor is required when 'spring.jpa.bootstrap' is set to 'async'")); + } + + @Test + void whenBackgroundBootstrapingAndCustomizedBackgroundExecutorThrowsException() { + this.contextRunner.withPropertyValues("spring.jpa.bootstrap=async") + .withBean(EntityManagerFactoryBuilderCustomizer.class, this::bootstrapExecutorCustomizer) + .run((context) -> assertThat( + context.getBean(LocalContainerEntityManagerFactoryBean.class).getBootstrapExecutor()) + .isInstanceOf(SimpleAsyncTaskExecutor.class)); + } + + private EntityManagerFactoryBuilderCustomizer bootstrapExecutorCustomizer() { + return (builder) -> builder.setBootstrapExecutor(new SimpleAsyncTaskExecutor()); + } + @Test void customJpaProperties() { this.contextRunner @@ -1383,4 +1439,39 @@ JtaTransactionManager jtaTransactionManager() { } + @Configuration(proxyBeanMethods = false) + static class SingleAsyncTaskExecutorConfiguration { + + @Bean + SimpleAsyncTaskExecutor exampleTaskExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class MultipleAsyncTaskExecutorsConfiguration { + + @Bean + SimpleAsyncTaskExecutor exampleTaskExecutor1() { + return new SimpleAsyncTaskExecutor(); + } + + @Bean + SimpleAsyncTaskExecutor exampleTaskExecutor2() { + return new SimpleAsyncTaskExecutor(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ApplicationTaskExecutorConfiguration { + + @Bean + SimpleAsyncTaskExecutor applicationTaskExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + } + } diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java index 032d26361d79..c4c14902b256 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java @@ -40,6 +40,7 @@ import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jpa.EntityManagerFactoryBuilder; +import org.springframework.boot.jpa.autoconfigure.JpaProperties.Bootstrap; import org.springframework.boot.persistence.autoconfigure.EntityScanPackages; import org.springframework.boot.transaction.autoconfigure.TransactionManagerCustomizers; import org.springframework.context.annotation.Bean; @@ -127,6 +128,10 @@ public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter @Nullable AsyncTaskExecutor bootstrapExecutor = determineBootstrapExecutor(taskExecutors); EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(jpaVendorAdapter, this::buildJpaProperties, persistenceUnitManager.getIfAvailable(), null, bootstrapExecutor); + if (this.properties.getBootstrap() == Bootstrap.ASYNC) { + builder.requireBootstrapExecutor( + new PropertyBasedRequiredBackgroundBootstrapping("spring.jpa.bootstrap", "async")); + } customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder; } diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaProperties.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaProperties.java index c065424b2b47..5bcbc156318e 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaProperties.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaProperties.java @@ -77,6 +77,11 @@ public class JpaProperties { */ private @Nullable Boolean openInView; + /** + * Bootstrap method to use. + */ + private Bootstrap bootstrap = Bootstrap.DEFAULT; + public Map getProperties() { return this.properties; } @@ -129,4 +134,31 @@ public void setOpenInView(@Nullable Boolean openInView) { this.openInView = openInView; } + public Bootstrap getBootstrap() { + return this.bootstrap; + } + + public void setBootstrap(Bootstrap bootstrap) { + this.bootstrap = bootstrap; + } + + /** + * Bootstrap methods that can be used with JPA. + */ + public enum Bootstrap { + + /** + * Default JPA bootstrapping. + */ + DEFAULT, + + /** + * Asynchronous JPA bootstrapping. The ApplicationContext must either have a + * single AsyncTaskExecutor bean, or an AsyncTaskExecutor bean named + * 'applicationTaskExecutor'. + */ + ASYNC + + } + } From 6c5b3a06ecfa1a4279a786739888f938ff22911a Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Wed, 25 Mar 2026 09:18:49 +0800 Subject: [PATCH 404/930] Fix nullable declaration in EntityManagerFactoryBuilder See gh-49740 Signed-off-by: Yanming Zhou --- .../springframework/boot/jpa/EntityManagerFactoryBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java index d9251fb96350..65a56b7e13fa 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java @@ -74,7 +74,7 @@ public class EntityManagerFactoryBuilder { private @Nullable List persistenceUnitPostProcessors; - private @Nullable Supplier requireBootstrapExecutorExceptionSupplier; + private @Nullable Supplier<@Nullable ? extends RuntimeException> requireBootstrapExecutorExceptionSupplier; /** * Create a new instance passing in the common pieces that will be shared if multiple @@ -161,7 +161,7 @@ public void setBootstrapExecutor(AsyncTaskExecutor bootstrapExecutor) { * @param exceptionSupplier a supplier providing the exception to throw * @since 4.1.0 */ - public void requireBootstrapExecutor(Supplier exceptionSupplier) { + public void requireBootstrapExecutor(Supplier<@Nullable ? extends RuntimeException> exceptionSupplier) { Assert.notNull(exceptionSupplier, "'exceptionSupplier' must not be null"); this.requireBootstrapExecutorExceptionSupplier = exceptionSupplier; } From 7c268112a10bccbb5aa52d7f293924798a22832b Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Tue, 24 Mar 2026 20:26:11 +0300 Subject: [PATCH 405/930] Add server-side support for custom gRPC conventions In this commit, we make `GrpcServerObservationAutoConfiguration` and `GrpcClientObservationAutoConfiguration` consistent by supporting a custom `GrpcClientObservationConvention` and adding the `@ConditionalOnMissingBean` annotation to the `observationGrpcServerInterceptor` bean. See gh-49730 Signed-off-by: Andrey Litvitski --- .../GrpcServerObservationAutoConfiguration.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java index 19d2da8c2acf..6fc6a7527a89 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java @@ -17,15 +17,18 @@ package org.springframework.boot.grpc.server.autoconfigure; import io.grpc.BindableService; +import io.micrometer.core.instrument.binder.grpc.GrpcServerObservationConvention; import io.micrometer.core.instrument.binder.grpc.ObservationGrpcServerInterceptor; import io.micrometer.core.instrument.kotlin.ObservationCoroutineContextServerInterceptor; import io.micrometer.observation.ObservationRegistry; +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.ConditionalOnBooleanProperty; 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.annotation.Order; @@ -38,6 +41,7 @@ * @author Sunny Tang * @author Chris Bono * @author Dave Syer + * @author Andrey Litvitski * @since 1.0.0 */ @AutoConfiguration( @@ -52,8 +56,12 @@ public final class GrpcServerObservationAutoConfiguration { @Bean @Order(0) @GlobalServerInterceptor - ObservationGrpcServerInterceptor grpcServerObservationInterceptor(ObservationRegistry observationRegistry) { - return new ObservationGrpcServerInterceptor(observationRegistry); + @ConditionalOnMissingBean + ObservationGrpcServerInterceptor grpcServerObservationInterceptor(ObservationRegistry observationRegistry, + ObjectProvider convention) { + ObservationGrpcServerInterceptor interceptor = new ObservationGrpcServerInterceptor(observationRegistry); + convention.ifAvailable(interceptor::setCustomConvention); + return interceptor; } @Configuration(proxyBeanMethods = false) From 3ebc8653d583464a2ff9728f6d89ada28dfa84e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 25 Mar 2026 10:47:41 +0100 Subject: [PATCH 406/930] Polish "Add server-side support for custom gRPC conventions" See gh-49730 --- .../GrpcServerObservationAutoConfigurationTests.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java index 7aee14b5a0d2..0f71043a6f90 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java @@ -22,6 +22,7 @@ import io.grpc.BindableService; import io.grpc.ServerInterceptor; import io.micrometer.context.ContextRegistry; +import io.micrometer.core.instrument.binder.grpc.GrpcServerObservationConvention; import io.micrometer.core.instrument.binder.grpc.ObservationGrpcServerInterceptor; import io.micrometer.core.instrument.kotlin.ObservationCoroutineContextServerInterceptor; import io.micrometer.observation.ObservationRegistry; @@ -35,6 +36,7 @@ import org.springframework.grpc.server.GrpcServerFactory; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for the {@link GrpcServerObservationAutoConfiguration}. @@ -123,6 +125,14 @@ void whenAllConditionsAreMetThenInterceptorConfiguredAsExpected() { }); } + @Test + void whenCustomConventionBeanIsPresentThenInterceptorUsesIt() { + GrpcServerObservationConvention customConvention = mock(GrpcServerObservationConvention.class); + this.contextRunner.withBean(GrpcServerObservationConvention.class, () -> customConvention) + .run((context) -> assertThat(context.getBean(ObservationGrpcServerInterceptor.class)) + .hasFieldOrPropertyWithValue("customConvention", customConvention)); + } + @Test void whenMicrometerContextPropagationIsNotOnClasspathCoroutineInterceptorIsNotCreated() { this.contextRunner.withClassLoader(new FilteredClassLoader(ContextRegistry.class)) From fc58d9e9d9a6f52ab3b729f34a4f763a94ac38ad Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Wed, 25 Mar 2026 15:16:52 +0900 Subject: [PATCH 407/930] Reuse reactive CloudFoundry UAA URL across calls Reactive SecurityService#getUaaUrl() creates a new Mono on each invocation, so the resolved UAA URL is not reused across calls. During token validation, this can lead to duplicate /info requests when token keys are fetched and the issuer is validated in the same flow. Cache the resolved UAA URL after a successful lookup and add regression tests that verify reuse after success and retry after failure. Signed-off-by: LeeJiWon --- .../endpoint/reactive/SecurityService.java | 8 ++++++ .../reactive/SecurityServiceTests.java | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityService.java b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityService.java index cbcc94d792d3..462741cb408a 100644 --- a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityService.java +++ b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityService.java @@ -22,6 +22,7 @@ import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import reactor.netty.http.Http11SslContextSpec; import reactor.netty.http.client.HttpClient; @@ -54,6 +55,8 @@ class SecurityService { private final String cloudControllerUrl; + private volatile @Nullable String uaaUrl; + SecurityService(WebClient.Builder webClientBuilder, String cloudControllerUrl, boolean skipSslValidation) { Assert.notNull(webClientBuilder, "'webClientBuilder' must not be null"); Assert.notNull(cloudControllerUrl, "'cloudControllerUrl' must not be null"); @@ -149,6 +152,10 @@ private Map extractTokenKeys(Map response) { * @return the UAA url Mono */ Mono getUaaUrl() { + String uaaUrl = this.uaaUrl; + if (uaaUrl != null) { + return Mono.just(uaaUrl); + } return this.webClient.get() .uri(this.cloudControllerUrl + "/info") .retrieve() @@ -156,6 +163,7 @@ Mono getUaaUrl() { .map((response) -> { String tokenEndpoint = (String) response.get("token_endpoint"); Assert.state(tokenEndpoint != null, "No 'token_endpoint' found in response"); + this.uaaUrl = tokenEndpoint; return tokenEndpoint; }) .cache() diff --git a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityServiceTests.java b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityServiceTests.java index f35f919c385f..35675aba4e9e 100644 --- a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityServiceTests.java +++ b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityServiceTests.java @@ -209,6 +209,10 @@ void getUaaUrlShouldCallCloudControllerInfoOnlyOnce() throws Exception { response.setBody("{\"token_endpoint\":\"" + UAA_URL + "\"}"); response.setHeader("Content-Type", "application/json"); }); + StepVerifier.create(this.securityService.getUaaUrl()) + .consumeNextWith((uaaUrl) -> assertThat(uaaUrl).isEqualTo(UAA_URL)) + .expectComplete() + .verify(); StepVerifier.create(this.securityService.getUaaUrl()) .consumeNextWith((uaaUrl) -> assertThat(uaaUrl).isEqualTo(UAA_URL)) .expectComplete() @@ -228,6 +232,27 @@ void getUaaUrlWhenCloudControllerUrlIsNotReachableShouldThrowException() throws expectRequest((request) -> assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER + "/info")); } + @Test + void getUaaUrlShouldRetryAfterFailure() throws Exception { + prepareResponse((response) -> response.setResponseCode(500)); + StepVerifier.create(this.securityService.getUaaUrl()).consumeErrorWith((throwable) -> { + assertThat(throwable).isInstanceOf(CloudFoundryAuthorizationException.class); + assertThat(((CloudFoundryAuthorizationException) throwable).getReason()) + .isEqualTo(Reason.SERVICE_UNAVAILABLE); + }).verify(); + prepareResponse((response) -> { + response.setBody("{\"token_endpoint\":\"" + UAA_URL + "\"}"); + response.setHeader("Content-Type", "application/json"); + }); + StepVerifier.create(this.securityService.getUaaUrl()) + .consumeNextWith((uaaUrl) -> assertThat(uaaUrl).isEqualTo(UAA_URL)) + .expectComplete() + .verify(); + expectRequest((request) -> assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER + "/info")); + expectRequest((request) -> assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER + "/info")); + expectRequestCount(2); + } + private void prepareResponse(Consumer consumer) { MockResponse response = new MockResponse(); consumer.accept(response); From dba19c73c7ccdc0401a3a4ee401690a07fbcab08 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 25 Mar 2026 11:05:28 +0100 Subject: [PATCH 408/930] Polishing contribution See gh-49744 Signed-off-by: Brian Clozel --- .../endpoint/reactive/SecurityService.java | 33 ++++++++----------- .../reactive/SecurityServiceTests.java | 4 +++ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityService.java b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityService.java index 462741cb408a..a26ea579c18f 100644 --- a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityService.java +++ b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityService.java @@ -22,7 +22,6 @@ import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import reactor.netty.http.Http11SslContextSpec; import reactor.netty.http.client.HttpClient; @@ -55,7 +54,7 @@ class SecurityService { private final String cloudControllerUrl; - private volatile @Nullable String uaaUrl; + private final Mono uaaUrl; SecurityService(WebClient.Builder webClientBuilder, String cloudControllerUrl, boolean skipSslValidation) { Assert.notNull(webClientBuilder, "'webClientBuilder' must not be null"); @@ -65,6 +64,18 @@ class SecurityService { } this.webClient = webClientBuilder.build(); this.cloudControllerUrl = cloudControllerUrl; + this.uaaUrl = this.webClient.get() + .uri(this.cloudControllerUrl + "/info") + .retrieve() + .bodyToMono(Map.class) + .map((response) -> { + String tokenEndpoint = (String) response.get("token_endpoint"); + Assert.state(tokenEndpoint != null, "No 'token_endpoint' found in response"); + return tokenEndpoint; + }) + .cacheInvalidateIf((token) -> false) + .onErrorMap((ex) -> new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE, + "Unable to fetch token keys from UAA.")); } protected ReactorClientHttpConnector buildTrustAllSslConnector() { @@ -152,23 +163,7 @@ private Map extractTokenKeys(Map response) { * @return the UAA url Mono */ Mono getUaaUrl() { - String uaaUrl = this.uaaUrl; - if (uaaUrl != null) { - return Mono.just(uaaUrl); - } - return this.webClient.get() - .uri(this.cloudControllerUrl + "/info") - .retrieve() - .bodyToMono(Map.class) - .map((response) -> { - String tokenEndpoint = (String) response.get("token_endpoint"); - Assert.state(tokenEndpoint != null, "No 'token_endpoint' found in response"); - this.uaaUrl = tokenEndpoint; - return tokenEndpoint; - }) - .cache() - .onErrorMap((ex) -> new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE, - "Unable to fetch token keys from UAA.")); + return this.uaaUrl; } } diff --git a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityServiceTests.java b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityServiceTests.java index 35675aba4e9e..11cf987ba620 100644 --- a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityServiceTests.java +++ b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/SecurityServiceTests.java @@ -213,6 +213,10 @@ void getUaaUrlShouldCallCloudControllerInfoOnlyOnce() throws Exception { .consumeNextWith((uaaUrl) -> assertThat(uaaUrl).isEqualTo(UAA_URL)) .expectComplete() .verify(); + prepareResponse((response) -> { + response.setBody("{\"token_endpoint\":\"" + UAA_URL + "\"}"); + response.setHeader("Content-Type", "application/json"); + }); StepVerifier.create(this.securityService.getUaaUrl()) .consumeNextWith((uaaUrl) -> assertThat(uaaUrl).isEqualTo(UAA_URL)) .expectComplete() From c7e7a19c5d331f03cbfbcedb4bdd20291381aaa1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 12:47:10 +0000 Subject: [PATCH 409/930] Upgrade to ActiveMQ 6.2.2 Closes gh-49768 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index f8e6ae8da672..be798151320b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -31,7 +31,7 @@ bom { ] } } - library("ActiveMQ", "6.2.1") { + library("ActiveMQ", "6.2.2") { group("org.apache.activemq") { modules = [ "activemq-console", From 620944c0fb85dc04d09f43d3dc7ca15abb983dc7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 12:47:15 +0000 Subject: [PATCH 410/930] Upgrade to Artemis 2.53.0 Closes gh-49769 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index be798151320b..bcdde4c942ed 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -65,7 +65,7 @@ bom { releaseNotes("https://github.com/eclipse-ee4j/angus-mail/releases/tag/{version}") } } - library("Artemis", "2.52.0") { + library("Artemis", "2.53.0") { group("org.apache.artemis") { bom("artemis-bom") { permit("org.apache.activemq:artemis-amqp-protocol") From a97c8aa0da642bb18430487c8f158635ffd1d15a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 12:47:19 +0000 Subject: [PATCH 411/930] Upgrade to Zipkin Reporter 3.5.3 Closes gh-49770 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index bcdde4c942ed..96bc6d791b22 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -157,7 +157,7 @@ bom { .formatted(version.major(), version.minor())) } } - library("Zipkin Reporter", "3.5.1") { + library("Zipkin Reporter", "3.5.3") { group("io.zipkin.reporter2") { bom("zipkin-reporter-bom") } From 031f12996f199370627d32c307624dfe4e8d2b7b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 12:47:23 +0000 Subject: [PATCH 412/930] Upgrade to Brave 6.3.1 Closes gh-49771 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 96bc6d791b22..10a7c4dbe66c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -166,7 +166,7 @@ bom { releaseNotes("https://github.com/openzipkin/zipkin-reporter-java/releases/tag/{version}") } } - library("Brave", "6.3.0") { + library("Brave", "6.3.1") { group("io.zipkin.brave") { bom("brave-bom") } From f8a9804e858c038242c262d4c6bc3059ec6a25e0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 12:47:28 +0000 Subject: [PATCH 413/930] Upgrade to Grpc Java 1.80.0 Closes gh-49772 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 10a7c4dbe66c..780d792e801f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -562,7 +562,7 @@ bom { site("https://groovy-lang.org") } } - library("Grpc Java", "1.78.0") { + library("Grpc Java", "1.80.0") { group("io.grpc") { bom("grpc-bom") } From 9c511fae3951a4a8bed51ab076120934f0cf96a4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 12:47:32 +0000 Subject: [PATCH 414/930] Upgrade to Jackson 2 Bom 2.21.2 Closes gh-49773 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 32a7df567a2e..14784509e147 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ checkstyleToolVersion=10.12.4 commonsCodecVersion=1.21.0 graalVersion=25 hamcrestVersion=3.0 -jackson2Version=2.21.1 +jackson2Version=2.21.2 jacksonVersion=3.1.0 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 From d4b1478742655551e0ee303b8409317050b7b415 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 12:47:37 +0000 Subject: [PATCH 415/930] Upgrade to jOOQ 3.21.0 Closes gh-49774 --- .../DefaultExceptionTranslatorExecuteListenerTests.java | 1 + .../boot/jooq/autoconfigure/SqlDialectLookupTests.java | 1 + .../org/springframework/boot/jooq/autoconfigure/settings.xml | 2 +- platform/spring-boot-dependencies/build.gradle | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-jooq/src/test/java/org/springframework/boot/jooq/autoconfigure/DefaultExceptionTranslatorExecuteListenerTests.java b/module/spring-boot-jooq/src/test/java/org/springframework/boot/jooq/autoconfigure/DefaultExceptionTranslatorExecuteListenerTests.java index 6746eb26f75a..e4df31d85405 100644 --- a/module/spring-boot-jooq/src/test/java/org/springframework/boot/jooq/autoconfigure/DefaultExceptionTranslatorExecuteListenerTests.java +++ b/module/spring-boot-jooq/src/test/java/org/springframework/boot/jooq/autoconfigure/DefaultExceptionTranslatorExecuteListenerTests.java @@ -93,6 +93,7 @@ private ExecuteContext mockContext(SQLDialect dialect, SQLException sqlException return context; } + @SuppressWarnings({ "deprecation", "removal" }) static Object[] exceptionTranslatesSqlExceptions() { return new Object[] { new Object[] { SQLDialect.DERBY, sqlException("42802") }, new Object[] { SQLDialect.DERBY, new SQLSyntaxErrorException() }, diff --git a/module/spring-boot-jooq/src/test/java/org/springframework/boot/jooq/autoconfigure/SqlDialectLookupTests.java b/module/spring-boot-jooq/src/test/java/org/springframework/boot/jooq/autoconfigure/SqlDialectLookupTests.java index 35b93a3054d3..1d9c20b71bd4 100644 --- a/module/spring-boot-jooq/src/test/java/org/springframework/boot/jooq/autoconfigure/SqlDialectLookupTests.java +++ b/module/spring-boot-jooq/src/test/java/org/springframework/boot/jooq/autoconfigure/SqlDialectLookupTests.java @@ -47,6 +47,7 @@ void getSqlDialectWhenDataSourceIsUnknownShouldReturnDefault() throws Exception } @Test + @SuppressWarnings({ "deprecation", "removal" }) void getSqlDialectWhenDerbyShouldReturnDerby() throws Exception { testGetSqlDialect("jdbc:derby:", SQLDialect.DERBY); } diff --git a/module/spring-boot-jooq/src/test/resources/org/springframework/boot/jooq/autoconfigure/settings.xml b/module/spring-boot-jooq/src/test/resources/org/springframework/boot/jooq/autoconfigure/settings.xml index 8a8b68db0f67..de98de569439 100644 --- a/module/spring-boot-jooq/src/test/resources/org/springframework/boot/jooq/autoconfigure/settings.xml +++ b/module/spring-boot-jooq/src/test/resources/org/springframework/boot/jooq/autoconfigure/settings.xml @@ -1,4 +1,4 @@ - + 100 diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 780d792e801f..1f2f70afbab9 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1143,7 +1143,7 @@ bom { ] } } - library("jOOQ", "3.20.11") { + library("jOOQ", "3.21.0") { group("org.jooq") { bom("jooq-bom") plugins = [ From 88479f705dec53752a6d23950f0c7100d75e8d6e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 12:47:41 +0000 Subject: [PATCH 416/930] Upgrade to Netty 4.2.11.Final Closes gh-49775 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1f2f70afbab9..8dd9b1a76fae 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1752,7 +1752,7 @@ bom { releaseNotes("https://github.com/neo4j/neo4j-java-driver/releases/tag/{version}") } } - library("Netty", "4.2.10.Final") { + library("Netty", "4.2.11.Final") { prohibit { contains ".Alpha" contains ".Beta" From 02932e22fba77c03592299ba62ab815501fc3784 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 12:47:45 +0000 Subject: [PATCH 417/930] Upgrade to Protobuf Maven Plugin 5.1.0 Closes gh-49776 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8dd9b1a76fae..39ef4a35fcf9 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1841,7 +1841,7 @@ bom { releaseNotes("https://github.com/oracle/oracle-r2dbc/releases/tag/{version}") } } - library("Protobuf Maven Plugin", "5.0.1") { + library("Protobuf Maven Plugin", "5.1.0") { group("io.github.ascopes") { plugins = [ "protobuf-maven-plugin" From 84a156f28a626a9c94f5e802e45fe68bc26354a4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 12:47:50 +0000 Subject: [PATCH 418/930] Upgrade to Prometheus Client 1.5.1 Closes gh-49777 --- .../export/prometheus/PrometheusScrapeEndpoint.java | 2 +- platform/spring-boot-dependencies/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/prometheus/PrometheusScrapeEndpoint.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/prometheus/PrometheusScrapeEndpoint.java index dbd3489ed54d..70b89d658498 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/prometheus/PrometheusScrapeEndpoint.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/prometheus/PrometheusScrapeEndpoint.java @@ -64,7 +64,7 @@ public PrometheusScrapeEndpoint(PrometheusRegistry prometheusRegistry, @Nullable this.prometheusRegistry = prometheusRegistry; PrometheusProperties prometheusProperties = (exporterProperties != null) ? PrometheusPropertiesLoader.load(exporterProperties) : PrometheusPropertiesLoader.load(); - this.expositionFormats = ExpositionFormats.init(prometheusProperties.getExporterProperties()); + this.expositionFormats = ExpositionFormats.init(prometheusProperties); } @ReadOperation(producesFrom = PrometheusOutputFormat.class) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 39ef4a35fcf9..435acf477c52 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1874,7 +1874,7 @@ bom { releaseNotes("https://github.com/pgjdbc/pgjdbc/releases/tag/REL{version}") } } - library("Prometheus Client", "1.5.0") { + library("Prometheus Client", "1.5.1") { group("io.prometheus") { bom("prometheus-metrics-bom") } From cf8044b17c29987b617304f2716437c60fd922c1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 12:47:54 +0000 Subject: [PATCH 419/930] Upgrade to Protobuf Common Protos 2.67.0 Closes gh-49778 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 435acf477c52..654dcc885a2d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1918,7 +1918,7 @@ bom { releaseNotes("https://qpid.apache.org/releases/qpid-protonj2-{version}/release-notes.html") } } - library("Protobuf Common Protos", "2.65.0") { + library("Protobuf Common Protos", "2.67.0") { group("com.google.api.grpc") { modules = [ "proto-google-common-protos" From 3427c1f8e1aae9621e6469c8225ca4bc451ca6fe Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 12:47:58 +0000 Subject: [PATCH 420/930] Upgrade to Protobuf Java 4.34.1 Closes gh-49779 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 654dcc885a2d..27d89c17c5de 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1929,7 +1929,7 @@ bom { releaseNotes("https://github.com/googleapis/sdk-platform-java/releases/tag/v-{version}") } } - library("Protobuf Java", "4.34.0") { + library("Protobuf Java", "4.34.1") { group("com.google.protobuf") { bom("protobuf-bom") } From 21942cbc204176ff642af7f21d7723db9ad9e06f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 12:48:02 +0000 Subject: [PATCH 421/930] Upgrade to Thymeleaf Layout Dialect 4.0.1 Closes gh-49780 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 27d89c17c5de..a3761e6f20a9 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2836,7 +2836,7 @@ bom { ] } } - library("Thymeleaf Layout Dialect", "4.0.0") { + library("Thymeleaf Layout Dialect", "4.0.1") { group("nz.net.ultraq.thymeleaf") { modules = [ "thymeleaf-layout-dialect" From 50070c444a3756f6aa5a800d98f24ea191c3a1c1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 12:48:07 +0000 Subject: [PATCH 422/930] Upgrade to Tomcat 11.0.20 Closes gh-49781 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 14784509e147..6f186151dac3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,7 +23,7 @@ nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.6 springFrameworkVersion=7.0.6 springFramework60xVersion=6.0.23 -tomcatVersion=11.0.18 +tomcatVersion=11.0.20 protobufGradlePluginVersion=0.9.6 kotlin.stdlib.default.dependency=false From 5b6d346c602bc76f0fc982a9a75077d7a4806fba Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 15:55:43 +0000 Subject: [PATCH 423/930] Prohibit upgrades to ActiveMQ 6.2.2 Closes gh-49768 --- platform/spring-boot-dependencies/build.gradle | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a3761e6f20a9..a07efa31410e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -31,7 +31,11 @@ bom { ] } } - library("ActiveMQ", "6.2.2") { + library("ActiveMQ", "6.2.1") { + prohibit { + versionRange "[6.2.2]" + because "it contains a regression on Windows (https://github.com/apache/activemq/issues/1830)" + } group("org.apache.activemq") { modules = [ "activemq-console", From d28cec064e9be099eb08b50b50a1971004ed57ef Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 25 Mar 2026 19:19:48 -0700 Subject: [PATCH 424/930] Replace PropertyBasedRequiredBackgroundBootstrapping with exception Replace `PropertyBasedRequiredBackgroundBootstrapping` with a simpler `BootstrapExecutorRequiredException` and use a regular failure analyzer to provide help. See gh-49733 See gh-49688 --- .../DataJpaRepositoriesAutoConfiguration.java | 6 +- .../HibernateJpaAutoConfigurationTests.java | 6 +- .../BootstrapExecutorRequiredException.java | 69 +++++++++++++++++++ ...tstrapExecutorRequiredFailureAnalyzer.java | 50 ++++++++++++++ .../autoconfigure/JpaBaseConfiguration.java | 2 +- ...yBasedRequiredBackgroundBootstrapping.java | 60 ---------------- .../main/resources/META-INF/spring.factories | 4 ++ ...pExecutorRequiredFailureAnalyzerTests.java | 66 ++++++++++++++++++ ...dRequiredBackgroundBootstrappingTests.java | 40 ----------- 9 files changed, 196 insertions(+), 107 deletions(-) create mode 100644 module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredException.java create mode 100644 module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzer.java delete mode 100644 module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrapping.java create mode 100644 module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzerTests.java delete mode 100644 module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrappingTests.java diff --git a/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java b/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java index a66405b5ac98..5bcaed3c0fc5 100644 --- a/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java +++ b/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java @@ -33,8 +33,8 @@ import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.data.jpa.autoconfigure.DataJpaRepositoriesAutoConfiguration.JpaRepositoriesImportSelector; import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration; +import org.springframework.boot.jpa.autoconfigure.BootstrapExecutorRequiredException; import org.springframework.boot.jpa.autoconfigure.EntityManagerFactoryBuilderCustomizer; -import org.springframework.boot.jpa.autoconfigure.PropertyBasedRequiredBackgroundBootstrapping; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; @@ -85,8 +85,8 @@ public final class DataJpaRepositoriesAutoConfiguration { @ConditionalOnProperty(name = "spring.data.jpa.repositories.bootstrap-mode", havingValue = "deferred") EntityManagerFactoryBuilderCustomizer entityManagerFactoryBootstrapExecutorCustomizer( Map taskExecutors) { - return (builder) -> builder.requireBootstrapExecutor(new PropertyBasedRequiredBackgroundBootstrapping( - "spring.data.jpa.repositories.bootstrap-mode", "deferred")); + return (builder) -> builder.requireBootstrapExecutor(() -> BootstrapExecutorRequiredException + .ofProperty("spring.data.jpa.repositories.bootstrap-mode", "deferred")); } @Bean diff --git a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java index 5e0ab0717235..5bbb2992cf15 100644 --- a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java +++ b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java @@ -68,7 +68,6 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; -import org.springframework.boot.diagnostics.FailureAnalyzedException; import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration; import org.springframework.boot.hibernate.SpringImplicitNamingStrategy; import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfigurationTests.JpaUsingApplicationListenerConfiguration.EventCapturingApplicationListener; @@ -82,6 +81,7 @@ import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.jdbc.autoconfigure.XADataSourceAutoConfiguration; import org.springframework.boot.jpa.EntityManagerFactoryBuilder; +import org.springframework.boot.jpa.autoconfigure.BootstrapExecutorRequiredException; import org.springframework.boot.jpa.autoconfigure.EntityManagerFactoryBuilderCustomizer; import org.springframework.boot.jpa.autoconfigure.JpaBaseConfiguration; import org.springframework.boot.jpa.autoconfigure.JpaProperties; @@ -274,7 +274,7 @@ void whenBackgroundBootstrapingAndMissingTaksExecutorThrowsException() { this.contextRunner.withPropertyValues("spring.jpa.bootstrap=async") .run((context) -> assertThat(context).getFailure() .rootCause() - .isInstanceOf(FailureAnalyzedException.class) + .isInstanceOf(BootstrapExecutorRequiredException.class) .message() .contains("bootstrap executor is required when 'spring.jpa.bootstrap' is set to 'async'")); } @@ -285,7 +285,7 @@ void whenBackgroundBootstrapingAndMultipleTaksExecutorThrowsException() { .withUserConfiguration(MultipleAsyncTaskExecutorsConfiguration.class) .run((context) -> assertThat(context).getFailure() .rootCause() - .isInstanceOf(FailureAnalyzedException.class) + .isInstanceOf(BootstrapExecutorRequiredException.class) .message() .contains("bootstrap executor is required when 'spring.jpa.bootstrap' is set to 'async'")); } diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredException.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredException.java new file mode 100644 index 000000000000..f2f2dc5da550 --- /dev/null +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredException.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jpa.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.util.Assert; + +/** + * Exception thrown then the auto-configured + * {@link LocalContainerEntityManagerFactoryBean} is missing a required bootstrap + * executor. + * + * @author Phillip Webb + * @since 4.1.0 + */ +public class BootstrapExecutorRequiredException extends IllegalStateException { + + private final @Nullable String propertyName; + + private final @Nullable String propertyValue; + + public BootstrapExecutorRequiredException(String message) { + this(message, null); + } + + public BootstrapExecutorRequiredException(String message, @Nullable Throwable cause) { + this(message, null, null, cause); + } + + private BootstrapExecutorRequiredException(String message, @Nullable String propertyName, + @Nullable String propertyValue, @Nullable Throwable cause) { + super(message, cause); + this.propertyName = propertyName; + this.propertyValue = propertyValue; + } + + @Nullable String getPropertyName() { + return this.propertyName; + } + + @Nullable String getPropertyValue() { + return this.propertyValue; + } + + public static BootstrapExecutorRequiredException ofProperty(String name, String value) { + Assert.hasText(name, "'name' must not be empty"); + Assert.hasText(value, "'value' must not be empty"); + String message = "An EntityManagerFactoryBean bootstrap executor is required when '%s' is set to '%s'" + .formatted(name, value); + return new BootstrapExecutorRequiredException(message, name, value, null); + } + +} diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzer.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzer.java new file mode 100644 index 000000000000..c0fd5557876d --- /dev/null +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzer.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jpa.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.diagnostics.FailureAnalyzer; + +/** + * {@link FailureAnalyzer} for {@link BootstrapExecutorRequiredException}. + * + * @author Phillip Webb + */ +class BootstrapExecutorRequiredFailureAnalyzer extends AbstractFailureAnalyzer { + + @Override + protected @Nullable FailureAnalysis analyze(Throwable rootFailure, BootstrapExecutorRequiredException cause) { + StringBuilder action = new StringBuilder(); + action.append(actionPreamble(cause.getPropertyName())); + action.append("\t- With an auto-configured task executor " + + "(you may need to set 'spring.task.execution.mode' to 'force').\n"); + action.append("\t- With an AsyncTaskExecutor bean named 'applicationTaskExecutor'.\n"); + action.append("\t- Using a EntityManagerFactoryBuilderCustomizer.\n"); + return new FailureAnalysis(cause.getMessage(), action.toString(), cause); + } + + private String actionPreamble(@Nullable String propertyName) { + return (propertyName != null) + ? "Use a different '%s' value or provide a bootstrap executor using one of the following methods:\n" + .formatted(propertyName) + : "Provide a bootstrap executor using one of the following methods:\n"; + } + +} diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java index c4c14902b256..726c06f9ae99 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java @@ -130,7 +130,7 @@ public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter this::buildJpaProperties, persistenceUnitManager.getIfAvailable(), null, bootstrapExecutor); if (this.properties.getBootstrap() == Bootstrap.ASYNC) { builder.requireBootstrapExecutor( - new PropertyBasedRequiredBackgroundBootstrapping("spring.jpa.bootstrap", "async")); + () -> BootstrapExecutorRequiredException.ofProperty("spring.jpa.bootstrap", "async")); } customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder; diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrapping.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrapping.java deleted file mode 100644 index 1f139a2a8c6f..000000000000 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrapping.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.jpa.autoconfigure; - -import java.util.function.Supplier; - -import org.springframework.boot.diagnostics.FailureAnalyzedException; -import org.springframework.boot.jpa.EntityManagerFactoryBuilder; -import org.springframework.util.Assert; - -/** - * {@link Supplier} to use with - * {@link EntityManagerFactoryBuilder#requireBootstrapExecutor} when a property indicates - * background bootstrapping is required. - * - * @author Phillip Webb - * @since 4.1.0 - */ -public class PropertyBasedRequiredBackgroundBootstrapping implements Supplier { - - private final String propertyName; - - private final String propertyValue; - - public PropertyBasedRequiredBackgroundBootstrapping(String propertyName, String propertyValue) { - Assert.notNull(propertyName, "'propertyName' must not be null"); - Assert.notNull(propertyValue, "'propertyValue' must not be null"); - this.propertyName = propertyName; - this.propertyValue = propertyValue; - } - - @Override - public RuntimeException get() { - String description = "A LocalContainerEntityManagerFactoryBean bootstrap executor is required when '%s' is set to '%s'" - .formatted(this.propertyName, this.propertyValue); - StringBuilder action = new StringBuilder(); - action.append("Use a different '%s' or provide a bootstrap executor using one of the following methods:\n" - .formatted(this.propertyName)); - action.append("\tWith an auto-configured task executor " - + "(you may need to set 'spring.task.execution.mode' to 'force')."); - action.append("\tWith an AsyncTaskExecutor bean named 'applicationTaskExecutor.'"); - action.append("\tUsing a EntityManagerFactoryBuilderCustomizer."); - return new FailureAnalyzedException(description, action.toString()); - } - -} diff --git a/module/spring-boot-jpa/src/main/resources/META-INF/spring.factories b/module/spring-boot-jpa/src/main/resources/META-INF/spring.factories index 6437bb94c7a4..8b972a2e3545 100644 --- a/module/spring-boot-jpa/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-jpa/src/main/resources/META-INF/spring.factories @@ -1,3 +1,7 @@ +# Failure Analyzers +org.springframework.boot.diagnostics.FailureAnalyzer=\ +org.springframework.boot.jpa.autoconfigure.BootstrapExecutorRequiredFailureAnalyzer + # Database Initializer Detectors org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\ org.springframework.boot.jpa.JpaDatabaseInitializerDetector diff --git a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzerTests.java b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzerTests.java new file mode 100644 index 000000000000..b172256f40c8 --- /dev/null +++ b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzerTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jpa.autoconfigure; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.diagnostics.FailureAnalysis; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BootstrapExecutorRequiredFailureAnalyzer}. + * + * @author Phillip Webb + */ +class BootstrapExecutorRequiredFailureAnalyzerTests { + + private final BootstrapExecutorRequiredFailureAnalyzer analyzer = new BootstrapExecutorRequiredFailureAnalyzer(); + + @Test + void analyzeWhenBootstrapExecutorRequiredExceptionWithProperties() { + BootstrapExecutorRequiredException exception = BootstrapExecutorRequiredException.ofProperty("testname", + "testvalue"); + FailureAnalysis result = this.analyzer.analyze(exception); + assertThat(result).isNotNull(); + assertThat(result.getDescription()).isEqualTo( + "An EntityManagerFactoryBean bootstrap executor is required when 'testname' is set to 'testvalue'"); + assertThat(result.getAction()).isEqualTo( + """ + Use a different 'testname' value or provide a bootstrap executor using one of the following methods: + - With an auto-configured task executor (you may need to set 'spring.task.execution.mode' to 'force'). + - With an AsyncTaskExecutor bean named 'applicationTaskExecutor'. + - Using a EntityManagerFactoryBuilderCustomizer. + """); + } + + @Test + void analyzeWhenBootstrapExecutorRequiredExceptionWithMessage() { + BootstrapExecutorRequiredException exception = new BootstrapExecutorRequiredException("A custom message"); + FailureAnalysis result = this.analyzer.analyze(exception); + assertThat(result).isNotNull(); + assertThat(result.getDescription()).isEqualTo("A custom message"); + assertThat(result.getAction()).isEqualTo( + """ + Provide a bootstrap executor using one of the following methods: + - With an auto-configured task executor (you may need to set 'spring.task.execution.mode' to 'force'). + - With an AsyncTaskExecutor bean named 'applicationTaskExecutor'. + - Using a EntityManagerFactoryBuilderCustomizer. + """); + } + +} diff --git a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrappingTests.java b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrappingTests.java deleted file mode 100644 index 765b0f1fa633..000000000000 --- a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrappingTests.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.jpa.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.diagnostics.FailureAnalyzedException; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link PropertyBasedRequiredBackgroundBootstrapping}. - * - * @author Phillip Webb - */ -class PropertyBasedRequiredBackgroundBootstrappingTests { - - @Test - void getReturnsFailureAnalyzableException() { - RuntimeException exception = new PropertyBasedRequiredBackgroundBootstrapping("test.bootstrap", "true").get(); - assertThat(exception).isInstanceOf(FailureAnalyzedException.class) - .hasMessage("A LocalContainerEntityManagerFactoryBean bootstrap executor is required " - + "when 'test.bootstrap' is set to 'true'"); - } - -} From 6b6550d10d9f0caa600c9e71bb53118548665f1d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 26 Mar 2026 07:16:45 +0000 Subject: [PATCH 425/930] Upgrade to Netty 4.2.12.Final Closes gh-49795 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a07efa31410e..94c4c54d9adf 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1756,7 +1756,7 @@ bom { releaseNotes("https://github.com/neo4j/neo4j-java-driver/releases/tag/{version}") } } - library("Netty", "4.2.11.Final") { + library("Netty", "4.2.12.Final") { prohibit { contains ".Alpha" contains ".Beta" From f368ac4805a104407ab5eb0a6ecdef3b697ed837 Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Wed, 25 Mar 2026 20:15:47 +0300 Subject: [PATCH 426/930] Rename GrpcClientObservationConvention parameter to convention See gh-49787 Signed-off-by: Andrey Litvitski --- .../autoconfigure/GrpcClientObservationAutoConfiguration.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java index 394b7aabe310..1e7486579d29 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java @@ -53,9 +53,9 @@ public final class GrpcClientObservationAutoConfiguration { @GlobalClientInterceptor @ConditionalOnMissingBean ObservationGrpcClientInterceptor grpcClientObservationInterceptor(ObservationRegistry observationRegistry, - ObjectProvider GrpcClientObservationConvention) { + ObjectProvider convention) { ObservationGrpcClientInterceptor interceptor = new ObservationGrpcClientInterceptor(observationRegistry); - GrpcClientObservationConvention.ifAvailable(interceptor::setCustomConvention); + convention.ifAvailable(interceptor::setCustomConvention); return interceptor; } From d7a4561da851cbf7b73b59fd040218d477c6d9d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 26 Mar 2026 08:38:42 +0100 Subject: [PATCH 427/930] Harmonize ObservationConvention parameters in gRPC support Such parameters are usually named "customConvention" so this commit aligns to that naming. See gh-49730 --- .../autoconfigure/GrpcClientObservationAutoConfiguration.java | 4 ++-- .../autoconfigure/GrpcServerObservationAutoConfiguration.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java index 1e7486579d29..ae7a4ae6ded9 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java @@ -53,9 +53,9 @@ public final class GrpcClientObservationAutoConfiguration { @GlobalClientInterceptor @ConditionalOnMissingBean ObservationGrpcClientInterceptor grpcClientObservationInterceptor(ObservationRegistry observationRegistry, - ObjectProvider convention) { + ObjectProvider customConvention) { ObservationGrpcClientInterceptor interceptor = new ObservationGrpcClientInterceptor(observationRegistry); - convention.ifAvailable(interceptor::setCustomConvention); + customConvention.ifAvailable(interceptor::setCustomConvention); return interceptor; } diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java index 6fc6a7527a89..cf5522b4517c 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java @@ -58,9 +58,9 @@ public final class GrpcServerObservationAutoConfiguration { @GlobalServerInterceptor @ConditionalOnMissingBean ObservationGrpcServerInterceptor grpcServerObservationInterceptor(ObservationRegistry observationRegistry, - ObjectProvider convention) { + ObjectProvider customConvention) { ObservationGrpcServerInterceptor interceptor = new ObservationGrpcServerInterceptor(observationRegistry); - convention.ifAvailable(interceptor::setCustomConvention); + customConvention.ifAvailable(interceptor::setCustomConvention); return interceptor; } From 4d074fcb6487a24b7d4bd0471d317a2e381511e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 26 Mar 2026 10:21:33 +0100 Subject: [PATCH 428/930] Polish --- .../autoconfigure/GrpcClientCodecConfiguration.java | 8 +++++--- .../GrpcClientDefaultServiceConfigCustomizer.java | 4 ++-- .../GrpcClientObservationAutoConfiguration.java | 1 - .../client/autoconfigure/GrpcClientProperties.java | 4 ++-- .../grpc/client/autoconfigure/ServiceConfig.java | 2 +- .../GrpcChannelBuilderCustomizersTests.java | 2 +- .../GrpcClientAutoConfigurationTests.java | 2 +- .../PropertiesChannelCredentialsProviderTests.java | 4 ++-- .../client/autoconfigure/ServiceConfigTests.java | 2 +- .../autoconfigure/GrpcServerCodecConfiguration.java | 2 +- .../GrpcServerObservationAutoConfiguration.java | 2 +- .../boot/grpc/server/autoconfigure/NettyAddress.java | 2 +- .../PropertiesServerBuilderCustomizer.java | 3 +-- .../health/GrpcServerHealthScheduler.java | 2 +- .../GrpcServerSecurityAutoConfiguration.java | 2 +- .../security/web/reactive/package-info.java | 2 +- .../security/web/servlet/package-info.java | 2 +- .../boot/grpc/server/health/StatusMapper.java | 4 ++-- .../GrpcServerAutoConfigurationTests.java | 2 +- .../PropertiesServerBuilderCustomizerTests.java | 2 +- ...utoConfiguredHealthCheckedGrpcComponentTests.java | 4 ++-- ...toConfiguredHealthCheckedGrpcComponentsTests.java | 4 ++-- .../GrpcServerHealthAutoConfigurationTests.java | 4 ++-- .../security/GrpcDisableCsrfHttpConfigurerTests.java | 12 ++++++------ .../grpc/server/health/GrpcServerHealthTests.java | 8 ++++---- .../grpc/server/health/StatusAggregatorTests.java | 1 - .../boot/grpc/server/health/StatusMapperTests.java | 4 ++-- .../TestGrpcTransportAutoConfiguration.java | 10 +++------- ...pcPortInfoApplicationContextInitializerTests.java | 6 +++--- 29 files changed, 51 insertions(+), 56 deletions(-) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientCodecConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientCodecConfiguration.java index 0ed44cb87fb1..bafcd60dc700 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientCodecConfiguration.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientCodecConfiguration.java @@ -23,7 +23,7 @@ import io.grpc.CompressorRegistry; import io.grpc.Decompressor; import io.grpc.DecompressorRegistry; -import io.grpc.ServerBuilder; +import io.grpc.ManagedChannelBuilder; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -41,7 +41,8 @@ class GrpcClientCodecConfiguration { /** * The compressor registry that is set on the - * {@link ServerBuilder#compressorRegistry(CompressorRegistry) server builder} . + * {@link ManagedChannelBuilder#compressorRegistry(CompressorRegistry) channel + * builder}. * @param compressors the compressors to use on the registry * @return a new {@link CompressorRegistry#newEmptyInstance() registry} with the * specified compressors or the {@link CompressorRegistry#getDefaultInstance() default @@ -60,7 +61,8 @@ CompressorRegistry grpcCompressorRegistry(List compressors) { /** * The decompressor registry that is set on the - * {@link ServerBuilder#decompressorRegistry(DecompressorRegistry) server builder}. + * {@link ManagedChannelBuilder#decompressorRegistry(DecompressorRegistry) channel + * builder}. * @param decompressors the decompressors to use on the registry * @return a new {@link DecompressorRegistry#emptyInstance() registry} with the * specified decompressors or the {@link DecompressorRegistry#getDefaultInstance() diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientDefaultServiceConfigCustomizer.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientDefaultServiceConfigCustomizer.java index 2b39c51f24b4..849d25261fba 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientDefaultServiceConfigCustomizer.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientDefaultServiceConfigCustomizer.java @@ -24,8 +24,8 @@ import org.springframework.grpc.client.VirtualTargets; /** - * Callback interface that can be used to customize the default service config of the - * {@link GrpcChannelBuilderCustomizer}. + * Callback interface that can be used to customize the default service config of the gRPC + * channel. *

    * This customizer should be used instead of calling * {@link ManagedChannelBuilder#defaultServiceConfig(Map)} from a diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java index ae7a4ae6ded9..3148a48a5050 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java @@ -39,7 +39,6 @@ * @author Phillip Webb * @since 4.1.0 */ - @AutoConfiguration( afterName = "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration") @ConditionalOnClass({ AbstractStub.class, GrpcChannelBuilderCustomizer.class, ObservationRegistry.class, diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java index 164580bd8232..71bccb055ba1 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java @@ -48,7 +48,7 @@ public Map getChannel() { } /** - * Channel Properties. + * Channel properties. */ public static class Channel { @@ -277,7 +277,7 @@ public static class Keepalive { private Duration time = Duration.ofMinutes(5); /** - * Default timeout for a keepAlives ping request. If a duration suffix is not + * Default timeout for a keep-alive ping request. If a duration suffix is not * specified, seconds will be used. */ @DurationUnit(ChronoUnit.SECONDS) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java index 8491084289a7..bc1749efb01c 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java @@ -413,7 +413,7 @@ Map grpcJavaConfig() { /** * Internal helper to collection gRPC java config. * - * @param asMap the underling data as a map + * @param asMap the underlying data as a map */ record GrpcJavaConfig(Map asMap) { diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizersTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizersTests.java index ac59391bca28..190abb3dfb92 100644 --- a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizersTests.java +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelBuilderCustomizersTests.java @@ -219,7 +219,7 @@ void applyWhenHealthEnabledAndNoServiceNameAddsHealthConfig() { } @Test - void applyWhenNoCustomizersOrHealthDoesSetDefaultServiceConfig() { + void applyWhenNoCustomizersOrHealthDoesNotSetDefaultServiceConfig() { GrpcClientProperties properties = new GrpcClientProperties(); GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null, Collections.emptyList(), Collections.emptyList()); diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java index 1aecb618ec7d..3bfe7a59f824 100644 --- a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java @@ -167,7 +167,7 @@ void compressionCustomizerAutoConfiguredAsExpected() { } @Test - void compressionCustomizerWhenNoRegistrry() { + void compressionCustomizerWhenNoRegistry() { // Codec class guards the imported GrpcCodecConfiguration to hide registry this.contextRunner.withClassLoader(new FilteredClassLoader(Codec.class)).run((context) -> { GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class); diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesChannelCredentialsProviderTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesChannelCredentialsProviderTests.java index 45e08db1dcdb..5fb8035ea90a 100644 --- a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesChannelCredentialsProviderTests.java +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/PropertiesChannelCredentialsProviderTests.java @@ -85,7 +85,7 @@ void getChannelCredentialsWhenTargetDoesNotMatchChannelAndHasNoDefaultUsesInsecu } @Test - void getChannelCredentialsWhenSslExplictlyDisabled() { + void getChannelCredentialsWhenSslExplicitlyDisabled() { GrpcClientProperties properties = new GrpcClientProperties(); Channel channelProperties = new Channel(); channelProperties.getSsl().setEnabled(false); @@ -97,7 +97,7 @@ void getChannelCredentialsWhenSslExplictlyDisabled() { } @Test - void getChannelCredentialsWhenSslExplictlyEnabledAndNoBundle() { + void getChannelCredentialsWhenSslExplicitlyEnabledAndNoBundle() { GrpcClientProperties properties = new GrpcClientProperties(); Channel channelProperties = new Channel(); channelProperties.getSsl().setEnabled(true); diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfigTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfigTests.java index f7b4e56174ed..dff92d54a9dd 100644 --- a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfigTests.java +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfigTests.java @@ -201,7 +201,7 @@ void multipleLoadBalancerPolicies() throws Exception { - pickfirst: {} weightedroundrobin: {} """) - void whenMultileLoadBalancingPoliciesInListItemThrowsException() { + void whenMultipleLoadBalancingPoliciesInListItemThrowsException() { assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindAndGetAsMap()) .havingRootCause() .isInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class); diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerCodecConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerCodecConfiguration.java index abeed22da135..a88576cb51ea 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerCodecConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerCodecConfiguration.java @@ -29,7 +29,7 @@ import org.springframework.context.annotation.Configuration; /** - * The configuration that contains all gRPC codec related beans. + * {@link Configuration @Configuration} for gRPC codecs. * * @author Andrei Lisa */ diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java index cf5522b4517c..3c2004dcbb1a 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java @@ -42,7 +42,7 @@ * @author Chris Bono * @author Dave Syer * @author Andrey Litvitski - * @since 1.0.0 + * @since 4.1.0 */ @AutoConfiguration( afterName = "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration") diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddress.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddress.java index 077881fa1c4b..dd0966e7e855 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddress.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyAddress.java @@ -28,7 +28,7 @@ import org.springframework.util.StringUtils; /** - * Address {@link GrpcServerFactory} address. + * Represents the network address for a {@link GrpcServerFactory}. * * @author Phillip Webb * @param transport the transport to use diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizer.java index 7e32d05a0f7a..f66c4125d125 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizer.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizer.java @@ -21,7 +21,6 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; -import io.grpc.ManagedChannelBuilder; import io.grpc.ServerBuilder; import org.springframework.boot.context.properties.PropertyMapper; @@ -33,7 +32,7 @@ /** * {@link ServerBuilderCustomizer} that maps {@link GrpcServerProperties} to a - * {@link ManagedChannelBuilder}. + * {@link ServerBuilder}. * * @param the type of server builder * @param properties the properties to map diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java index 3bf7cb8a0af2..e2b361882883 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java @@ -33,7 +33,7 @@ import org.springframework.util.function.SingletonSupplier; /** - * Schedules gRPC health updates one the gRPC server has been started. + * Schedules gRPC health updates once the gRPC server has been started. * * @author Phillip Webb * @author Chris Bono diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfiguration.java index 07ef0e7afc0b..5f378c9eef38 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfiguration.java @@ -113,7 +113,7 @@ GrpcServerExecutorProvider grpcServerExecutorProvider() { @Configuration(proxyBeanMethods = false) @ConditionalOnClass(name = "io.grpc.kotlin.CoroutineContextServerInterceptor") - static class GrpcClientCoroutineStubConfiguration { + static class GrpcServerCoroutineStubConfiguration { @Bean @GlobalServerInterceptor diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/package-info.java index 51f413b9a251..76a61f9d4920 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/package-info.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/package-info.java @@ -15,7 +15,7 @@ */ /** - * Auto-configuration for gRPC web security when using a servlet stack. + * Auto-configuration for gRPC web security when using a reactive stack. */ @NullMarked package org.springframework.boot.grpc.server.autoconfigure.security.web.reactive; diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/package-info.java index 441fff7c423a..4a4bc19bf103 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/package-info.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/package-info.java @@ -15,7 +15,7 @@ */ /** - * Auto-configuration for gRPC web security when using a reactive stack. + * Auto-configuration for gRPC web security when using a servlet stack. */ @NullMarked package org.springframework.boot.grpc.server.autoconfigure.security.web.servlet; diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/StatusMapper.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/StatusMapper.java index 837a1be208d9..107e9d92d1e2 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/StatusMapper.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/health/StatusMapper.java @@ -34,10 +34,10 @@ public interface StatusMapper { /** - * Return the HTTP status code that corresponds to the given {@link Status health + * Return the gRPC serving status that corresponds to the given {@link Status health * status}. * @param status the health status to map - * @return the corresponding HTTP status code + * @return the corresponding gRPC serving status */ ServingStatus getServingStatus(Status status); diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java index 497715b3f445..4c83859b1dff 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java @@ -63,7 +63,7 @@ import static org.mockito.Mockito.mock; /** - * Tests fir {@link GrpcServerAutoConfiguration}. + * Tests for {@link GrpcServerAutoConfiguration}. * * @author Chris Bono * @author Andrey Litvitski diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizerTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizerTests.java index 1a0d74875dac..8bfa93524674 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizerTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/PropertiesServerBuilderCustomizerTests.java @@ -60,7 +60,7 @@ void customizeWhenInProcessServerBuilder() { } @Test - void customizerWhenServletServerBuilder() { + void customizeWhenServletServerBuilder() { ServletServerBuilder builder = mock(); PropertiesServerBuilderCustomizer customizer = new PropertiesServerBuilderCustomizer<>( getProperties()); diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentTests.java index 13e14d764761..f44a5fdb0f18 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentTests.java @@ -51,7 +51,7 @@ void isMemberWhenMembershipMatchesAcceptsTrue() { } @Test - void isMemberWhenMembershipRejectsReturnsTrue() { + void isMemberWhenMembershipRejectsReturnsFalse() { AutoConfiguredHealthCheckedGrpcComponent component = new AutoConfiguredHealthCheckedGrpcComponent( (name) -> name.startsWith("a"), this.statusAggregator, this.statusMapper); assertThat(component.isMember("bert")).isFalse(); @@ -66,7 +66,7 @@ void getStatusAggregatorReturnsStatusAggregator() { } @Test - void getStatusMapperReturnsHttpCodeStatusMapper() { + void getStatusMapperReturnsStatusMapper() { AutoConfiguredHealthCheckedGrpcComponent component = new AutoConfiguredHealthCheckedGrpcComponent( (name) -> true, this.statusAggregator, this.statusMapper); assertThat(component.getStatusMapper()).isSameAs(this.statusMapper); diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentsTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentsTests.java index e0ece6931d10..8a47232a3ca2 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentsTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentsTests.java @@ -417,7 +417,7 @@ static class CustomStatusMapperConfiguration { @Bean @Primary - StatusMapper httpCodeStatusMapper() { + StatusMapper statusMapper() { return StatusMapper.of(Collections.singletonMap(Status.DOWN.getCode(), ServingStatus.SERVING)); } @@ -428,7 +428,7 @@ static class CustomStatusMapperServiceAConfiguration { @Bean @Qualifier("a") - StatusMapper httpCodeStatusMapper() { + StatusMapper statusMapper() { return StatusMapper.of(Collections.singletonMap(Status.DOWN.getCode(), ServingStatus.SERVING)); } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java index ad9a30bbeeb2..527c284f61ca 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java @@ -213,7 +213,7 @@ void createsStatusMapperFromProperties() { } @Test - void whenHasHttpCodeStatusMapperBeanIgnoresProperties() { + void whenHasStatusMapperBeanIgnoresProperties() { this.contextRunner.withUserConfiguration(StatusMapperConfiguration.class) .withPropertyValues("spring.grpc.server.health.status.mapping.up=not-serving") .run((context) -> { @@ -427,7 +427,7 @@ StatusAggregator statusAggregator() { static class StatusMapperConfiguration { @Bean - StatusMapper httpCodeStatusMapper() { + StatusMapper statusMapper() { return (status) -> ServingStatus.UNRECOGNIZED; } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java index 418b257a7658..7d60d3f1c3c5 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java @@ -54,7 +54,7 @@ void initDisablesCsrf() { AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); StaticApplicationContext applicationContext = addApplicationContext(http); - addServiceDiscover(applicationContext); + addServiceDiscoverer(applicationContext); addGrpcServletRegistration(applicationContext); CsrfConfigurer csrf = addCsrf(http); this.configurer.init(http); @@ -80,7 +80,7 @@ void initWhenNoCsrfConfigurerDoesNothing() { AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); StaticApplicationContext applicationContext = addApplicationContext(http); - addServiceDiscover(applicationContext); + addServiceDiscoverer(applicationContext); addGrpcServletRegistration(applicationContext); this.configurer.init(http); CsrfConfigurer csrfConfigurer = http.getConfigurer(CsrfConfigurer.class); @@ -105,7 +105,7 @@ void initWhenNoGrpcServletRegistrationBeanDoesNothing() { AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); StaticApplicationContext applicationContext = addApplicationContext(http); - addServiceDiscover(applicationContext); + addServiceDiscoverer(applicationContext); CsrfConfigurer csrf = addCsrf(http); this.configurer.init(http); then(csrf).should(never()).requireCsrfProtectionMatcher(any()); @@ -118,7 +118,7 @@ void initWhenEnabledPropertyFalseDoesNothing() { HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); StaticApplicationContext applicationContext = addApplicationContext(http); TestPropertyValues.of("spring.grpc.server.security.csrf.enabled=false").applyTo(applicationContext); - addServiceDiscover(applicationContext); + addServiceDiscoverer(applicationContext); addGrpcServletRegistration(applicationContext); CsrfConfigurer csrf = addCsrf(http); this.configurer.init(http); @@ -132,7 +132,7 @@ void initWhenEnabledPropertyTrueDisablesCsrf() { HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); StaticApplicationContext applicationContext = addApplicationContext(http); TestPropertyValues.of("spring.grpc.server.security.csrf.enabled=true").applyTo(applicationContext); - addServiceDiscover(applicationContext); + addServiceDiscoverer(applicationContext); addGrpcServletRegistration(applicationContext); CsrfConfigurer csrf = addCsrf(http); this.configurer.init(http); @@ -154,7 +154,7 @@ private StaticApplicationContext addApplicationContext(HttpSecurity http) { return applicationContext; } - private void addServiceDiscover(StaticApplicationContext applicationContext) { + private void addServiceDiscoverer(StaticApplicationContext applicationContext) { GrpcServiceDiscoverer serviceDiscoverer = mock(); applicationContext.registerBean(GrpcServiceDiscoverer.class, serviceDiscoverer); } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/GrpcServerHealthTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/GrpcServerHealthTests.java index 9a57df1153a4..b253080e3987 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/GrpcServerHealthTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/GrpcServerHealthTests.java @@ -262,13 +262,13 @@ private static class TestHealthCheckedGrpcComponent implements HealthCheckedGrpc private final StatusMapper statusMapper; - TestHealthCheckedGrpcComponent(Set memebers) { - this(memebers, StatusAggregator.getDefault(), StatusMapper.getDefault()); + TestHealthCheckedGrpcComponent(Set members) { + this(members, StatusAggregator.getDefault(), StatusMapper.getDefault()); } - TestHealthCheckedGrpcComponent(Set memebers, StatusAggregator statusAggregator, + TestHealthCheckedGrpcComponent(Set members, StatusAggregator statusAggregator, StatusMapper statusMapper) { - this.members = memebers; + this.members = members; this.statusAggregator = statusAggregator; this.statusMapper = statusMapper; } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusAggregatorTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusAggregatorTests.java index bbc8a6abe62b..32864d95239e 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusAggregatorTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusAggregatorTests.java @@ -18,7 +18,6 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.health.actuate.endpoint.StatusAggregator; import org.springframework.boot.health.contributor.Status; import static org.assertj.core.api.Assertions.assertThat; diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusMapperTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusMapperTests.java index af027f7200c1..0c177729c820 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusMapperTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/health/StatusMapperTests.java @@ -43,7 +43,7 @@ void createWhenMappingsAreNullUsesDefaultMappings() { } @Test - void getStatusCodeReturnsMappedStatus() { + void getServingStatusReturnsMappedStatus() { Map map = new LinkedHashMap<>(); map.put("up", ServingStatus.UNRECOGNIZED); map.put("down", ServingStatus.UNKNOWN); @@ -54,7 +54,7 @@ void getStatusCodeReturnsMappedStatus() { } @Test - void getStatusCodeWhenMappingsAreNotUniformReturnsMappedStatus() { + void getServingStatusWhenMappingsAreNotUniformReturnsMappedStatus() { Map map = new LinkedHashMap<>(); map.put("out-of-service", ServingStatus.SERVING); StatusMapper mapper = StatusMapper.of(map); diff --git a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcTransportAutoConfiguration.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcTransportAutoConfiguration.java index 6ef189c4c879..106a77b9c90a 100644 --- a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcTransportAutoConfiguration.java +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/TestGrpcTransportAutoConfiguration.java @@ -16,13 +16,11 @@ package org.springframework.boot.grpc.test.autoconfigure; -import java.util.List; - import io.grpc.BindableService; import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.stub.AbstractStub; -import org.jspecify.annotations.Nullable; +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; @@ -40,7 +38,6 @@ import org.springframework.grpc.client.GrpcChannelFactory; import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.grpc.server.InProcessGrpcServerFactory; -import org.springframework.grpc.server.ServerBuilderCustomizer; import org.springframework.grpc.server.ServerServiceDefinitionFilter; import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; import org.springframework.grpc.server.service.GrpcServiceConfigurer; @@ -70,10 +67,9 @@ static class TestGrpcServerTransportAutoConfiguration { @Bean @Order(Ordered.HIGHEST_PRECEDENCE) TestGrpcServerFactory testGrpcServerFactory(GrpcServiceDiscoverer serviceDiscoverer, - GrpcServiceConfigurer serviceConfigurer, - List> customizers, - @Nullable ServerServiceDefinitionFilter serviceFilter) { + GrpcServiceConfigurer serviceConfigurer, ObjectProvider serviceFilter) { TestGrpcServerFactory factory = new TestGrpcServerFactory(address); + serviceFilter.ifAvailable(factory::setServiceFilter); serviceDiscoverer.findServices() .stream() .map((spec) -> serviceConfigurer.configure(spec, factory)) diff --git a/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializerTests.java b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializerTests.java index bb5ffdb80cc1..7e37a43647cd 100644 --- a/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializerTests.java +++ b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializerTests.java @@ -70,10 +70,10 @@ void whenTestGrpcServerFactorySetsNoPortProperty() { private void testListener(GrpcServerFactory factory, int port, @Nullable String expected) { try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(Config.class)) { context.getBean(GrpcPortInfoApplicationContextInitializer.class).initialize(context); - GrpcServerLifecycle lifecyle = mock(); + GrpcServerLifecycle lifecycle = mock(); Server server = mock(); - given(lifecyle.getFactory()).willReturn(factory); - GrpcServerStartedEvent event = new GrpcServerStartedEvent(lifecyle, server, "localhost", port); + given(lifecycle.getFactory()).willReturn(factory); + GrpcServerStartedEvent event = new GrpcServerStartedEvent(lifecycle, server, "localhost", port); context.publishEvent(event); assertThat(context.getEnvironment().getProperty(PORT_PROPERTY)).isEqualTo(expected); } From 59f8dca1a44d550488f4040ddb8502c459460c82 Mon Sep 17 00:00:00 2001 From: husseinvr97 Date: Thu, 26 Mar 2026 00:18:21 +0200 Subject: [PATCH 429/930] Add configuration for HTML escaping configuration with WebFlux See gh-49791 Signed-off-by: husseinvr97 --- .../WebFluxAutoConfiguration.java | 5 +++ .../autoconfigure/WebFluxProperties.java | 13 +++++++ .../WebFluxAutoConfigurationTests.java | 34 +++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java index d19f3a49b061..eddb93aca312 100644 --- a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java +++ b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java @@ -304,6 +304,11 @@ private void configureApiVersioningUse(ApiVersionConfigurer configurer, Use use) .forEach((mediaType, parameterName) -> configurer.useMediaTypeParameter(mediaType, parameterName)); } + @Bean + WebHttpHandlerBuilderCustomizer defaultHtmlEscapeCustomizer() { + return (builder) -> builder.defaultHtmlEscape(this.webFluxProperties.getDefaultHtmlEscape()); + } + } /** diff --git a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxProperties.java b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxProperties.java index 7a5d19329247..aadd36b79ac4 100644 --- a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxProperties.java +++ b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxProperties.java @@ -58,6 +58,11 @@ public class WebFluxProperties { */ private String webjarsPathPattern = "/webjars/**"; + /** + * Whether default HTML escaping is enabled for the web application. + */ + private @Nullable Boolean defaultHtmlEscape; + public @Nullable String getBasePath() { return this.basePath; } @@ -110,6 +115,14 @@ public void setWebjarsPathPattern(String webjarsPathPattern) { this.webjarsPathPattern = webjarsPathPattern; } + public @Nullable Boolean getDefaultHtmlEscape() { + return this.defaultHtmlEscape; + } + + public void setDefaultHtmlEscape(@Nullable Boolean defaultHtmlEscape) { + this.defaultHtmlEscape = defaultHtmlEscape; + } + public static class Format { /** diff --git a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java index 43084d0f1c48..e6a7fc1e52db 100644 --- a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java +++ b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java @@ -120,6 +120,7 @@ import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebHandler; import org.springframework.web.server.WebSession; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; @@ -461,6 +462,39 @@ void hiddenHttpMethodFilterCanBeEnabled() { .run((context) -> assertThat(context).hasSingleBean(OrderedHiddenHttpMethodFilter.class)); } + @Test + void defaultHtmlEscapeIsNotConfiguredByDefault() { + this.contextRunner.run((context) -> { + WebHttpHandlerBuilderCustomizer customizer = context.getBean("defaultHtmlEscapeCustomizer", + WebHttpHandlerBuilderCustomizer.class); + WebHttpHandlerBuilder builder = WebHttpHandlerBuilder.webHandler(mock(WebHandler.class)); + customizer.customize(builder); + assertThat(builder.getDefaultHtmlEscape()).isNull(); + }); + } + + @Test + void defaultHtmlEscapeCanBeEnabled() { + this.contextRunner.withPropertyValues("spring.webflux.default-html-escape=true").run((context) -> { + WebHttpHandlerBuilderCustomizer customizer = context.getBean("defaultHtmlEscapeCustomizer", + WebHttpHandlerBuilderCustomizer.class); + WebHttpHandlerBuilder builder = WebHttpHandlerBuilder.webHandler(mock(WebHandler.class)); + customizer.customize(builder); + assertThat(builder.getDefaultHtmlEscape()).isTrue(); + }); + } + + @Test + void defaultHtmlEscapeCanBeDisabled() { + this.contextRunner.withPropertyValues("spring.webflux.default-html-escape=false").run((context) -> { + WebHttpHandlerBuilderCustomizer customizer = context.getBean("defaultHtmlEscapeCustomizer", + WebHttpHandlerBuilderCustomizer.class); + WebHttpHandlerBuilder builder = WebHttpHandlerBuilder.webHandler(mock(WebHandler.class)); + customizer.customize(builder); + assertThat(builder.getDefaultHtmlEscape()).isFalse(); + }); + } + @Test void customRequestMappingHandlerMapping() { this.contextRunner.withUserConfiguration(CustomRequestMappingHandlerMapping.class).run((context) -> { From c747c9c53dc9d4a4a1a45832c2fec6f88a3e7328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 26 Mar 2026 15:11:36 +0100 Subject: [PATCH 430/930] Polish "Add configuration for HTML escaping configuration with WebFlux" See gh-49791 --- .../HttpHandlerAutoConfiguration.java | 5 ++- .../WebFluxAutoConfiguration.java | 5 --- .../HttpHandlerAutoConfigurationTests.java | 26 ++++++++++++++ .../WebFluxAutoConfigurationTests.java | 34 ------------------- 4 files changed, 30 insertions(+), 40 deletions(-) diff --git a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/HttpHandlerAutoConfiguration.java b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/HttpHandlerAutoConfiguration.java index 18361fceb75f..81f5930d3bf2 100644 --- a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/HttpHandlerAutoConfiguration.java +++ b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/HttpHandlerAutoConfiguration.java @@ -64,10 +64,13 @@ static class AnnotationConfig { @Bean HttpHandler httpHandler(ObjectProvider propsProvider, ObjectProvider handlerBuilderCustomizers) { + WebFluxProperties properties = propsProvider.getIfAvailable(); WebHttpHandlerBuilder handlerBuilder = WebHttpHandlerBuilder.applicationContext(this.applicationContext); + if (properties != null) { + handlerBuilder.defaultHtmlEscape(properties.getDefaultHtmlEscape()); + } 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); return new ContextPathCompositeHandler(handlersMap); diff --git a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java index eddb93aca312..d19f3a49b061 100644 --- a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java +++ b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java @@ -304,11 +304,6 @@ private void configureApiVersioningUse(ApiVersionConfigurer configurer, Use use) .forEach((mediaType, parameterName) -> configurer.useMediaTypeParameter(mediaType, parameterName)); } - @Bean - WebHttpHandlerBuilderCustomizer defaultHtmlEscapeCustomizer() { - return (builder) -> builder.defaultHtmlEscape(this.webFluxProperties.getDefaultHtmlEscape()); - } - } /** diff --git a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/HttpHandlerAutoConfigurationTests.java b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/HttpHandlerAutoConfigurationTests.java index e2e4cb7d70ee..bcb6439fcb42 100644 --- a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/HttpHandlerAutoConfigurationTests.java +++ b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/HttpHandlerAutoConfigurationTests.java @@ -18,6 +18,8 @@ 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; import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -35,6 +37,7 @@ import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.server.WebHandler; +import org.springframework.web.server.adapter.HttpWebHandlerAdapter; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; @@ -100,6 +103,29 @@ void shouldConfigureBasePathCompositeHandler() { }); } + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void shouldConfigureDefaultHtmlEscape(boolean enabled) { + this.contextRunner.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class)) + .withPropertyValues("spring.webflux.default-html-escape=" + enabled) + .run((context) -> { + assertThat(context).hasSingleBean(HttpHandler.class); + assertThat(context.getBean(HttpHandler.class)).isInstanceOfSatisfying(HttpWebHandlerAdapter.class, + (adapter) -> assertThat(adapter.getDefaultHtmlEscape()).isEqualTo(enabled)); + }); + } + + @Test + void shouldNotConfigureDefaultHtmlEscaperWithoutWebFluxAutoConfiguration() { + this.contextRunner.withUserConfiguration(CustomWebHandler.class) + .withPropertyValues("spring.webflux.default-html-escape=true") + .run((context) -> { + assertThat(context).hasSingleBean(HttpHandler.class); + assertThat(context.getBean(HttpHandler.class)).isInstanceOfSatisfying(HttpWebHandlerAdapter.class, + (adapter) -> assertThat(adapter.getDefaultHtmlEscape()).isNull()); + }); + } + @Configuration(proxyBeanMethods = false) static class CustomHttpHandler { diff --git a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java index e6a7fc1e52db..43084d0f1c48 100644 --- a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java +++ b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java @@ -120,7 +120,6 @@ import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebHandler; import org.springframework.web.server.WebSession; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; @@ -462,39 +461,6 @@ void hiddenHttpMethodFilterCanBeEnabled() { .run((context) -> assertThat(context).hasSingleBean(OrderedHiddenHttpMethodFilter.class)); } - @Test - void defaultHtmlEscapeIsNotConfiguredByDefault() { - this.contextRunner.run((context) -> { - WebHttpHandlerBuilderCustomizer customizer = context.getBean("defaultHtmlEscapeCustomizer", - WebHttpHandlerBuilderCustomizer.class); - WebHttpHandlerBuilder builder = WebHttpHandlerBuilder.webHandler(mock(WebHandler.class)); - customizer.customize(builder); - assertThat(builder.getDefaultHtmlEscape()).isNull(); - }); - } - - @Test - void defaultHtmlEscapeCanBeEnabled() { - this.contextRunner.withPropertyValues("spring.webflux.default-html-escape=true").run((context) -> { - WebHttpHandlerBuilderCustomizer customizer = context.getBean("defaultHtmlEscapeCustomizer", - WebHttpHandlerBuilderCustomizer.class); - WebHttpHandlerBuilder builder = WebHttpHandlerBuilder.webHandler(mock(WebHandler.class)); - customizer.customize(builder); - assertThat(builder.getDefaultHtmlEscape()).isTrue(); - }); - } - - @Test - void defaultHtmlEscapeCanBeDisabled() { - this.contextRunner.withPropertyValues("spring.webflux.default-html-escape=false").run((context) -> { - WebHttpHandlerBuilderCustomizer customizer = context.getBean("defaultHtmlEscapeCustomizer", - WebHttpHandlerBuilderCustomizer.class); - WebHttpHandlerBuilder builder = WebHttpHandlerBuilder.webHandler(mock(WebHandler.class)); - customizer.customize(builder); - assertThat(builder.getDefaultHtmlEscape()).isFalse(); - }); - } - @Test void customRequestMappingHandlerMapping() { this.contextRunner.withUserConfiguration(CustomRequestMappingHandlerMapping.class).run((context) -> { From 29b03edf6f638c6e024e446742f1844b02ca0be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 27 Mar 2026 11:11:22 +0100 Subject: [PATCH 431/930] Do not configure a ReactiveJmtDecoder without spring-webflux This commit guards the creation of a ReactiveJwtDecoder with the presence of Spring WebFlux. WebClient is used behind the scenes and the sole presences of the authorization server and reactive types were not precise enough. Closes gh-49807 --- .../reactive/ReactiveJwtDecoderConfiguration.java | 3 +++ ...tiveOAuth2ResourceServerAutoConfigurationTests.java | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtDecoderConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtDecoderConfiguration.java index 28e0eca5f930..820b57044cb1 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtDecoderConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveJwtDecoderConfiguration.java @@ -31,6 +31,7 @@ import org.jspecify.annotations.Nullable; 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.ConditionalOnProperty; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; @@ -53,6 +54,7 @@ import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.web.reactive.function.client.WebClient; /** * {@link Configuration @Configuration} for reactive JWT decoder beans. @@ -67,6 +69,7 @@ * @author Phillip Webb */ @Configuration(proxyBeanMethods = false) +@ConditionalOnClass(WebClient.class) @ConditionalOnMissingBean(ReactiveJwtDecoder.class) class ReactiveJwtDecoderConfiguration { diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index 0e4822986536..e7e9b473801d 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -83,6 +83,7 @@ import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.reactive.function.client.WebClient; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -300,6 +301,15 @@ void autoConfigurationShouldConfigureResourceServerUsingPublicKeyValue() { }); } + @Test + void autoConfigurationShouldBackOffIfWebClientIsNotAvailable() { + this.contextRunner.withClassLoader(new FilteredClassLoader(WebClient.class)) + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") + .run((context) -> assertThat(context).hasNotFailed() + .doesNotHaveBean(NimbusReactiveJwtDecoder.class) + .doesNotHaveBean(ReactiveJwtDecoder.class)); + } + @Test void autoConfigurationShouldFailIfPublicKeyLocationDoesNotExist() { this.contextRunner From b089e2148d3f15730228a3a069a1499f0e11ba22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 27 Mar 2026 17:25:44 +0100 Subject: [PATCH 432/930] Adapt Javadoc reference --- .../boot/restclient/test/RootUriRequestExpectationManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManager.java b/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManager.java index 486188c2f44c..df22482633df 100644 --- a/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManager.java +++ b/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManager.java @@ -50,7 +50,7 @@ * * @author Phillip Webb * @since 4.0.0 - * @see RootUriTemplateHandler + * @see DefaultUriBuilderFactory * @see #bindTo(RestTemplate) * @see #forRestTemplate(RestTemplate, RequestExpectationManager) */ From 3972d42a8fd93ed0ac794d5a8ed07701306bab05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 24 Mar 2026 16:05:44 +0100 Subject: [PATCH 433/930] Add support for loading layers configuration from the plugin classpath This commit introduces an alternative to load layers configuration from the classpath. This lets user package their custom layers XML at META-INF/spring/layers/.xml in a plugin dependency rather than referencing a file path directly. The name of the layers configuration can then be referenced using the new `configurationName` property. Closes gh-32466 --- .../packaging/custom-layers-classpath-pom.xml | 26 ++++++++ .../modules/maven-plugin/pages/packaging.adoc | 15 ++++- .../boot/maven/JarIntegrationTests.java | 21 ++++++ .../jar-classifier/pom.xml | 40 ++++++++++++ .../jar-layers-configuration/pom.xml | 11 ++++ .../META-INF/spring/layers/custom.xml | 26 ++++++++ .../jar-release/pom.xml | 11 ++++ .../jar-snapshot/pom.xml | 11 ++++ .../jar-layered-custom-name/jar/pom.xml | 65 +++++++++++++++++++ .../main/java/org/test/SampleApplication.java | 24 +++++++ .../jar/src/main/resources/application.yml | 0 .../projects/jar-layered-custom-name/pom.xml | 21 ++++++ .../boot/maven/AbstractPackagerMojo.java | 64 ++++++++++++++---- .../springframework/boot/maven/Layers.java | 15 +++++ .../META-INF/spring/layers/default.xml | 26 ++++++++ 15 files changed, 363 insertions(+), 13 deletions(-) create mode 100644 build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging/custom-layers-classpath-pom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-classifier/pom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/pom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/src/main/resources/META-INF/spring/layers/custom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-release/pom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-snapshot/pom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/pom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/src/main/java/org/test/SampleApplication.java create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/src/main/resources/application.yml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/pom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/main/resources/META-INF/spring/layers/default.xml diff --git a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging/custom-layers-classpath-pom.xml b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging/custom-layers-classpath-pom.xml new file mode 100644 index 000000000000..ec4f10c356ce --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging/custom-layers-classpath-pom.xml @@ -0,0 +1,26 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + true + custom + + + + + com.example + layers-configuration + 1.0.0 + + + + + + + diff --git a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/packaging.adoc b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/packaging.adoc index b0249a0c67d4..1da8d495310c 100644 --- a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/packaging.adoc +++ b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/packaging.adoc @@ -83,13 +83,26 @@ include::example$packaging/disable-layers-pom.xml[tags=disable-layers] === Custom Layers Configuration Depending on your application, you may want to tune how layers are created and add new ones. -This can be done using a separate configuration file that should be registered as shown below: +This can be done using a configuration loaded from a file or from the classpath. + +To use a configuration file, register it as shown below: [source,xml,indent=0,subs="verbatim,attributes"] ---- include::example$packaging/custom-layers-pom.xml[tags=custom-layers] ---- +Alternatively, the configuration can be loaded from the classpath by specifying its name. +A matching file is expected at `META-INF/spring/layers/.xml` on the plugin's classpath. +The jar that contains the configuration can be added as a plugin dependency, as shown below: + +[source,xml,indent=0,subs="verbatim,attributes"] +---- +include::example$packaging/custom-layers-classpath-pom.xml[tags=custom-layers-classpath] +---- + +In the example above, `META-INF/spring/layers/custom.xml` is expected to be available on the plugin's classpath. + The configuration file describes how an archive can be separated into layers, and the order of those layers. The following example shows how the default ordering described above can be defined explicitly: diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java index f7dd021d626f..14f247d8e2fb 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java @@ -437,6 +437,27 @@ void whenJarIsRepackagedWithTheCustomLayers(MavenBuild mavenBuild) { }); } + @TestTemplate + void whenJarIsRepackagedWithTheCustomLayersFromClasspath(MavenBuild mavenBuild) { + mavenBuild.project("jar-layered-custom-name").execute((project) -> { + File repackaged = new File(project, "jar/target/jar-layered-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/") + .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-release") + .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-snapshot"); + try (JarFile jarFile = new JarFile(repackaged)) { + Map> layerIndex = readLayerIndex(jarFile); + assertThat(layerIndex.keySet()).containsExactly("my-dependencies-name", "snapshot-dependencies", + "configuration", "application"); + assertThat(layerIndex.get("application")) + .contains("BOOT-INF/lib/jar-release-0.0.1.RELEASE.jar", + "BOOT-INF/lib/jar-snapshot-0.0.1.BUILD-SNAPSHOT.jar", + "BOOT-INF/lib/jar-classifier-0.0.1-bravo.jar") + .doesNotContain("BOOT-INF/lib/jar-classifier-0.0.1-alpha.jar", + "BOOT-INF/lib/jar-layers-configuration-0.0.1.jar"); + } + }); + } + @TestTemplate void repackagedJarContainsClasspathIndex(MavenBuild mavenBuild) { mavenBuild.project("jar").execute((project) -> { diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-classifier/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-classifier/pom.xml new file mode 100644 index 000000000000..3dd0f2f83b44 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-classifier/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-classifier + 0.0.1 + jar + jar + Classifier Jar dependency + + + + maven-jar-plugin + + + alpha + package + + jar + + + alpha + + + + bravo + package + + jar + + + bravo + + + + + + + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/pom.xml new file mode 100644 index 000000000000..fe90fa206c28 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-layers-configuration + 0.0.1 + jar + jar + Layers configuration dependency + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/src/main/resources/META-INF/spring/layers/custom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/src/main/resources/META-INF/spring/layers/custom.xml new file mode 100644 index 000000000000..434cd9fbb7fd --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/src/main/resources/META-INF/spring/layers/custom.xml @@ -0,0 +1,26 @@ + + + + **/application*.* + + + + + + + + + *:*:*-SNAPSHOT + + + + + my-dependencies-name + snapshot-dependencies + configuration + application + + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-release/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-release/pom.xml new file mode 100644 index 000000000000..a06fe545f187 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-release/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + jar + jar + Release Jar dependency + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-snapshot/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-snapshot/pom.xml new file mode 100644 index 000000000000..ab31e719baf5 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-snapshot/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + jar + jar + Snapshot Jar dependency + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/pom.xml new file mode 100644 index 000000000000..ead499fa8b89 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-layered + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + true + custom + + + + + + + org.springframework.boot.maven.it + jar-layers-configuration + 0.0.1 + + + + + + + + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + + + org.springframework.boot.maven.it + jar-classifier + 0.0.1 + bravo + + + org.apache.logging.log4j + log4j-api + @log4j2.version@ + + + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/src/main/java/org/test/SampleApplication.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..547d0cf01711 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/src/main/resources/application.yml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/src/main/resources/application.yml new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/pom.xml new file mode 100644 index 000000000000..55914809a3f1 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-classifier + jar-layers-configuration + jar-release + jar-snapshot + jar + + diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java index f4383ab65bd7..a6b432f2b9c8 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java @@ -18,6 +18,8 @@ import java.io.File; import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -31,6 +33,7 @@ import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Dependency; import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; @@ -78,6 +81,14 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo @SuppressWarnings("NullAway.Init") protected MavenSession session; + /** + * The plugin descriptor. + * @since 4.1.0 + */ + @Parameter(defaultValue = "${plugin}", readonly = true, required = true) + @SuppressWarnings("NullAway.Init") + private PluginDescriptor pluginDescriptor; + /** * Maven project helper utils. * @since 1.0.0 @@ -174,8 +185,7 @@ protected

    P getConfiguredPackager(Supplier

    supplier) { packager.setLayout(layout.layout()); } if (this.layers.isEnabled()) { - packager.setLayers((this.layers.getConfiguration() != null) - ? getCustomLayers(this.layers.getConfiguration()) : IMPLICIT_LAYERS); + packager.setLayers(loadLayersConfiguration()); } packager.setIncludeRelevantJarModeJars(getIncludeRelevantJarModeJars()); return packager; @@ -185,23 +195,46 @@ private boolean getIncludeRelevantJarModeJars() { return this.includeTools; } - private CustomLayers getCustomLayers(File configuration) { + private org.springframework.boot.loader.tools.Layers loadLayersConfiguration() { + File configuration = this.layers.getConfiguration(); + if (configuration != null) { + return getCustomLayers(configuration.getAbsolutePath(), () -> new FileInputStream(configuration)); + } + String configurationName = this.layers.getConfigurationName(); + if (configurationName != null) { + String location = "META-INF/spring/layers/%s.xml".formatted(configurationName); + return getCustomLayers(location, () -> loadLayersConfigurationFromClasspath(configurationName, location)); + } + return IMPLICIT_LAYERS; + } + + private InputStream loadLayersConfigurationFromClasspath(String name, String location) { + InputStream in = this.pluginDescriptor.getClassRealm().getResourceAsStream(location); + if (in == null) { + throw new IllegalStateException( + "Failed to load layers configuration with name '%s': '%s' not found".formatted(name, location)); + } + return in; + } + + private CustomLayers getCustomLayers(String source, InputStreamSource inputStreamSource) { try { - Document document = getDocumentIfAvailable(configuration); + Document document = getDocumentIfAvailable(inputStreamSource); return new CustomLayersProvider().getLayers(document); } catch (Exception ex) { - throw new IllegalStateException( - "Failed to process custom layers configuration " + configuration.getAbsolutePath(), ex); + throw new IllegalStateException("Failed to process custom layers configuration " + source, ex); } } - private Document getDocumentIfAvailable(File xmlFile) throws Exception { - InputSource inputSource = new InputSource(new FileInputStream(xmlFile)); - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); - DocumentBuilder builder = factory.newDocumentBuilder(); - return builder.parse(inputSource); + private Document getDocumentIfAvailable(InputStreamSource source) throws Exception { + try (InputStream in = source.getInputStream()) { + InputSource inputSource = new InputSource(in); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(inputSource); + } } /** @@ -311,4 +344,11 @@ public Layout layout() { } + @FunctionalInterface + private interface InputStreamSource { + + InputStream getInputStream() throws IOException; + + } + } diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java index 9ac86ff3b1a1..20d33f585d7f 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java @@ -32,6 +32,8 @@ public class Layers { private @Nullable File configuration; + private @Nullable String configurationName; + /** * Whether a {@code layers.idx} file should be added to the jar. * @return true if a {@code layers.idx} file should be added. @@ -54,4 +56,17 @@ public void setConfiguration(@Nullable File configuration) { this.configuration = configuration; } + /** + * The name of the layers configuration to load from the classpath. A matching file is + * expected at {@code META-INF/spring/layers/.xml} + * @return the layers configuration name to load from the classpath + */ + public @Nullable String getConfigurationName() { + return this.configurationName; + } + + public void setConfigurationName(@Nullable String configurationName) { + this.configurationName = configurationName; + } + } diff --git a/build-plugin/spring-boot-maven-plugin/src/main/resources/META-INF/spring/layers/default.xml b/build-plugin/spring-boot-maven-plugin/src/main/resources/META-INF/spring/layers/default.xml new file mode 100644 index 000000000000..0d2fedf8334d --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/main/resources/META-INF/spring/layers/default.xml @@ -0,0 +1,26 @@ + + + + org/springframework/boot/loader/** + + + + + + + + + *:*:*SNAPSHOT + + + + + dependencies + spring-boot-loader + snapshot-dependencies + application + + From c56cbd46b22fb930ee2467550120b713522555c1 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 10 Mar 2026 10:02:43 +0100 Subject: [PATCH 434/930] Map OpenTelemetry environment variables to properties Closes gh-48799 --- .../pages/actuator/observability.adoc | 203 ++- ...nfiguredOpenTelemetrySdkConfiguration.java | 2 +- ...nmentVariableEnvironmentPostProcessor.java | 436 +++++++ .../OpenTelemetryEnvironmentVariables.java | 292 +++++ .../autoconfigure/W3CHeaderParser.java | 77 ++ ...itional-spring-configuration-metadata.json | 6 + .../main/resources/META-INF/spring.factories | 2 + ...VariableEnvironmentPostProcessorTests.java | 1089 +++++++++++++++++ ...penTelemetryEnvironmentVariablesTests.java | 526 ++++++++ .../autoconfigure/W3CHeaderParserTests.java | 107 ++ settings.gradle | 1 + .../build.gradle | 68 + .../OtlpCollectorIntegrationTests.java | 109 ++ .../smoketest/opentelemetry/package-info.java | 23 + .../src/dockerTest/resources/certs/ca.crt | 29 + .../src/dockerTest/resources/certs/ca.key | 52 + .../src/dockerTest/resources/certs/server.crt | 30 + .../src/dockerTest/resources/certs/server.key | 52 + .../resources/otel-collector-config.yaml | 39 + .../opentelemetry/SampleController.java | 30 + .../SampleOpenTelemetryApplication.java | 29 + .../smoketest/opentelemetry/package-info.java | 23 + .../opentelemetry/DisabledMappingTests.java | 50 + .../HeadersAndCompressionTests.java | 79 ++ .../SampleOpenTelemetryApplicationTests.java | 40 + .../SignalSpecificPrecedenceTests.java | 48 + .../smoketest/opentelemetry/package-info.java | 23 + 27 files changed, 3457 insertions(+), 8 deletions(-) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/opentelemetry/environmentvariables/{ => unsupported}/AutoConfiguredOpenTelemetrySdkConfiguration.java (96%) create mode 100644 module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariableEnvironmentPostProcessor.java create mode 100644 module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariables.java create mode 100644 module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParser.java create mode 100644 module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariableEnvironmentPostProcessorTests.java create mode 100644 module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariablesTests.java create mode 100644 module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParserTests.java create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/java/smoketest/opentelemetry/OtlpCollectorIntegrationTests.java create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/java/smoketest/opentelemetry/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/ca.crt create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/ca.key create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/server.crt create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/server.key create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/otel-collector-config.yaml create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/main/java/smoketest/opentelemetry/SampleController.java create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/main/java/smoketest/opentelemetry/SampleOpenTelemetryApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/main/java/smoketest/opentelemetry/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/DisabledMappingTests.java create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/HeadersAndCompressionTests.java create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/SampleOpenTelemetryApplicationTests.java create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/SignalSpecificPrecedenceTests.java create mode 100644 smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/package-info.java diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc index b7d5be43b32c..6e2efcb04110 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc @@ -139,7 +139,200 @@ NOTE: Keep in mind that Spring Boot doesn't use OpenTelemetry's metrics function [[actuator.observability.opentelemetry.environment-variables]] === Environment variables -Spring Boot supports the following environment variables to configure the OpenTelemetry resource: +Spring Boot supports a subset of the https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/[OpenTelemetry SDK environment variables]. +These environment variables are automatically mapped to Spring Boot configuration properties at startup. +When an environment variable has a signal-specific variant (for example, `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`) and a general variant (`OTEL_EXPORTER_OTLP_ENDPOINT`), the signal-specific variant takes precedence and is used as-is. +When the general variant is used as a fallback, the signal-specific path (`v1/traces`, `v1/metrics`, or `v1/logs`) is appended to it. +For example, setting `OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318` results in `http://collector:4318/v1/traces` for traces. + +This mapping can be disabled by setting configprop:management.opentelemetry.map-environment-variables[] to `false`. + +[[actuator.observability.opentelemetry.environment-variables.general]] +==== General + +[cols="1,1"] +|=== +| Environment Variable | Spring Boot Property + +| `OTEL_SDK_DISABLED` +| configprop:management.opentelemetry.enabled[] (inverted) + +| `OTEL_PROPAGATORS` +| configprop:management.tracing.propagation.type[] and configprop:management.tracing.baggage.enabled[] + +| `OTEL_TRACES_SAMPLER` +| configprop:management.opentelemetry.tracing.sampler[] + +| `OTEL_TRACES_SAMPLER_ARG` +| configprop:management.tracing.sampling.probability[] + +| `OTEL_METRICS_EXEMPLAR_FILTER` +| configprop:management.tracing.exemplars.include[] +|=== + +[[actuator.observability.opentelemetry.environment-variables.metrics]] +==== Metrics Exporter + +[cols="1,1"] +|=== +| Environment Variable | Spring Boot Property + +| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` / `OTEL_EXPORTER_OTLP_ENDPOINT` +| configprop:management.otlp.metrics.export.url[] + +| `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` +| configprop:management.otlp.metrics.export.aggregation-temporality[] + +| `OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` +| configprop:management.otlp.metrics.export.histogram-flavor[] + +| `OTEL_EXPORTER_OTLP_METRICS_COMPRESSION` / `OTEL_EXPORTER_OTLP_COMPRESSION` +| configprop:management.otlp.metrics.export.compression-mode[] + +| `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` / `OTEL_EXPORTER_OTLP_TIMEOUT` +| configprop:management.otlp.metrics.export.read-timeout[] + +| `OTEL_EXPORTER_OTLP_METRICS_HEADERS` / `OTEL_EXPORTER_OTLP_HEADERS` +| configprop:management.otlp.metrics.export.headers[] + +| `OTEL_METRIC_EXPORT_INTERVAL` +| configprop:management.otlp.metrics.export.step[] + +| `OTEL_METRICS_EXPORTER` +| configprop:management.otlp.metrics.export.enabled[] (only `otlp` enables OTLP export; other values disable it but do not enable alternative exporters) + +| `OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CERTIFICATE` +| configprop:management.otlp.metrics.export.ssl.bundle[] (auto-configured SSL bundle) + +| `OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY` / `OTEL_EXPORTER_OTLP_CLIENT_KEY` +| configprop:management.otlp.metrics.export.ssl.bundle[] (auto-configured SSL bundle) + +| `OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` +| configprop:management.otlp.metrics.export.ssl.bundle[] (auto-configured SSL bundle) +|=== + +[[actuator.observability.opentelemetry.environment-variables.traces]] +==== Traces Exporter + +[cols="1,1"] +|=== +| Environment Variable | Spring Boot Property + +| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` / `OTEL_EXPORTER_OTLP_ENDPOINT` +| configprop:management.opentelemetry.tracing.export.otlp.endpoint[] + +| `OTEL_EXPORTER_OTLP_TRACES_COMPRESSION` / `OTEL_EXPORTER_OTLP_COMPRESSION` +| configprop:management.opentelemetry.tracing.export.otlp.compression[] + +| `OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` / `OTEL_EXPORTER_OTLP_TIMEOUT` +| configprop:management.opentelemetry.tracing.export.otlp.timeout[] + +| `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` / `OTEL_EXPORTER_OTLP_PROTOCOL` +| configprop:management.opentelemetry.tracing.export.otlp.transport[] + +| `OTEL_EXPORTER_OTLP_TRACES_HEADERS` / `OTEL_EXPORTER_OTLP_HEADERS` +| configprop:management.opentelemetry.tracing.export.otlp.headers[] + +| `OTEL_BSP_SCHEDULE_DELAY` +| configprop:management.opentelemetry.tracing.export.schedule-delay[] + +| `OTEL_BSP_EXPORT_TIMEOUT` +| configprop:management.opentelemetry.tracing.export.timeout[] + +| `OTEL_BSP_MAX_QUEUE_SIZE` +| configprop:management.opentelemetry.tracing.export.max-queue-size[] + +| `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` +| configprop:management.opentelemetry.tracing.export.max-batch-size[] + +| `OTEL_TRACES_EXPORTER` +| configprop:management.tracing.export.otlp.enabled[] (only `otlp` enables OTLP export; other values disable it but do not enable alternative exporters) + +| `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` / `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` +| configprop:management.opentelemetry.tracing.limits.max-attribute-value-length[] + +| `OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT` / `OTEL_ATTRIBUTE_COUNT_LIMIT` +| configprop:management.opentelemetry.tracing.limits.max-attributes[] + +| `OTEL_SPAN_EVENT_COUNT_LIMIT` +| configprop:management.opentelemetry.tracing.limits.max-events[] + +| `OTEL_SPAN_LINK_COUNT_LIMIT` +| configprop:management.opentelemetry.tracing.limits.max-links[] + +| `OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT` +| configprop:management.opentelemetry.tracing.limits.max-attributes-per-event[] + +| `OTEL_LINK_ATTRIBUTE_COUNT_LIMIT` +| configprop:management.opentelemetry.tracing.limits.max-attributes-per-link[] + +| `OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CERTIFICATE` +| configprop:management.opentelemetry.tracing.export.otlp.ssl.bundle[] (auto-configured SSL bundle) + +| `OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY` / `OTEL_EXPORTER_OTLP_CLIENT_KEY` +| configprop:management.opentelemetry.tracing.export.otlp.ssl.bundle[] (auto-configured SSL bundle) + +| `OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` +| configprop:management.opentelemetry.tracing.export.otlp.ssl.bundle[] (auto-configured SSL bundle) +|=== + +[[actuator.observability.opentelemetry.environment-variables.logs]] +==== Logs Exporter + +[cols="1,1"] +|=== +| Environment Variable | Spring Boot Property + +| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` / `OTEL_EXPORTER_OTLP_ENDPOINT` +| configprop:management.opentelemetry.logging.export.otlp.endpoint[] + +| `OTEL_EXPORTER_OTLP_LOGS_COMPRESSION` / `OTEL_EXPORTER_OTLP_COMPRESSION` +| configprop:management.opentelemetry.logging.export.otlp.compression[] + +| `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` / `OTEL_EXPORTER_OTLP_TIMEOUT` +| configprop:management.opentelemetry.logging.export.otlp.timeout[] + +| `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` / `OTEL_EXPORTER_OTLP_PROTOCOL` +| configprop:management.opentelemetry.logging.export.otlp.transport[] + +| `OTEL_EXPORTER_OTLP_LOGS_HEADERS` / `OTEL_EXPORTER_OTLP_HEADERS` +| configprop:management.opentelemetry.logging.export.otlp.headers[] + +| `OTEL_BLRP_SCHEDULE_DELAY` +| configprop:management.opentelemetry.logging.export.schedule-delay[] + +| `OTEL_BLRP_EXPORT_TIMEOUT` +| configprop:management.opentelemetry.logging.export.timeout[] + +| `OTEL_BLRP_MAX_QUEUE_SIZE` +| configprop:management.opentelemetry.logging.export.max-queue-size[] + +| `OTEL_BLRP_MAX_EXPORT_BATCH_SIZE` +| configprop:management.opentelemetry.logging.export.max-batch-size[] + +| `OTEL_LOGS_EXPORTER` +| configprop:management.logging.export.otlp.enabled[] (only `otlp` enables OTLP export; other values disable it but do not enable alternative exporters) + +| `OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT` / `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` +| configprop:management.opentelemetry.logging.limits.max-attribute-value-length[] + +| `OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT` / `OTEL_ATTRIBUTE_COUNT_LIMIT` +| configprop:management.opentelemetry.logging.limits.max-attributes[] + +| `OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CERTIFICATE` +| configprop:management.opentelemetry.logging.export.otlp.ssl.bundle[] (auto-configured SSL bundle) + +| `OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY` / `OTEL_EXPORTER_OTLP_CLIENT_KEY` +| configprop:management.opentelemetry.logging.export.otlp.ssl.bundle[] (auto-configured SSL bundle) + +| `OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` +| configprop:management.opentelemetry.logging.export.otlp.ssl.bundle[] (auto-configured SSL bundle) +|=== + +[[actuator.observability.opentelemetry.environment-variables.resource]] +==== Resource + +The following environment variables configure the OpenTelemetry resource: * https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration[`OTEL_RESOURCE_ATTRIBUTES`] * https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration[`OTEL_SERVICE_NAME`] @@ -148,12 +341,8 @@ NOTE: The `OTEL_RESOURCE_ATTRIBUTES` environment variable consists of a list of For example: `key1=value1,key2=value2,key3=spring%20boot`. All attribute values are treated as strings, and any characters outside the baggage-octet range must be **percent-encoded**. -Micrometer also supports the following environment variables to configure the metrics export over OTLP: - -* https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_endpoint[`OTEL_EXPORTER_OTLP_ENDPOINT`] -* https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_metrics_endpoint[`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`] -* https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_headers[`OTEL_EXPORTER_OTLP_HEADERS`] -* https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_metrics_headers[`OTEL_EXPORTER_OTLP_METRICS_HEADERS`] +[[actuator.observability.opentelemetry.environment-variables.unsupported]] +==== Unsupported Environment Variables Other environment variables as described in https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/[the OpenTelemetry documentation] are not supported. diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/opentelemetry/environmentvariables/AutoConfiguredOpenTelemetrySdkConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/opentelemetry/environmentvariables/unsupported/AutoConfiguredOpenTelemetrySdkConfiguration.java similarity index 96% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/opentelemetry/environmentvariables/AutoConfiguredOpenTelemetrySdkConfiguration.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/opentelemetry/environmentvariables/unsupported/AutoConfiguredOpenTelemetrySdkConfiguration.java index 00264351c0ec..66ac9fe59e34 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/opentelemetry/environmentvariables/AutoConfiguredOpenTelemetrySdkConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/opentelemetry/environmentvariables/unsupported/AutoConfiguredOpenTelemetrySdkConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.actuator.observability.opentelemetry.environmentvariables; +package org.springframework.boot.docs.actuator.observability.opentelemetry.environmentvariables.unsupported; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariableEnvironmentPostProcessor.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariableEnvironmentPostProcessor.java new file mode 100644 index 000000000000..5400a70a45bf --- /dev/null +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariableEnvironmentPostProcessor.java @@ -0,0 +1,436 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.opentelemetry.autoconfigure; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.EnvironmentPostProcessor; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.OriginTrackedMapPropertySource; +import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryEnvironmentVariables.EnvVariable; +import org.springframework.boot.origin.OriginTrackedValue; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Converts OpenTelemetry SDK environment variables into Spring Boot properties. + *

    + * Can be disabled by setting {@code management.opentelemetry.map-environment-variables} + * to {@code false}. + * + * @author Moritz Halbritter + * @since 4.1.0 + */ +public class OpenTelemetryEnvironmentVariableEnvironmentPostProcessor implements EnvironmentPostProcessor { + + private static final String ENABLED_PROPERTY = "management.opentelemetry.map-environment-variables"; + + private final Log logger; + + private final OpenTelemetryEnvironmentVariables environmentVariables; + + public OpenTelemetryEnvironmentVariableEnvironmentPostProcessor(DeferredLogFactory logFactory) { + this(logFactory, OpenTelemetryEnvironmentVariables.forSystemEnv(logFactory)); + } + + OpenTelemetryEnvironmentVariableEnvironmentPostProcessor(DeferredLogFactory logFactory, + OpenTelemetryEnvironmentVariables environmentVariables) { + this.logger = logFactory.getLog(OpenTelemetryEnvironmentVariableEnvironmentPostProcessor.class); + this.environmentVariables = environmentVariables; + } + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + if (!environment.getProperty(ENABLED_PROPERTY, Boolean.class, true)) { + return; + } + Map map = new HashMap<>(); + mapEnabled(map); + mapPropagators(map); + mapSampler(map); + mapMetricsEnvironmentVariables(map); + mapTracesEnvironmentVariables(map); + mapLogsEnvironmentVariables(map); + if (map.isEmpty()) { + return; + } + environment.getPropertySources() + .addFirst(new OriginTrackedMapPropertySource("openTelemetryEnvironmentVariables", map)); + } + + private void mapEnabled(Map map) { + this.environmentVariables.getBoolean("OTEL_SDK_DISABLED") + .addToMap(map, "management.opentelemetry.enabled", this::mapDisabledFlag); + } + + private void mapSampler(Map map) { + this.environmentVariables.getString("OTEL_TRACES_SAMPLER") + .addToMap(map, "management.opentelemetry.tracing.sampler", this::mapSamplerType); + this.environmentVariables.getString("OTEL_TRACES_SAMPLER_ARG") + .addToMap(map, "management.tracing.sampling.probability", this::mapSamplerProbability); + } + + private void mapPropagators(Map map) { + this.environmentVariables.getString("OTEL_PROPAGATORS") + .addListToMap(map, "management.tracing.propagation.type", this::mapPropagationType); + this.environmentVariables.getString("OTEL_PROPAGATORS") + .addToMap(map, "management.tracing.baggage.enabled", this::mapBaggageEnabled); + } + + private void mapMetricsEnvironmentVariables(Map map) { + this.environmentVariables + .getString("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "OTEL_EXPORTER_OTLP_ENDPOINT", + (value) -> combineUrl(value, "v1/metrics")) + .addToMap(map, "management.otlp.metrics.export.url"); + this.environmentVariables.getString("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE") + .addToMap(map, "management.otlp.metrics.export.aggregation-temporality", this::mapAggregationTemporality); + this.environmentVariables.getString("OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION") + .addToMap(map, "management.otlp.metrics.export.histogram-flavor", this::mapHistogramFlavor); + this.environmentVariables.getString("OTEL_EXPORTER_OTLP_METRICS_COMPRESSION", "OTEL_EXPORTER_OTLP_COMPRESSION") + .addToMap(map, "management.otlp.metrics.export.compression-mode", this::mapCompression); + this.environmentVariables.getTimeout("OTEL_EXPORTER_OTLP_METRICS_TIMEOUT", "OTEL_EXPORTER_OTLP_TIMEOUT") + .addToMap(map, "management.otlp.metrics.export.read-timeout"); + this.environmentVariables.getString("OTEL_EXPORTER_OTLP_METRICS_HEADERS", "OTEL_EXPORTER_OTLP_HEADERS") + .addMappingToMap(map, "management.otlp.metrics.export.headers", this::mapHeaders); + this.environmentVariables.getDuration("OTEL_METRIC_EXPORT_INTERVAL") + .addToMap(map, "management.otlp.metrics.export.step"); + this.environmentVariables.getString("OTEL_METRICS_EXPORTER") + .addToMap(map, "management.otlp.metrics.export.enabled", this::mapExportEnabled); + EnvVariable certificate = this.environmentVariables.getString("OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE", + "OTEL_EXPORTER_OTLP_CERTIFICATE"); + EnvVariable clientKey = this.environmentVariables.getString("OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY", + "OTEL_EXPORTER_OTLP_CLIENT_KEY"); + EnvVariable clientCertificate = this.environmentVariables + .getString("OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE", "OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE"); + addSslBundle(map, "management.otlp.metrics.export.ssl.bundle", "opentelemetry-metrics", certificate, clientKey, + clientCertificate); + } + + private void mapTracesEnvironmentVariables(Map map) { + this.environmentVariables + .getString("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "OTEL_EXPORTER_OTLP_ENDPOINT", + (value) -> combineUrl(value, "v1/traces")) + .addToMap(map, "management.opentelemetry.tracing.export.otlp.endpoint"); + this.environmentVariables.getString("OTEL_EXPORTER_OTLP_TRACES_COMPRESSION", "OTEL_EXPORTER_OTLP_COMPRESSION") + .addToMap(map, "management.opentelemetry.tracing.export.otlp.compression", this::mapCompression); + this.environmentVariables.getTimeout("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", "OTEL_EXPORTER_OTLP_TIMEOUT") + .addToMap(map, "management.opentelemetry.tracing.export.otlp.timeout"); + this.environmentVariables.getString("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "OTEL_EXPORTER_OTLP_PROTOCOL") + .addToMap(map, "management.opentelemetry.tracing.export.otlp.transport", this::mapProtocol); + this.environmentVariables.getString("OTEL_EXPORTER_OTLP_TRACES_HEADERS", "OTEL_EXPORTER_OTLP_HEADERS") + .addMappingToMap(map, "management.opentelemetry.tracing.export.otlp.headers", this::mapHeaders); + this.environmentVariables.getDuration("OTEL_BSP_SCHEDULE_DELAY") + .addToMap(map, "management.opentelemetry.tracing.export.schedule-delay"); + this.environmentVariables.getTimeout("OTEL_BSP_EXPORT_TIMEOUT") + .addToMap(map, "management.opentelemetry.tracing.export.timeout"); + this.environmentVariables.getInt("OTEL_BSP_MAX_QUEUE_SIZE") + .addToMap(map, "management.opentelemetry.tracing.export.max-queue-size"); + this.environmentVariables.getInt("OTEL_BSP_MAX_EXPORT_BATCH_SIZE") + .addToMap(map, "management.opentelemetry.tracing.export.max-batch-size"); + this.environmentVariables.getString("OTEL_TRACES_EXPORTER") + .addToMap(map, "management.tracing.export.otlp.enabled", this::mapExportEnabled); + this.environmentVariables.getInt("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT") + .addToMap(map, "management.opentelemetry.tracing.limits.max-attribute-value-length"); + this.environmentVariables.getInt("OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", "OTEL_ATTRIBUTE_COUNT_LIMIT") + .addToMap(map, "management.opentelemetry.tracing.limits.max-attributes"); + this.environmentVariables.getInt("OTEL_SPAN_EVENT_COUNT_LIMIT") + .addToMap(map, "management.opentelemetry.tracing.limits.max-events"); + this.environmentVariables.getInt("OTEL_SPAN_LINK_COUNT_LIMIT") + .addToMap(map, "management.opentelemetry.tracing.limits.max-links"); + this.environmentVariables.getInt("OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT") + .addToMap(map, "management.opentelemetry.tracing.limits.max-attributes-per-event"); + this.environmentVariables.getInt("OTEL_LINK_ATTRIBUTE_COUNT_LIMIT") + .addToMap(map, "management.opentelemetry.tracing.limits.max-attributes-per-link"); + this.environmentVariables.getString("OTEL_METRICS_EXEMPLAR_FILTER") + .addToMap(map, "management.tracing.exemplars.include", this::mapExemplarsInclude); + EnvVariable certificate = this.environmentVariables.getString("OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE", + "OTEL_EXPORTER_OTLP_CERTIFICATE"); + EnvVariable clientKey = this.environmentVariables.getString("OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY", + "OTEL_EXPORTER_OTLP_CLIENT_KEY"); + EnvVariable clientCertificate = this.environmentVariables + .getString("OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE", "OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE"); + addSslBundle(map, "management.opentelemetry.tracing.export.otlp.ssl.bundle", "opentelemetry-tracing", + certificate, clientKey, clientCertificate); + } + + private void mapLogsEnvironmentVariables(Map map) { + this.environmentVariables + .getString("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "OTEL_EXPORTER_OTLP_ENDPOINT", + (value) -> combineUrl(value, "v1/logs")) + .addToMap(map, "management.opentelemetry.logging.export.otlp.endpoint"); + this.environmentVariables.getString("OTEL_EXPORTER_OTLP_LOGS_COMPRESSION", "OTEL_EXPORTER_OTLP_COMPRESSION") + .addToMap(map, "management.opentelemetry.logging.export.otlp.compression", this::mapCompression); + this.environmentVariables.getTimeout("OTEL_EXPORTER_OTLP_LOGS_TIMEOUT", "OTEL_EXPORTER_OTLP_TIMEOUT") + .addToMap(map, "management.opentelemetry.logging.export.otlp.timeout"); + this.environmentVariables.getString("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", "OTEL_EXPORTER_OTLP_PROTOCOL") + .addToMap(map, "management.opentelemetry.logging.export.otlp.transport", this::mapProtocol); + this.environmentVariables.getString("OTEL_EXPORTER_OTLP_LOGS_HEADERS", "OTEL_EXPORTER_OTLP_HEADERS") + .addMappingToMap(map, "management.opentelemetry.logging.export.otlp.headers", this::mapHeaders); + this.environmentVariables.getDuration("OTEL_BLRP_SCHEDULE_DELAY") + .addToMap(map, "management.opentelemetry.logging.export.schedule-delay"); + this.environmentVariables.getTimeout("OTEL_BLRP_EXPORT_TIMEOUT") + .addToMap(map, "management.opentelemetry.logging.export.timeout"); + this.environmentVariables.getInt("OTEL_BLRP_MAX_QUEUE_SIZE") + .addToMap(map, "management.opentelemetry.logging.export.max-queue-size"); + this.environmentVariables.getInt("OTEL_BLRP_MAX_EXPORT_BATCH_SIZE") + .addToMap(map, "management.opentelemetry.logging.export.max-batch-size"); + this.environmentVariables.getString("OTEL_LOGS_EXPORTER") + .addToMap(map, "management.logging.export.otlp.enabled", this::mapExportEnabled); + this.environmentVariables + .getInt("OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT", "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT") + .addToMap(map, "management.opentelemetry.logging.limits.max-attribute-value-length"); + this.environmentVariables.getInt("OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT", "OTEL_ATTRIBUTE_COUNT_LIMIT") + .addToMap(map, "management.opentelemetry.logging.limits.max-attributes"); + EnvVariable certificate = this.environmentVariables.getString("OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE", + "OTEL_EXPORTER_OTLP_CERTIFICATE"); + EnvVariable clientKey = this.environmentVariables.getString("OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY", + "OTEL_EXPORTER_OTLP_CLIENT_KEY"); + EnvVariable clientCertificate = this.environmentVariables + .getString("OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE", "OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE"); + addSslBundle(map, "management.opentelemetry.logging.export.otlp.ssl.bundle", "opentelemetry-logging", + certificate, clientKey, clientCertificate); + } + + private void addSslBundle(Map map, String bundleKey, String bundleName, + EnvVariable certificateFile, EnvVariable clientKeyFile, EnvVariable clientCertificateFile) { + EnvVariable firstPresent = this.environmentVariables.getFirstPresent(certificateFile, clientKeyFile, + clientCertificateFile); + if (firstPresent == null) { + return; + } + map.put(bundleKey, OriginTrackedValue.of(bundleName, this.environmentVariables.getOrigin(firstPresent))); + if (certificateFile.isPresent()) { + String value = certificateFile.value(); + map.put("spring.ssl.bundle.pem.%s.truststore.certificate".formatted(bundleName), OriginTrackedValue + .of("file:%s".formatted(value), this.environmentVariables.getOrigin(certificateFile))); + } + if (clientKeyFile.isPresent()) { + String value = clientKeyFile.value(); + map.put("spring.ssl.bundle.pem.%s.keystore.private-key".formatted(bundleName), OriginTrackedValue + .of("file:%s".formatted(value), this.environmentVariables.getOrigin(clientKeyFile))); + } + if (clientCertificateFile.isPresent()) { + String value = clientCertificateFile.value(); + map.put("spring.ssl.bundle.pem.%s.keystore.certificate".formatted(bundleName), OriginTrackedValue + .of("file:%s".formatted(value), this.environmentVariables.getOrigin(clientCertificateFile))); + } + } + + private @Nullable String mapDisabledFlag(String name, String value) { + // OTEL_SDK_DISABLED: true means disabled + // management.opentelemetry.enabled: true means enabled + return switch (value) { + case "true" -> "false"; + case "false" -> "true"; + default -> { + this.logger.warn("Invalid value for environment variable '%s': '%s'".formatted(name, value)); + yield null; + } + }; + } + + private String mapBaggageEnabled(String name, String value) { + Set propagators = toSet(value); + if (propagators.contains("none")) { + if (propagators.size() > 1) { + this.logger.warn("Environment variable '%s' contains 'none', but also contains more elements: '%s'" + .formatted(name, value)); + } + return "false"; + } + return Boolean.toString(propagators.contains("baggage")); + } + + private List mapPropagationType(String name, String value) { + Set propagators = toSet(value); + if (propagators.contains("none")) { + if (propagators.size() > 1) { + this.logger.warn("Environment variable '%s' contains 'none', but also contains more elements: '%s'" + .formatted(name, value)); + } + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (String propagator : propagators) { + switch (propagator) { + case "tracecontext" -> result.add("W3C"); + case "b3" -> result.add("B3"); + case "b3multi" -> result.add("B3_MULTI"); + case "baggage" -> { + } + default -> this.logger + .warn("Unsupported propagator '%s' in environment variable '%s'".formatted(propagator, name)); + } + } + return result; + } + + private Map mapHeaders(String name, String value) { + return W3CHeaderParser.parse(value); + } + + private String mapExportEnabled(String name, String value) { + Set exporters = toSet(value); + if (exporters.contains("none")) { + if (exporters.size() > 1) { + this.logger.warn("Environment variable '%s' contains 'none', but also contains more elements: '%s'" + .formatted(name, value)); + } + return "false"; + } + if (exporters.contains("otlp")) { + return "true"; + } + return "false"; + } + + private @Nullable String mapProtocol(String name, String value) { + return switch (value.toLowerCase(Locale.ROOT)) { + case "grpc" -> "GRPC"; + case "http/protobuf" -> "HTTP"; + default -> { + this.logger.warn("Invalid value for environment variable '%s': '%s'".formatted(name, value)); + yield null; + } + }; + } + + private @Nullable String mapExemplarsInclude(String name, String value) { + return switch (value.toLowerCase(Locale.ROOT)) { + case "always_on" -> "ALL"; + case "always_off" -> "NONE"; + case "trace_based" -> "SAMPLED_TRACES"; + default -> { + this.logger.warn("Invalid value for environment variable '%s': '%s'".formatted(name, value)); + yield null; + } + }; + } + + private @Nullable String mapSamplerType(String name, String value) { + return switch (value.toLowerCase(Locale.ROOT)) { + case "always_on" -> "ALWAYS_ON"; + case "always_off" -> "ALWAYS_OFF"; + case "traceidratio" -> "TRACE_ID_RATIO"; + case "parentbased_always_on" -> "PARENT_BASED_ALWAYS_ON"; + case "parentbased_always_off" -> "PARENT_BASED_ALWAYS_OFF"; + case "parentbased_traceidratio" -> "PARENT_BASED_TRACE_ID_RATIO"; + default -> { + this.logger.warn("Invalid value for environment variable '%s': '%s'".formatted(name, value)); + yield null; + } + }; + } + + private @Nullable String mapSamplerProbability(String name, String value) { + EnvVariable sampler = this.environmentVariables.getString("OTEL_TRACES_SAMPLER"); + if (!sampler.isPresent()) { + return null; + } + String samplerValue = sampler.value(); + Assert.state(samplerValue != null, "'samplerValue' must not be null"); + return switch (samplerValue.toLowerCase(Locale.ROOT)) { + case "traceidratio", "parentbased_traceidratio" -> { + try { + double probability = Double.parseDouble(value); + if (probability < 0.0 || probability > 1.0) { + this.logger + .warn("Invalid value for environment variable '%s'. Must be between 0.0 and 1.0: '%s'" + .formatted(name, value)); + yield null; + } + yield String.valueOf(probability); + } + catch (NumberFormatException ex) { + this.logger.warn("Invalid value for environment variable '%s': '%s'".formatted(name, value)); + yield null; + } + } + default -> { + this.logger + .warn("Unsupported environment variable '%s' for sampler '%s' ".formatted(name, samplerValue)); + yield null; + } + }; + } + + private @Nullable String mapCompression(String name, String value) { + return switch (value.toLowerCase(Locale.ROOT)) { + case "gzip" -> "GZIP"; + case "none" -> "NONE"; + default -> { + this.logger.warn("Invalid value for environment variable '%s': '%s'".formatted(name, value)); + yield null; + } + }; + } + + private @Nullable String mapHistogramFlavor(String name, String value) { + return switch (value.toLowerCase(Locale.ROOT)) { + case "explicit_bucket_histogram" -> "EXPLICIT_BUCKET_HISTOGRAM"; + case "base2_exponential_bucket_histogram" -> "BASE2_EXPONENTIAL_BUCKET_HISTOGRAM"; + default -> { + this.logger.warn("Invalid value for environment variable '%s': '%s'".formatted(name, value)); + yield null; + } + }; + } + + private @Nullable String mapAggregationTemporality(String name, String value) { + return switch (value.toLowerCase(Locale.ROOT)) { + case "cumulative" -> "CUMULATIVE"; + case "delta" -> "DELTA"; + default -> { + this.logger.warn("Invalid value for environment variable '%s': '%s'".formatted(name, value)); + yield null; + } + }; + } + + private String combineUrl(String base, String path) { + Assert.state(!path.startsWith("/"), "'path' must not start with '/'"); + if (base.endsWith("/")) { + return base + path; + } + return base + "/" + path; + } + + private Set toSet(String value) { + Set result = new LinkedHashSet<>(); + for (String entry : StringUtils.commaDelimitedListToStringArray(value)) { + entry = entry.toLowerCase(Locale.ROOT).trim(); + if (entry.isEmpty()) { + continue; + } + result.add(entry); + } + return result; + } + +} diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariables.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariables.java new file mode 100644 index 000000000000..7f880abddb74 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariables.java @@ -0,0 +1,292 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.opentelemetry.autoconfigure; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginTrackedValue; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Reads and transforms OpenTelemetry environment variables. + * + * @author Moritz Halbritter + */ +class OpenTelemetryEnvironmentVariables { + + private final Log logger; + + private final Function envLookup; + + OpenTelemetryEnvironmentVariables(DeferredLogFactory deferredLogFactory, + Function envLookup) { + this.logger = deferredLogFactory.getLog(OpenTelemetryEnvironmentVariables.class); + this.envLookup = envLookup; + } + + EnvVariable getInt(String name) { + // https://opentelemetry.io/docs/specs/otel/configuration/common/#integer + String value = this.envLookup.apply(name); + if (!StringUtils.hasLength(value)) { + return EnvVariable.none(); + } + try { + return EnvVariable.of(name, Integer.toString(Integer.parseInt(value))); + } + catch (NumberFormatException ex) { + this.logger.warn("Invalid value for integer environment variable '%s': '%s'".formatted(name, value)); + return EnvVariable.none(); + } + } + + EnvVariable getInt(String name, String fallback) { + EnvVariable envVariable = getInt(name); + return (envVariable.isPresent()) ? envVariable : getInt(fallback); + } + + EnvVariable getBoolean(String name) { + // https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#boolean + String value = this.envLookup.apply(name); + if (!StringUtils.hasLength(value)) { + return EnvVariable.none(); + } + if (value.equalsIgnoreCase("true")) { + return EnvVariable.of(name, "true"); + } + if (value.equalsIgnoreCase("false")) { + return EnvVariable.of(name, "false"); + } + this.logger.warn("Invalid value for boolean environment variable '%s': '%s'".formatted(name, value)); + return EnvVariable.none(); + } + + EnvVariable getDuration(String name) { + // https://opentelemetry.io/docs/specs/otel/configuration/common/#duration + String value = this.envLookup.apply(name); + if (!StringUtils.hasLength(value)) { + return EnvVariable.none(); + } + int intValue; + try { + intValue = Integer.parseInt(value); + } + catch (NumberFormatException ex) { + this.logger.warn("Invalid value for duration environment variable '%s': '%s'".formatted(name, value)); + return EnvVariable.none(); + } + if (intValue < 0) { + this.logger + .warn("Duration environment variable '%s' must be non-negative, but was: '%s'".formatted(name, value)); + return EnvVariable.none(); + } + return EnvVariable.of(name, Duration.ofMillis(intValue).toString()); + } + + EnvVariable getTimeout(String name) { + // https://opentelemetry.io/docs/specs/otel/configuration/common/#timeout + String value = this.envLookup.apply(name); + if (!StringUtils.hasLength(value)) { + return EnvVariable.none(); + } + int intValue; + try { + intValue = Integer.parseInt(value); + } + catch (NumberFormatException ex) { + this.logger.warn("Invalid value for duration environment variable '%s': '%s'".formatted(name, value)); + return EnvVariable.none(); + } + if (intValue < 0) { + this.logger + .warn("Duration environment variable '%s' must be non-negative, but was: '%s'".formatted(name, value)); + return EnvVariable.none(); + } + if (intValue == 0) { + return EnvVariable.of(name, Duration.ofSeconds(Long.MAX_VALUE).toString()); + } + return EnvVariable.of(name, Duration.ofMillis(intValue).toString()); + } + + EnvVariable getTimeout(String name, String fallback) { + EnvVariable envVariable = getTimeout(name); + return (envVariable.isPresent()) ? envVariable : getTimeout(fallback); + } + + EnvVariable getString(String name) { + return getString(name, null, null); + } + + EnvVariable getString(String name, String fallback) { + return getString(name, fallback, null); + } + + EnvVariable getString(String name, @Nullable String fallback, + @Nullable Function fallbackTransformer) { + String value = this.envLookup.apply(name); + if (StringUtils.hasLength(value)) { + return EnvVariable.of(name, value); + } + if (fallback == null) { + return EnvVariable.none(); + } + value = this.envLookup.apply(fallback); + if (!StringUtils.hasLength(value)) { + return EnvVariable.none(); + } + if (fallbackTransformer != null) { + value = fallbackTransformer.apply(value); + } + return EnvVariable.of(fallback, value); + } + + @Nullable Origin getOrigin(EnvVariable envVariable) { + return OpenTelemetryEnvironmentVariableOrigin.from(envVariable); + } + + @Nullable EnvVariable getFirstPresent(EnvVariable... variables) { + for (EnvVariable variable : variables) { + if (variable.isPresent()) { + return variable; + } + } + return null; + } + + static OpenTelemetryEnvironmentVariables forSystemEnv(DeferredLogFactory deferredLogFactory) { + return new OpenTelemetryEnvironmentVariables(deferredLogFactory, System::getenv); + } + + static OpenTelemetryEnvironmentVariables forMap(DeferredLogFactory deferredLogFactory, Map map) { + return new OpenTelemetryEnvironmentVariables(deferredLogFactory, map::get); + } + + record EnvVariable(@Nullable String name, @Nullable String value) { + boolean isPresent() { + return this.name != null && this.value != null; + } + + static EnvVariable of(String name, String value) { + return new EnvVariable(name, value); + } + + static EnvVariable none() { + return new EnvVariable(null, null); + } + + void addToMap(Map map, String key) { + addToMap(map, key, (ignore, value) -> value); + } + + void addToMap(Map map, String key, ValueTransformer valueTransformer) { + if (this.name == null || this.value == null) { + return; + } + Assert.state(!map.containsKey(key), "Duplicate key '%s'".formatted(key)); + String value = valueTransformer.transform(this.name, this.value); + if (value == null) { + return; + } + map.put(key, OriginTrackedValue.of(value, OpenTelemetryEnvironmentVariableOrigin.from(this))); + } + + void addMappingToMap(Map map, String key, MapValueTransformer valueTransformer) { + if (this.name == null || this.value == null) { + return; + } + Map value = valueTransformer.transform(this.name, this.value); + if (value == null) { + return; + } + Origin origin = OpenTelemetryEnvironmentVariableOrigin.from(this); + for (Map.Entry entry : value.entrySet()) { + String entryKey = "%s[%s]".formatted(key, entry.getKey()); + Assert.state(!map.containsKey(entryKey), "Duplicate key '%s'".formatted(entryKey)); + map.put(entryKey, OriginTrackedValue.of(entry.getValue(), origin)); + } + } + + void addListToMap(Map map, String key, ListValueTransformer valueTransformer) { + if (this.name == null || this.value == null) { + return; + } + List value = valueTransformer.transform(this.name, this.value); + if (value == null) { + return; + } + Origin origin = OpenTelemetryEnvironmentVariableOrigin.from(this); + if (value.isEmpty()) { + Assert.state(!map.containsKey(key), "Duplicate key '%s'".formatted(key)); + map.put(key, OriginTrackedValue.of("", origin)); + return; + } + for (int i = 0; i < value.size(); i++) { + String entryKey = "%s[%d]".formatted(key, i); + Assert.state(!map.containsKey(entryKey), "Duplicate key '%s'".formatted(entryKey)); + map.put(entryKey, OriginTrackedValue.of(value.get(i), origin)); + } + } + } + + @FunctionalInterface + interface ValueTransformer { + + @Nullable String transform(String name, String value); + + } + + @FunctionalInterface + interface MapValueTransformer { + + @Nullable Map transform(String name, String value); + + } + + @FunctionalInterface + interface ListValueTransformer { + + @Nullable List transform(String name, String value); + + } + + private record OpenTelemetryEnvironmentVariableOrigin(String name) implements Origin { + + @Override + public String toString() { + return "Converted OpenTelemetry environment variable '%s'".formatted(this.name); + } + + static @Nullable OpenTelemetryEnvironmentVariableOrigin from(EnvVariable envVariable) { + if (!envVariable.isPresent()) { + return null; + } + String name = envVariable.name(); + Assert.state(name != null, "'name' must not be null"); + return new OpenTelemetryEnvironmentVariableOrigin(name); + } + + } + +} diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParser.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParser.java new file mode 100644 index 000000000000..faf4963cfa2a --- /dev/null +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParser.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.opentelemetry.autoconfigure; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.util.StringUtils; + +/** + * Parser for the W3C Baggage HTTP header as defined in the + * W3C Baggage specification. + * + * @author Moritz Halbritter + */ +final class W3CHeaderParser { + + private W3CHeaderParser() { + } + + /** + * Parses a W3C Baggage header value into a map of key-value pairs. Properties + * attached to list members are ignored. Values are percent-decoded. + * @param header the baggage header string to parse + * @return the baggage entries + */ + static Map parse(String header) { + if (!StringUtils.hasLength(header)) { + return Collections.emptyMap(); + } + Map result = new LinkedHashMap<>(); + for (String part : header.split(",", -1)) { + part = part.strip(); + if (part.isEmpty()) { + continue; + } + int semicolon = part.indexOf(';'); + String keyValue = (semicolon != -1) ? part.substring(0, semicolon) : part; + int equals = keyValue.indexOf('='); + if (equals == -1) { + continue; + } + String key = keyValue.substring(0, equals).strip(); + String value = keyValue.substring(equals + 1).strip(); + if (key.isEmpty()) { + continue; + } + result.put(key, percentDecode(value)); + } + return result; + } + + private static String percentDecode(String value) { + if (value.indexOf('%') == -1) { + return value; + } + return URLDecoder.decode(value, StandardCharsets.UTF_8); + } + +} diff --git a/module/spring-boot-opentelemetry/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-opentelemetry/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 4566a76af666..6b28e21fe015 100644 --- a/module/spring-boot-opentelemetry/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-opentelemetry/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -7,6 +7,12 @@ "defaultValue": true, "description": "Whether auto-configuration of logging is enabled to export logs over OTLP." }, + { + "name": "management.opentelemetry.map-environment-variables", + "type": "java.lang.Boolean", + "defaultValue": true, + "description": "Whether to map OpenTelemetry SDK environment variables to Spring Boot configuration properties." + }, { "name": "management.otlp.logging.compression", "type": "org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportProperties$Compression", diff --git a/module/spring-boot-opentelemetry/src/main/resources/META-INF/spring.factories b/module/spring-boot-opentelemetry/src/main/resources/META-INF/spring.factories index 21c507180a3d..7dd33709f905 100644 --- a/module/spring-boot-opentelemetry/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-opentelemetry/src/main/resources/META-INF/spring.factories @@ -3,3 +3,5 @@ org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFacto org.springframework.boot.opentelemetry.docker.compose.OtlpLoggingDockerComposeConnectionDetailsFactory,\ org.springframework.boot.opentelemetry.testcontainers.GrafanaOtlpLoggingContainerConnectionDetailsFactory,\ org.springframework.boot.opentelemetry.testcontainers.OtelCollectorOltpLoggingContainerConnectionDetailsFactory +org.springframework.boot.EnvironmentPostProcessor=\ +org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryEnvironmentVariableEnvironmentPostProcessor diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariableEnvironmentPostProcessorTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariableEnvironmentPostProcessorTests.java new file mode 100644 index 000000000000..34f0345390ae --- /dev/null +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariableEnvironmentPostProcessorTests.java @@ -0,0 +1,1089 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.opentelemetry.autoconfigure; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.assertj.core.api.ThrowingConsumer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.logging.DeferredLogs; +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link OpenTelemetryEnvironmentVariableEnvironmentPostProcessor}. + * + * @author Moritz Halbritter + */ +@ExtendWith(OutputCaptureExtension.class) +class OpenTelemetryEnvironmentVariableEnvironmentPostProcessorTests { + + @Test + void shouldMapOtelSdkEnabled(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.enabled")).isNull(); + environment = runProcessor(Map.of("OTEL_SDK_DISABLED", "true")); + assertThat(environment.getProperty("management.opentelemetry.enabled")).isEqualTo("false"); + environment = runProcessor(Map.of("OTEL_SDK_DISABLED", "false")); + assertThat(environment.getProperty("management.opentelemetry.enabled")).isEqualTo("true"); + environment = runProcessor(Map.of("OTEL_SDK_DISABLED", "invalid-value")); + assertThat(environment.getProperty("management.opentelemetry.enabled")).isNull(); + assertThat(output) + .contains("Invalid value for boolean environment variable 'OTEL_SDK_DISABLED': 'invalid-value'"); + } + + @Test + void shouldMapOtelPropagators(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertHasNoList(environment, "management.tracing.propagation.type"); + environment = runProcessor(Map.of("OTEL_PROPAGATORS", "none")); + assertHasEmptyList(environment, "management.tracing.propagation.type"); + assertThat(environment.getProperty("management.tracing.baggage.enabled")).isEqualTo("false"); + environment = runProcessor(Map.of("OTEL_PROPAGATORS", "tracecontext")); + assertHasList(environment, "management.tracing.propagation.type", "W3C"); + environment = runProcessor(Map.of("OTEL_PROPAGATORS", "b3")); + assertHasList(environment, "management.tracing.propagation.type", "B3"); + environment = runProcessor(Map.of("OTEL_PROPAGATORS", "b3multi")); + assertHasList(environment, "management.tracing.propagation.type", "B3_MULTI"); + environment = runProcessor(Map.of("OTEL_PROPAGATORS", "tracecontext,b3,b3multi")); + assertHasList(environment, "management.tracing.propagation.type", "W3C", "B3", "B3_MULTI"); + environment = runProcessor(Map.of("OTEL_PROPAGATORS", " TraceContext , B3 , B3multi ")); + assertHasList(environment, "management.tracing.propagation.type", "W3C", "B3", "B3_MULTI"); + environment = runProcessor(Map.of("OTEL_PROPAGATORS", "none,tracecontext,b3,b3multi")); + assertHasEmptyList(environment, "management.tracing.propagation.type"); + assertThat(environment.getProperty("management.tracing.baggage.enabled")).isEqualTo("false"); + assertThat(output).contains( + "Environment variable 'OTEL_PROPAGATORS' contains 'none', but also contains more elements: 'none,tracecontext,b3,b3multi'"); + environment = runProcessor(Map.of("OTEL_PROPAGATORS", "baggage")); + assertHasEmptyList(environment, "management.tracing.propagation.type"); + assertThat(environment.getProperty("management.tracing.baggage.enabled")).isEqualTo("true"); + environment = runProcessor(Map.of("OTEL_PROPAGATORS", "jaeger")); + assertHasEmptyList(environment, "management.tracing.propagation.type"); + assertThat(output).contains("Unsupported propagator 'jaeger' in environment variable 'OTEL_PROPAGATORS'"); + environment = runProcessor(Map.of("OTEL_PROPAGATORS", "tracecontext,xray,b3")); + assertHasList(environment, "management.tracing.propagation.type", "W3C", "B3"); + assertThat(output).contains("Unsupported propagator 'xray' in environment variable 'OTEL_PROPAGATORS'"); + } + + @Test + void shouldMapOtelExporterOtlpMetricsHeaders() { + Environment environment = runProcessor(Collections.emptyMap()); + assertHasNoMap(environment, "management.otlp.metrics.export.headers"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_HEADERS", "a=b,c=d")); + assertHasMap(environment, "management.otlp.metrics.export.headers", Map.of("a", "b", "c", "d")); + } + + @Test + void shouldMapOtelExporterOtlpMetricsHeadersFallback() { + Environment environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_HEADERS", "x=y")); + assertHasMap(environment, "management.otlp.metrics.export.headers", Map.of("x", "y")); + } + + @Test + void shouldMapOtelTracesSampler(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.sampler")).isNull(); + environment = runProcessor(Map.of("OTEL_TRACES_SAMPLER", "always_on")); + assertThat(environment.getProperty("management.opentelemetry.tracing.sampler")).isEqualTo("ALWAYS_ON"); + environment = runProcessor(Map.of("OTEL_TRACES_SAMPLER", "always_off")); + assertThat(environment.getProperty("management.opentelemetry.tracing.sampler")).isEqualTo("ALWAYS_OFF"); + environment = runProcessor(Map.of("OTEL_TRACES_SAMPLER", "traceidratio")); + assertThat(environment.getProperty("management.opentelemetry.tracing.sampler")).isEqualTo("TRACE_ID_RATIO"); + environment = runProcessor(Map.of("OTEL_TRACES_SAMPLER", "parentbased_always_on")); + assertThat(environment.getProperty("management.opentelemetry.tracing.sampler")) + .isEqualTo("PARENT_BASED_ALWAYS_ON"); + environment = runProcessor(Map.of("OTEL_TRACES_SAMPLER", "parentbased_always_off")); + assertThat(environment.getProperty("management.opentelemetry.tracing.sampler")) + .isEqualTo("PARENT_BASED_ALWAYS_OFF"); + environment = runProcessor(Map.of("OTEL_TRACES_SAMPLER", "parentbased_traceidratio")); + assertThat(environment.getProperty("management.opentelemetry.tracing.sampler")) + .isEqualTo("PARENT_BASED_TRACE_ID_RATIO"); + environment = runProcessor(Map.of("OTEL_TRACES_SAMPLER", "invalid")); + assertThat(environment.getProperty("management.opentelemetry.tracing.sampler")).isNull(); + assertThat(output).contains("Invalid value for environment variable 'OTEL_TRACES_SAMPLER': 'invalid'"); + } + + @Test + void shouldMapOtelTracesSamplerArg(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.tracing.sampling.probability")).isNull(); + environment = runProcessor(Map.of("OTEL_TRACES_SAMPLER", "traceidratio", "OTEL_TRACES_SAMPLER_ARG", "0.5")); + assertThat(environment.getProperty("management.tracing.sampling.probability")).isEqualTo("0.5"); + environment = runProcessor( + Map.of("OTEL_TRACES_SAMPLER", "parentbased_traceidratio", "OTEL_TRACES_SAMPLER_ARG", "0.25")); + assertThat(environment.getProperty("management.tracing.sampling.probability")).isEqualTo("0.25"); + environment = runProcessor(Map.of("OTEL_TRACES_SAMPLER_ARG", "0.5")); + assertThat(environment.getProperty("management.tracing.sampling.probability")).isNull(); + environment = runProcessor(Map.of("OTEL_TRACES_SAMPLER", "always_on", "OTEL_TRACES_SAMPLER_ARG", "0.5")); + assertThat(environment.getProperty("management.tracing.sampling.probability")).isNull(); + assertThat(output) + .contains("Unsupported environment variable 'OTEL_TRACES_SAMPLER_ARG' for sampler 'always_on'"); + environment = runProcessor(Map.of("OTEL_TRACES_SAMPLER", "traceidratio", "OTEL_TRACES_SAMPLER_ARG", "2.0")); + assertThat(environment.getProperty("management.tracing.sampling.probability")).isNull(); + assertThat(output).contains( + "Invalid value for environment variable 'OTEL_TRACES_SAMPLER_ARG'. Must be between 0.0 and 1.0: '2.0'"); + environment = runProcessor( + Map.of("OTEL_TRACES_SAMPLER", "traceidratio", "OTEL_TRACES_SAMPLER_ARG", "not-a-number")); + assertThat(environment.getProperty("management.tracing.sampling.probability")).isNull(); + assertThat(output).contains("Invalid value for environment variable 'OTEL_TRACES_SAMPLER_ARG': 'not-a-number'"); + } + + @Test + void shouldMapOtelExporterOtlpMetricsEndpoint() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.otlp.metrics.export.url")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://metrics:4318/v1/metrics")); + assertThat(environment.getProperty("management.otlp.metrics.export.url")) + .isEqualTo("http://metrics:4318/v1/metrics"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318")); + assertThat(environment.getProperty("management.otlp.metrics.export.url")) + .isEqualTo("http://collector:4318/v1/metrics"); + } + + @Test + void shouldMapOtelExporterOtlpMetricsEndpointSpecificOverridesFallback() { + Environment environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "http://metrics:4318/v1/metrics", "OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318")); + assertThat(environment.getProperty("management.otlp.metrics.export.url")) + .isEqualTo("http://metrics:4318/v1/metrics"); + } + + @Test + void shouldMapOtelExporterOtlpMetricsTemporalityPreference(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.otlp.metrics.export.aggregation-temporality")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", "cumulative")); + assertThat(environment.getProperty("management.otlp.metrics.export.aggregation-temporality")) + .isEqualTo("CUMULATIVE"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", "delta")); + assertThat(environment.getProperty("management.otlp.metrics.export.aggregation-temporality")) + .isEqualTo("DELTA"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", "invalid")); + assertThat(environment.getProperty("management.otlp.metrics.export.aggregation-temporality")).isNull(); + assertThat(output).contains( + "Invalid value for environment variable 'OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE': 'invalid'"); + } + + @Test + void shouldMapOtelExporterOtlpMetricsDefaultHistogramAggregation(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.otlp.metrics.export.histogram-flavor")).isNull(); + environment = runProcessor( + Map.of("OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION", "explicit_bucket_histogram")); + assertThat(environment.getProperty("management.otlp.metrics.export.histogram-flavor")) + .isEqualTo("EXPLICIT_BUCKET_HISTOGRAM"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION", + "base2_exponential_bucket_histogram")); + assertThat(environment.getProperty("management.otlp.metrics.export.histogram-flavor")) + .isEqualTo("BASE2_EXPONENTIAL_BUCKET_HISTOGRAM"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION", "invalid")); + assertThat(environment.getProperty("management.otlp.metrics.export.histogram-flavor")).isNull(); + assertThat(output).contains( + "Invalid value for environment variable 'OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION': 'invalid'"); + } + + @Test + void shouldMapOtelExporterOtlpMetricsCompression(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.otlp.metrics.export.compression-mode")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_COMPRESSION", "gzip")); + assertThat(environment.getProperty("management.otlp.metrics.export.compression-mode")).isEqualTo("GZIP"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_COMPRESSION", "none")); + assertThat(environment.getProperty("management.otlp.metrics.export.compression-mode")).isEqualTo("NONE"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_COMPRESSION", "gzip")); + assertThat(environment.getProperty("management.otlp.metrics.export.compression-mode")).isEqualTo("GZIP"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_COMPRESSION", "invalid")); + assertThat(environment.getProperty("management.otlp.metrics.export.compression-mode")).isNull(); + assertThat(output) + .contains("Invalid value for environment variable 'OTEL_EXPORTER_OTLP_METRICS_COMPRESSION': 'invalid'"); + } + + @Test + void shouldMapOtelExporterOtlpMetricsTimeout() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.otlp.metrics.export.read-timeout")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_TIMEOUT", "5000")); + assertThat(environment.getProperty("management.otlp.metrics.export.read-timeout")).isEqualTo("PT5S"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TIMEOUT", "3000")); + assertThat(environment.getProperty("management.otlp.metrics.export.read-timeout")).isEqualTo("PT3S"); + } + + @Test + void shouldMapOtelMetricExportInterval() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.otlp.metrics.export.step")).isNull(); + environment = runProcessor(Map.of("OTEL_METRIC_EXPORT_INTERVAL", "60000")); + assertThat(environment.getProperty("management.otlp.metrics.export.step")).isEqualTo("PT1M"); + } + + @Test + void shouldMapOtelMetricsExporter(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.otlp.metrics.export.enabled")).isNull(); + environment = runProcessor(Map.of("OTEL_METRICS_EXPORTER", "otlp")); + assertThat(environment.getProperty("management.otlp.metrics.export.enabled")).isEqualTo("true"); + environment = runProcessor(Map.of("OTEL_METRICS_EXPORTER", "none")); + assertThat(environment.getProperty("management.otlp.metrics.export.enabled")).isEqualTo("false"); + environment = runProcessor(Map.of("OTEL_METRICS_EXPORTER", "prometheus")); + assertThat(environment.getProperty("management.otlp.metrics.export.enabled")).isEqualTo("false"); + environment = runProcessor(Map.of("OTEL_METRICS_EXPORTER", "none,otlp")); + assertThat(environment.getProperty("management.otlp.metrics.export.enabled")).isEqualTo("false"); + assertThat(output).contains( + "Environment variable 'OTEL_METRICS_EXPORTER' contains 'none', but also contains more elements: 'none,otlp'"); + environment = runProcessor(Map.of("OTEL_METRICS_EXPORTER", " OTLP , prometheus ")); + assertThat(environment.getProperty("management.otlp.metrics.export.enabled")).isEqualTo("true"); + } + + @Test + void shouldMapOtelExporterOtlpMetricsSslBundle() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.otlp.metrics.export.ssl.bundle")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE", "/path/to/cert.pem")); + assertThat(environment.getProperty("management.otlp.metrics.export.ssl.bundle")) + .isEqualTo("opentelemetry-metrics"); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-metrics.truststore.certificate")) + .isEqualTo("file:/path/to/cert.pem"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY", "/path/to/key.pem", + "OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE", "/path/to/client-cert.pem")); + assertThat(environment.getProperty("management.otlp.metrics.export.ssl.bundle")) + .isEqualTo("opentelemetry-metrics"); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-metrics.keystore.private-key")) + .isEqualTo("file:/path/to/key.pem"); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-metrics.keystore.certificate")) + .isEqualTo("file:/path/to/client-cert.pem"); + } + + @Test + void shouldMapOtelExporterOtlpMetricsSslBundleFallback() { + Environment environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_CERTIFICATE", "/path/to/cert.pem")); + assertThat(environment.getProperty("management.otlp.metrics.export.ssl.bundle")) + .isEqualTo("opentelemetry-metrics"); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-metrics.truststore.certificate")) + .isEqualTo("file:/path/to/cert.pem"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_CLIENT_KEY", "/path/to/key.pem")); + assertThat(environment.getProperty("management.otlp.metrics.export.ssl.bundle")) + .isEqualTo("opentelemetry-metrics"); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-metrics.keystore.private-key")) + .isEqualTo("file:/path/to/key.pem"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", "/path/to/client-cert.pem")); + assertThat(environment.getProperty("management.otlp.metrics.export.ssl.bundle")) + .isEqualTo("opentelemetry-metrics"); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-metrics.keystore.certificate")) + .isEqualTo("file:/path/to/client-cert.pem"); + } + + @Test + void shouldMapOtelExporterOtlpTracesEndpoint() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.endpoint")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://traces:4318/v1/traces")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.endpoint")) + .isEqualTo("http://traces:4318/v1/traces"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.endpoint")) + .isEqualTo("http://collector:4318/v1/traces"); + } + + @Test + void shouldMapOtelExporterOtlpTracesCompression(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.compression")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TRACES_COMPRESSION", "gzip")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.compression")) + .isEqualTo("GZIP"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TRACES_COMPRESSION", "none")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.compression")) + .isEqualTo("NONE"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_COMPRESSION", "gzip")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.compression")) + .isEqualTo("GZIP"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TRACES_COMPRESSION", "invalid")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.compression")).isNull(); + assertThat(output) + .contains("Invalid value for environment variable 'OTEL_EXPORTER_OTLP_TRACES_COMPRESSION': 'invalid'"); + } + + @Test + void shouldMapOtelExporterOtlpTracesTimeout() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.timeout")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", "5000")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.timeout")).isEqualTo("PT5S"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TIMEOUT", "3000")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.timeout")).isEqualTo("PT3S"); + } + + @Test + void shouldMapOtelExporterOtlpTracesProtocol(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.transport")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.transport")).isEqualTo("GRPC"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "http/protobuf")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.transport")).isEqualTo("HTTP"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.transport")).isEqualTo("GRPC"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "invalid")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.transport")).isNull(); + assertThat(output) + .contains("Invalid value for environment variable 'OTEL_EXPORTER_OTLP_TRACES_PROTOCOL': 'invalid'"); + } + + @Test + void shouldMapOtelExporterOtlpTracesHeaders() { + Environment environment = runProcessor(Collections.emptyMap()); + assertHasNoMap(environment, "management.opentelemetry.tracing.export.otlp.headers"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TRACES_HEADERS", "a=b,c=d")); + assertHasMap(environment, "management.opentelemetry.tracing.export.otlp.headers", Map.of("a", "b", "c", "d")); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_HEADERS", "x=y")); + assertHasMap(environment, "management.opentelemetry.tracing.export.otlp.headers", Map.of("x", "y")); + } + + @Test + void shouldMapOtelBspScheduleDelay() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.schedule-delay")).isNull(); + environment = runProcessor(Map.of("OTEL_BSP_SCHEDULE_DELAY", "5000")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.schedule-delay")).isEqualTo("PT5S"); + } + + @Test + void shouldMapOtelBspExportTimeout() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.timeout")).isNull(); + environment = runProcessor(Map.of("OTEL_BSP_EXPORT_TIMEOUT", "30000")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.timeout")).isEqualTo("PT30S"); + } + + @Test + void shouldMapOtelBspMaxQueueSize() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.max-queue-size")).isNull(); + environment = runProcessor(Map.of("OTEL_BSP_MAX_QUEUE_SIZE", "2048")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.max-queue-size")).isEqualTo("2048"); + } + + @Test + void shouldMapOtelBspMaxExportBatchSize() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.max-batch-size")).isNull(); + environment = runProcessor(Map.of("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", "512")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.max-batch-size")).isEqualTo("512"); + } + + @Test + void shouldMapOtelTracesExporter(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.tracing.export.otlp.enabled")).isNull(); + environment = runProcessor(Map.of("OTEL_TRACES_EXPORTER", "otlp")); + assertThat(environment.getProperty("management.tracing.export.otlp.enabled")).isEqualTo("true"); + environment = runProcessor(Map.of("OTEL_TRACES_EXPORTER", "none")); + assertThat(environment.getProperty("management.tracing.export.otlp.enabled")).isEqualTo("false"); + environment = runProcessor(Map.of("OTEL_TRACES_EXPORTER", "zipkin")); + assertThat(environment.getProperty("management.tracing.export.otlp.enabled")).isEqualTo("false"); + environment = runProcessor(Map.of("OTEL_TRACES_EXPORTER", "none,otlp")); + assertThat(environment.getProperty("management.tracing.export.otlp.enabled")).isEqualTo("false"); + assertThat(output).contains( + "Environment variable 'OTEL_TRACES_EXPORTER' contains 'none', but also contains more elements: 'none,otlp'"); + environment = runProcessor(Map.of("OTEL_TRACES_EXPORTER", " OTLP , zipkin ")); + assertThat(environment.getProperty("management.tracing.export.otlp.enabled")).isEqualTo("true"); + } + + @Test + void shouldMapOtelSpanAttributeValueLengthLimit() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-attribute-value-length")) + .isNull(); + environment = runProcessor(Map.of("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", "4096")); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-attribute-value-length")) + .isEqualTo("4096"); + environment = runProcessor(Map.of("OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", "2048")); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-attribute-value-length")) + .isEqualTo("2048"); + } + + @Test + void shouldMapOtelSpanAttributeCountLimit() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-attributes")).isNull(); + environment = runProcessor(Map.of("OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", "128")); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-attributes")).isEqualTo("128"); + environment = runProcessor(Map.of("OTEL_ATTRIBUTE_COUNT_LIMIT", "64")); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-attributes")).isEqualTo("64"); + } + + @Test + void shouldMapOtelSpanEventCountLimit() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-events")).isNull(); + environment = runProcessor(Map.of("OTEL_SPAN_EVENT_COUNT_LIMIT", "128")); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-events")).isEqualTo("128"); + } + + @Test + void shouldMapOtelSpanLinkCountLimit() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-links")).isNull(); + environment = runProcessor(Map.of("OTEL_SPAN_LINK_COUNT_LIMIT", "128")); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-links")).isEqualTo("128"); + } + + @Test + void shouldMapOtelEventAttributeCountLimit() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-attributes-per-event")) + .isNull(); + environment = runProcessor(Map.of("OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT", "128")); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-attributes-per-event")) + .isEqualTo("128"); + } + + @Test + void shouldMapOtelLinkAttributeCountLimit() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-attributes-per-link")).isNull(); + environment = runProcessor(Map.of("OTEL_LINK_ATTRIBUTE_COUNT_LIMIT", "128")); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-attributes-per-link")) + .isEqualTo("128"); + } + + @Test + void shouldMapOtelMetricsExemplarFilter(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.tracing.exemplars.include")).isNull(); + environment = runProcessor(Map.of("OTEL_METRICS_EXEMPLAR_FILTER", "always_on")); + assertThat(environment.getProperty("management.tracing.exemplars.include")).isEqualTo("ALL"); + environment = runProcessor(Map.of("OTEL_METRICS_EXEMPLAR_FILTER", "always_off")); + assertThat(environment.getProperty("management.tracing.exemplars.include")).isEqualTo("NONE"); + environment = runProcessor(Map.of("OTEL_METRICS_EXEMPLAR_FILTER", "trace_based")); + assertThat(environment.getProperty("management.tracing.exemplars.include")).isEqualTo("SAMPLED_TRACES"); + environment = runProcessor(Map.of("OTEL_METRICS_EXEMPLAR_FILTER", "invalid")); + assertThat(environment.getProperty("management.tracing.exemplars.include")).isNull(); + assertThat(output).contains("Invalid value for environment variable 'OTEL_METRICS_EXEMPLAR_FILTER': 'invalid'"); + } + + @Test + void shouldMapOtelExporterOtlpTracesSslBundle() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.ssl.bundle")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE", "/path/to/cert.pem")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.ssl.bundle")) + .isEqualTo("opentelemetry-tracing"); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-tracing.truststore.certificate")) + .isEqualTo("file:/path/to/cert.pem"); + } + + @Test + void shouldMapOtelExporterOtlpTracesSslBundleFallback() { + Environment environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_CERTIFICATE", "/path/to/cert.pem")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.ssl.bundle")) + .isEqualTo("opentelemetry-tracing"); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-tracing.truststore.certificate")) + .isEqualTo("file:/path/to/cert.pem"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_CLIENT_KEY", "/path/to/key.pem")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.ssl.bundle")) + .isEqualTo("opentelemetry-tracing"); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-tracing.keystore.private-key")) + .isEqualTo("file:/path/to/key.pem"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", "/path/to/client-cert.pem")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.ssl.bundle")) + .isEqualTo("opentelemetry-tracing"); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-tracing.keystore.certificate")) + .isEqualTo("file:/path/to/client-cert.pem"); + } + + @Test + void shouldMapOtelExporterOtlpLogsEndpoint() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.endpoint")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "http://logs:4318/v1/logs")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.endpoint")) + .isEqualTo("http://logs:4318/v1/logs"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.endpoint")) + .isEqualTo("http://collector:4318/v1/logs"); + } + + @Test + void shouldMapOtelExporterOtlpLogsCompression(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.compression")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_LOGS_COMPRESSION", "gzip")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.compression")) + .isEqualTo("GZIP"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_LOGS_COMPRESSION", "none")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.compression")) + .isEqualTo("NONE"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_COMPRESSION", "gzip")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.compression")) + .isEqualTo("GZIP"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_LOGS_COMPRESSION", "invalid")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.compression")).isNull(); + assertThat(output) + .contains("Invalid value for environment variable 'OTEL_EXPORTER_OTLP_LOGS_COMPRESSION': 'invalid'"); + } + + @Test + void shouldMapOtelExporterOtlpLogsTimeout() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.timeout")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_LOGS_TIMEOUT", "5000")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.timeout")).isEqualTo("PT5S"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TIMEOUT", "3000")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.timeout")).isEqualTo("PT3S"); + } + + @Test + void shouldMapOtelExporterOtlpLogsProtocol(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.transport")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", "grpc")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.transport")).isEqualTo("GRPC"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", "http/protobuf")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.transport")).isEqualTo("HTTP"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.transport")).isEqualTo("GRPC"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", "invalid")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.transport")).isNull(); + assertThat(output) + .contains("Invalid value for environment variable 'OTEL_EXPORTER_OTLP_LOGS_PROTOCOL': 'invalid'"); + } + + @Test + void shouldMapOtelExporterOtlpLogsHeaders() { + Environment environment = runProcessor(Collections.emptyMap()); + assertHasNoMap(environment, "management.opentelemetry.logging.export.otlp.headers"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_LOGS_HEADERS", "a=b,c=d")); + assertHasMap(environment, "management.opentelemetry.logging.export.otlp.headers", Map.of("a", "b", "c", "d")); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_HEADERS", "x=y")); + assertHasMap(environment, "management.opentelemetry.logging.export.otlp.headers", Map.of("x", "y")); + } + + @Test + void shouldMapOtelBlrpScheduleDelay() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.logging.export.schedule-delay")).isNull(); + environment = runProcessor(Map.of("OTEL_BLRP_SCHEDULE_DELAY", "5000")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.schedule-delay")).isEqualTo("PT5S"); + } + + @Test + void shouldMapOtelBlrpExportTimeout() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.logging.export.timeout")).isNull(); + environment = runProcessor(Map.of("OTEL_BLRP_EXPORT_TIMEOUT", "30000")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.timeout")).isEqualTo("PT30S"); + } + + @Test + void shouldMapOtelBlrpMaxQueueSize() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.logging.export.max-queue-size")).isNull(); + environment = runProcessor(Map.of("OTEL_BLRP_MAX_QUEUE_SIZE", "2048")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.max-queue-size")).isEqualTo("2048"); + } + + @Test + void shouldMapOtelBlrpMaxExportBatchSize() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.logging.export.max-batch-size")).isNull(); + environment = runProcessor(Map.of("OTEL_BLRP_MAX_EXPORT_BATCH_SIZE", "512")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.max-batch-size")).isEqualTo("512"); + } + + @Test + void shouldMapOtelLogsExporter(CapturedOutput output) { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.logging.export.otlp.enabled")).isNull(); + environment = runProcessor(Map.of("OTEL_LOGS_EXPORTER", "otlp")); + assertThat(environment.getProperty("management.logging.export.otlp.enabled")).isEqualTo("true"); + environment = runProcessor(Map.of("OTEL_LOGS_EXPORTER", "none")); + assertThat(environment.getProperty("management.logging.export.otlp.enabled")).isEqualTo("false"); + environment = runProcessor(Map.of("OTEL_LOGS_EXPORTER", "console")); + assertThat(environment.getProperty("management.logging.export.otlp.enabled")).isEqualTo("false"); + environment = runProcessor(Map.of("OTEL_LOGS_EXPORTER", "none,otlp")); + assertThat(environment.getProperty("management.logging.export.otlp.enabled")).isEqualTo("false"); + assertThat(output).contains( + "Environment variable 'OTEL_LOGS_EXPORTER' contains 'none', but also contains more elements: 'none,otlp'"); + environment = runProcessor(Map.of("OTEL_LOGS_EXPORTER", " OTLP , console ")); + assertThat(environment.getProperty("management.logging.export.otlp.enabled")).isEqualTo("true"); + } + + @Test + void shouldMapOtelLogrecordAttributeValueLengthLimit() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.logging.limits.max-attribute-value-length")) + .isNull(); + environment = runProcessor(Map.of("OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT", "4096")); + assertThat(environment.getProperty("management.opentelemetry.logging.limits.max-attribute-value-length")) + .isEqualTo("4096"); + environment = runProcessor(Map.of("OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", "2048")); + assertThat(environment.getProperty("management.opentelemetry.logging.limits.max-attribute-value-length")) + .isEqualTo("2048"); + } + + @Test + void shouldMapOtelLogrecordAttributeCountLimit() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.logging.limits.max-attributes")).isNull(); + environment = runProcessor(Map.of("OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT", "128")); + assertThat(environment.getProperty("management.opentelemetry.logging.limits.max-attributes")).isEqualTo("128"); + environment = runProcessor(Map.of("OTEL_ATTRIBUTE_COUNT_LIMIT", "64")); + assertThat(environment.getProperty("management.opentelemetry.logging.limits.max-attributes")).isEqualTo("64"); + } + + @Test + void shouldMapOtelExporterOtlpLogsSslBundle() { + Environment environment = runProcessor(Collections.emptyMap()); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.ssl.bundle")).isNull(); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE", "/path/to/cert.pem")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.ssl.bundle")) + .isEqualTo("opentelemetry-logging"); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-logging.truststore.certificate")) + .isEqualTo("file:/path/to/cert.pem"); + } + + @Test + void shouldMapOtelExporterOtlpLogsSslBundleFallback() { + Environment environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_CERTIFICATE", "/path/to/cert.pem")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.ssl.bundle")) + .isEqualTo("opentelemetry-logging"); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-logging.truststore.certificate")) + .isEqualTo("file:/path/to/cert.pem"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_CLIENT_KEY", "/path/to/key.pem")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.ssl.bundle")) + .isEqualTo("opentelemetry-logging"); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-logging.keystore.private-key")) + .isEqualTo("file:/path/to/key.pem"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", "/path/to/client-cert.pem")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.ssl.bundle")) + .isEqualTo("opentelemetry-logging"); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-logging.keystore.certificate")) + .isEqualTo("file:/path/to/client-cert.pem"); + } + + @Test + void enumValuesShouldBeCaseInsensitive() { + Environment environment = runProcessor(Map.of("OTEL_TRACES_SAMPLER", "ALWAYS_ON")); + assertThat(environment.getProperty("management.opentelemetry.tracing.sampler")).isEqualTo("ALWAYS_ON"); + environment = runProcessor(Map.of("OTEL_TRACES_SAMPLER", "Always_On")); + assertThat(environment.getProperty("management.opentelemetry.tracing.sampler")).isEqualTo("ALWAYS_ON"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TRACES_COMPRESSION", "GZIP")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.compression")) + .isEqualTo("GZIP"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "GRPC")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.transport")).isEqualTo("GRPC"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "HTTP/PROTOBUF")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.transport")).isEqualTo("HTTP"); + environment = runProcessor(Map.of("OTEL_METRICS_EXEMPLAR_FILTER", "TRACE_BASED")); + assertThat(environment.getProperty("management.tracing.exemplars.include")).isEqualTo("SAMPLED_TRACES"); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", "CUMULATIVE")); + assertThat(environment.getProperty("management.otlp.metrics.export.aggregation-temporality")) + .isEqualTo("CUMULATIVE"); + environment = runProcessor( + Map.of("OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION", "EXPLICIT_BUCKET_HISTOGRAM")); + assertThat(environment.getProperty("management.otlp.metrics.export.histogram-flavor")) + .isEqualTo("EXPLICIT_BUCKET_HISTOGRAM"); + } + + @Test + void fallbackEndpointWithTrailingSlashShouldAppendPathCorrectly() { + Environment environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318/")); + assertThat(environment.getProperty("management.otlp.metrics.export.url")) + .isEqualTo("http://collector:4318/v1/metrics"); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.endpoint")) + .isEqualTo("http://collector:4318/v1/traces"); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.endpoint")) + .isEqualTo("http://collector:4318/v1/logs"); + } + + @Test + void timeoutZeroShouldBeInterpretedAsInfinite() { + Environment environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", "0")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.timeout")) + .isEqualTo(Duration.ofSeconds(Long.MAX_VALUE).toString()); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_LOGS_TIMEOUT", "0")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.timeout")) + .isEqualTo(Duration.ofSeconds(Long.MAX_VALUE).toString()); + environment = runProcessor(Map.of("OTEL_EXPORTER_OTLP_METRICS_TIMEOUT", "0")); + assertThat(environment.getProperty("management.otlp.metrics.export.read-timeout")) + .isEqualTo(Duration.ofSeconds(Long.MAX_VALUE).toString()); + environment = runProcessor(Map.of("OTEL_BSP_EXPORT_TIMEOUT", "0")); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.timeout")) + .isEqualTo(Duration.ofSeconds(Long.MAX_VALUE).toString()); + environment = runProcessor(Map.of("OTEL_BLRP_EXPORT_TIMEOUT", "0")); + assertThat(environment.getProperty("management.opentelemetry.logging.export.timeout")) + .isEqualTo(Duration.ofSeconds(Long.MAX_VALUE).toString()); + } + + @Test + void shouldNotAddPropertySourceWhenNoEnvironmentVariablesAreSet() { + ConfigurableEnvironment environment = new StandardEnvironment(); + int sourceCountBefore = environment.getPropertySources().size(); + DeferredLogs logFactory = new DeferredLogs(); + OpenTelemetryEnvironmentVariableEnvironmentPostProcessor processor = new OpenTelemetryEnvironmentVariableEnvironmentPostProcessor( + logFactory, new OpenTelemetryEnvironmentVariables(logFactory, (name) -> null)); + processor.postProcessEnvironment(environment, new SpringApplication()); + assertThat(environment.getPropertySources()).hasSize(sourceCountBefore); + } + + @Test + void shouldNotMapWhenDisabledViaProperty() { + DeferredLogs logFactory = new DeferredLogs(); + Map envVars = Map.of("OTEL_SDK_DISABLED", "true"); + OpenTelemetryEnvironmentVariableEnvironmentPostProcessor processor = new OpenTelemetryEnvironmentVariableEnvironmentPostProcessor( + logFactory, new OpenTelemetryEnvironmentVariables(logFactory, envVars::get)); + ConfigurableEnvironment environment = new StandardEnvironment(); + environment.getPropertySources() + .addFirst(new org.springframework.core.env.MapPropertySource("test", + Map.of("management.opentelemetry.map-environment-variables", "false"))); + int sourceCountBefore = environment.getPropertySources().size(); + processor.postProcessEnvironment(environment, new SpringApplication()); + assertThat(environment.getPropertySources()).hasSize(sourceCountBefore); + assertThat(environment.getProperty("management.opentelemetry.enabled")).isNull(); + } + + @Test + void specificMetricsCompressionShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_METRICS_COMPRESSION", "gzip"); + env.put("OTEL_EXPORTER_OTLP_COMPRESSION", "none"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("management.otlp.metrics.export.compression-mode")).isEqualTo("GZIP"); + } + + @Test + void specificMetricsTimeoutShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_METRICS_TIMEOUT", "5000"); + env.put("OTEL_EXPORTER_OTLP_TIMEOUT", "3000"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("management.otlp.metrics.export.read-timeout")).isEqualTo("PT5S"); + } + + @Test + void specificMetricsHeadersShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_METRICS_HEADERS", "a=b"); + env.put("OTEL_EXPORTER_OTLP_HEADERS", "x=y"); + Environment environment = runProcessor(env); + assertHasMap(environment, "management.otlp.metrics.export.headers", Map.of("a", "b")); + } + + @Test + void specificMetricsCertificateShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE", "/specific/cert.pem"); + env.put("OTEL_EXPORTER_OTLP_CERTIFICATE", "/generic/cert.pem"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-metrics.truststore.certificate")) + .isEqualTo("file:/specific/cert.pem"); + } + + @Test + void specificMetricsClientKeyShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY", "/specific/key.pem"); + env.put("OTEL_EXPORTER_OTLP_CLIENT_KEY", "/generic/key.pem"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-metrics.keystore.private-key")) + .isEqualTo("file:/specific/key.pem"); + } + + @Test + void specificMetricsClientCertificateShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE", "/specific/client.pem"); + env.put("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", "/generic/client.pem"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-metrics.keystore.certificate")) + .isEqualTo("file:/specific/client.pem"); + } + + @Test + void specificTracesEndpointShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://traces:4318/v1/traces"); + env.put("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.endpoint")) + .isEqualTo("http://traces:4318/v1/traces"); + } + + @Test + void specificTracesCompressionShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_TRACES_COMPRESSION", "gzip"); + env.put("OTEL_EXPORTER_OTLP_COMPRESSION", "none"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.compression")) + .isEqualTo("GZIP"); + } + + @Test + void specificTracesTimeoutShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", "5000"); + env.put("OTEL_EXPORTER_OTLP_TIMEOUT", "3000"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.timeout")).isEqualTo("PT5S"); + } + + @Test + void specificTracesProtocolShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "http/protobuf"); + env.put("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("management.opentelemetry.tracing.export.otlp.transport")).isEqualTo("HTTP"); + } + + @Test + void specificTracesHeadersShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_TRACES_HEADERS", "a=b"); + env.put("OTEL_EXPORTER_OTLP_HEADERS", "x=y"); + Environment environment = runProcessor(env); + assertHasMap(environment, "management.opentelemetry.tracing.export.otlp.headers", Map.of("a", "b")); + } + + @Test + void specificTracesAttributeValueLengthLimitShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", "4096"); + env.put("OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", "2048"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-attribute-value-length")) + .isEqualTo("4096"); + } + + @Test + void specificTracesAttributeCountLimitShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", "128"); + env.put("OTEL_ATTRIBUTE_COUNT_LIMIT", "64"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("management.opentelemetry.tracing.limits.max-attributes")).isEqualTo("128"); + } + + @Test + void specificTracesCertificateShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE", "/specific/cert.pem"); + env.put("OTEL_EXPORTER_OTLP_CERTIFICATE", "/generic/cert.pem"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-tracing.truststore.certificate")) + .isEqualTo("file:/specific/cert.pem"); + } + + @Test + void specificTracesClientKeyShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY", "/specific/key.pem"); + env.put("OTEL_EXPORTER_OTLP_CLIENT_KEY", "/generic/key.pem"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-tracing.keystore.private-key")) + .isEqualTo("file:/specific/key.pem"); + } + + @Test + void specificTracesClientCertificateShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE", "/specific/client.pem"); + env.put("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", "/generic/client.pem"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-tracing.keystore.certificate")) + .isEqualTo("file:/specific/client.pem"); + } + + @Test + void specificLogsEndpointShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "http://logs:4318/v1/logs"); + env.put("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.endpoint")) + .isEqualTo("http://logs:4318/v1/logs"); + } + + @Test + void specificLogsCompressionShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_LOGS_COMPRESSION", "gzip"); + env.put("OTEL_EXPORTER_OTLP_COMPRESSION", "none"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.compression")) + .isEqualTo("GZIP"); + } + + @Test + void specificLogsTimeoutShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_LOGS_TIMEOUT", "5000"); + env.put("OTEL_EXPORTER_OTLP_TIMEOUT", "3000"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.timeout")).isEqualTo("PT5S"); + } + + @Test + void specificLogsProtocolShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", "http/protobuf"); + env.put("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("management.opentelemetry.logging.export.otlp.transport")).isEqualTo("HTTP"); + } + + @Test + void specificLogsHeadersShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_LOGS_HEADERS", "a=b"); + env.put("OTEL_EXPORTER_OTLP_HEADERS", "x=y"); + Environment environment = runProcessor(env); + assertHasMap(environment, "management.opentelemetry.logging.export.otlp.headers", Map.of("a", "b")); + } + + @Test + void specificLogsAttributeValueLengthLimitShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT", "4096"); + env.put("OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", "2048"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("management.opentelemetry.logging.limits.max-attribute-value-length")) + .isEqualTo("4096"); + } + + @Test + void specificLogsAttributeCountLimitShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT", "128"); + env.put("OTEL_ATTRIBUTE_COUNT_LIMIT", "64"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("management.opentelemetry.logging.limits.max-attributes")).isEqualTo("128"); + } + + @Test + void specificLogsCertificateShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE", "/specific/cert.pem"); + env.put("OTEL_EXPORTER_OTLP_CERTIFICATE", "/generic/cert.pem"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-logging.truststore.certificate")) + .isEqualTo("file:/specific/cert.pem"); + } + + @Test + void specificLogsClientKeyShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY", "/specific/key.pem"); + env.put("OTEL_EXPORTER_OTLP_CLIENT_KEY", "/generic/key.pem"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-logging.keystore.private-key")) + .isEqualTo("file:/specific/key.pem"); + } + + @Test + void specificLogsClientCertificateShouldOverrideFallback() { + Map env = new HashMap<>(); + env.put("OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE", "/specific/client.pem"); + env.put("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", "/generic/client.pem"); + Environment environment = runProcessor(env); + assertThat(environment.getProperty("spring.ssl.bundle.pem.opentelemetry-logging.keystore.certificate")) + .isEqualTo("file:/specific/client.pem"); + } + + private Environment runProcessor(Map environmentVariables) { + DeferredLogs logFactory = new DeferredLogs(); + OpenTelemetryEnvironmentVariableEnvironmentPostProcessor processor = new OpenTelemetryEnvironmentVariableEnvironmentPostProcessor( + logFactory, new OpenTelemetryEnvironmentVariables(logFactory, environmentVariables::get)); + ConfigurableEnvironment configurableEnvironment = new StandardEnvironment(); + processor.postProcessEnvironment(configurableEnvironment, new SpringApplication()); + logFactory.switchOverAll(); + return configurableEnvironment; + } + + private void assertHasNoMap(Environment environment, String key) { + enumerateAllProperties(environment, (name) -> assertThat(name).doesNotStartWith(key + "[")); + } + + private void assertHasMap(Environment environment, String key, Map map) { + Set ignoredProperties = new HashSet<>(); + for (Entry entry : map.entrySet()) { + String property = "%s[%s]".formatted(key, entry.getKey()); + assertThat(environment.getProperty(property)).isEqualTo(entry.getValue()); + ignoredProperties.add(property); + } + enumerateAllProperties(environment, ignoredProperties, (name) -> assertThat(name).doesNotStartWith(key + "[")); + } + + private void assertHasList(Environment environment, String key, String... values) { + Set ignoredProperties = new HashSet<>(); + for (int i = 0; i < values.length; i++) { + String property = "%s[%d]".formatted(key, i); + assertThat(environment.getProperty(property)).isEqualTo(values[i]); + ignoredProperties.add(property); + } + enumerateAllProperties(environment, ignoredProperties, (name) -> assertThat(name).doesNotStartWith(key + "[")); + } + + private void assertHasEmptyList(Environment environment, String key) { + assertThat(environment.getProperty(key)).isEqualTo(""); + enumerateAllProperties(environment, (name) -> assertThat(name).doesNotStartWith(key + "[")); + } + + private void assertHasNoList(Environment environment, String key) { + enumerateAllProperties(environment, (name) -> assertThat(name).doesNotStartWith(key + "[")); + } + + private void enumerateAllProperties(Environment environment, ThrowingConsumer consumer) { + enumerateAllProperties(environment, Collections.emptySet(), consumer::accept); + } + + private void enumerateAllProperties(Environment environment, Set ignoredProperties, + ThrowingConsumer consumer) { + ConfigurableEnvironment configurableEnvironment = (ConfigurableEnvironment) environment; + configurableEnvironment.getPropertySources().forEach((source) -> { + if (source instanceof EnumerablePropertySource enumerablePropertySource) { + assertThat(enumerablePropertySource.getPropertyNames()).allSatisfy((name) -> { + if (!ignoredProperties.contains(name)) { + consumer.accept(name); + } + }); + } + else { + fail("Property source %s doesn't support enumerating properties", source); + } + }); + } + +} diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariablesTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariablesTests.java new file mode 100644 index 000000000000..833daf1b1158 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryEnvironmentVariablesTests.java @@ -0,0 +1,526 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.opentelemetry.autoconfigure; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.logging.DeferredLogs; +import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryEnvironmentVariables.EnvVariable; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginTrackedValue; +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.assertThatIllegalStateException; + +/** + * Tests for {@link OpenTelemetryEnvironmentVariables}. + * + * @author Moritz Halbritter + */ +@ExtendWith(OutputCaptureExtension.class) +class OpenTelemetryEnvironmentVariablesTests { + + private DeferredLogs logFactory; + + @BeforeEach + void setUp() { + this.logFactory = new DeferredLogs(); + } + + @Test + void getIntShouldReturnParsedValue() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "42")); + EnvVariable result = env.getInt("VAR"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.name()).isEqualTo("VAR"); + assertThat(result.value()).isEqualTo("42"); + } + + @Test + void getIntShouldReturnNoneWhenMissing() { + OpenTelemetryEnvironmentVariables env = createEnv(Collections.emptyMap()); + EnvVariable result = env.getInt("VAR"); + assertThat(result.isPresent()).isFalse(); + } + + @Test + void getIntShouldReturnNoneWhenEmpty() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "")); + EnvVariable result = env.getInt("VAR"); + assertThat(result.isPresent()).isFalse(); + } + + @Test + void getIntShouldReturnNoneAndWarnWhenNotANumber(CapturedOutput output) { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "abc")); + EnvVariable result = env.getInt("VAR"); + assertThat(result.isPresent()).isFalse(); + assertThatLogContains(output, "Invalid value for integer environment variable 'VAR': 'abc'"); + } + + @Test + void getIntWithFallbackShouldUsePrimaryWhenPresent() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("PRIMARY", "10", "FALLBACK", "20")); + EnvVariable result = env.getInt("PRIMARY", "FALLBACK"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.name()).isEqualTo("PRIMARY"); + assertThat(result.value()).isEqualTo("10"); + } + + @Test + void getIntWithFallbackShouldUseFallbackWhenPrimaryMissing() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("FALLBACK", "20")); + EnvVariable result = env.getInt("PRIMARY", "FALLBACK"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.name()).isEqualTo("FALLBACK"); + assertThat(result.value()).isEqualTo("20"); + } + + @Test + void getIntWithFallbackShouldReturnNoneWhenBothMissing() { + OpenTelemetryEnvironmentVariables env = createEnv(Collections.emptyMap()); + EnvVariable result = env.getInt("PRIMARY", "FALLBACK"); + assertThat(result.isPresent()).isFalse(); + } + + @Test + void getBooleanShouldReturnTrueValue() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "true")); + EnvVariable result = env.getBoolean("VAR"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.value()).isEqualTo("true"); + } + + @Test + void getBooleanShouldReturnFalseValue() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "false")); + EnvVariable result = env.getBoolean("VAR"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.value()).isEqualTo("false"); + } + + @Test + void getBooleanShouldBeCaseInsensitive() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "TRUE")); + EnvVariable result = env.getBoolean("VAR"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.value()).isEqualTo("true"); + env = createEnv(Map.of("VAR", "False")); + result = env.getBoolean("VAR"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.value()).isEqualTo("false"); + } + + @Test + void getBooleanShouldReturnNoneWhenMissing() { + OpenTelemetryEnvironmentVariables env = createEnv(Collections.emptyMap()); + EnvVariable result = env.getBoolean("VAR"); + assertThat(result.isPresent()).isFalse(); + } + + @Test + void getBooleanShouldReturnNoneWhenEmpty() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "")); + EnvVariable result = env.getBoolean("VAR"); + assertThat(result.isPresent()).isFalse(); + } + + @Test + void getBooleanShouldReturnNoneAndWarnWhenInvalid(CapturedOutput output) { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "yes")); + EnvVariable result = env.getBoolean("VAR"); + assertThat(result.isPresent()).isFalse(); + assertThatLogContains(output, "Invalid value for boolean environment variable 'VAR': 'yes'"); + } + + @Test + void getDurationShouldConvertMilliseconds() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "5000")); + EnvVariable result = env.getDuration("VAR"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.name()).isEqualTo("VAR"); + assertThat(result.value()).isEqualTo("PT5S"); + } + + @Test + void getDurationShouldHandleZero() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "0")); + EnvVariable result = env.getDuration("VAR"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.value()).isEqualTo("PT0S"); + } + + @Test + void getDurationShouldReturnNoneWhenMissing() { + OpenTelemetryEnvironmentVariables env = createEnv(Collections.emptyMap()); + EnvVariable result = env.getDuration("VAR"); + assertThat(result.isPresent()).isFalse(); + } + + @Test + void getDurationShouldReturnNoneWhenEmpty() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "")); + EnvVariable result = env.getDuration("VAR"); + assertThat(result.isPresent()).isFalse(); + } + + @Test + void getDurationShouldReturnNoneAndWarnWhenNotANumber(CapturedOutput output) { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "abc")); + EnvVariable result = env.getDuration("VAR"); + assertThat(result.isPresent()).isFalse(); + assertThatLogContains(output, "Invalid value for duration environment variable 'VAR': 'abc'"); + } + + @Test + void getDurationShouldReturnNoneAndWarnWhenNegative(CapturedOutput output) { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "-1")); + EnvVariable result = env.getDuration("VAR"); + assertThat(result.isPresent()).isFalse(); + assertThatLogContains(output, "Duration environment variable 'VAR' must be non-negative, but was: '-1'"); + } + + @Test + void getTimeoutShouldConvertMilliseconds() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "10000")); + EnvVariable result = env.getTimeout("VAR"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.name()).isEqualTo("VAR"); + assertThat(result.value()).isEqualTo("PT10S"); + } + + @Test + void getTimeoutShouldTreatZeroAsInfinity() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "0")); + EnvVariable result = env.getTimeout("VAR"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.value()).isEqualTo(Duration.ofSeconds(Long.MAX_VALUE).toString()); + } + + @Test + void getTimeoutShouldReturnNoneWhenMissing() { + OpenTelemetryEnvironmentVariables env = createEnv(Collections.emptyMap()); + EnvVariable result = env.getTimeout("VAR"); + assertThat(result.isPresent()).isFalse(); + } + + @Test + void getTimeoutShouldReturnNoneWhenEmpty() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "")); + EnvVariable result = env.getTimeout("VAR"); + assertThat(result.isPresent()).isFalse(); + } + + @Test + void getTimeoutShouldReturnNoneAndWarnWhenNotANumber(CapturedOutput output) { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "abc")); + EnvVariable result = env.getTimeout("VAR"); + assertThat(result.isPresent()).isFalse(); + assertThatLogContains(output, "Invalid value for duration environment variable 'VAR': 'abc'"); + } + + @Test + void getTimeoutShouldReturnNoneAndWarnWhenNegative(CapturedOutput output) { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "-5")); + EnvVariable result = env.getTimeout("VAR"); + assertThat(result.isPresent()).isFalse(); + assertThatLogContains(output, "Duration environment variable 'VAR' must be non-negative, but was: '-5'"); + } + + @Test + void getTimeoutWithFallbackShouldUsePrimaryWhenPresent() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("PRIMARY", "5000", "FALLBACK", "3000")); + EnvVariable result = env.getTimeout("PRIMARY", "FALLBACK"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.name()).isEqualTo("PRIMARY"); + assertThat(result.value()).isEqualTo("PT5S"); + } + + @Test + void getTimeoutWithFallbackShouldUseFallbackWhenPrimaryMissing() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("FALLBACK", "3000")); + EnvVariable result = env.getTimeout("PRIMARY", "FALLBACK"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.name()).isEqualTo("FALLBACK"); + assertThat(result.value()).isEqualTo("PT3S"); + } + + @Test + void getTimeoutWithFallbackShouldReturnNoneWhenBothMissing() { + OpenTelemetryEnvironmentVariables env = createEnv(Collections.emptyMap()); + EnvVariable result = env.getTimeout("PRIMARY", "FALLBACK"); + assertThat(result.isPresent()).isFalse(); + } + + @Test + void getStringShouldReturnValue() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "hello")); + EnvVariable result = env.getString("VAR"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.name()).isEqualTo("VAR"); + assertThat(result.value()).isEqualTo("hello"); + } + + @Test + void getStringShouldReturnNoneWhenMissing() { + OpenTelemetryEnvironmentVariables env = createEnv(Collections.emptyMap()); + EnvVariable result = env.getString("VAR"); + assertThat(result.isPresent()).isFalse(); + } + + @Test + void getStringShouldReturnNoneWhenEmpty() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("VAR", "")); + EnvVariable result = env.getString("VAR"); + assertThat(result.isPresent()).isFalse(); + } + + @Test + void getStringWithFallbackShouldUsePrimaryWhenPresent() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("PRIMARY", "a", "FALLBACK", "b")); + EnvVariable result = env.getString("PRIMARY", "FALLBACK"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.name()).isEqualTo("PRIMARY"); + assertThat(result.value()).isEqualTo("a"); + } + + @Test + void getStringWithFallbackShouldUseFallbackWhenPrimaryMissing() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("FALLBACK", "b")); + EnvVariable result = env.getString("PRIMARY", "FALLBACK"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.name()).isEqualTo("FALLBACK"); + assertThat(result.value()).isEqualTo("b"); + } + + @Test + void getStringWithFallbackShouldReturnNoneWhenBothMissing() { + OpenTelemetryEnvironmentVariables env = createEnv(Collections.emptyMap()); + EnvVariable result = env.getString("PRIMARY", "FALLBACK"); + assertThat(result.isPresent()).isFalse(); + } + + @Test + void getStringWithFallbackTransformerShouldApplyTransformerToFallbackValue() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("FALLBACK", "base")); + EnvVariable result = env.getString("PRIMARY", "FALLBACK", (value) -> value + "/appended"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.name()).isEqualTo("FALLBACK"); + assertThat(result.value()).isEqualTo("base/appended"); + } + + @Test + void getStringWithFallbackTransformerShouldNotApplyTransformerToPrimaryValue() { + OpenTelemetryEnvironmentVariables env = createEnv(Map.of("PRIMARY", "primary-value", "FALLBACK", "base")); + EnvVariable result = env.getString("PRIMARY", "FALLBACK", (value) -> value + "/appended"); + assertThat(result.isPresent()).isTrue(); + assertThat(result.name()).isEqualTo("PRIMARY"); + assertThat(result.value()).isEqualTo("primary-value"); + } + + @Test + void getFirstPresentShouldReturnFirstPresentVariable() { + EnvVariable first = EnvVariable.of("A", "1"); + EnvVariable second = EnvVariable.of("B", "2"); + OpenTelemetryEnvironmentVariables env = createEnv(Collections.emptyMap()); + EnvVariable result = env.getFirstPresent(first, second); + assertThat(result).isSameAs(first); + } + + @Test + void getFirstPresentShouldSkipAbsentVariables() { + EnvVariable absent = EnvVariable.none(); + EnvVariable present = EnvVariable.of("B", "2"); + OpenTelemetryEnvironmentVariables env = createEnv(Collections.emptyMap()); + EnvVariable result = env.getFirstPresent(absent, present); + assertThat(result).isSameAs(present); + } + + @Test + void getFirstPresentShouldReturnNullWhenAllAbsent() { + OpenTelemetryEnvironmentVariables env = createEnv(Collections.emptyMap()); + EnvVariable result = env.getFirstPresent(EnvVariable.none(), EnvVariable.none()); + assertThat(result).isNull(); + } + + @Test + void getOriginShouldReturnOriginForPresentVariable() { + OpenTelemetryEnvironmentVariables env = createEnv(Collections.emptyMap()); + Origin origin = env.getOrigin(EnvVariable.of("MY_VAR", "value")); + assertThat(origin).isNotNull(); + assertThat(origin).hasToString("Converted OpenTelemetry environment variable 'MY_VAR'"); + } + + @Test + void getOriginShouldReturnNullForAbsentVariable() { + OpenTelemetryEnvironmentVariables env = createEnv(Collections.emptyMap()); + Origin origin = env.getOrigin(EnvVariable.none()); + assertThat(origin).isNull(); + } + + @Test + void envVariableNoneShouldNotBePresent() { + EnvVariable none = EnvVariable.none(); + assertThat(none.isPresent()).isFalse(); + assertThat(none.name()).isNull(); + assertThat(none.value()).isNull(); + } + + @Test + void addToMapShouldAddValueWithOrigin() { + Map map = new HashMap<>(); + EnvVariable.of("VAR", "value").addToMap(map, "my.property"); + assertThat(map).containsKey("my.property"); + assertThat(map.get("my.property")).satisfies((tracked) -> { + assertThat(tracked.getValue()).isEqualTo("value"); + assertThat(tracked.getOrigin()).hasToString("Converted OpenTelemetry environment variable 'VAR'"); + }); + } + + @Test + void addToMapWithTransformerShouldTransformValue() { + Map map = new HashMap<>(); + EnvVariable.of("VAR", "input").addToMap(map, "my.property", (name, value) -> value.toUpperCase(Locale.ROOT)); + assertThat(map).hasEntrySatisfying("my.property", + (tracked) -> assertThat(tracked.getValue()).isEqualTo("INPUT")); + } + + @Test + void addToMapShouldNotAddWhenTransformerReturnsNull() { + Map map = new HashMap<>(); + EnvVariable.of("VAR", "input").addToMap(map, "my.property", (name, value) -> null); + assertThat(map).isEmpty(); + } + + @Test + void addToMapShouldBeNoOpForNoneVariable() { + Map map = new HashMap<>(); + EnvVariable.none().addToMap(map, "my.property"); + assertThat(map).isEmpty(); + } + + @Test + void addToMapShouldThrowOnDuplicateKey() { + Map map = new HashMap<>(); + EnvVariable.of("VAR1", "value1").addToMap(map, "my.property"); + assertThatIllegalStateException() + .isThrownBy(() -> EnvVariable.of("VAR2", "value2").addToMap(map, "my.property")) + .withMessageContaining("Duplicate key 'my.property'"); + } + + @Test + void addMappingToMapShouldAddEntries() { + Map map = new HashMap<>(); + EnvVariable.of("VAR", "a=1,b=2").addMappingToMap(map, "my.headers", (name, value) -> { + Map result = new LinkedHashMap<>(); + for (String part : value.split(",")) { + String[] keyValue = part.split("=", 2); + result.put(keyValue[0], keyValue[1]); + } + return result; + }); + assertThat(map).hasEntrySatisfying("my.headers[a]", (tracked) -> assertThat(tracked.getValue()).isEqualTo("1")); + assertThat(map).hasEntrySatisfying("my.headers[b]", (tracked) -> assertThat(tracked.getValue()).isEqualTo("2")); + } + + @Test + void addMappingToMapShouldBeNoOpForNoneVariable() { + Map map = new HashMap<>(); + EnvVariable.none().addMappingToMap(map, "my.headers", (name, value) -> Map.of("a", "1")); + assertThat(map).isEmpty(); + } + + @Test + void addMappingToMapShouldBeNoOpWhenTransformerReturnsNull() { + Map map = new HashMap<>(); + EnvVariable.of("VAR", "value").addMappingToMap(map, "my.headers", (name, value) -> null); + assertThat(map).isEmpty(); + } + + @Test + void addMappingToMapShouldThrowOnDuplicateKey() { + Map map = new HashMap<>(); + EnvVariable.of("VAR1", "v").addMappingToMap(map, "my.headers", (name, value) -> Map.of("a", "1")); + assertThatIllegalStateException().isThrownBy( + () -> EnvVariable.of("VAR2", "v").addMappingToMap(map, "my.headers", (name, value) -> Map.of("a", "2"))) + .withMessageContaining("Duplicate key 'my.headers[a]'"); + } + + @Test + void addListToMapShouldAddEntries() { + Map map = new HashMap<>(); + EnvVariable.of("VAR", "a,b,c") + .addListToMap(map, "my.list", + (name, value) -> Stream.of(value.split(",")).map(String::toUpperCase).toList()); + assertThat(map).hasEntrySatisfying("my.list[0]", (tracked) -> assertThat(tracked.getValue()).isEqualTo("A")); + assertThat(map).hasEntrySatisfying("my.list[1]", (tracked) -> assertThat(tracked.getValue()).isEqualTo("B")); + assertThat(map).hasEntrySatisfying("my.list[2]", (tracked) -> assertThat(tracked.getValue()).isEqualTo("C")); + } + + @Test + void addListToMapShouldBeNoOpForNoneVariable() { + Map map = new HashMap<>(); + EnvVariable.none().addListToMap(map, "my.list", (name, value) -> List.of("A")); + assertThat(map).isEmpty(); + } + + @Test + void addListToMapShouldBeNoOpWhenTransformerReturnsNull() { + Map map = new HashMap<>(); + EnvVariable.of("VAR", "value").addListToMap(map, "my.list", (name, value) -> null); + assertThat(map).isEmpty(); + } + + @Test + void addListToMapShouldAddEmptyStringWhenListIsEmpty() { + Map map = new HashMap<>(); + EnvVariable.of("VAR", "value").addListToMap(map, "my.list", (name, value) -> Collections.emptyList()); + assertThat(map).hasEntrySatisfying("my.list", (tracked) -> assertThat(tracked.getValue()).isEqualTo("")); + } + + @Test + void addListToMapShouldThrowOnDuplicateKey() { + Map map = new HashMap<>(); + EnvVariable.of("VAR1", "v").addListToMap(map, "my.list", (name, value) -> List.of("A")); + assertThatIllegalStateException() + .isThrownBy(() -> EnvVariable.of("VAR2", "v").addListToMap(map, "my.list", (name, value) -> List.of("B"))) + .withMessageContaining("Duplicate key 'my.list[0]'"); + } + + private OpenTelemetryEnvironmentVariables createEnv(Map map) { + return OpenTelemetryEnvironmentVariables.forMap(this.logFactory, map); + } + + private void assertThatLogContains(CapturedOutput output, String message) { + this.logFactory.switchOverAll(); + assertThat(output).contains(message); + } + +} diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParserTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParserTests.java new file mode 100644 index 000000000000..a1a1945a4034 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParserTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.opentelemetry.autoconfigure; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link W3CHeaderParser}. + * + * @author Moritz Halbritter + */ +class W3CHeaderParserTests { + + @Test + void shouldParseEmptyString() { + assertThat(W3CHeaderParser.parse("")).isEmpty(); + } + + @Test + void shouldParseSingleEntry() { + Map result = W3CHeaderParser.parse("key1=value1"); + assertThat(result).containsExactly(Map.entry("key1", "value1")); + } + + @Test + void shouldParseMultipleEntries() { + Map result = W3CHeaderParser.parse("key1=value1,key2=value2,key3=value3"); + assertThat(result).containsExactly(Map.entry("key1", "value1"), Map.entry("key2", "value2"), + Map.entry("key3", "value3")); + } + + @Test + void shouldHandleWhitespaceAroundDelimiters() { + Map result = W3CHeaderParser.parse("key1 = value1 , key2 = value2"); + assertThat(result).containsExactly(Map.entry("key1", "value1"), Map.entry("key2", "value2")); + } + + @Test + void shouldPercentDecodeValues() { + Map result = W3CHeaderParser.parse("serverNode=DF%2028,userId=Am%C3%A9lie"); + assertThat(result).containsExactly(Map.entry("serverNode", "DF 28"), Map.entry("userId", "Amélie")); + } + + @Test + void shouldIgnoreProperties() { + Map result = W3CHeaderParser + .parse("key1=value1;property1;property2,key2=value2;propKey=propVal"); + assertThat(result).containsExactly(Map.entry("key1", "value1"), Map.entry("key2", "value2")); + } + + @Test + void shouldHandleEqualsSignInValue() { + Map result = W3CHeaderParser.parse("key1=val=ue"); + assertThat(result).containsExactly(Map.entry("key1", "val=ue")); + } + + @Test + void shouldSkipEntriesWithoutEqualsSign() { + Map result = W3CHeaderParser.parse("key1=value1,malformed,key2=value2"); + assertThat(result).containsExactly(Map.entry("key1", "value1"), Map.entry("key2", "value2")); + } + + @Test + void shouldSkipEntriesWithEmptyKey() { + Map result = W3CHeaderParser.parse("=value1,key2=value2"); + assertThat(result).containsExactly(Map.entry("key2", "value2")); + } + + @Test + void shouldAllowEmptyValue() { + Map result = W3CHeaderParser.parse("key1="); + assertThat(result).containsExactly(Map.entry("key1", "")); + } + + @Test + void shouldParseSpecExample() { + Map result = W3CHeaderParser + .parse("key1=value1;property1;property2, key2 = value2, key3=value3; propertyKey=propertyValue"); + assertThat(result).containsExactly(Map.entry("key1", "value1"), Map.entry("key2", "value2"), + Map.entry("key3", "value3")); + } + + @Test + void shouldKeepLastValueForDuplicateKeys() { + Map result = W3CHeaderParser.parse("key1=first,key1=second"); + assertThat(result).containsExactly(Map.entry("key1", "second")); + } + +} diff --git a/settings.gradle b/settings.gradle index 50e69eb92640..a1521d6b2e59 100644 --- a/settings.gradle +++ b/settings.gradle @@ -449,6 +449,7 @@ include ":smoke-test:spring-boot-smoke-test-logback" include ":smoke-test:spring-boot-smoke-test-oauth2-authorization-server" include ":smoke-test:spring-boot-smoke-test-oauth2-client" include ":smoke-test:spring-boot-smoke-test-oauth2-resource-server" +include ":smoke-test:spring-boot-smoke-test-opentelemetry" include ":smoke-test:spring-boot-smoke-test-parent-context" include ":smoke-test:spring-boot-smoke-test-profile" include ":smoke-test:spring-boot-smoke-test-prometheus" diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/build.gradle b/smoke-test/spring-boot-smoke-test-opentelemetry/build.gradle new file mode 100644 index 000000000000..cafa3a49a159 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/build.gradle @@ -0,0 +1,68 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java" + id "org.springframework.boot.docker-test" +} + +description = "Spring Boot OpenTelemetry smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-web")) + implementation(project(":starter:spring-boot-starter-opentelemetry")) + + testImplementation(project(":starter:spring-boot-starter-test")) + testImplementation("io.micrometer:micrometer-registry-otlp") + + dockerTestImplementation(project(":core:spring-boot-testcontainers")) + dockerTestImplementation(project(":starter:spring-boot-starter-test")) + dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) + dockerTestImplementation("org.awaitility:awaitility") + dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") +} + +tasks.named("test") { + environment "OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318" + environment "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://localhost:4319/custom/traces" + environment "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://localhost:4320/custom/metrics" + environment "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "http://localhost:4321/custom/logs" + environment "OTEL_RESOURCE_ATTRIBUTES", "service.name=my-service,service.version=1.0.0" + environment "OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Bearer token123,X-Custom-Header=value1" + environment "OTEL_EXPORTER_OTLP_TRACES_HEADERS", "X-Trace-Header=trace-value" + environment "OTEL_EXPORTER_OTLP_COMPRESSION", "gzip" + environment "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", "5000" + environment "OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf" +} + +tasks.named("dockerTest") { + environment "OTEL_EXPORTER_OTLP_ENDPOINT", "https://localhost:12345" + environment "OTEL_EXPORTER_OTLP_CERTIFICATE", "${project.projectDir}/src/dockerTest/resources/certs/ca.crt" + environment "OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Basic dGVzdHVzZXI6dGVzdHBhc3MxMjM=" // testuser:testpass123 + environment "OTEL_RESOURCE_ATTRIBUTES", "service.name=my-service,service.version=1.0.0" + environment "OTEL_TRACES_SAMPLER", "traceidratio" + environment "OTEL_TRACES_SAMPLER_ARG", "1.0" + environment "OTEL_METRIC_EXPORT_INTERVAL", "1000" +} + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +tasks.named("compileDockerTestJava") { + options.nullability.checking = "tests" +} + diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/java/smoketest/opentelemetry/OtlpCollectorIntegrationTests.java b/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/java/smoketest/opentelemetry/OtlpCollectorIntegrationTests.java new file mode 100644 index 000000000000..79a6d9adef8e --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/java/smoketest/opentelemetry/OtlpCollectorIntegrationTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.opentelemetry; + +import java.time.Duration; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.InternetProtocol; +import org.testcontainers.containers.output.ToStringConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.boot.testsupport.container.TestImage; +import org.springframework.web.client.RestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for OpenTelemetry environment variable mapping with a real OTLP + * collector. + * + * @author Moritz Halbritter + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Testcontainers(disabledWithoutDocker = true) +class OtlpCollectorIntegrationTests { + + private static final int OTLP_HTTP_PORT = 4318; + + private static final int OTLP_EXPOSED_PORT = 12345; + + private static final ToStringConsumer collectorOutput = new ToStringConsumer(); + + @Container + private static final OtlpCollectorContainer collector = new OtlpCollectorContainer() + .withCopyFileToContainer(MountableFile.forClasspathResource("otel-collector-config.yaml"), + "/etc/otel-collector-config.yaml") + .withCopyFileToContainer(MountableFile.forClasspathResource("certs/server.crt"), "/etc/certs/server.crt") + .withCopyFileToContainer(MountableFile.forClasspathResource("certs/server.key", 644), "/etc/certs/server.key") + .withCommand("--config=/etc/otel-collector-config.yaml") + .withLogConsumer(collectorOutput) + .waitingFor(Wait.forLogMessage(".*Everything is ready.*", 1).withStartupTimeout(Duration.ofSeconds(10))); + + @LocalServerPort + private int port; + + @Test + void tracesAreSentToCollector() { + RestClient restClient = RestClient.create(); + String response = restClient.get().uri("http://localhost:" + this.port + "/").retrieve().body(String.class); + assertThat(response).contains("Greetings from Spring Boot!"); + Awaitility.await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> { + String log = getContainerLog(); + assertThat(log).contains("service.name: Str(my-service)") + .contains("service.version: Str(1.0.0)") + .containsAnyOf("Span", "ScopeSpans"); + }); + } + + @Test + void metricsAreSentToCollector() { + Awaitility.await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> { + String log = getContainerLog(); + assertThat(log).contains("service.name: Str(my-service)") + .contains("service.version: Str(1.0.0)") + .containsAnyOf("jvm.memory.used", "ResourceMetrics", "ScopeMetrics"); + }); + } + + private static String getContainerLog() { + return collectorOutput.toUtf8String(); + } + + private static class OtlpCollectorContainer extends GenericContainer { + + OtlpCollectorContainer() { + super(DockerImageName.parse(TestImage.OTEL_COLLECTOR.toString())); + } + + @Override + protected void configure() { + super.configure(); + addFixedExposedPort(OTLP_EXPOSED_PORT, OTLP_HTTP_PORT, InternetProtocol.TCP); + } + + } + +} diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/java/smoketest/opentelemetry/package-info.java b/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/java/smoketest/opentelemetry/package-info.java new file mode 100644 index 000000000000..ab461e2ab180 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/java/smoketest/opentelemetry/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OpenTelemetry smoke test. + */ +@NullMarked +package smoketest.opentelemetry; + +import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/ca.crt b/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/ca.crt new file mode 100644 index 000000000000..9afc7f1bd294 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/ca.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFBzCCAu+gAwIBAgIUAO7sh3YWNEe7Ih2nYxN86PVe0SUwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHdGVzdC1jYTAgFw0yNjAzMzAxMjQ3MDVaGA8yMTI2MDMw +NjEyNDcwNVowEjEQMA4GA1UEAwwHdGVzdC1jYTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAMvDAR2nxrwHdPeEqF1UOqbrhnhp6Oz7usPqTsM0tmVapo7M +m61uFD/Mcz6/fY2PrZ6WcXIIHmznhm2VjFP052DUibpf7bI5MiBUXyt/livg1Pg6 +kNMs7gM60leIdRIKWb6zb6EveaX3FsTwKszChnDBW435tEZrWbaDIbnfX7zcujwt +PmVBUbZTGnnMKf73rPJMNYjvRRJH+8lj25oRU0MDCAZvJuCVJE8kQmcq2fWwTGtG +v2JlE92hW51jobgTrhvyLOofaidBCp/GGTUga9j1tiBU7qf4mqA3unXEKT9AnKYm +KPrWDq64SEizXPHJL4wACnLyPp3DUxSP/H2JA5GKF/+dpCJNZd85TkoaOsXauwh1 +rZe5RmO7KHs7AWABQeuGlpxtZkcbgt9of0j+2m33eUkuwceUKFAXuiLFfbxiSwXi +4pj/k4NpJqaGV489DdeKTVywFEmHP5lQCJoX1D7PaxQ8z1vpEkukkC3pENwYpXwY +WUf0W4E1tYqsq2fj0KnCf0lIZKi6+KQQqh9///N1XYy/6XuM8j8Z5KrOixRAKgpx +9bDB6M/OX6baToUwtXiiC/YtLEyR7GjczKS7r7FLJOCyZ9jb/dNTiGJdsLSKqpms ++n4D2BydWYgq1ulz9Pm3nifjqogsc8HyeUMatuKAhNZzJn4A9aip69y86G3HAgMB +AAGjUzBRMB0GA1UdDgQWBBSPSKkkcym4W7WBCvDI36OItoYaqjAfBgNVHSMEGDAW +gBSPSKkkcym4W7WBCvDI36OItoYaqjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4ICAQB1Jn5frNv8ZuVGAtr/Ay+S35NjNLPBJUWW2TJ/x6ua6ks0wDw+ +pRZ3yjpYoFUu7Z5egQ8NECFvGSxaHy7ACa5eeE330agEhkqSP2XYb9NnEydLPXug +rm+U4/1W+V5iUd8P0aQ+5InLpGzoO3Germs+idEz6gO0vdIHY2wZHPyNw4qoiJAd +jPuIAk/Dnan8NvtnyTg7mTLvTJtZ/mwdRVGoCVBWnvb/j6ymlH4T7NzhyqivCxvu +jh+biVCE9TGYqfUotYyp/TLWoM1iiQrM7JDAV77+mV/JvsPbuc7ddJmEttFuCf3Z +iXWk5fcNc59VXvWBWwWnKhwA9srigw44xwGv7gW1e3xaBPAGb3EtJ3uDWn37ggba +0KuMjhT1btyQD4WgtgHQBt/5qbAr0q+EA5vnpn2hekk74KvCaHWQHPo4tg/ygKTL +jNCoV2125KaLsGO/1/35rgs4DhauA2RzmGuM+QDN65PQjTpWYFjTmC08tn1KewK6 +mbJzgZWoUj3EKHXb9m1AQLPzAYDzKXZgtdE263TjVOgWk/IE/WWEo7P+8He6/ymq +b9pLCbDJTTJD1fuvMdaEhzBTyCU88rVDswbY++m1KVwABcqvNYQoThOtD8Sz2EcG +zepFBLRiZoX2/jm+b3nfM5OJItHjYUlAsvU3X16xPWG+BLslyAdD/Rf8JQ== +-----END CERTIFICATE----- diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/ca.key b/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/ca.key new file mode 100644 index 000000000000..e42887f0f601 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/ca.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDLwwEdp8a8B3T3 +hKhdVDqm64Z4aejs+7rD6k7DNLZlWqaOzJutbhQ/zHM+v32Nj62elnFyCB5s54Zt +lYxT9Odg1Im6X+2yOTIgVF8rf5Yr4NT4OpDTLO4DOtJXiHUSClm+s2+hL3ml9xbE +8CrMwoZwwVuN+bRGa1m2gyG531+83Lo8LT5lQVG2Uxp5zCn+96zyTDWI70USR/vJ +Y9uaEVNDAwgGbybglSRPJEJnKtn1sExrRr9iZRPdoVudY6G4E64b8izqH2onQQqf +xhk1IGvY9bYgVO6n+JqgN7p1xCk/QJymJij61g6uuEhIs1zxyS+MAApy8j6dw1MU +j/x9iQORihf/naQiTWXfOU5KGjrF2rsIda2XuUZjuyh7OwFgAUHrhpacbWZHG4Lf +aH9I/tpt93lJLsHHlChQF7oixX28YksF4uKY/5ODaSamhlePPQ3Xik1csBRJhz+Z +UAiaF9Q+z2sUPM9b6RJLpJAt6RDcGKV8GFlH9FuBNbWKrKtn49Cpwn9JSGSouvik +EKoff//zdV2Mv+l7jPI/GeSqzosUQCoKcfWwwejPzl+m2k6FMLV4ogv2LSxMkexo +3Myku6+xSyTgsmfY2/3TU4hiXbC0iqqZrPp+A9gcnVmIKtbpc/T5t54n46qILHPB +8nlDGrbigITWcyZ+APWoqevcvOhtxwIDAQABAoICABRmgjqXXSjfMEFF2Vt6vuKO +ayisJeNwMptB3r/hi69rG4jpiF6dSk626ATsxCPDslTR6CrMEEvEBGv9UI7pUq8Z +qG5uA0zYOCIPgHXI8N6UxI3Tw0M5WN0e6wdThLEZMzIKIg+Hwsywh+mQyCA3C6HH +YiS9TuTMA6/nwh0rdZjhmo3C09/sYcN434o5PC0oW/roUWXT2t0BMv7PTiHHQUhQ +yeFnCIPsurVnwnQ00JyuGYCWFTxDNXF3ocYIvxzAb6qhnQiVu5mRhABKI6TziYiv +1Ru4lSphFA4ddkArMxv/mo9x+6FdhBvPHIGW36PfijCgomsuwMEn2K4S2msxKBeg ++THxW6u0LQXR4l4uOeYmh7mxqB3gUKLlcJKxthutKmelJpKiZJE93dvqUHaY0QWm +KbzOSa791xRzb7ITjy7iTq9QH5XQqe6YihR88iGKBctXsLwNbFV8+kFTx8aWGtD5 +BY3MFpdIi+5UaptmVQwMDWiFl/CiU456Q6Sertk8ZBm2U71qqDWkMYYd8ZMmb+ws +QjMcagX69RZG/CBW504XgLteg7LyTzOGmdCM/Jk2IpycGoJdfugIOv4OkoXQuHbB +6qtaqBXsVG/Z8dmGQiM04LHxtSgzXaoTN5ma6Ni1ieDFfKJJB1DtcoCAU2CgCIzb +3noMedyQ7LFbTgU3DbeNAoIBAQDmMKgF5t7hNU6Mrfa4Yi3rteUTRlmr6CsupQsk +TBk8dLu4XHbWSd3/ZxmXVMe6zwhFi7exBSHiB5/WUcyqJe/rGot713aBYkQUA2Is +3jtPTfS8jGLDdaaW241PbxHHmTSDUppqgQaB0nYoOdBTmZNMfh3l8S6tkvajutLV +e67S/l8gKMyQosCKB2YJNOzZUbIsFb6qFB6CM4nbk+mus2EpOh+iNiyYWNpdM6iA +RUq82WeCUST+Siwufrv+56a66mp46LYtXFCe6DDrVW3wqDyW0AXg5Bnk0g9UDq7n +8idlw6oWDCfAGfTczMeZkPXrkdiRIayRog9tbIPN56nXNOy1AoIBAQDim8DBwSmZ +sUIA1XZWHmqetimDsN0A/+mKInqrmU6Fq9r72OPAxYmSwCdFW9zr9mOFv1YR0g5L +nJwaduN9KgQpGON6iSM5eftKa9Cv6HbIdQ8+MYHFHvY5kwd0pbDk5yJqReWdnrXi +Gcd3MJYUo5JWdyHXA678623DqS4paan/E2eCJuX3Twin5OG9Qy0mQiiLGsRJuAIn +/AL6m5zHwm+yp6BmP2QXYBfch3WcOE/w5EAmEKwATsiUKGRfnin7xAds/iPzz/PT +uou0Y27GMlViXnThmb2BNhoMidiZAKtBYKTsmcsDVYH6XgBhxMejP3TcMrqmUJkm +iTEISMSKBHoLAoIBAQDMTObzxgKOszcyGLxezZSgHWJhBxFTR837qjQHVRsY0ypA +6J/4dLr5kVHfRDm4jUN0Jhs0m6+E4utz1knlwaH8ciTWF1dwYfy2HoKl+9979V5/ +ccG+ZM8kGVyD4bpQAqPe4hN272kNE/QWo6lIaYeYXl6+1J5vI36PtqKlIw746RcY +fd2BiVGFsCdZiIWmI6IVCn+ciBrDyGZUYiUHDuHvK00kNRzDYij8i3rDzbwVfIcn +3EkPAFnSMl08UfFi9+bjxPbbr5FGWCIC3GGlNEwCNkLhZNGuitlb62RFLGDXKx/s +TYGCOAdhaMbGfygkmd8ygEafQsyae7NHrpLJFIn5AoIBAAjfXE65x20zGYh60yVQ +D5CjRUDYve56FqTH8NGjMQXe0rYlqdU0ZsyOJXt7u0yp2v+znCUaemDPgvUuTn3q +FTues9lqYSyw7/vb+NHKYkw6+RvQSGp1aeKbNSGGE0UDH7/VeMYRHsH5jGvIRTgN +p38nLvYCOOnCNgwBJcONVZCdK+JWr45dU9NvDL+iT7Q9xuSbabAROlRic6NoUF0p +6cGu+oLGXJ7lrkl+LEavlcGyzigvYuyoKQZUBooP4UjwRfIoiTPUopFZrnqjogS0 +l89AL4ddw6O5gI+ZHhfvhMUMiaiR5Pb+dBZfc9hgIgjsRSESA2jbowN1P6w5XSii +QD0CggEBAJz9Y4bmOHRC+Jb01sdsKPWE6OgGDnApc8ng5IqgkCjZS4/rgAb/cAAZ +cqBdI5iSsNrv3SGAMJAeJEuxJ4tFF3+NOIZFXwkzKLM+SW1C1wDkUQwCSBJgOA1F +rQlp8XnvBEM7HKAtRyuAMsEq+fa7Ad0AXDKt7VRDD0iSOLDBVErQiStkUEhdKbuc +BQ7fAuY738KgKBo66CRHBD0ZEBXObAnYEbrZUH0ot1wtvKfQyQkydNjhtkOrhslY +nBcGDyNk98+XKedYPlz44nry5Oe9w2EpywiYGxorQueOkFbFi7y+pZLQEvWvRZ8g +egUiLGwkss03YP534t8VzQ6ewsW9Wpg= +-----END PRIVATE KEY----- diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/server.crt b/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/server.crt new file mode 100644 index 000000000000..6d9c8fd079f8 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/server.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFFDCCAvygAwIBAgIUa3TPUMqR0hxgacc4KhQa2fxeZycwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHdGVzdC1jYTAgFw0yNjAzMzAxMjQ3MDVaGA8yMTI2MDMw +NjEyNDcwNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAq6zgJxaQPo/WFxlGMbpQhFOg/Lw9eg/le1GtYEMUE92Q +Wx9wdmZmfBsYF0Ph0h/9JS6noqhRehYy1KaTsul5V9caddgjgPZiJ7GxBBAWZtCT +k9fHnp+QN9+Gp8IDHzn0Ao/DK+HDzd5UNKr/NJIjeElZ9aSFZiW9c/gGlciZwr4J +U4QvXghg/UeY4UDGHGAYBdKe6fCuViTYFVE0Z4XkA2nb4I5Hxfvkl1JdiJvI65lT +zjm9SjCN+/zm/Q78iccmb604yA6PxiOxDDNLeV4vduHwZzVoV2yDYy1AJ1ReEuia +omEYote7dZ/ZWfbpIZKLZiZPTqZeCU0tbNmZvfEpfryOlaN9wDZdc2btATxG4UV/ +alwVQQfIt+hHvtabQU90Bq/UPCVDJWGjbaEGFwF5fzkDakWAGIWmS0fM2jAn8PU4 +9reVZKz4cfOCeqHis+VkLqbQyF3dnXAe7mmcCHf2Bs7EvOvRNS7tHmSxGmt0howB +jrM/rJCwkfiMPurE0d7Ig9h+UUnZ3K/iVjSlWxm6bSaCiS72KO3rRWRZAOpma4+T +buOYRtaOw4yZZhXJqdNHePa4nq6Xl7RmBQVKbVG92olq6fLp6S4iMDZUVocXldh8 +qE9BbKefNQEO1+eG4V/IKLgM8JTHqHl6B4YxjgZgA8fU2NScsNxxH89e0Yiuk8MC +AwEAAaNeMFwwGgYDVR0RBBMwEYcEfwAAAYIJbG9jYWxob3N0MB0GA1UdDgQWBBS4 +/fykVDFeP1dWQTPPJwE4Ptg5TzAfBgNVHSMEGDAWgBSPSKkkcym4W7WBCvDI36OI +toYaqjANBgkqhkiG9w0BAQsFAAOCAgEADsgCihVnbxyJbVIq0c0cdWw7wSp8ck9j +zTbgA8TBCr+IeT31D2mWL6Y8TLtCvJ7utn3NiNgsPEqZn0gClLTB5QEGwv1t5kjG +/+IZHYFqXgzjJwLUaTUu+bzfL5AWj5o6nmd01ThAgcoiqBUfU8FhiePA9oiXWhSe +OSY5gQfSlPpddp/k5RUsseM1JmhZGRgl9rFbx5XpYpALx5Elk+sX4tgojBWGPM3t +JyFxAxLplsO8JbYCKvTLodCWk5w1w1ndlIkGWyMlymhbe8F1L16/9EtHBLpMRU2t +a6Sn67sb0oQ6TDfzYHnzwXKMmvyYnNEtA5J8m35tcLLTvPo/cZDh6T16ZBLvuKVO +2FgFnCsuUyc4TbY4/aaxO/N/ENGp5PmHJEYqqFCwh5FgqzpkaIl+5xjC6aoAoR/p +7nii83zPToFZzfcupbiLCjsZQUb0DEoLvLVo7h4ByobJ5vdnVjseiG2kkGwz2N4W +if0bSb/mRH7hsQCfOAuBAytetUuejogCeiJwMj2xUcK2LJGmLpzQb5U0KuZXbgLe +y+dVo+znbpx1grpyNR3P+RkSZmxZDj2Sl6YUciTMtRtlcmPqi1xPAQ+knmwgLAi1 +qyPGsYtVT0clcGeXemOaUEBRis+1tnerVVgxkVBBcoxNWG+Klud/iudW3NzmbCf5 +saVQ3Tw+6a8= +-----END CERTIFICATE----- diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/server.key b/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/server.key new file mode 100644 index 000000000000..932439b62ac3 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/certs/server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQCrrOAnFpA+j9YX +GUYxulCEU6D8vD16D+V7Ua1gQxQT3ZBbH3B2ZmZ8GxgXQ+HSH/0lLqeiqFF6FjLU +ppOy6XlX1xp12COA9mInsbEEEBZm0JOT18een5A334anwgMfOfQCj8Mr4cPN3lQ0 +qv80kiN4SVn1pIVmJb1z+AaVyJnCvglThC9eCGD9R5jhQMYcYBgF0p7p8K5WJNgV +UTRnheQDadvgjkfF++SXUl2Im8jrmVPOOb1KMI37/Ob9DvyJxyZvrTjIDo/GI7EM +M0t5Xi924fBnNWhXbINjLUAnVF4S6JqiYRii17t1n9lZ9ukhkotmJk9Opl4JTS1s +2Zm98Sl+vI6Vo33ANl1zZu0BPEbhRX9qXBVBB8i36Ee+1ptBT3QGr9Q8JUMlYaNt +oQYXAXl/OQNqRYAYhaZLR8zaMCfw9Tj2t5VkrPhx84J6oeKz5WQuptDIXd2dcB7u +aZwId/YGzsS869E1Lu0eZLEaa3SGjAGOsz+skLCR+Iw+6sTR3siD2H5RSdncr+JW +NKVbGbptJoKJLvYo7etFZFkA6mZrj5Nu45hG1o7DjJlmFcmp00d49rierpeXtGYF +BUptUb3aiWrp8unpLiIwNlRWhxeV2HyoT0Fsp581AQ7X54bhX8gouAzwlMeoeXoH +hjGOBmADx9TY1Jyw3HEfz17RiK6TwwIDAQABAoICAAjg/IjZAGXT75krO/encLGf +V6Sqoa1V9CLwIvznCztJysUDt7/P5P4HMrRFVQEWmXOaWZ4ugWREkKVx5hKrQ5TY +8avlnVR2qbXu2T5iDJ6NrAxJ7S+VhhJ1s5Oqk5nCWA3n3IrA9u0HUiiVLXTNGBuJ +A6eSNjoFuqosSj7l7JnnVTkmDYMaANYopU4FGRnvmF54ubJUudeHuUtE82ZlRNku +WOpgwXKKaBSxdFfz4qzYtb5lOxaCs100g8fTxhjkHWdKOLUUF7iCMQ51+rHQvrix +z8qutvJKkPMIrJg8I09sC09ZoToO2UmolkeyXvVXDHGgipqBPTzS6oYGLQ7nUaOA +Lr8/dvuvTH8eZL3hRnlIaWZzxxNGFi8eX+GeFu3FRPpxCNrC0/PD3DyOogC1zXMY +Y4FUey9rF0XA/v/IHAaZhQFGP0AygUH4UOMbNDz7UQO1zlzHhwRR2BwAnsQYCJ3q +yVCF6KcE/fn/RAISxmUOm/SmeIJOhME8gaqbQ6dOvez5oDqJWvp79AYXAsozn49r +3oP+gQ5MxGbSxBz2z1S89mjLnuegTs/qs26vYoGhRF6yZUmmt5v4Rm8dSBlZwOZF +jo/Q5eEqeiX1kPwa3L/1Mfi3NHgdRBjigHMCu0K29aybV3REu1lD3jWWZ3DIzNfr +MFC/0i08tEn2+HMY8/OpAoIBAQDZrTZY7hkWYsEjCvBzPiPQuF+4xy6nNo77eM3z +kxPqnO0dTQt+7S1N9qm0AWwkZ/RywbBYo+rVegG68LvJrcs7+HhFm7kSuTfsoR8y +X513OJu+tlpwOa1z72P6mskz9BqHZUjKCnb74Yi+Bt6o1E4Loe2HF50GG+2+fVar +/XkyUQGEoEUUoWtgmdmYXPtjCGa5yORJZC5aazFOs7A9HlXBw3dg+KL1GuuWWnca +QMfSneCgwAiz1iafcXv55CyVekYk2v2UcX0gbUIxKKMWXvX/P/h/u66O/tvfrJpV +4sSWarkBwuzOjE6UpAcCh1hh9cBKYQN2ocVd7yPT6a2eEOz7AoIBAQDJ5ly+vhdD +AqII+3W58JEhV/0kkWaKTZDFTrvea9ijIarPfntodnn/gkmU1ePdGB7lVCNwIB93 +2loSCIIDP6xisI3oxyWPVjBak042H32iZc2uNL6GOJY7N/lAr7L9G/IjtNoKDCh0 +mnIFhuYXQbX9aHwEwcaUbqt1Km2rZ7kq9SHpvGz9Z5V0Z3zL8xZiFcFvj+zyyKM9 +/PTECxIlYrR2+4bSNt10SEj80/7RwOxKmpFeBA/+XHw7GpJbGjl+9GMm+3I04Lh1 +1dOlGqgVwms1zRoQdkXqFeUmkeW3iQo6XMBpb3fO3vGTAQSo5dL8PmdJn34yvw9g +79N64PNSbKnZAoIBAQCHDX2bnVX5WpoDuOTIS3SDKfFRvlrGqgX6JdB53/XKaUSA +Gm4YeOTT60cNBJjo2YqDrIJR7Jlli4YJSxTnoyll8toELgKciEgXSzwEz+ftsTSI +FbF00YQ5WvyH3oMY9cmD2aZYnDeIPTzO0YpR6y7k7rllSJRG1wshVbhxixkny7SK +abmSfDgXvjyP2Jirr+m0ONzSYBKWTM1RzBB7Sxff2Pt3MaxBMEuchFJC+pA/r25P +rfV4RlS+NfW6bAPxVeSxNyF/eQ4uETuxLkpJNe+gRztRXMuHsr4OP5Od180qmdph +KmQsHkce18n1mq2OUZ/VRe2VK/c9OYLcFq5+VHgDAoIBAQCfprBqCEi+rWNR9P33 +xXoNwtUiyHMsI/+5dfPrAp6xCJtYLm0Jgr4fxHfI4IWpvDwaHblUHaKklJ4qql2c +kgoPlArzKh7SRFZCGT98XtRqyVOmUIDkA/UP6MgJVaYUrbeAcq8DyO9onQRSyIRD +mat+MdP3sv8DOqwMHzvPkVqD1Ik3mc8ZZoJxJLAzWgq2DvwB6u50YSFskA1AxHxZ +4b5QY78NlWLyY4KLoltao9J1f9o5XL8E5T87QVy/+3HBqSCKn8SEebqDO017viNx +6WX7ii71G4FXhG7KXA4pXo0e718bQJCdnf+qveBgajO3UOU9+4fAOwK/JYW2IhFm +eRSpAoIBAQDAFxukiGd8Jz4P++F+P/HofnlkZ/pibY6ziQAsltHWOok4c7E+T7lg +E307s+HDIzOLW7Ed9c5TvDG8qN3do/tQMi6B/yiKRtS73hs9Sdu04DQeVvP+DJ35 +AiMwNWBOi6xZiC6ZpUKs+8pJ+UYvQ8vFeVhgEkJyamOBDFf+S+I0KbLSifDf/O0e +ION3vbOnlf9qgV7xsouo787FSWdegON7hyT+h6tR2gQQCXEBTgM91OayOPTIdgLv +EkVwjVJ40XNZwLdXEzkLFJxbgeqWBmzE7FV234seqvPMvM7cpso5yXoceW3YNP6J +mfrhfPLpoXr3DxpZfiyqnDIYvP2LJ9qX +-----END PRIVATE KEY----- diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/otel-collector-config.yaml b/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/otel-collector-config.yaml new file mode 100644 index 000000000000..6ee510b1abe3 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/dockerTest/resources/otel-collector-config.yaml @@ -0,0 +1,39 @@ +receivers: + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 + tls: + cert_file: /etc/certs/server.crt + key_file: /etc/certs/server.key + auth: + authenticator: basicauth/server + +processors: + batch: + +exporters: + logging: + loglevel: debug + +extensions: + basicauth/server: + htpasswd: + inline: | + testuser:testpass123 + +service: + extensions: [basicauth/server] + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [logging] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [logging] + logs: + receivers: [otlp] + processors: [batch] + exporters: [logging] diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/main/java/smoketest/opentelemetry/SampleController.java b/smoke-test/spring-boot-smoke-test-opentelemetry/src/main/java/smoketest/opentelemetry/SampleController.java new file mode 100644 index 000000000000..a1ed41c6087e --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/main/java/smoketest/opentelemetry/SampleController.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.opentelemetry; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class SampleController { + + @GetMapping("/") + public String home() { + return "Greetings from Spring Boot!"; + } + +} diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/main/java/smoketest/opentelemetry/SampleOpenTelemetryApplication.java b/smoke-test/spring-boot-smoke-test-opentelemetry/src/main/java/smoketest/opentelemetry/SampleOpenTelemetryApplication.java new file mode 100644 index 000000000000..397c3b372992 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/main/java/smoketest/opentelemetry/SampleOpenTelemetryApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.opentelemetry; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleOpenTelemetryApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleOpenTelemetryApplication.class, args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/main/java/smoketest/opentelemetry/package-info.java b/smoke-test/spring-boot-smoke-test-opentelemetry/src/main/java/smoketest/opentelemetry/package-info.java new file mode 100644 index 000000000000..ab461e2ab180 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/main/java/smoketest/opentelemetry/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OpenTelemetry smoke test. + */ +@NullMarked +package smoketest.opentelemetry; + +import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/DisabledMappingTests.java b/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/DisabledMappingTests.java new file mode 100644 index 000000000000..d024fc0fadc9 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/DisabledMappingTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.opentelemetry; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsProperties; +import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingProperties; +import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@TestPropertySource(properties = "management.opentelemetry.map-environment-variables=false") +class DisabledMappingTests { + + @Autowired + private OtlpTracingProperties tracingProperties; + + @Autowired + private OtlpLoggingProperties loggingProperties; + + @Autowired + private OtlpMetricsProperties metricsProperties; + + @Test + void environmentVariablesAreNotMappedWhenDisabled() { + assertThat(this.tracingProperties.getEndpoint()).isNull(); + assertThat(this.loggingProperties.getEndpoint()).isNull(); + assertThat(this.metricsProperties.getUrl()).isNull(); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/HeadersAndCompressionTests.java b/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/HeadersAndCompressionTests.java new file mode 100644 index 000000000000..8424048bf165 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/HeadersAndCompressionTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.opentelemetry; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsProperties; +import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingProperties; +import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.Transport; +import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingProperties; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class HeadersAndCompressionTests { + + @Autowired + private OtlpTracingProperties tracingProperties; + + @Autowired + private OtlpLoggingProperties loggingProperties; + + @Autowired + private OtlpMetricsProperties metricsProperties; + + @Test + void generalHeadersAreMappedToAllSignalsWhenSignalSpecificNotSet() { + assertThat(this.loggingProperties.getHeaders()).containsEntry("Authorization", "Bearer token123") + .containsEntry("X-Custom-Header", "value1"); + assertThat(this.metricsProperties.getHeaders()).containsEntry("Authorization", "Bearer token123") + .containsEntry("X-Custom-Header", "value1"); + } + + @Test + void signalSpecificHeadersCompletelyOverrideGeneral() { + assertThat(this.tracingProperties.getHeaders()).containsEntry("X-Trace-Header", "trace-value") + .as("Signal-specific headers completely override general headers, not merge") + .doesNotContainKey("Authorization"); + } + + @Test + void compressionIsMapped() { + assertThat(this.tracingProperties.getCompression()).isEqualTo(OtlpTracingProperties.Compression.GZIP); + assertThat(this.loggingProperties.getCompression()).isEqualTo(OtlpLoggingProperties.Compression.GZIP); + assertThat(this.metricsProperties.getCompressionMode()) + .isEqualTo(io.micrometer.registry.otlp.CompressionMode.GZIP); + } + + @Test + void timeoutIsMapped() { + assertThat(this.tracingProperties.getTimeout()).isEqualTo(Duration.ofMillis(5000)); + } + + @Test + void protocolIsMapped() { + assertThat(this.tracingProperties.getTransport()).isEqualTo(Transport.HTTP); + assertThat(this.loggingProperties.getTransport()) + .isEqualTo(org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.Transport.HTTP); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/SampleOpenTelemetryApplicationTests.java b/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/SampleOpenTelemetryApplicationTests.java new file mode 100644 index 000000000000..0a29b09e9ad4 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/SampleOpenTelemetryApplicationTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.opentelemetry; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.resources.Resource; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class SampleOpenTelemetryApplicationTests { + + @Autowired + private Resource resource; + + @Test + void resourceAttributesAreMapped() { + assertThat(this.resource.getAttribute(AttributeKey.stringKey("service.name"))).isEqualTo("my-service"); + assertThat(this.resource.getAttribute(AttributeKey.stringKey("service.version"))).isEqualTo("1.0.0"); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/SignalSpecificPrecedenceTests.java b/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/SignalSpecificPrecedenceTests.java new file mode 100644 index 000000000000..471c5eafaa38 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/SignalSpecificPrecedenceTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.opentelemetry; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsProperties; +import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingProperties; +import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingProperties; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class SignalSpecificPrecedenceTests { + + @Autowired + private OtlpTracingProperties tracingProperties; + + @Autowired + private OtlpLoggingProperties loggingProperties; + + @Autowired + private OtlpMetricsProperties metricsProperties; + + @Test + void signalSpecificEndpointTakesPrecedenceOverGeneral() { + assertThat(this.tracingProperties.getEndpoint()).isEqualTo("http://localhost:4319/custom/traces"); + assertThat(this.loggingProperties.getEndpoint()).isEqualTo("http://localhost:4321/custom/logs"); + assertThat(this.metricsProperties.getUrl()).isEqualTo("http://localhost:4320/custom/metrics"); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/package-info.java b/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/package-info.java new file mode 100644 index 000000000000..ab461e2ab180 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-opentelemetry/src/test/java/smoketest/opentelemetry/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OpenTelemetry smoke test. + */ +@NullMarked +package smoketest.opentelemetry; + +import org.jspecify.annotations.NullMarked; From a04b65ea7437b9226902df26f403db72bd5551cf Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 16 Nov 2023 17:26:00 +0100 Subject: [PATCH 435/930] Set embeddedValueResolver on WebConversionService Closes gh-8923 --- .../web/format/WebConversionService.java | 19 ++++++++++++++++- .../web/format/WebConversionServiceTests.java | 21 +++++++++++++++++++ .../WebFluxAutoConfiguration.java | 14 +++++++++++-- .../WebFluxAutoConfigurationTests.java | 20 ++++++++++++++++++ .../WebMvcAutoConfiguration.java | 14 +++++++++++-- .../WebMvcAutoConfigurationTests.java | 20 ++++++++++++++++++ 6 files changed, 103 insertions(+), 5 deletions(-) diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java index 06826ddb9e0c..c33694a5c11b 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java @@ -31,6 +31,7 @@ import org.springframework.format.number.money.MonetaryAmountFormatter; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.util.ClassUtils; +import org.springframework.util.StringValueResolver; /** * {@link org.springframework.format.support.FormattingConversionService} dedicated to web @@ -53,7 +54,23 @@ public class WebConversionService extends DefaultFormattingConversionService { * @since 2.3.0 */ public WebConversionService(DateTimeFormatters dateTimeFormatters) { - super(false); + this(null, dateTimeFormatters); + } + + /** + * Create a new WebConversionService that configures formatters with the provided + * date, time, and date-time formats, or registers the default if no custom format is + * provided. The given {@code embeddedValueResolver} is used to resolve embedded + * values such as property placeholders in + * {@link org.springframework.format.annotation.DateTimeFormat#pattern()} patterns. + * @param embeddedValueResolver the embedded value resolver to use, or {@code null} + * @param dateTimeFormatters the formatters to use for date, time, and date-time + * formatting + * @since 4.1.0 + */ + public WebConversionService(@Nullable StringValueResolver embeddedValueResolver, + DateTimeFormatters dateTimeFormatters) { + super(embeddedValueResolver, false); if (dateTimeFormatters.isCustomized()) { addFormatters(dateTimeFormatters); } diff --git a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java index 4b17ace39f3c..5e49ad9da926 100644 --- a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java +++ b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java @@ -29,8 +29,12 @@ import java.util.Calendar; import java.util.Date; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.format.annotation.DateTimeFormat; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -178,10 +182,27 @@ void convertFromStringToDateWithIsoFormatting() { assertThat(calendar.get(Calendar.DAY_OF_MONTH)).isOne(); } + @Test + void embeddedValueResolverIsAppliedToAnnotationPattern() throws Exception { + WebConversionService conversionService = new WebConversionService( + (value) -> value.replace("${my.date.format}", "yyyy-MM-dd"), new DateTimeFormatters()); + TypeDescriptor dateType = new TypeDescriptor(FormattedDate.class.getDeclaredField("date")); + Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC")).toInstant()); + assertThat(conversionService.convert(date, dateType, TypeDescriptor.valueOf(String.class))) + .isEqualTo("2000-01-02"); + } + private void customDateFormat(Object input) { WebConversionService conversionService = new WebConversionService( new DateTimeFormatters().dateFormat("dd*MM*yyyy")); assertThat(conversionService.convert(input, String.class)).isEqualTo("01*01*2018"); } + static class FormattedDate { + + @DateTimeFormat(pattern = "${my.date.format}") + @Nullable Date date; + + } + } diff --git a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java index d19f3a49b061..b39129feea4b 100644 --- a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java +++ b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java @@ -59,6 +59,7 @@ import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Format; import org.springframework.boot.webflux.filter.OrderedHiddenHttpMethodFilter; import org.springframework.context.ApplicationContext; +import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -73,6 +74,7 @@ import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.StringValueResolver; import org.springframework.validation.Validator; import org.springframework.web.accept.ApiVersionParser; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; @@ -312,7 +314,8 @@ private void configureApiVersioningUse(ApiVersionConfigurer configurer, Use use) @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ WebProperties.class, ServerProperties.class }) @ImportRuntimeHints(WebFluxValidatorRuntimeHints.class) - static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration { + static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration + implements EmbeddedValueResolverAware { private final WebFluxProperties webFluxProperties; @@ -322,6 +325,8 @@ static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration { private final @Nullable WebFluxRegistrations webFluxRegistrations; + private @Nullable StringValueResolver embeddedValueResolver; + EnableWebFluxConfiguration(WebFluxProperties webFluxProperties, WebProperties webProperties, ServerProperties serverProperties, ObjectProvider webFluxRegistrations) { this.webFluxProperties = webFluxProperties; @@ -334,7 +339,7 @@ static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration { @Override public FormattingConversionService webFluxConversionService() { Format format = this.webFluxProperties.getFormat(); - WebConversionService conversionService = new WebConversionService( + WebConversionService conversionService = new WebConversionService(this.embeddedValueResolver, new DateTimeFormatters().dateFormat(format.getDate()) .timeFormat(format.getTime()) .dateTimeFormat(format.getDateTime())); @@ -342,6 +347,11 @@ public FormattingConversionService webFluxConversionService() { return conversionService; } + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } + @Bean @Override public Validator webFluxValidator() { diff --git a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java index 43084d0f1c48..6eb10ac2724a 100644 --- a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java +++ b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java @@ -74,10 +74,12 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; 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.annotation.DateTimeFormat; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.CacheControl; import org.springframework.http.ResponseCookie; @@ -340,6 +342,17 @@ void customDateTimeTimeFormat() { }); } + @Test + void embeddedValueResolverIsAppliedToConversionService() throws Exception { + this.contextRunner.withPropertyValues("my.date.format=yyyy-MM-dd").run((context) -> { + FormattingConversionService conversionService = context.getBean(FormattingConversionService.class); + TypeDescriptor dateType = new TypeDescriptor(FormattedDate.class.getDeclaredField("date")); + Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC")).toInstant()); + assertThat(conversionService.convert(date, dateType, TypeDescriptor.valueOf(String.class))) + .isEqualTo("2000-01-02"); + }); + } + @Test void validatorWhenNoValidatorShouldUseDefault() { this.contextRunner.run((context) -> { @@ -1333,4 +1346,11 @@ ApiVersionParser apiVersionParser() { } + static class FormattedDate { + + @DateTimeFormat(pattern = "${my.date.format}") + @Nullable Date date; + + } + } diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java index 8697b65a3a64..cd00acd22240 100644 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java @@ -67,6 +67,7 @@ import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Apiversion.Use; import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Format; import org.springframework.context.ApplicationContext; +import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -87,6 +88,7 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringValueResolver; import org.springframework.validation.DefaultMessageCodesResolver; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; @@ -438,7 +440,8 @@ static RequestContextFilter requestContextFilter() { @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(WebProperties.class) @ImportRuntimeHints(MvcValidatorRuntimeHints.class) - static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware { + static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration + implements ResourceLoaderAware, EmbeddedValueResolverAware { private final Resources resourceProperties; @@ -450,6 +453,8 @@ static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration imp private final @Nullable WebMvcRegistrations mvcRegistrations; + private @Nullable StringValueResolver embeddedValueResolver; + @SuppressWarnings("NullAway.Init") private ResourceLoader resourceLoader; @@ -566,7 +571,7 @@ public RequestToViewNameTranslator viewNameTranslator() { @Override public FormattingConversionService mvcConversionService() { Format format = this.mvcProperties.getFormat(); - WebConversionService conversionService = new WebConversionService( + WebConversionService conversionService = new WebConversionService(this.embeddedValueResolver, new DateTimeFormatters().dateFormat(format.getDate()) .timeFormat(format.getTime()) .dateTimeFormat(format.getDateTime())); @@ -574,6 +579,11 @@ public FormattingConversionService mvcConversionService() { return conversionService; } + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } + @Bean @Override public Validator mvcValidator() { diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java index 38c329ca4283..5387a811399d 100644 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java @@ -78,11 +78,13 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.format.Parser; import org.springframework.format.Printer; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; @@ -509,6 +511,17 @@ void customDateTimeTimeFormat() { }); } + @Test + void embeddedValueResolverIsAppliedToConversionService() throws Exception { + this.contextRunner.withPropertyValues("my.date.format=yyyy-MM-dd").run((context) -> { + FormattingConversionService conversionService = context.getBean(FormattingConversionService.class); + TypeDescriptor dateType = new TypeDescriptor(FormattedDate.class.getDeclaredField("date")); + Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC")).toInstant()); + assertThat(conversionService.convert(date, dateType, TypeDescriptor.valueOf(String.class))) + .isEqualTo("2000-01-02"); + }); + } + @Test void noMessageCodesResolver() { this.contextRunner.run( @@ -1756,4 +1769,11 @@ ServerHttpMessageConvertersCustomizer customizer3() { } + static class FormattedDate { + + @DateTimeFormat(pattern = "${my.date.format}") + @Nullable Date date; + + } + } From 13c795a1025040772ddda05ae6e21e40ea27424b Mon Sep 17 00:00:00 2001 From: Philemon Hilscher Date: Sat, 28 Jun 2025 18:46:38 +0200 Subject: [PATCH 436/930] Enable ansi support if Windows 11 or later is detected See gh-49571 Signed-off-by: Philemon Hilscher --- .../springframework/boot/ansi/AnsiOutput.java | 15 ++++++++-- .../boot/ansi/AnsiOutputTests.java | 30 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java index 22f7afffd107..835317e87c85 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java @@ -18,6 +18,7 @@ import java.io.Console; import java.lang.reflect.Method; +import java.util.List; import java.util.Locale; import org.jspecify.annotations.Nullable; @@ -31,6 +32,7 @@ * * @author Phillip Webb * @author Yong-Hyun Kim + * @author Philemon Hilscher * @since 1.0.0 */ public abstract class AnsiOutput { @@ -43,7 +45,7 @@ public abstract class AnsiOutput { private static @Nullable Boolean ansiCapable; - private static final String OPERATING_SYSTEM_NAME = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); + private static final String OPERATING_SYSTEM_NAME = System.getProperty("os.name"); private static final String ENCODE_START = "\033["; @@ -51,6 +53,8 @@ public abstract class AnsiOutput { private static final String RESET = "0;" + AnsiColor.DEFAULT; + private static final List WINDOWS_ANSI_COMPATIBLE = List.of("Windows 11", "Windows Server 2025"); + /** * Sets if ANSI output is enabled. * @param enabled if ANSI is enabled, disabled or detected @@ -171,13 +175,20 @@ private static boolean detectIfAnsiCapable() { } } } - return !(OPERATING_SYSTEM_NAME.contains("win")); + if (isWindows(OPERATING_SYSTEM_NAME)) { + return WINDOWS_ANSI_COMPATIBLE.contains(OPERATING_SYSTEM_NAME); + } + return true; } catch (Throwable ex) { return false; } } + static boolean isWindows(String osName) { + return osName.toLowerCase(Locale.ENGLISH).contains("win"); + } + /** * Possible values to pass to {@link AnsiOutput#setEnabled}. Determines when to output * ANSI escape sequences for coloring application output. diff --git a/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java b/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java index a89c3ece113d..fd413750fe78 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java @@ -16,18 +16,25 @@ package org.springframework.boot.ansi; +import java.util.stream.Stream; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.ansi.AnsiOutput.Enabled; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for {@link AnsiOutput}. * * @author Phillip Webb + * @author Philemon Hilscher */ class AnsiOutputTests { @@ -48,4 +55,27 @@ void encoding() { assertThat(encoded).isEqualTo("ABDEF"); } + private static Stream provideOsNames() { + return Stream.of( + Arguments.of("", false), + Arguments.of("Windows NT 3.51", true), + Arguments.of("Windows 7", true), + Arguments.of("Windows 8", true), + Arguments.of("Windows 8.1", true), + Arguments.of("Windows 10", true), + Arguments.of("Windows 11", true), + Arguments.of("Windows Server 2025", true), + Arguments.of("Linux", false), + Arguments.of("Mac OS X", false), + Arguments.of("Mac OS", false) + ); + } + + @ParameterizedTest + @MethodSource("provideOsNames") + void testDetectIfIsWindows(String osName, boolean expected) { + boolean actual = AnsiOutput.isWindows(osName); + assertEquals(expected, actual); + } + } From 11d149850994bfa6bb05418384003cf6885630f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 3 Apr 2026 08:53:41 +0200 Subject: [PATCH 437/930] Polish "Enable ansi support if Windows 11 or later is detected" See gh-49571 --- .../springframework/boot/ansi/AnsiOutput.java | 10 ++----- .../boot/ansi/AnsiOutputTests.java | 30 ------------------- 2 files changed, 3 insertions(+), 37 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java index 835317e87c85..c0d9f272f0b0 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java @@ -47,14 +47,14 @@ public abstract class AnsiOutput { private static final String OPERATING_SYSTEM_NAME = System.getProperty("os.name"); + private static final List WINDOWS_ANSI_COMPATIBLE = List.of("Windows 11", "Windows Server 2025"); + private static final String ENCODE_START = "\033["; private static final String ENCODE_END = "m"; private static final String RESET = "0;" + AnsiColor.DEFAULT; - private static final List WINDOWS_ANSI_COMPATIBLE = List.of("Windows 11", "Windows Server 2025"); - /** * Sets if ANSI output is enabled. * @param enabled if ANSI is enabled, disabled or detected @@ -175,7 +175,7 @@ private static boolean detectIfAnsiCapable() { } } } - if (isWindows(OPERATING_SYSTEM_NAME)) { + if (OPERATING_SYSTEM_NAME.toLowerCase(Locale.ENGLISH).contains("win")) { return WINDOWS_ANSI_COMPATIBLE.contains(OPERATING_SYSTEM_NAME); } return true; @@ -185,10 +185,6 @@ private static boolean detectIfAnsiCapable() { } } - static boolean isWindows(String osName) { - return osName.toLowerCase(Locale.ENGLISH).contains("win"); - } - /** * Possible values to pass to {@link AnsiOutput#setEnabled}. Determines when to output * ANSI escape sequences for coloring application output. diff --git a/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java b/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java index fd413750fe78..a89c3ece113d 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java @@ -16,25 +16,18 @@ package org.springframework.boot.ansi; -import java.util.stream.Stream; - import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.ansi.AnsiOutput.Enabled; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for {@link AnsiOutput}. * * @author Phillip Webb - * @author Philemon Hilscher */ class AnsiOutputTests { @@ -55,27 +48,4 @@ void encoding() { assertThat(encoded).isEqualTo("ABDEF"); } - private static Stream provideOsNames() { - return Stream.of( - Arguments.of("", false), - Arguments.of("Windows NT 3.51", true), - Arguments.of("Windows 7", true), - Arguments.of("Windows 8", true), - Arguments.of("Windows 8.1", true), - Arguments.of("Windows 10", true), - Arguments.of("Windows 11", true), - Arguments.of("Windows Server 2025", true), - Arguments.of("Linux", false), - Arguments.of("Mac OS X", false), - Arguments.of("Mac OS", false) - ); - } - - @ParameterizedTest - @MethodSource("provideOsNames") - void testDetectIfIsWindows(String osName, boolean expected) { - boolean actual = AnsiOutput.isWindows(osName); - assertEquals(expected, actual); - } - } From cdbf74fc3a6a04776fb3db1d0a162bca4d577bf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 3 Apr 2026 12:33:35 +0200 Subject: [PATCH 438/930] Document missing duration unit in GrpcClientProperties See gh-49722 Closes gh-49879 --- .../client/autoconfigure/GrpcClientProperties.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java index 71bccb055ba1..a2638489cfa9 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java @@ -25,8 +25,10 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.bind.Name; +import org.springframework.boot.convert.DataSizeUnit; import org.springframework.boot.convert.DurationUnit; import org.springframework.util.unit.DataSize; +import org.springframework.util.unit.DataUnit; /** * Configuration properties for gRPC clients. @@ -167,9 +169,11 @@ public Metadata getMetadata() { public static class Message { /** - * Maximum message size allowed to be received by the channel. Set to '-1' - * to use the highest possible limit (not recommended). + * Maximum message size allowed to be received by the channel. If a data + * size suffix is not specified, bytes will be used. Set to '-1' to use + * the highest possible limit (not recommended). */ + @DataSizeUnit(DataUnit.BYTES) private DataSize maxSize = DataSize.ofBytes(4194304); public DataSize getMaxSize() { @@ -188,9 +192,11 @@ public void setMaxSize(DataSize maxSize) { public static class Metadata { /** - * Maximum metadata size allowed to be received by the channel. Set to - * '-1' to use the highest possible limit (not recommended). + * Maximum metadata size allowed to be received by the channel. If a data + * size suffix is not specified, bytes will be used.Set to '-1' to use the + * highest possible limit (not recommended). */ + @DataSizeUnit(DataUnit.BYTES) private DataSize maxSize = DataSize.ofBytes(8192); public DataSize getMaxSize() { From 2d4c2355224135e559b8d438a28291906df0fb96 Mon Sep 17 00:00:00 2001 From: 1233day <1233day@naver.com> Date: Tue, 24 Mar 2026 09:44:19 +0900 Subject: [PATCH 439/930] Add support for additional Homebrew home on macOS Add /opt/homebrew/bin as an additional macOS fallback location when starting external processes. The previous fallback assumed /usr/local/bin only, which can fail on Apple Silicon Homebrew setups in restricted PATH environments (for example, IDE or UI-launched processes). Update CredentialHelperTests to verify both macOS fallback paths are attempted. See gh-49721 Signed-off-by: 1233day <1233day@naver.com> --- .../configuration/CredentialHelper.java | 28 ++++++++++----- .../configuration/CredentialHelperTests.java | 4 ++- .../docker/compose/core/ProcessRunner.java | 25 ++++++++++--- ...DisabledIfProcessUnavailableCondition.java | 36 +++++++++++++------ 4 files changed, 68 insertions(+), 25 deletions(-) diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java index 6169667708d1..44364674b17f 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java @@ -41,6 +41,10 @@ class CredentialHelper { private static final String USR_LOCAL_BIN = "/usr/local/bin/"; + private static final String OPT_HOMEBREW_BIN = "/opt/homebrew/bin/"; + + private static final String[] MAC_OS_BIN_DIRECTORIES = { OPT_HOMEBREW_BIN, USR_LOCAL_BIN }; + private static final Set CREDENTIAL_NOT_FOUND_MESSAGES = Set.of("credentials not found in native keychain", "no credentials server URL", "no credentials username"); @@ -92,16 +96,22 @@ private Process start(ProcessBuilder processBuilder) throws IOException { if (!Platform.isMac()) { throw ex; } - try { - List command = new ArrayList<>(processBuilder.command()); - command.set(0, USR_LOCAL_BIN + command.get(0)); - return processBuilder.command(command).start(); - } - catch (Exception suppressed) { - // Suppresses the exception and rethrows the original exception - ex.addSuppressed(suppressed); - throw ex; + String executable = processBuilder.command().get(0); + for (String binDirectory : MAC_OS_BIN_DIRECTORIES) { + try { + List command = new ArrayList<>(processBuilder.command()); + if (executable.startsWith(binDirectory)) { + continue; + } + command.set(0, binDirectory + executable); + return processBuilder.command(command).start(); + } + catch (Exception suppressed) { + // Suppresses the exception and rethrows the original exception + ex.addSuppressed(suppressed); + } } + throw ex; } } diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java index 84a14ddd7f5b..1c7ff4da54cb 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java @@ -106,7 +106,9 @@ void getWhenExecutableDoesNotExistErrorThrowsException() { .satisfies((ex) -> { if (Platform.isMac()) { assertThat(ex.getMessage()).doesNotContain("/usr/local/bin/"); - assertThat(ex.getSuppressed()).allSatisfy((suppressed) -> assertThat(suppressed) + assertThat(ex.getSuppressed()).anySatisfy((suppressed) -> assertThat(suppressed) + .hasMessageContaining("/opt/homebrew/bin/" + executable)); + assertThat(ex.getSuppressed()).anySatisfy((suppressed) -> assertThat(suppressed) .hasMessageContaining("/usr/local/bin/" + executable)); } }); diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java index 9a603aecedae..03a337367a7f 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java @@ -44,6 +44,10 @@ class ProcessRunner { private static final String USR_LOCAL_BIN = "/usr/local/bin"; + private static final String OPT_HOMEBREW_BIN = "/opt/homebrew/bin"; + + private static final String[] MAC_OS_BIN_DIRECTORIES = { OPT_HOMEBREW_BIN, USR_LOCAL_BIN }; + private static final boolean MAC_OS = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac"); private static final Log logger = LogFactory.getLog(ProcessRunner.class); @@ -108,11 +112,22 @@ private Process startProcess(String[] command) { } catch (IOException ex) { String path = processBuilder.environment().get("PATH"); - if (MAC_OS && path != null && !path.contains(USR_LOCAL_BIN) - && !command[0].startsWith(USR_LOCAL_BIN + "/")) { - String[] localCommand = command.clone(); - localCommand[0] = USR_LOCAL_BIN + "/" + localCommand[0]; - return startProcess(localCommand); + if (MAC_OS && path != null) { + for (String binDirectory : MAC_OS_BIN_DIRECTORIES) { + if (path.contains(binDirectory) || command[0].startsWith(binDirectory + "/")) { + continue; + } + String[] localCommand = command.clone(); + localCommand[0] = binDirectory + "/" + command[0]; + ProcessBuilder localProcessBuilder = new ProcessBuilder(localCommand); + localProcessBuilder.directory(this.workingDirectory); + try { + return localProcessBuilder.start(); + } + catch (IOException suppressed) { + ex.addSuppressed(suppressed); + } + } } throw new ProcessStartException(command, ex); } diff --git a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java index 6996e38c06df..397f73faf2ed 100644 --- a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java +++ b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java @@ -43,6 +43,10 @@ class DisabledIfProcessUnavailableCondition implements ExecutionCondition { private static final String USR_LOCAL_BIN = "/usr/local/bin"; + private static final String OPT_HOMEBREW_BIN = "/opt/homebrew/bin"; + + private static final String[] MAC_OS_BIN_DIRECTORIES = { OPT_HOMEBREW_BIN, USR_LOCAL_BIN }; + private static final boolean MAC_OS = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac"); @Override @@ -68,23 +72,35 @@ private Stream getAnnotationValue(AnnotatedElement testElement) { private void check(String[] command) { ProcessBuilder processBuilder = new ProcessBuilder(command); try { - Process process = processBuilder.start(); - Assert.state(process.waitFor(30, TimeUnit.SECONDS), "Process did not exit within 30 seconds"); - Assert.state(process.exitValue() == 0, () -> "Process exited with %d".formatted(process.exitValue())); - process.destroy(); + check(processBuilder); } catch (Exception ex) { String path = processBuilder.environment().get("PATH"); - if (MAC_OS && path != null && !path.contains(USR_LOCAL_BIN) - && !command[0].startsWith(USR_LOCAL_BIN + "/")) { - String[] localCommand = command.clone(); - localCommand[0] = USR_LOCAL_BIN + "/" + localCommand[0]; - check(localCommand); - return; + if (MAC_OS && path != null) { + for (String binDirectory : MAC_OS_BIN_DIRECTORIES) { + if (path.contains(binDirectory) || command[0].startsWith(binDirectory + "/")) { + continue; + } + String[] localCommand = command.clone(); + localCommand[0] = binDirectory + "/" + command[0]; + try { + check(new ProcessBuilder(localCommand)); + return; + } + catch (Exception ignored) { + } + } } throw new RuntimeException( "Unable to start process '%s'".formatted(StringUtils.arrayToDelimitedString(command, " "))); } } + private void check(ProcessBuilder processBuilder) throws Exception { + Process process = processBuilder.start(); + Assert.state(process.waitFor(30, TimeUnit.SECONDS), "Process did not exit within 30 seconds"); + Assert.state(process.exitValue() == 0, () -> "Process exited with %d".formatted(process.exitValue())); + process.destroy(); + } + } From 1519bcf1e237a5430e8a6676fae41577c921eac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 3 Apr 2026 13:18:05 +0200 Subject: [PATCH 440/930] Polish "Add support for additional Homebrew home on macOS" See gh-49721 --- .../docker/configuration/CredentialHelper.java | 11 ++++------- .../docker/configuration/CredentialHelperTests.java | 9 +++++---- .../boot/docker/compose/core/ProcessRunner.java | 2 +- .../DisabledIfProcessUnavailableCondition.java | 5 +++-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java index 44364674b17f..e2786a90bb02 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java @@ -96,18 +96,15 @@ private Process start(ProcessBuilder processBuilder) throws IOException { if (!Platform.isMac()) { throw ex; } - String executable = processBuilder.command().get(0); + List originalCommand = List.copyOf(processBuilder.command()); + String executable = originalCommand.get(0); for (String binDirectory : MAC_OS_BIN_DIRECTORIES) { + List command = new ArrayList<>(originalCommand); + command.set(0, binDirectory + executable); try { - List command = new ArrayList<>(processBuilder.command()); - if (executable.startsWith(binDirectory)) { - continue; - } - command.set(0, binDirectory + executable); return processBuilder.command(command).start(); } catch (Exception suppressed) { - // Suppresses the exception and rethrows the original exception ex.addSuppressed(suppressed); } } diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java index 1c7ff4da54cb..6a8b8251c5df 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java @@ -106,10 +106,11 @@ void getWhenExecutableDoesNotExistErrorThrowsException() { .satisfies((ex) -> { if (Platform.isMac()) { assertThat(ex.getMessage()).doesNotContain("/usr/local/bin/"); - assertThat(ex.getSuppressed()).anySatisfy((suppressed) -> assertThat(suppressed) - .hasMessageContaining("/opt/homebrew/bin/" + executable)); - assertThat(ex.getSuppressed()).anySatisfy((suppressed) -> assertThat(suppressed) - .hasMessageContaining("/usr/local/bin/" + executable)); + assertThat(ex.getSuppressed()).satisfiesExactlyInAnyOrder( + (suppressed) -> assertThat(suppressed) + .hasMessageContaining("/opt/homebrew/bin/" + executable), + (suppressed) -> assertThat(suppressed) + .hasMessageContaining("/usr/local/bin/" + executable)); } }); } diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java index 03a337367a7f..1e82bd68be4a 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java @@ -114,7 +114,7 @@ private Process startProcess(String[] command) { String path = processBuilder.environment().get("PATH"); if (MAC_OS && path != null) { for (String binDirectory : MAC_OS_BIN_DIRECTORIES) { - if (path.contains(binDirectory) || command[0].startsWith(binDirectory + "/")) { + if (path.contains(binDirectory) || command[0].startsWith("/")) { continue; } String[] localCommand = command.clone(); diff --git a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java index 397f73faf2ed..fa939a46d5c3 100644 --- a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java +++ b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java @@ -78,7 +78,7 @@ private void check(String[] command) { String path = processBuilder.environment().get("PATH"); if (MAC_OS && path != null) { for (String binDirectory : MAC_OS_BIN_DIRECTORIES) { - if (path.contains(binDirectory) || command[0].startsWith(binDirectory + "/")) { + if (path.contains(binDirectory) || command[0].startsWith("/")) { continue; } String[] localCommand = command.clone(); @@ -87,7 +87,8 @@ private void check(String[] command) { check(new ProcessBuilder(localCommand)); return; } - catch (Exception ignored) { + catch (Exception suppressed) { + ex.addSuppressed(suppressed); } } } From 85af0d645d4f648fea6a433b720453d09350ef08 Mon Sep 17 00:00:00 2001 From: onobc Date: Mon, 6 Apr 2026 15:02:32 -0500 Subject: [PATCH 441/930] Upgrade to Pulsar 4.2.0 This commit updates the managed version of Pulsar from `4.1.3` to `4.2.0` and the test image version from `3.3.3` to `4.2.0`. Since Pulsar `4.2.0` standalone mode advertises the container's FQDN (the container ID) instead of localhost. The Pulsar client reconnects to the advertised address during servicediscovery, but is unable to reach the FQDN. Therefore, this also sets the `advertisedAddress` on the Pulsar testcontainers used by the tests. See gh-49900 Signed-off-by: onobc --- platform/spring-boot-dependencies/build.gradle | 2 +- .../boot/testsupport/container/TestImage.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 94c4c54d9adf..cc7b335552c7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1898,7 +1898,7 @@ bom { releaseNotes("https://github.com/prometheus/client_java/releases/tag/parent-{version}") } } - library("Pulsar", "4.1.3") { + library("Pulsar", "4.2.0") { group("org.apache.pulsar") { bom("pulsar-bom") } diff --git a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java index 02e3ec661007..6d7d83ed93ff 100644 --- a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java +++ b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java @@ -238,9 +238,10 @@ public enum TestImage { /** * A container image suitable for testing Pulsar. */ - PULSAR("apachepulsar/pulsar", "3.3.3", () -> PulsarContainer.class, + PULSAR("apachepulsar/pulsar", "4.2.0", () -> PulsarContainer.class, (container) -> ((PulsarContainer) container).withStartupAttempts(2) - .withStartupTimeout(Duration.ofMinutes(3))), + .withStartupTimeout(Duration.ofMinutes(3)) + .withEnv("PULSAR_PREFIX_advertisedAddress", "localhost")), /** * A container image suitable for testing Pulsar using the deprecated From d7334fed642fd7f2afb601f6ae52ed2600e5bcd5 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 7 Apr 2026 09:31:33 +0200 Subject: [PATCH 442/930] Upgrade to Native Build Tools Plugin 1.0.0 Closes gh-49920 --- ...tiveImagePluginActionIntegrationTests.java | 14 +- .../logback-classic/1.2.11/index.json | 3 - .../1.2.11/reachability-metadata.json | 297 ++++++++++++ .../1.2.11/reflect-config.json | 299 ------------ .../org.jline/jline/3.21.0/index.json | 6 - .../org.jline/jline/3.21.0/jni-config.json | 128 ----- .../org.jline/jline/3.21.0/proxy-config.json | 10 - .../jline/3.21.0/reachability-metadata.json | 380 +++++++++++++++ .../jline/3.21.0/reflect-config.json | 104 ---- .../jline/3.21.0/resource-config.json | 139 ------ ...metadata-library-index-schema-v2.0.0.json} | 61 ++- .../metadata-root-index-schema-v1.0.0.json | 67 --- .../reachability-metadata-schema-v1.2.0.json | 458 ++++++++++++++++++ gradle.properties | 2 +- 14 files changed, 1190 insertions(+), 778 deletions(-) delete mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/index.json create mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/reachability-metadata.json delete mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/reflect-config.json delete mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/index.json delete mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/jni-config.json delete mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/proxy-config.json create mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/reachability-metadata.json delete mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/reflect-config.json delete mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/resource-config.json rename build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/{metadata-library-index-schema-v1.0.0.json => metadata-library-index-schema-v2.0.0.json} (61%) delete mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-root-index-schema-v1.0.0.json create mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/reachability-metadata-schema-v1.2.0.json diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java index 688ef99de8f1..9dccd61391c3 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java @@ -67,11 +67,8 @@ void reachabilityMetadataConfigurationFilesAreCopiedToJar() throws IOException { File jarFile = new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".jar"); assertThat(buildLibs.listFiles()).contains(jarFile); assertThat(getEntryNames(jarFile)).contains( - "META-INF/native-image/ch.qos.logback/logback-classic/1.2.11/reflect-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/jni-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/proxy-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/reflect-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/resource-config.json"); + "META-INF/native-image/ch.qos.logback/logback-classic/1.2.11/reachability-metadata.json", + "META-INF/native-image/org.jline/jline/3.21.0/reachability-metadata.json"); } @TestTemplate @@ -87,11 +84,8 @@ void reachabilityMetadataConfigurationFilesFromFileRepositoryAreCopiedToJar() th File jarFile = new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".jar"); assertThat(buildLibs.listFiles()).contains(jarFile); assertThat(getEntryNames(jarFile)).contains( - "META-INF/native-image/ch.qos.logback/logback-classic/1.2.11/reflect-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/jni-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/proxy-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/reflect-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/resource-config.json"); + "META-INF/native-image/ch.qos.logback/logback-classic/1.2.11/reachability-metadata.json", + "META-INF/native-image/org.jline/jline/3.21.0/reachability-metadata.json"); } @TestTemplate diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/index.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/index.json deleted file mode 100644 index 768b5502a0b7..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/index.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "reflect-config.json" -] diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/reachability-metadata.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/reachability-metadata.json new file mode 100644 index 000000000000..d42db680a786 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/reachability-metadata.json @@ -0,0 +1,297 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.CallerDataConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.ClassOfCallerConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.ContextNameConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.DateConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.FileOfCallerConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.LevelConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.LineOfCallerConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.LineSeparatorConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.LocalSequenceNumberConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.LoggerConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.MDCConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.MarkerConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.MessageConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.MethodOfCallerConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.NopThrowableInformationConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.PrefixCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.PropertyConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.RelativeTimeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.ThreadConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.ThrowableProxyConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.color.HighlightingCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.IdentityCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.ReplacingCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BlackCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BlueCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BoldBlueCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BoldCyanCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BoldGreenCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BoldMagentaCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BoldRedCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BoldWhiteCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BoldYellowCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.CyanCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.GrayCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.GreenCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.MagentaCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.RedCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.WhiteCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.YellowCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "org.slf4j.impl.StaticLoggerBinder" + } + ] +} \ No newline at end of file diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/reflect-config.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/reflect-config.json deleted file mode 100644 index a0856ee205ba..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/reflect-config.json +++ /dev/null @@ -1,299 +0,0 @@ -[ - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.CallerDataConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.CallerDataConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.ClassOfCallerConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.ContextNameConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.DateConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.FileOfCallerConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.LevelConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.LineOfCallerConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.LineSeparatorConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.LocalSequenceNumberConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.LoggerConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.MDCConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.MarkerConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.MessageConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.MethodOfCallerConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.NopThrowableInformationConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.PrefixCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.PropertyConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.RelativeTimeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.ThreadConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.ThrowableProxyConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.color.HighlightingCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.IdentityCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.ReplacingCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BlackCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BlueCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BoldBlueCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BoldCyanCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BoldGreenCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BoldMagentaCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BoldRedCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BoldWhiteCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BoldYellowCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.CyanCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.GrayCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.GreenCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.MagentaCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.RedCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.WhiteCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.YellowCompositeConverter", - "allPublicConstructors": true - }, - { - "name": "org.slf4j.impl.StaticLoggerBinder" - } -] diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/index.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/index.json deleted file mode 100644 index 554ddf047aa7..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/index.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - "jni-config.json", - "proxy-config.json", - "reflect-config.json", - "resource-config.json" -] diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/jni-config.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/jni-config.json deleted file mode 100644 index 85d5b8db21cc..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/jni-config.json +++ /dev/null @@ -1,128 +0,0 @@ -[ - { - "condition": { - "typeReachable": "org.jline.terminal.impl.jansi.JansiNativePty" - }, - "fields": [ - { - "name": "HAVE_ISATTY" - }, - { - "name": "HAVE_TTYNAME" - }, - { - "name": "TCSADRAIN" - }, - { - "name": "TCSAFLUSH" - }, - { - "name": "TCSANOW" - }, - { - "name": "TIOCGETD" - }, - { - "name": "TIOCGWINSZ" - }, - { - "name": "TIOCSETD" - }, - { - "name": "TIOCSWINSZ" - } - ], - "name": "org.fusesource.jansi.internal.CLibrary" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.CLibrary$WinSize" - }, - "name": "org.fusesource.jansi.internal.CLibrary$WinSize" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.CLibrary$Termios" - }, - "name": "org.fusesource.jansi.internal.CLibrary$Termios" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32" - }, - "name": "org.fusesource.jansi.internal.Kernel32" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$SMALL_RECT" - }, - "name": "org.fusesource.jansi.internal.Kernel32$SMALL_RECT" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$COORD" - }, - "name": "org.fusesource.jansi.internal.Kernel32$COORD" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$CONSOLE_SCREEN_BUFFER_INFO" - }, - "name": "org.fusesource.jansi.internal.Kernel32$CONSOLE_SCREEN_BUFFER_INFO" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$CHAR_INFO" - }, - "name": "org.fusesource.jansi.internal.Kernel32$CHAR_INFO" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$KEY_EVENT_RECORD" - }, - "name": "org.fusesource.jansi.internal.Kernel32$KEY_EVENT_RECORD" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$MOUSE_EVENT_RECORD" - }, - "name": "org.fusesource.jansi.internal.Kernel32$MOUSE_EVENT_RECORD" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$WINDOW_BUFFER_SIZE_RECORD" - }, - "name": "org.fusesource.jansi.internal.Kernel32$WINDOW_BUFFER_SIZE_RECORD" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$FOCUS_EVENT_RECORD" - }, - "name": "org.fusesource.jansi.internal.Kernel32$FOCUS_EVENT_RECORD" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$MENU_EVENT_RECORD" - }, - "name": "org.fusesource.jansi.internal.Kernel32$MENU_EVENT_RECORD" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$INPUT_RECORD" - }, - "name": "org.fusesource.jansi.internal.Kernel32$INPUT_RECORD" - } -] diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/proxy-config.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/proxy-config.json deleted file mode 100644 index 30918f9df6f4..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/proxy-config.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "condition": { - "typeReachable": "org.jline.utils.Signals" - }, - "interfaces": [ - "sun.misc.SignalHandler" - ] - } -] diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/reachability-metadata.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/reachability-metadata.json new file mode 100644 index 000000000000..9e039ee47e03 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/reachability-metadata.json @@ -0,0 +1,380 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.jline.terminal.impl.jansi.JansiNativePty" + }, + "type": "java.io.FileDescriptor" + }, + { + "condition": { + "typeReached": "org.jline.terminal.TerminalBuilder" + }, + "type": "java.lang.ProcessHandle", + "methods": [ + { + "name": "current", + "parameterTypes": [] + }, + { + "name": "info", + "parameterTypes": [] + }, + { + "name": "parent", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReached": "org.jline.terminal.TerminalBuilder" + }, + "type": "java.lang.ProcessHandle$Info", + "methods": [ + { + "name": "command", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.jansi.JansiNativePty" + }, + "type": "org.fusesource.jansi.internal.CLibrary", + "jniAccessible": true, + "fields": [ + { + "name": "HAVE_ISATTY" + }, + { + "name": "HAVE_TTYNAME" + }, + { + "name": "TCSADRAIN" + }, + { + "name": "TCSAFLUSH" + }, + { + "name": "TCSANOW" + }, + { + "name": "TIOCGETD" + }, + { + "name": "TIOCGWINSZ" + }, + { + "name": "TIOCSETD" + }, + { + "name": "TIOCSWINSZ" + } + ] + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.CLibrary$Termios" + }, + "type": "org.fusesource.jansi.internal.CLibrary$Termios", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.CLibrary$WinSize" + }, + "type": "org.fusesource.jansi.internal.CLibrary$WinSize", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32" + }, + "type": "org.fusesource.jansi.internal.Kernel32", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$CHAR_INFO" + }, + "type": "org.fusesource.jansi.internal.Kernel32$CHAR_INFO", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$CONSOLE_SCREEN_BUFFER_INFO" + }, + "type": "org.fusesource.jansi.internal.Kernel32$CONSOLE_SCREEN_BUFFER_INFO", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$COORD" + }, + "type": "org.fusesource.jansi.internal.Kernel32$COORD", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$FOCUS_EVENT_RECORD" + }, + "type": "org.fusesource.jansi.internal.Kernel32$FOCUS_EVENT_RECORD", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$INPUT_RECORD" + }, + "type": "org.fusesource.jansi.internal.Kernel32$INPUT_RECORD", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$KEY_EVENT_RECORD" + }, + "type": "org.fusesource.jansi.internal.Kernel32$KEY_EVENT_RECORD", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$MENU_EVENT_RECORD" + }, + "type": "org.fusesource.jansi.internal.Kernel32$MENU_EVENT_RECORD", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$MOUSE_EVENT_RECORD" + }, + "type": "org.fusesource.jansi.internal.Kernel32$MOUSE_EVENT_RECORD", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$SMALL_RECT" + }, + "type": "org.fusesource.jansi.internal.Kernel32$SMALL_RECT", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$WINDOW_BUFFER_SIZE_RECORD" + }, + "type": "org.fusesource.jansi.internal.Kernel32$WINDOW_BUFFER_SIZE_RECORD", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.jline.builtins.Styles" + }, + "type": "org.jline.console.SystemRegistry", + "methods": [ + { + "name": "get", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReached": "org.jline.utils.Signals" + }, + "type": "sun.misc.Signal", + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "handle", + "parameterTypes": [ + "sun.misc.Signal", + "sun.misc.SignalHandler" + ] + } + ] + }, + { + "condition": { + "typeReached": "org.jline.terminal.TerminalBuilder" + }, + "type": "sun.misc.SignalHandler", + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredConstructors": true, + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "org.jline.utils.Signals" + }, + "type": "sun.misc.SignalHandler", + "fields": [ + { + "name": "SIG_DFL" + } + ] + }, + { + "condition": { + "typeReached": "org.jline.utils.Signals" + }, + "type": { + "proxy": [ + "sun.misc.SignalHandler" + ] + } + } + ], + "resources": [ + { + "condition": { + "typeReached": "org.jline.terminal.impl.jansi.JansiNativePty" + }, + "glob": "META-INF/native/windows64/jansi.dll" + }, + { + "condition": { + "typeReached": "org.jline.terminal.TerminalBuilder" + }, + "glob": "META-INF/services/org.jline.terminal.spi.JansiSupport" + }, + { + "condition": { + "typeReached": "org.jline.terminal.TerminalBuilder" + }, + "glob": "META-INF/services/org.jline.terminal.spi.JnaSupport" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.jansi.JansiNativePty" + }, + "glob": "org/fusesource/jansi/internal/native/Linux/x86_64/libjansi.so" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.jansi.JansiNativePty" + }, + "glob": "org/fusesource/jansi/jansi.properties" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.jansi.JansiSupportImpl" + }, + "glob": "org/fusesource/jansi/jansi.properties" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/ansi.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/capabilities.txt" + }, + { + "condition": { + "typeReached": "org.jline.utils.Colors" + }, + "glob": "org/jline/utils/colors.txt" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.DumbTerminal" + }, + "glob": "org/jline/utils/dumb-color.caps" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.DumbTerminal" + }, + "glob": "org/jline/utils/dumb.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/rxvt-basic.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/rxvt-unicode-256color.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/rxvt-unicode.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/rxvt.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/screen-256color.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/screen.caps" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.AbstractWindowsTerminal" + }, + "glob": "org/jline/utils/windows-256color.caps" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.AbstractWindowsTerminal" + }, + "glob": "org/jline/utils/windows-conemu.caps" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.AbstractWindowsTerminal" + }, + "glob": "org/jline/utils/windows.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/xterm-256color.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/xterm.caps" + } + ] +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/reflect-config.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/reflect-config.json deleted file mode 100644 index 665ebda94772..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/reflect-config.json +++ /dev/null @@ -1,104 +0,0 @@ -[ - { - "condition": { - "typeReachable": "org.jline.terminal.impl.jansi.JansiNativePty" - }, - "name": "java.io.FileDescriptor", - "queriedMethods": [ - { - "name": "", - "parameterTypes": [ - "int" - ] - } - ] - }, - { - "condition": { - "typeReachable": "org.jline.terminal.TerminalBuilder" - }, - "methods": [ - { - "name": "current", - "parameterTypes": [] - }, - { - "name": "info", - "parameterTypes": [] - }, - { - "name": "parent", - "parameterTypes": [] - } - ], - "name": "java.lang.ProcessHandle" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.TerminalBuilder" - }, - "methods": [ - { - "name": "command", - "parameterTypes": [] - } - ], - "name": "java.lang.ProcessHandle$Info" - }, - { - "condition": { - "typeReachable": "org.jline.builtins.Styles" - }, - "methods": [ - { - "name": "get", - "parameterTypes": [] - } - ], - "name": "org.jline.console.SystemRegistry" - }, - { - "condition": { - "typeReachable": "org.jline.utils.Signals" - }, - "methods": [ - { - "name": "", - "parameterTypes": [ - "java.lang.String" - ] - }, - { - "name": "handle", - "parameterTypes": [ - "sun.misc.Signal", - "sun.misc.SignalHandler" - ] - } - ], - "name": "sun.misc.Signal" - }, - { - "condition": { - "typeReachable": "org.jline.utils.Signals" - }, - "fields": [ - { - "name": "SIG_DFL" - } - ], - "name": "sun.misc.SignalHandler" - }, - { - "allDeclaredClasses": true, - "allDeclaredConstructors": true, - "allDeclaredMethods": true, - "allPublicClasses": true, - "allPublicConstructors": true, - "allPublicMethods": true, - "condition": { - "typeReachable": "org.jline.terminal.TerminalBuilder" - }, - "name": "sun.misc.SignalHandler" - } -] diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/resource-config.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/resource-config.json deleted file mode 100644 index 65fed15e1053..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/resource-config.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "bundles": [], - "resources": { - "includes": [ - { - "condition": { - "typeReachable": "org.jline.terminal.TerminalBuilder" - }, - "pattern": "\\QMETA-INF/services/org.jline.terminal.spi.JansiSupport\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.TerminalBuilder" - }, - "pattern": "\\QMETA-INF/services/org.jline.terminal.spi.JnaSupport\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.jansi.JansiNativePty" - }, - "pattern": "\\Qorg/fusesource/jansi/internal/native/Linux/x86_64/libjansi.so\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.jansi.JansiNativePty" - }, - "pattern": "\\QMETA-INF/native/windows64/jansi.dll\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.jansi.JansiNativePty" - }, - "pattern": "\\Qorg/fusesource/jansi/jansi.properties\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.jansi.JansiSupportImpl" - }, - "pattern": "\\Qorg/fusesource/jansi/jansi.properties\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/capabilities.txt\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.Colors" - }, - "pattern": "\\Qorg/jline/utils/colors.txt\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/ansi.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.DumbTerminal" - }, - "pattern": "\\Qorg/jline/utils/dumb.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.DumbTerminal" - }, - "pattern": "\\Qorg/jline/utils/dumb-color.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/rxvt.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/rxvt-basic.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/rxvt-unicode.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/rxvt-unicode-256color.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/screen.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/screen-256color.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.AbstractWindowsTerminal" - }, - "pattern": "\\Qorg/jline/utils/windows.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.AbstractWindowsTerminal" - }, - "pattern": "\\Qorg/jline/utils/windows-256color.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.AbstractWindowsTerminal" - }, - "pattern": "\\Qorg/jline/utils/windows-conemu.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/xterm.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/xterm-256color.caps\\E" - } - ] - } -} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v1.0.0.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v2.0.0.json similarity index 61% rename from build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v1.0.0.json rename to build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v2.0.0.json index 2461661166eb..ef7edc27ba2c 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v1.0.0.json +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v2.0.0.json @@ -1,5 +1,5 @@ { - "$id": "https://raw.githubusercontent.com/oracle/graalvm-reachability-metadata/master/metadata/schemas/metadata-library-index-schema-v1.0.0.json", + "$id": "https://raw.githubusercontent.com/oracle/graalvm-reachability-metadata/master/metadata/schemas/metadata-library-index-schema-v2.0.0.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "Schema for metadata///index.json. Each entry describes a metadata bundle for a range of library versions.", "type": "array", @@ -8,15 +8,11 @@ "type": "object", "additionalProperties": false, "required": [ - "module", + "allowed-packages", "metadata-version", "tested-versions" ], "properties": { - "module": { - "$ref": "#/$defs/moduleCoordinate", - "description": "Maven coordinates in the form ':'." - }, "metadata-version": { "type": "string", "minLength": 1, @@ -37,6 +33,26 @@ "minLength": 1, "description": "Overrides 'metadata-version' for use in test directories. This allows one metadata entry to share tests defined for a different version's metadata directory." }, + "source-code-url": { + "type": "string", + "minLength": 1, + "description": "Optional URL pointing to the metadata source code for this entry." + }, + "repository-url": { + "type": "string", + "minLength": 1, + "description": "Optional URL pointing to the canonical project repository for this entry." + }, + "documentation-url": { + "type": "string", + "minLength": 1, + "description": "Optional URL pointing to the project documentation for this entry." + }, + "test-code-url": { + "type": "string", + "minLength": 1, + "description": "Optional URL pointing to the test source code for this entry." + }, "latest": { "type": "boolean", "description": "Marks this entry as the latest/default metadata for currently supported versions." @@ -49,6 +65,16 @@ "type": "boolean", "description": "When true, expresses the intent to exclude outdated built-in metadata shipped with Native Image for the matched versions." }, + "allowed-packages": { + "type": "array", + "description": "Packages expected to contain metadata entries for this library. Used by CI checks.", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, "skipped-versions": { "type": "array", "description": "Versions explicitly excluded from support, each with a reason.", @@ -68,6 +94,15 @@ } } } + }, + "requires": { + "type": "array", + "description": "List of dependent modules (group:artifact) whose metadata is required at runtime.", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/moduleCoordinate" + } } } }, @@ -81,22 +116,26 @@ [ { "metadata-version": "0.0.1", - "module": "org.example:library", "tested-versions": [ "0.0.1", "0.0.2" ], "default-for": "0\\.0\\..*", - "test-version": "0.0.1" + "test-version": "0.0.1", + "source-code-url": "https://github.com/example/project/tree/main/metadata/org.example/library/0.0.1", + "repository-url": "https://github.com/example/project", + "test-code-url": "https://github.com/example/project/tree/main/tests/src/org.example/library/0.0.1", + "documentation-url": "https://github.com/example/project/tree/main/README.md", + "allowed-packages": [ "org.example.library" ] }, { "latest": true, "metadata-version": "1.0.0", - "module": "org.example:library", - "tested-versions": [ "1.0.0", "1.1.0" , "1.2.0"] + "tested-versions": [ "1.0.0", "1.1.0" , "1.2.0"], + "allowed-packages": [ "org.example.library" ] }, { "metadata-version": "1.19.0", - "module": "io.opentelemetry:opentelemetry-sdk-trace", "tested-versions": [ "1.19.0" ], "override": true, + "allowed-packages": [ "io.opentelemetry" ], "skipped-versions": [ { "version": "1.34.0", "reason": "Dependency io.opentelemetry:opentelemetry-api:1.34.0 provides reflect-config.json which does not parse." } ] diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-root-index-schema-v1.0.0.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-root-index-schema-v1.0.0.json deleted file mode 100644 index 7087aaa1d29c..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-root-index-schema-v1.0.0.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "$id": "https://raw.githubusercontent.com/oracle/graalvm-reachability-metadata/master/metadata/schemas/metadata-root-index-schema-v1.0.0.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "Schema for metadata/index.json. This file lists modules known to the repository and optionally the directory where their metadata is stored, the allowed package prefixes for metadata, and inter-module requirements. See docs/CONTRIBUTING.md (Metadata structure).", - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["module"], - "properties": { - "module": { - "$ref": "#/$defs/moduleCoordinate", - "description": "Maven coordinates for the module in the form ':'." - }, - "directory": { - "type": "string", - "minLength": 1, - "pattern": "^[^\\s].*$", - "description": "Repository-relative path under 'metadata/' containing this module's metadata (e.g. 'org.example/library'). If omitted, the entry may reference requirements only." - }, - "allowed-packages": { - "type": "array", - "description": "List of package (or fully-qualified name) prefixes considered valid sources of metadata entries for this module. Used to filter-in relevant JSON entries.", - "minItems": 1, - "uniqueItems": true, - "items": { - "type": "string", - "minLength": 1 - } - }, - "requires": { - "type": "array", - "description": "Optional list of module coordinates this module depends on. Each item is ':'.", - "minItems": 1, - "uniqueItems": true, - "items": { - "$ref": "#/$defs/moduleCoordinate" - } - } - } - }, - "$defs": { - "moduleCoordinate": { - "type": "string", - "pattern": "^[^:]+:[^:]+$" - } - }, - "examples": [ - [ - { - "directory": "org.example/library", - "module": "org.example:library" - }, - { - "allowed-packages": ["org.package.name"], - "module": "org.example:dependant-library", - "requires": ["org.example:library"] - }, - { - "allowed-packages" : [ "org.hibernate", "jakarta" ], - "directory" : "org.hibernate.orm/hibernate-envers", - "module" : "org.hibernate.orm:hibernate-envers", - "requires" : [ "org.hibernate.orm:hibernate-core" ] - } - ] - ] -} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/reachability-metadata-schema-v1.2.0.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/reachability-metadata-schema-v1.2.0.json new file mode 100644 index 000000000000..ef4ca27a589e --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/reachability-metadata-schema-v1.2.0.json @@ -0,0 +1,458 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/reachability-metadata-schema-v1.2.0.json", + "title": "JSON schema for the reachability metadata used by GraalVM Native Image", + "version": "1.2.0", + "type": "object", + "default": {}, + "properties": { + "comment": { + "title": "Comment(s) applying to the whole file (e.g., generation date, author, etc.)", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "default": "" + }, + "reflection": { + "title": "Metadata to ensure elements are accessible via reflection", + "type": "array", + "default": [], + "items": { + "title": "Elements that should be registered for reflection for a specified type", + "type": "object", + "required": ["type"], + "properties": { + "reason": { + "$ref": "#/$defs/reason" + }, + "condition": { + "$ref": "#/$defs/condition" + }, + "type": { + "title": "Type descriptor of the class that should be registered for reflection", + "$ref": "#/$defs/type" + }, + "methods": { + "title": "List of methods that should be registered for the type declared in ", + "type": "array", + "default": [], + "items": { + "title": "Method descriptor of the method that should be registered for reflection", + "$ref": "#/$defs/method" + } + }, + "fields": { + "title": "List of class fields that can be read or written to for the type declared in ", + "type": "array", + "default": [], + "items": { + "title": "Field descriptor of the field that should be registered for reflection", + "type": "object", + "properties": { + "name": { + "title": "Name of the field that should be registered for reflection", + "type": "string", + "pattern": "^[^.;\\[/]+$" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + }, + "allDeclaredMethods": { + "title": "Register all declared methods from the type for reflective invocation", + "type": "boolean", + "default": false + }, + "allDeclaredFields": { + "title": "Register all declared fields from the type for reflective access", + "type": "boolean", + "default": false + }, + "allDeclaredConstructors": { + "title": "Register all declared constructors from the type for reflective invocation", + "type": "boolean", + "default": false + }, + "allPublicMethods": { + "title": "Register all public methods from the type for reflective invocation", + "type": "boolean", + "default": false + }, + "allPublicFields": { + "title": "Register all public fields from the type for reflective access", + "type": "boolean", + "default": false + }, + "allPublicConstructors": { + "title": "Register all public constructors from the type for reflective invocation", + "type": "boolean", + "default": false + }, + "unsafeAllocated": { + "title": "Allow objects of this class to be instantiated with a call to jdk.internal.misc.Unsafe#allocateInstance or JNI's AllocObject", + "type": "boolean", + "default": false + }, + "serializable": { + "title": "Allow objects of this class to be serialized and deserialized", + "type": "boolean", + "default": false + }, + "jniAccessible": { + "title": "Register the type, including all registered fields and methods, for runtime JNI access", + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + } + }, + "resources": { + "title": "Metadata to ensure resources are accessible", + "type": "array", + "default": [], + "items": { + "oneOf": [ + { + "title": "Resource that should be accessible", + "type": "object", + "properties": { + "reason": { + "$ref": "#/$defs/reason" + }, + "condition": { + "$ref": "#/$defs/condition" + }, + "module": { + "title": "Module containing the resource", + "type": "string", + "default": "" + }, + "glob": { + "title": "Resource name or pattern matching multiple resources (accepts * and ** wildcards)", + "type": "string" + } + }, + "required": [ + "glob" + ], + "additionalProperties": false + }, + { + "title": "Resource bundle that should be available", + "type": "object", + "properties": { + "reason": { + "$ref": "#/$defs/reason" + }, + "condition": { + "$ref": "#/$defs/condition" + }, + "module": { + "title": "Module containing the resource bundle", + "type": "string", + "default": "" + }, + "bundle": { + "title": "Resource bundle name", + "type": "string" + } + }, + "required": [ + "bundle" + ], + "additionalProperties": false + } + ] + } + }, + "foreign": { + "properties": { + "downcalls": { + "default": [], + "items": { + "properties": { + "reason": { + "$ref": "#/$defs/reason" + }, + "condition": { + "$ref": "#/$defs/condition" + }, + "returnType": { + "type": "string", + "title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)" + }, + "parameterTypes": { + "default": [], + "items": { + "type": "string", + "title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)" + }, + "type": "array", + "title": "List of the function descriptor's parameter types" + }, + "options": { + "type": "object", + "title": "Linker options (see `java.lang.foreign.Linker.Option`)", + "properties": { + "captureCallState": { + "type": "boolean", + "title": "Specifies whether a call state should be captured. The specific states to capture are determined at run time. See also: `java.lang.foreign.Linker.Option.captureCallState`" + }, + "critical": { + "type": "object", + "title": "See `java.lang.foreign.Linker.Option.critical`", + "properties": { + "allowHeapAccess": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "firstVariadicArg": { + "type": "integer", + "title": "See `java.lang.foreign.Linker.Option.firstVariadicArg`" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": ["returnType", "parameterTypes"], + "type": "object", + "title": "Function descriptor to be registered for a downcall" + }, + "type": "array", + "title": "List of function descriptors that should be registered for downcalls" + }, + "upcalls": { + "default": [], + "items": { + "properties": { + "reason": { + "$ref": "#/$defs/reason" + }, + "condition": { + "$ref": "#/$defs/condition" + }, + "returnType": { + "type": "string", + "title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)" + }, + "parameterTypes": { + "default": [], + "items": { + "type": "string", + "title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)" + }, + "type": "array", + "title": "List of the function descriptor's parameter types" + }, + "options": { + "type": "object", + "title": "Linker options (see `java.lang.foreign.Linker.Option`)", + "description": "Currently, no linker options are allowed for upcalls. This may change in the future.", + "properties": { }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": ["returnType", "parameterTypes"], + "type": "object", + "title": "Function descriptor to be registered for an upcall" + }, + "type": "array", + "title": "List of function descriptors that should be registered for upcalls" + }, + "directUpcalls": { + "default": [], + "items": { + "properties": { + "reason": { + "$ref": "#/$defs/reason" + }, + "condition": { + "$ref": "#/$defs/condition" + }, + "class": { + "$ref": "#/$defs/className", + "title": "Fully-qualified class name (e.g., `org.package.OuterClass$InnerClass`)" + }, + "method": { + "type": "string", + "title": "Method name" + }, + "returnType": { + "type": "string", + "title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)" + }, + "parameterTypes": { + "default": [], + "items": { + "type": "string", + "title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)" + }, + "type": "array", + "title": "List of the function descriptor's parameter types" + }, + "options": { + "type": "object", + "title": "Linker options (see `java.lang.foreign.Linker.Option`)", + "description": "Currently, no linker options are allowed for direct upcalls. This may change in the future.", + "properties": { }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": ["class", "method"], + "type": "object", + "title": "Java method and function descriptor to be registered for a direct upcall" + }, + "type": "array", + "title": "List of Java methods and function descriptors that should be registered for direct upcalls" + } + }, + "type": "object", + "additionalProperties": false, + "title": "JSON schema for the FFM API configuration used by GraalVM Native Image", + "description": "For a description and examples of writing an FFM API configuration, see: https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/FFM-API.md" + } + }, + "required": [], + "additionalProperties": false, + + "$defs": { + "className": { + "type": "string", + "pattern": "^[^.;\\[/]+(\\.[^.;\\[/]+)*$" + }, + "typeName": { + "type": "string", + "pattern": "^[^.;\\[/]+(\\.[^.;\\[/]+)*(\\[])*$" + }, + "reason": { + "title": "Reason(s) for including this element (e.g., needed for establishing connections, or needed by method X#Y for task Z)", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "default": "" + }, + "condition": { + "title": "Condition used to determine if the element will be accessible at run time", + "type": "object", + "properties": { + "typeReached": { + "title": "Type descriptor of a class that must be reached in order to allow access to the element", + "$ref": "#/$defs/type" + } + }, + "required": [ + "typeReached" + ], + "additionalProperties": false + }, + "type": { + "title": "Type descriptors used by GraalVM Native Image metadata files", + "oneOf": [ + { + "$ref": "#/$defs/typeName" + }, + { + "type": "object", + "properties": { + "proxy": { + "title": "A list of interfaces defining the proxy class", + "type": "array", + "items": { + "title": "Fully-qualified name of the interface defining the proxy class", + "$ref": "#/$defs/className" + } + }, + "lambda": { + "title": "Lambda class descriptor", + "type": "object", + "properties": { + "declaringClass": { + "title": "The class in which the lambda class is defined", + "$ref": "#/$defs/className" + }, + "declaringMethod": { + "title": "The method in which the lambda class is defined", + "$ref": "#/$defs/method" + }, + "interfaces": { + "title": "Non-empty list of interfaces implemented by the lambda class", + "type": "array", + "items": { + "title": "Fully-qualified name of the interface implemented by the lambda class", + "$ref": "#/$defs/className" + }, + "minItems": 1 + } + }, + "required": [ + "declaringClass", + "interfaces" + ], + "additionalProperties": false + } + }, + "oneOf": [ + { + "required": [ + "proxy" + ] + }, + { + "required": [ + "lambda" + ] + } + ], + "additionalProperties": false + } + ] + }, + "method": { + "title": "Method descriptors used by GraalVM Native Image metadata files", + "type": "object", + "properties": { + "name": { + "title": "Method name that should be registered for this class", + "type": "string", + "pattern": "^[^.;\\[/]+$" + }, + "parameterTypes": { + "items": { + "title": "List of the method's parameter types", + "$ref": "#/$defs/typeName" + }, + "type": "array" + } + }, + "required": [ + "name", "parameterTypes" + ], + "additionalProperties": false + } + } +} diff --git a/gradle.properties b/gradle.properties index 6f186151dac3..f46b720dadc0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ junitJupiterVersion=6.0.3 kotlinVersion=2.3.20 mavenVersion=3.9.13 mockitoVersion=5.23.0 -nativeBuildToolsVersion=0.11.5 +nativeBuildToolsVersion=1.0.0 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.6 springFrameworkVersion=7.0.6 From e0fa796e9dc92bad47c958a1fa6fa891e0524070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:03:52 +0200 Subject: [PATCH 443/930] Upgrade to ActiveMQ 6.2.3 Closes gh-49921 --- platform/spring-boot-dependencies/build.gradle | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index cc7b335552c7..a7cfdbb4e116 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -31,11 +31,7 @@ bom { ] } } - library("ActiveMQ", "6.2.1") { - prohibit { - versionRange "[6.2.2]" - because "it contains a regression on Windows (https://github.com/apache/activemq/issues/1830)" - } + library("ActiveMQ", "6.2.3") { group("org.apache.activemq") { modules = [ "activemq-console", From 4707e932a4b96c35b9502ce12f65cd5a7d1d6aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:03:56 +0200 Subject: [PATCH 444/930] Upgrade to Byte Buddy 1.18.8 Closes gh-49922 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a7cfdbb4e116..b321f1c6c802 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -186,7 +186,7 @@ bom { releaseNotes("https://github.com/mojohaus/build-helper-maven-plugin/releases/tag/{version}") } } - library("Byte Buddy", "1.18.7") { + library("Byte Buddy", "1.18.8") { group("net.bytebuddy") { modules = [ "byte-buddy", From a8bb07ea3cf4235d8badf56b0111c49224b0f8ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:04:00 +0200 Subject: [PATCH 445/930] Upgrade to Ehcache3 3.12.0 Closes gh-49923 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b321f1c6c802..8c4d959f7b1f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -382,7 +382,7 @@ bom { ] } } - library("Ehcache3", "3.11.1") { + library("Ehcache3", "3.12.0") { group("org.ehcache") { modules = [ "ehcache", From 25150f41de62abd665fd2f2afbd08cc10c5f2081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:04:04 +0200 Subject: [PATCH 446/930] Upgrade to Flyway 12.3.0 Closes gh-49924 --- .../boot/flyway/autoconfigure/FlywayPropertiesTests.java | 2 +- platform/spring-boot-dependencies/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/FlywayPropertiesTests.java b/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/FlywayPropertiesTests.java index 74049b8f65ab..fd3f64d118e8 100644 --- a/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/FlywayPropertiesTests.java +++ b/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/FlywayPropertiesTests.java @@ -140,7 +140,7 @@ void expectedPropertiesAreManaged() { // Handled as createSchemas ignoreProperties(configuration, "shouldCreateSchemas"); // Getters for the DataSource settings rather than actual properties - ignoreProperties(configuration, "databaseType", "password", "url", "user"); + ignoreProperties(configuration, "password", "url", "user"); // Properties not exposed by Flyway ignoreProperties(configuration, "failOnMissingTarget"); // Properties managed by a proprietary extension diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8c4d959f7b1f..ecd7a1b7d1df 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -425,7 +425,7 @@ bom { javadoc("elasticsearch-java", version -> "https://javadoc.io/doc/co.elastic.clients/elasticsearch-java/%s/index.html".formatted(version), "co.elastic.clients.elasticsearch", "co.elastic.clients.transport") } } - library("Flyway", "12.1.1") { + library("Flyway", "12.3.0") { group("org.flywaydb") { modules = [ "flyway-command-test-connection", From 62677df961e648107e3622bcbf2f4886c938b525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:04:08 +0200 Subject: [PATCH 447/930] Upgrade to Git Commit ID Maven Plugin 9.2.0 Closes gh-49925 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ecd7a1b7d1df..71aafe32056e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -473,7 +473,7 @@ bom { .formatted(version.toString("_"))) } } - library("Git Commit ID Maven Plugin", "9.0.2") { + library("Git Commit ID Maven Plugin", "9.2.0") { group("io.github.git-commit-id") { plugins = [ "git-commit-id-maven-plugin" From 43dcd58aeb841f4258bd2dd7441d92ffbf762837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:04:12 +0200 Subject: [PATCH 448/930] Upgrade to Groovy 5.0.5 Closes gh-49926 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 71aafe32056e..a5c334109fd5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -550,7 +550,7 @@ bom { releaseNotes("https://github.com/graphql-java/graphql-java/releases/tag/v{version}") } } - library("Groovy", "5.0.4") { + library("Groovy", "5.0.5") { prohibit { contains "-alpha-" because "we don't want alpha dependencies" From 4a78987f1337d1651fca153dde03510a3ecb9f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:04:16 +0200 Subject: [PATCH 449/930] Upgrade to Hibernate 7.2.10.Final Closes gh-49927 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a5c334109fd5..5cbd6d254a0a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -639,7 +639,7 @@ bom { releaseNotes("https://github.com/hazelcast/hazelcast/releases/tag/v{version}") } } - library("Hibernate", "7.2.7.Final") { + library("Hibernate", "7.2.10.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From 909d7af36dbbd8d33408fd3f939a082de67df7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:04:20 +0200 Subject: [PATCH 450/930] Upgrade to Infinispan 16.1.3 Closes gh-49928 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 5cbd6d254a0a..36e8d21c8de3 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -758,7 +758,7 @@ bom { ] } } - library("Infinispan", "16.1.2") { + library("Infinispan", "16.1.3") { group("org.infinispan") { bom("infinispan-bom") } From 877405d5bc8cb75b3d8c7acb38c449e098f8e0a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:04:24 +0200 Subject: [PATCH 451/930] Upgrade to Jackson Bom 3.1.1 Closes gh-49929 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f46b720dadc0..61aa6bd80222 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ commonsCodecVersion=1.21.0 graalVersion=25 hamcrestVersion=3.0 jackson2Version=2.21.2 -jacksonVersion=3.1.0 +jacksonVersion=3.1.1 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 kotlinVersion=2.3.20 From 10093eb754803c1c5125749fbce316455215c96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:04:28 +0200 Subject: [PATCH 452/930] Upgrade to Jaybird 6.0.5 Closes gh-49930 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 36e8d21c8de3..1ba7d7480d48 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1049,7 +1049,7 @@ bom { releaseNotes("https://github.com/jaxen-xpath/jaxen/releases/tag/v{version}") } } - library("Jaybird", "6.0.4") { + library("Jaybird", "6.0.5") { prohibit { endsWith ".java8" because "we use the .java11 version" From 35b4e0173812344cac75dbc899cf0ceb3f0e0a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:04:32 +0200 Subject: [PATCH 453/930] Upgrade to Jetty 12.1.8 Closes gh-49931 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1ba7d7480d48..216c6310b5f2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1119,7 +1119,7 @@ bom { ] } } - library("Jetty", "12.1.7") { + library("Jetty", "12.1.8") { prohibit { contains ".alpha" because "we don't want alpha dependencies" From 4721d46d23b5c84b6d35998366fa018c2a59f08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:04:36 +0200 Subject: [PATCH 454/930] Upgrade to jOOQ 3.21.1 Closes gh-49932 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 216c6310b5f2..c5657f2d6f4d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1143,7 +1143,7 @@ bom { ] } } - library("jOOQ", "3.21.0") { + library("jOOQ", "3.21.1") { group("org.jooq") { bom("jooq-bom") plugins = [ From 5c7b5348131361fad55b3d5dd06ac944cf65d324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:04:40 +0200 Subject: [PATCH 455/930] Upgrade to Log4j2 2.25.4 Closes gh-49933 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c5657f2d6f4d..3dd4c0037b98 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1356,7 +1356,7 @@ bom { releaseNotes("https://github.com/liquibase/liquibase/releases/tag/v{version}") } } - library("Log4j2", "2.25.3") { + library("Log4j2", "2.25.4") { prohibit { contains "-alpha" contains "-beta" From feb0622ee277de4d77e0e6fb5fa3998c92368bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:04:44 +0200 Subject: [PATCH 456/930] Upgrade to MariaDB 3.5.8 Closes gh-49934 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3dd4c0037b98..017985587d18 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1396,7 +1396,7 @@ bom { javadoc("https://projectlombok.org/api") } } - library("MariaDB", "3.5.7") { + library("MariaDB", "3.5.8") { group("org.mariadb.jdbc") { modules = [ "mariadb-java-client" From 33aa2a17e43e72c1c0d8fe94e3b178fbb308beb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:04:48 +0200 Subject: [PATCH 457/930] Upgrade to Protobuf Maven Plugin 5.1.2 Closes gh-49935 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 017985587d18..2fc66b91e66c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1841,7 +1841,7 @@ bom { releaseNotes("https://github.com/oracle/oracle-r2dbc/releases/tag/{version}") } } - library("Protobuf Maven Plugin", "5.1.0") { + library("Protobuf Maven Plugin", "5.1.2") { group("io.github.ascopes") { plugins = [ "protobuf-maven-plugin" From 3aede0cbc56965c323927011f2781c6d7d6a7c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 10:15:03 +0200 Subject: [PATCH 458/930] Upgrade to Tomcat 11.0.21 Closes gh-49936 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 61aa6bd80222..c591e672cd05 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,7 +23,7 @@ nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.6 springFrameworkVersion=7.0.6 springFramework60xVersion=6.0.23 -tomcatVersion=11.0.20 +tomcatVersion=11.0.21 protobufGradlePluginVersion=0.9.6 kotlin.stdlib.default.dependency=false From e0aa86fdb31ba93663088962aa087b63ec472466 Mon Sep 17 00:00:00 2001 From: Eric Haag Date: Mon, 30 Mar 2026 13:21:17 -0500 Subject: [PATCH 459/930] Improve GenerateProtoTask relocatability by clearing javaExecutablePath Clears the `javaExecutablePath` on the Protobuf extension. This prevents absolute Java installation paths from being tracked as task inputs, avoiding cross-machine build cache misses and ensuring the task is fully relocatable. This is safe because `protoc` and `grpc` are configured to use native binaries rather than executable JARs. See: https://github.com/google/protobuf-gradle-plugin/issues/785 Signed-off-by: Eric Haag See gh-49842 --- buildSrc/build.gradle | 1 + .../boot/build/ConventionsPlugin.java | 1 + .../boot/build/ProtobufConventions.java | 41 +++++++++++++++++++ .../build.gradle | 2 +- .../build.gradle | 2 +- .../build.gradle | 2 +- .../build.gradle | 2 +- .../build.gradle | 2 +- .../build.gradle | 2 +- .../build.gradle | 2 +- .../build.gradle | 2 +- 11 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 buildSrc/src/main/java/org/springframework/boot/build/ProtobufConventions.java diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 3ef8533c912b..889cac1a906a 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -46,6 +46,7 @@ dependencies { implementation(platform("tools.jackson:jackson-bom:${jacksonVersion}")) implementation(platform("org.springframework:spring-framework-bom:${springFrameworkVersion}")) implementation("com.github.node-gradle:gradle-node-plugin:7.1.0") + implementation("com.google.protobuf:protobuf-gradle-plugin:${protobufGradlePluginVersion}") implementation("com.gradle:develocity-gradle-plugin:4.2.2") implementation("com.tngtech.archunit:archunit:1.4.1") implementation("commons-codec:commons-codec:${commonsCodecVersion}") diff --git a/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java index b903e0634c87..849968738450 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java @@ -53,6 +53,7 @@ public void apply(Project project) { new WarConventions().apply(project); new EclipseConventions(systemRequirements).apply(project); new TestFixturesConventions().apply(project); + new ProtobufConventions().apply(project); RepositoryTransformersExtension.apply(project); } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/ProtobufConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/ProtobufConventions.java new file mode 100644 index 000000000000..406acc46be89 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/ProtobufConventions.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build; + +import com.google.protobuf.gradle.ProtobufExtension; +import com.google.protobuf.gradle.ProtobufPlugin; +import org.gradle.api.Project; + +/** + * Conventions that are applied in the presence of the {@link ProtobufPlugin} plugin. + * + * @author Eric Haag + */ +class ProtobufConventions { + + void apply(Project project) { + project.getPlugins().withId("com.google.protobuf", (plugin) -> { + ProtobufExtension protobuf = project.getExtensions().getByType(ProtobufExtension.class); + + // Clears javaExecutablePath to prevent build cache misses across machines. + // This is safe as long as the Protobuf tool is NOT configured as a JAR. + // See: https://github.com/google/protobuf-gradle-plugin/issues/785 + protobuf.getJavaExecutablePath().convention(""); + }); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc-client-test/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-client-test/build.gradle index 9f70966c6ee9..5c328e0c1e83 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-client-test/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-client-test/build.gradle @@ -16,7 +16,7 @@ plugins { id "java" - id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "com.google.protobuf" id "org.springframework.boot.docker-test" } diff --git a/smoke-test/spring-boot-smoke-test-grpc-client/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-client/build.gradle index a0e75c6aff3d..c4fb6ddc6409 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-client/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-client/build.gradle @@ -16,7 +16,7 @@ plugins { id "java" - id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "com.google.protobuf" id "org.springframework.boot.docker-test" } diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/build.gradle index 2b52f147aae4..b647f93a8729 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/build.gradle @@ -16,7 +16,7 @@ plugins { id "java" - id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "com.google.protobuf" id "org.springframework.boot.docker-test" } diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle index 758cc967819d..7584bb955041 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle @@ -16,7 +16,7 @@ plugins { id "java" - id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "com.google.protobuf" id "org.springframework.boot.docker-test" } diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle index a4ec289d2f2a..1249a7fece36 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle @@ -16,7 +16,7 @@ plugins { id "java" - id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "com.google.protobuf" id "org.springframework.boot.docker-test" } diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-servlet/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/build.gradle index c9389b5491d4..4d7901ea6326 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-servlet/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/build.gradle @@ -16,7 +16,7 @@ plugins { id "java" - id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "com.google.protobuf" id "org.springframework.boot.docker-test" } diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-test/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-test/build.gradle index 8f28729142eb..9f4f475ce295 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-test/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-server-test/build.gradle @@ -16,7 +16,7 @@ plugins { id "java" - id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "com.google.protobuf" id "org.springframework.boot.docker-test" } diff --git a/smoke-test/spring-boot-smoke-test-grpc-server/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server/build.gradle index 2f9fe70d0fac..88d957333254 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-server/build.gradle @@ -16,7 +16,7 @@ plugins { id "java" - id "com.google.protobuf" version "${protobufGradlePluginVersion}" + id "com.google.protobuf" id "org.springframework.boot.docker-test" } From bd72e50a294421aaa508abcbfa6b627882ade7b7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 7 Apr 2026 10:04:21 +0100 Subject: [PATCH 460/930] Polish "Improve GenerateProtoTask relocatability by clearing javaExecutablePath" See gh-49842 Signed-off-by: Andy Wilkinson --- .../boot/build/ProtobufConventions.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/ProtobufConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/ProtobufConventions.java index 406acc46be89..8b8cff22caf5 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/ProtobufConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/ProtobufConventions.java @@ -30,12 +30,13 @@ class ProtobufConventions { void apply(Project project) { project.getPlugins().withId("com.google.protobuf", (plugin) -> { ProtobufExtension protobuf = project.getExtensions().getByType(ProtobufExtension.class); - - // Clears javaExecutablePath to prevent build cache misses across machines. - // This is safe as long as the Protobuf tool is NOT configured as a JAR. - // See: https://github.com/google/protobuf-gradle-plugin/issues/785 - protobuf.getJavaExecutablePath().convention(""); + removeUnusedMachineSpecificConfiguration(protobuf); }); } + // See: https://github.com/google/protobuf-gradle-plugin/issues/785 + private void removeUnusedMachineSpecificConfiguration(ProtobufExtension protobuf) { + protobuf.getJavaExecutablePath().convention(""); + } + } From 4cef72b79832e6612f57aa732a3e0d38d0fd7255 Mon Sep 17 00:00:00 2001 From: Bart Sopers <15631208+bartsopers@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:25:00 +0200 Subject: [PATCH 461/930] Add support for configuring a custom SessionTimeout bean This commit allows a user-defined SessionTimeout bean to take control over the auto-configuration of the timeout. See gh-49883 Signed-off-by: Bart Sopers <15631208+bartsopers@users.noreply.github.com> --- .../boot/session/autoconfigure/SessionAutoConfiguration.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/module/spring-boot-session/src/main/java/org/springframework/boot/session/autoconfigure/SessionAutoConfiguration.java b/module/spring-boot-session/src/main/java/org/springframework/boot/session/autoconfigure/SessionAutoConfiguration.java index 3f76fcf6f80c..00681a82f2eb 100644 --- a/module/spring-boot-session/src/main/java/org/springframework/boot/session/autoconfigure/SessionAutoConfiguration.java +++ b/module/spring-boot-session/src/main/java/org/springframework/boot/session/autoconfigure/SessionAutoConfiguration.java @@ -97,6 +97,7 @@ DefaultCookieSerializerCustomizer rememberMeServicesCookieSerializerCustomizer() static class EmbeddedWebServerConfiguration { @Bean + @ConditionalOnMissingBean SessionTimeout embeddedWebServerSessionTimeout(SessionProperties sessionProperties, ServerProperties serverProperties) { return () -> determineTimeout(sessionProperties, @@ -137,6 +138,7 @@ public void setServletContext(ServletContext servletContext) { } @Bean + @ConditionalOnMissingBean SessionTimeout warDeplomentSessionTimeout(SessionProperties sessionProperties) { return sessionProperties::getTimeout; } @@ -172,6 +174,7 @@ DefaultCookieSerializer cookieSerializer( static class ReactiveSessionConfiguration { @Bean + @ConditionalOnMissingBean SessionTimeout embeddedWebServerSessionTimeout(SessionProperties sessionProperties, ServerProperties serverProperties) { return () -> determineTimeout(sessionProperties, serverProperties.getReactive().getSession()::getTimeout); From b103d2bb4cbdb3ba1fb3c353aa95914c9cdcf9e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 7 Apr 2026 15:04:36 +0200 Subject: [PATCH 462/930] Polish "Add support for configuring a custom SessionTimeout bean" See gh-49883 --- .../modules/reference/pages/web/spring-session.adoc | 1 + .../SessionDataRedisAutoConfigurationTests.java | 12 ++++++++++++ .../JdbcSessionAutoConfigurationTests.java | 12 ++++++++++++ 3 files changed, 25 insertions(+) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/spring-session.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/spring-session.adoc index 12326cf0df79..04f8530679f7 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/spring-session.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/spring-session.adoc @@ -27,6 +27,7 @@ spring: For setting the timeout of the session you can use the configprop:spring.session.timeout[] property. If that property is not set with a servlet web application, the auto-configuration falls back to the value of configprop:server.servlet.session.timeout[]. +To provide the timeout programmatically, define a javadoc:org.springframework.boot.session.autoconfigure.SessionTimeout[] bean. You can take control over Spring Session's configuration using `@Enable*HttpSession` (servlet) or `@EnableRedisWebSession` (reactive). This will cause the auto-configuration to back off. diff --git a/module/spring-boot-session-data-redis/src/dockerTest/java/org/springframework/boot/session/data/redis/autoconfigure/SessionDataRedisAutoConfigurationTests.java b/module/spring-boot-session-data-redis/src/dockerTest/java/org/springframework/boot/session/data/redis/autoconfigure/SessionDataRedisAutoConfigurationTests.java index 4d895344b499..3420f5ecc15e 100644 --- a/module/spring-boot-session-data-redis/src/dockerTest/java/org/springframework/boot/session/data/redis/autoconfigure/SessionDataRedisAutoConfigurationTests.java +++ b/module/spring-boot-session-data-redis/src/dockerTest/java/org/springframework/boot/session/data/redis/autoconfigure/SessionDataRedisAutoConfigurationTests.java @@ -30,6 +30,7 @@ import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration; import org.springframework.boot.session.autoconfigure.AbstractSessionAutoConfigurationTests; import org.springframework.boot.session.autoconfigure.SessionAutoConfiguration; +import org.springframework.boot.session.autoconfigure.SessionTimeout; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; @@ -104,6 +105,17 @@ void defaultConfigWithCustomTimeout() { }); } + @Test + void defaultConfigWithCustomSessionTimeoutBean() { + this.contextRunner.withBean(SessionTimeout.class, () -> () -> Duration.ofSeconds(4)) + .withPropertyValues("spring.data.redis.host=" + redis.getHost(), + "spring.data.redis.port=" + redis.getFirstMappedPort(), "spring.session.timeout=1m") + .run((context) -> { + RedisSessionRepository repository = validateSessionRepository(context, RedisSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", Duration.ofSeconds(4)); + }); + } + @Test void defaultRedisSessionStoreWithCustomizations() { this.contextRunner diff --git a/module/spring-boot-session-jdbc/src/test/java/org/springframework/boot/session/jdbc/autoconfigure/JdbcSessionAutoConfigurationTests.java b/module/spring-boot-session-jdbc/src/test/java/org/springframework/boot/session/jdbc/autoconfigure/JdbcSessionAutoConfigurationTests.java index 5a6759b5b1a4..ae3458a1a400 100644 --- a/module/spring-boot-session-jdbc/src/test/java/org/springframework/boot/session/jdbc/autoconfigure/JdbcSessionAutoConfigurationTests.java +++ b/module/spring-boot-session-jdbc/src/test/java/org/springframework/boot/session/jdbc/autoconfigure/JdbcSessionAutoConfigurationTests.java @@ -34,6 +34,7 @@ import org.springframework.boot.liquibase.autoconfigure.LiquibaseAutoConfiguration; import org.springframework.boot.session.autoconfigure.AbstractSessionAutoConfigurationTests; import org.springframework.boot.session.autoconfigure.SessionAutoConfiguration; +import org.springframework.boot.session.autoconfigure.SessionTimeout; import org.springframework.boot.sql.init.DatabaseInitializationMode; import org.springframework.boot.sql.init.DatabaseInitializationSettings; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; @@ -108,6 +109,17 @@ void customTimeout() { }); } + @Test + void customSessionTimeoutBean() { + this.contextRunner.withBean(SessionTimeout.class, () -> () -> Duration.ofSeconds(4)) + .withPropertyValues("spring.session.timeout=1m") + .run((context) -> { + JdbcIndexedSessionRepository repository = validateSessionRepository(context, + JdbcIndexedSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", Duration.ofSeconds(4)); + }); + } + @Test void customTableName() { this.contextRunner.withPropertyValues("spring.session.jdbc.table-name=FOO_BAR", From e1dd40a923586201e91abdc262fa9beca55488fd Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 7 Apr 2026 18:00:49 -0700 Subject: [PATCH 463/930] Fix test Timezone issue Fix test that fails in certain timezones. See gh-8923 --- .../webmvc/autoconfigure/WebMvcAutoConfigurationTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java index 5387a811399d..b6f3df9497f5 100644 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java @@ -512,11 +512,11 @@ void customDateTimeTimeFormat() { } @Test - void embeddedValueResolverIsAppliedToConversionService() throws Exception { + void embeddedValueResolverIsAppliedToConversionService() { this.contextRunner.withPropertyValues("my.date.format=yyyy-MM-dd").run((context) -> { FormattingConversionService conversionService = context.getBean(FormattingConversionService.class); TypeDescriptor dateType = new TypeDescriptor(FormattedDate.class.getDeclaredField("date")); - Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC")).toInstant()); + Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.systemDefault()).toInstant()); assertThat(conversionService.convert(date, dateType, TypeDescriptor.valueOf(String.class))) .isEqualTo("2000-01-02"); }); From 3753b9d466efbaaf20f6701c419c517af161e04f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 7 Apr 2026 21:19:03 -0700 Subject: [PATCH 464/930] Fix additional test Timezone issue Fix test that fails in certain timezones. See gh-8923 --- .../autoconfigure/web/format/WebConversionServiceTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java index 5e49ad9da926..800fa7f087f0 100644 --- a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java +++ b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java @@ -187,7 +187,7 @@ void embeddedValueResolverIsAppliedToAnnotationPattern() throws Exception { WebConversionService conversionService = new WebConversionService( (value) -> value.replace("${my.date.format}", "yyyy-MM-dd"), new DateTimeFormatters()); TypeDescriptor dateType = new TypeDescriptor(FormattedDate.class.getDeclaredField("date")); - Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC")).toInstant()); + Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.systemDefault()).toInstant()); assertThat(conversionService.convert(date, dateType, TypeDescriptor.valueOf(String.class))) .isEqualTo("2000-01-02"); } From e434ee17cae196b2beb767a17d2cdb20b711f6b1 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 7 Apr 2026 22:20:03 -0700 Subject: [PATCH 465/930] Fix WebFlux test Timezone issue Fix test that fails in certain timezones. See gh-8923 --- .../webflux/autoconfigure/WebFluxAutoConfigurationTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java index 6eb10ac2724a..60e2b39b853e 100644 --- a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java +++ b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java @@ -343,11 +343,11 @@ void customDateTimeTimeFormat() { } @Test - void embeddedValueResolverIsAppliedToConversionService() throws Exception { + void embeddedValueResolverIsAppliedToConversionService() { this.contextRunner.withPropertyValues("my.date.format=yyyy-MM-dd").run((context) -> { FormattingConversionService conversionService = context.getBean(FormattingConversionService.class); TypeDescriptor dateType = new TypeDescriptor(FormattedDate.class.getDeclaredField("date")); - Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC")).toInstant()); + Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.systemDefault()).toInstant()); assertThat(conversionService.convert(date, dateType, TypeDescriptor.valueOf(String.class))) .isEqualTo("2000-01-02"); }); From 6f409f7dd63867c56844dc22d4211641b6a75f0c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 7 Apr 2026 22:27:19 -0700 Subject: [PATCH 466/930] Polish code for upcoming Spring Security nullability changes Update code and tests in preparation of upcoming Spring Security `Nullable` annotation updates. --- .../autoconfigure/OAuth2ClientPropertiesMapperTests.java | 2 +- .../OAuth2ClientWebSecurityAutoConfigurationTests.java | 6 +++++- .../OpaqueTokenIntrospectionConfiguration.java | 5 ++++- ...ctiveOpaqueTokenIntrospectionClientConfiguration.java | 5 ++++- .../SampleGrpcServerOAuthApplicationTests.java | 1 + .../SampleOAuth2AuthorizationServerApplicationTests.java | 9 ++++++--- 6 files changed, 21 insertions(+), 7 deletions(-) diff --git a/module/spring-boot-security-oauth2-client/src/test/java/org/springframework/boot/security/oauth2/client/autoconfigure/OAuth2ClientPropertiesMapperTests.java b/module/spring-boot-security-oauth2-client/src/test/java/org/springframework/boot/security/oauth2/client/autoconfigure/OAuth2ClientPropertiesMapperTests.java index 5a14364b8ab0..94c987b7a8d8 100644 --- a/module/spring-boot-security-oauth2-client/src/test/java/org/springframework/boot/security/oauth2/client/autoconfigure/OAuth2ClientPropertiesMapperTests.java +++ b/module/spring-boot-security-oauth2-client/src/test/java/org/springframework/boot/security/oauth2/client/autoconfigure/OAuth2ClientPropertiesMapperTests.java @@ -315,7 +315,7 @@ private void testIssuerConfiguration(OAuth2ClientProperties.Registration registr assertThat(adapted.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); assertThat(adapted.getRegistrationId()).isEqualTo("okta"); assertThat(adapted.getClientName()).isEqualTo(issuer); - assertThat(adapted.getScopes()).isNull(); + assertThat(adapted.getScopes()).isNullOrEmpty(); assertThat(providerDetails.getAuthorizationUri()).isEqualTo("https://example.com/o/oauth2/v2/auth"); assertThat(providerDetails.getTokenUri()).isEqualTo("https://example.com/oauth2/v4/token"); assertThat(providerDetails.getJwkSetUri()).isEqualTo("https://example.com/oauth2/v3/certs"); diff --git a/module/spring-boot-security-oauth2-client/src/test/java/org/springframework/boot/security/oauth2/client/autoconfigure/servlet/OAuth2ClientWebSecurityAutoConfigurationTests.java b/module/spring-boot-security-oauth2-client/src/test/java/org/springframework/boot/security/oauth2/client/autoconfigure/servlet/OAuth2ClientWebSecurityAutoConfigurationTests.java index b5400bb556a5..8ae845927068 100644 --- a/module/spring-boot-security-oauth2-client/src/test/java/org/springframework/boot/security/oauth2/client/autoconfigure/servlet/OAuth2ClientWebSecurityAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-client/src/test/java/org/springframework/boot/security/oauth2/client/autoconfigure/servlet/OAuth2ClientWebSecurityAutoConfigurationTests.java @@ -20,6 +20,7 @@ import java.util.List; import jakarta.servlet.Filter; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -165,7 +166,10 @@ private FilterChainProxy getFilterChainProxy(Filter filter) { throw new IllegalStateException("No FilterChainProxy found"); } - private boolean isEqual(ClientRegistration reg1, ClientRegistration reg2) { + private boolean isEqual(@Nullable ClientRegistration reg1, @Nullable ClientRegistration reg2) { + if (reg1 == null || reg2 == null) { + return reg1 == reg2; + } boolean result = ObjectUtils.nullSafeEquals(reg1.getClientId(), reg2.getClientId()); result = result && ObjectUtils.nullSafeEquals(reg1.getClientName(), reg2.getClientName()); result = result && ObjectUtils.nullSafeEquals(reg1.getClientSecret(), reg2.getClientSecret()); diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OpaqueTokenIntrospectionConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OpaqueTokenIntrospectionConfiguration.java index 402eaab2e54b..4d39a1fb07fd 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OpaqueTokenIntrospectionConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/OpaqueTokenIntrospectionConfiguration.java @@ -40,12 +40,15 @@ class OpaqueTokenIntrospectionConfiguration { SpringOpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServerProperties properties, ObjectProvider customizers) { OAuth2ResourceServerProperties.Opaquetoken token = properties.getOpaquetoken(); + String introspectionUri = token.getIntrospectionUri(); + Assert.state(introspectionUri != null, + "No 'spring.security.oauth2.resourceserver.opaquetoken.introspection-uri' property specified"); Assert.state(token.getClientId() != null, "No 'spring.security.oauth2.resourceserver.opaquetoken.client-id' property specified"); Assert.state(token.getClientSecret() != null, "No 'spring.security.oauth2.resourceserver.opaquetoken.client-secret' property specified"); SpringOpaqueTokenIntrospector.Builder builder = SpringOpaqueTokenIntrospector - .withIntrospectionUri(token.getIntrospectionUri()) + .withIntrospectionUri(introspectionUri) .clientId(token.getClientId()) .clientSecret(token.getClientSecret()); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java index 9ea71025b939..0cbf30ee2d49 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java @@ -41,12 +41,15 @@ class ReactiveOpaqueTokenIntrospectionClientConfiguration { SpringReactiveOpaqueTokenIntrospector reactiveOpaqueTokenIntrospector(OAuth2ResourceServerProperties properties, ObjectProvider customizers) { OAuth2ResourceServerProperties.Opaquetoken token = properties.getOpaquetoken(); + String introspectionUri = token.getIntrospectionUri(); + Assert.state(introspectionUri != null, + "No 'spring.security.oauth2.resourceserver.opaquetoken.introspection-uri' property specified"); Assert.state(token.getClientId() != null, "No 'spring.security.oauth2.resourceserver.opaquetoken.client-id' property specified"); Assert.state(token.getClientSecret() != null, "No 'spring.security.oauth2.resourceserver.opaquetoken.client-secret' property specified"); SpringReactiveOpaqueTokenIntrospector.Builder builder = SpringReactiveOpaqueTokenIntrospector - .withIntrospectionUri(token.getIntrospectionUri()) + .withIntrospectionUri(introspectionUri) .clientId(token.getClientId()) .clientSecret(token.getClientSecret()); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/test/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/test/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplicationTests.java index 9bfeb8bab906..77fb58603417 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/test/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/test/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplicationTests.java @@ -146,6 +146,7 @@ > GrpcChannelBuilderCustomizer channelSecu private String token(ClientRegistrationRepository clientRegistrationRepository) { RestClientClientCredentialsTokenResponseClient client = new RestClientClientCredentialsTokenResponseClient(); ClientRegistration registration = clientRegistrationRepository.findByRegistrationId("spring"); + assertThat(registration).isNotNull(); OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(registration); return client.getTokenResponse(request).getAccessToken().getTokenValue(); } diff --git a/smoke-test/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java b/smoke-test/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java index daa32699cf87..871fe483d21f 100644 --- a/smoke-test/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java @@ -64,7 +64,9 @@ void openidConfigurationShouldAllowAccess() { ResponseEntity> entity = this.restTemplate.exchange("/.well-known/openid-configuration", HttpMethod.GET, null, MAP_TYPE_REFERENCE); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - OidcProviderConfiguration config = OidcProviderConfiguration.withClaims(entity.getBody()).build(); + Map body = entity.getBody(); + assertThat(body).isNotNull(); + OidcProviderConfiguration config = OidcProviderConfiguration.withClaims(body).build(); assertThat(config.getIssuer()).hasToString("https://provider.com"); assertThat(config.getAuthorizationEndpoint()).hasToString("https://provider.com/authorize"); assertThat(config.getTokenEndpoint()).hasToString("https://provider.com/token"); @@ -83,8 +85,9 @@ void authServerMetadataShouldAllowAccess() { ResponseEntity> entity = this.restTemplate .exchange("/.well-known/oauth-authorization-server", HttpMethod.GET, null, MAP_TYPE_REFERENCE); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - OAuth2AuthorizationServerMetadata config = OAuth2AuthorizationServerMetadata.withClaims(entity.getBody()) - .build(); + Map body = entity.getBody(); + assertThat(body).isNotNull(); + OAuth2AuthorizationServerMetadata config = OAuth2AuthorizationServerMetadata.withClaims(body).build(); assertThat(config.getIssuer()).hasToString("https://provider.com"); assertThat(config.getAuthorizationEndpoint()).hasToString("https://provider.com/authorize"); assertThat(config.getTokenEndpoint()).hasToString("https://provider.com/token"); From 08577572a5ff9d92e5af0a0d24205e431124d1df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 8 Apr 2026 08:18:26 +0200 Subject: [PATCH 467/930] Add missing configuration properties for KafkaTemplate Closes gh-49954 --- .../autoconfigure/KafkaAutoConfiguration.java | 10 ++-- .../kafka/autoconfigure/KafkaProperties.java | 26 ++++++++++ .../KafkaAutoConfigurationTests.java | 52 ++++++++++++++----- 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfiguration.java b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfiguration.java index 8a79e2803c76..130e975028c7 100644 --- a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfiguration.java +++ b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfiguration.java @@ -43,6 +43,7 @@ import org.springframework.boot.kafka.autoconfigure.KafkaConnectionDetails.Configuration; import org.springframework.boot.kafka.autoconfigure.KafkaProperties.Jaas; import org.springframework.boot.kafka.autoconfigure.KafkaProperties.Retry.Topic.Backoff; +import org.springframework.boot.kafka.autoconfigure.KafkaProperties.Template; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; @@ -111,9 +112,12 @@ PropertiesKafkaConnectionDetails kafkaConnectionDetails(ObjectProvider { + KafkaTemplate kafkaTemplate = context.getBean(KafkaTemplate.class); + Template defaultTemplateProperties = new KafkaProperties().getTemplate(); + assertThat(kafkaTemplate.getDefaultTopic()).isEqualTo(defaultTemplateProperties.getDefaultTopic()); + assertThat(kafkaTemplate).hasFieldOrPropertyWithValue("transactionIdPrefix", + defaultTemplateProperties.getTransactionIdPrefix()); + assertThat(kafkaTemplate).hasFieldOrPropertyWithValue("closeTimeout", + defaultTemplateProperties.getCloseTimeout()); + assertThat(kafkaTemplate.isAllowNonTransactional()) + .isEqualTo(defaultTemplateProperties.isAllowNonTransactional()); + assertThat(kafkaTemplate).hasFieldOrPropertyWithValue("observationEnabled", + defaultTemplateProperties.isObservationEnabled()); + }); + } + + @Test + void templateProperties() { + this.contextRunner.withPropertyValues("spring.kafka.template.default-topic=testTopic", + "spring.kafka.template.transaction-id-prefix=txOverride", "spring.kafka.template.close-timeout=3m", + "spring.kafka.template.allow-non-transactional=true", "spring.kafka.template.observation-enabled=true") + .run((context) -> { + KafkaTemplate kafkaTemplate = context.getBean(KafkaTemplate.class); + assertThat(kafkaTemplate.getDefaultTopic()).isEqualTo("testTopic"); + assertThat(kafkaTemplate).hasFieldOrPropertyWithValue("transactionIdPrefix", "txOverride"); + assertThat(kafkaTemplate).hasFieldOrPropertyWithValue("closeTimeout", Duration.ofMinutes(3)); + assertThat(kafkaTemplate.isAllowNonTransactional()).isTrue(); + assertThat(kafkaTemplate).hasFieldOrPropertyWithValue("observationEnabled", true); + }); + } + @SuppressWarnings("unchecked") @Test void listenerProperties() { this.contextRunner - .withPropertyValues("spring.kafka.template.default-topic=testTopic", - "spring.kafka.template.transaction-id-prefix=txOverride", "spring.kafka.listener.ack-mode=MANUAL", - "spring.kafka.listener.client-id=client", "spring.kafka.listener.ack-count=123", - "spring.kafka.listener.ack-time=456", "spring.kafka.listener.concurrency=3", - "spring.kafka.listener.poll-timeout=2000", "spring.kafka.listener.no-poll-threshold=2.5", - "spring.kafka.listener.type=batch", "spring.kafka.listener.idle-between-polls=1s", - "spring.kafka.listener.idle-event-interval=1s", + .withPropertyValues("spring.kafka.listener.ack-mode=MANUAL", "spring.kafka.listener.client-id=client", + "spring.kafka.listener.ack-count=123", "spring.kafka.listener.ack-time=456", + "spring.kafka.listener.concurrency=3", "spring.kafka.listener.poll-timeout=2000", + "spring.kafka.listener.no-poll-threshold=2.5", "spring.kafka.listener.type=batch", + "spring.kafka.listener.idle-between-polls=1s", "spring.kafka.listener.idle-event-interval=1s", "spring.kafka.listener.idle-partition-event-interval=1s", "spring.kafka.listener.monitor-interval=45", "spring.kafka.listener.log-container-config=true", "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.template.observation-enabled=true", "spring.kafka.listener.observation-enabled=true") + "spring.kafka.listener.observation-enabled=true") .run((context) -> { DefaultKafkaProducerFactory producerFactory = context.getBean(DefaultKafkaProducerFactory.class); DefaultKafkaConsumerFactory consumerFactory = context.getBean(DefaultKafkaConsumerFactory.class); KafkaTemplate kafkaTemplate = context.getBean(KafkaTemplate.class); + assertThat(kafkaTemplate).hasFieldOrPropertyWithValue("producerFactory", producerFactory); AbstractKafkaListenerContainerFactory kafkaListenerContainerFactory = (AbstractKafkaListenerContainerFactory) context .getBean(KafkaListenerContainerFactory.class); assertThat(kafkaTemplate.getMessageConverter()).isInstanceOf(MessagingMessageConverter.class); - 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); From fd778fd3f306aa109e37c5a286d7fea491a07424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 10:54:24 +0200 Subject: [PATCH 468/930] Start building against Micrometer 1.17.0-RC1 snapshots See gh-49990 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 2fc66b91e66c..6cb69ff2fff7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1613,7 +1613,7 @@ bom { releaseNotes("https://github.com/apache/maven-war-plugin/releases/tag/maven-war-plugin-{version}") } } - library("Micrometer", "1.17.0-M3") { + library("Micrometer", "1.17.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ From f8761fbb3d053ef8838921766adceabb92096d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 10:54:29 +0200 Subject: [PATCH 469/930] Start building against Micrometer Tracing 1.7.0-RC1 snapshots See gh-49991 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6cb69ff2fff7..7dea6ff91341 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1636,7 +1636,7 @@ bom { releaseNotes("https://github.com/micrometer-metrics/micrometer/releases/tag/v{version}") } } - library("Micrometer Tracing", "1.7.0-M4") { + library("Micrometer Tracing", "1.7.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { bom("micrometer-tracing-bom") From a75a4927913842f54a4f885acba7742301ee2293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 10:54:33 +0200 Subject: [PATCH 470/930] Start building against Reactor Bom 2025.0.5 snapshots See gh-49992 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7dea6ff91341..6b06a2fb488f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2080,7 +2080,7 @@ bom { ] } } - library("Reactor Bom", "2025.0.4") { + library("Reactor Bom", "2025.0.5-SNAPSHOT") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From b07316bc49afd891e870239e33668f2b2216cb54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 10:54:37 +0200 Subject: [PATCH 471/930] Start building against Spring AMQP 4.1.0-RC1 snapshots See gh-49993 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6b06a2fb488f..45742a5742fb 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2506,7 +2506,7 @@ bom { ] } } - library("Spring AMQP", "4.1.0-M3") { + library("Spring AMQP", "4.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.amqp") { bom("spring-amqp-bom") From 3c86ec39df9ed5d5f7f231fc9a4f321835291b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 10:54:41 +0200 Subject: [PATCH 472/930] Start building against Spring Data Bom 2026.0.0-RC1 snapshots See gh-49994 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 45742a5742fb..9ed759325d31 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2536,7 +2536,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-batch/releases/tag/v{version}") } } - library("Spring Data Bom", "2026.0.0-M2") { + library("Spring Data Bom", "2026.0.0-SNAPSHOT") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From 35f0b55bdc720193a8e3bee8551880aacb40eac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 10:54:46 +0200 Subject: [PATCH 473/930] Start building against Spring Framework 7.0.7 snapshots See gh-49995 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c591e672cd05..cbb7e0faf66e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ mockitoVersion=5.23.0 nativeBuildToolsVersion=1.0.0 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.6 -springFrameworkVersion=7.0.6 +springFrameworkVersion=7.0.7-SNAPSHOT springFramework60xVersion=6.0.23 tomcatVersion=11.0.21 protobufGradlePluginVersion=0.9.6 From dcee0cb5bd94c297c7674486a80e27d766779da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 10:54:50 +0200 Subject: [PATCH 474/930] Start building against Spring GraphQL 2.0.3 snapshots See gh-49996 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9ed759325d31..458ac28a1644 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2566,7 +2566,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-framework/releases/tag/v{version}") } } - library("Spring GraphQL", "2.0.2") { + library("Spring GraphQL", "2.0.3-SNAPSHOT") { considerSnapshots() group("org.springframework.graphql") { modules = [ From 6f19081536167ee5629ce0bbf462402d755d7f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 10:54:54 +0200 Subject: [PATCH 475/930] Start building against Spring Integration 7.1.0-RC1 snapshots See gh-49997 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 458ac28a1644..0dfa58b8510d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2618,7 +2618,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-hateoas/releases/tag/{version}") } } - library("Spring Integration", "7.1.0-M3") { + library("Spring Integration", "7.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.integration") { bom("spring-integration-bom") From 5ce4b8052f3fa4e933175cd50ac308ae664fbd06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 10:54:59 +0200 Subject: [PATCH 476/930] Start building against Spring Kafka 4.1.0-RC1 snapshots See gh-49998 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0dfa58b8510d..abc7ab9934eb 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2633,7 +2633,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-integration/releases/tag/v{version}") } } - library("Spring Kafka", "4.1.0-M2") { + library("Spring Kafka", "4.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.kafka") { modules = [ From 1a269f44837ba4a21a6333a9120220fa7e1c523d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 10:55:03 +0200 Subject: [PATCH 477/930] Start building against Spring LDAP 4.1.0-RC1 snapshots See gh-49999 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index abc7ab9934eb..c20690cb8dc5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2651,7 +2651,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-kafka/releases/tag/v{version}") } } - library("Spring LDAP", "4.1.0-M2") { + library("Spring LDAP", "4.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.ldap") { modules = [ From 4f9384157cf9ec28dc2d6d7c3b8e0a2faa4fe9a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 10:55:07 +0200 Subject: [PATCH 478/930] Start building against Spring Pulsar 2.0.5 snapshots See gh-50000 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c20690cb8dc5..a57712a6b611 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2671,7 +2671,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ldap/releases/tag/{version}") } } - library("Spring Pulsar", "2.0.4") { + library("Spring Pulsar", "2.0.5-SNAPSHOT") { considerSnapshots() group("org.springframework.pulsar") { bom("spring-pulsar-bom") From 974fb6b3901e6250ea05abc980b1d6e21c530645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 10:55:12 +0200 Subject: [PATCH 479/930] Start building against Spring Security 7.1.0-RC1 snapshots See gh-50001 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a57712a6b611..4fab0aaee5f7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2701,7 +2701,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-restdocs/releases/tag/v{version}") } } - library("Spring Security", "7.1.0-M3") { + library("Spring Security", "7.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { bom("spring-security-bom") From f62871ce05e347035204659b1d8faff49ed30ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 11:58:46 +0200 Subject: [PATCH 480/930] Upgrade to ActiveMQ 6.2.4 Closes gh-50002 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4fab0aaee5f7..1ad29aece2cf 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -31,7 +31,7 @@ bom { ] } } - library("ActiveMQ", "6.2.3") { + library("ActiveMQ", "6.2.4") { group("org.apache.activemq") { modules = [ "activemq-console", From 787f097724328333c8fa04adaa42480a26b4b515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 11:58:50 +0200 Subject: [PATCH 481/930] Upgrade to Elasticsearch Client 9.3.3 Closes gh-50003 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1ad29aece2cf..259f2f1bc9ef 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -401,7 +401,7 @@ bom { releaseNotes("https://github.com/ehcache/ehcache3/releases/tag/v{version}") } } - library("Elasticsearch Client", "9.3.1") { + library("Elasticsearch Client", "9.3.3") { prohibit { contains "-alpha" contains "-beta" From 24a8053233f39223f1d4830dce6731e48b6f5776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 9 Apr 2026 11:58:54 +0200 Subject: [PATCH 482/930] Upgrade to OpenTelemetry 1.60.1 Closes gh-50004 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 259f2f1bc9ef..de8303f94a29 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1767,7 +1767,7 @@ bom { javadoc(version -> "https://netty.io/%s.%s/api".formatted(version.major(), version.minor()), "io.netty") } } - library("OpenTelemetry", "1.59.0") { + library("OpenTelemetry", "1.60.1") { group("io.opentelemetry") { bom("opentelemetry-bom") } From c76b6b8c66764716483e21976a4c1e61e7cdb9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 23 Mar 2026 12:31:41 +0100 Subject: [PATCH 483/930] Add lazy JDBC connection fetching for pooled DataSources This commit allows wrapping the auto-configured pooled DataSource in a proxy that fetches connections as late as possible. JMX registration of the underlying pool is preserved. Closes gh-15480 --- .../modules/reference/pages/data/sql.adoc | 22 ++++ .../DataSourceAutoConfiguration.java | 3 +- ...LazyConnectionDataSourceConfiguration.java | 85 ++++++++++++++ ...itional-spring-configuration-metadata.json | 19 +++ .../boot/jdbc/DataSourceUnwrapperTests.java | 9 ++ ...onnectionDataSourceConfigurationTests.java | 109 ++++++++++++++++++ 6 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/autoconfigure/LazyConnectionDataSourceConfiguration.java create mode 100644 module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/autoconfigure/LazyConnectionDataSourceConfigurationTests.java diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc index 1fb0f2a513e2..fa8a38f48cf2 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc @@ -163,6 +163,28 @@ spring: +[[data.sql.datasource.lazy-connection]] +=== Lazy Connection Proxy + +When a pooled `DataSource` is auto-configured, it can be wrapped in a proxy that fetches JDBC connections as late as possible by setting configprop:spring.datasource.connection-fetch[] to `lazy`, as shown in the following example: + +[configprops,yaml] +---- +spring: + datasource: + connection-fetch: "lazy" +---- + +With this feature enabled, JDBC Connections are only fetched from the pool when actually necessary. +JDBC transaction control can happen without fetching a Connection from the pool or communicating with the database; this will be done lazily on the first creation of a JDBC Statement. +To get access to the target auto-configured `DataSource`, use `DataSource.unwrap`. + +TIP: If you are wrapping the target `DataSource` in a `TransactionAwareDataSourceProxy`, make sure to do so with a `BeanPostProcessor` that has a positive order. + +See javadoc:org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy[] for more details. + + + [[data.sql.jdbc-template]] == Using JdbcTemplate diff --git a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/autoconfigure/DataSourceAutoConfiguration.java b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/autoconfigure/DataSourceAutoConfiguration.java index 700e58ff0a10..ab58d9329d73 100644 --- a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/autoconfigure/DataSourceAutoConfiguration.java +++ b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/autoconfigure/DataSourceAutoConfiguration.java @@ -74,7 +74,8 @@ protected static class EmbeddedDatabaseConfiguration { @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, - DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class }) + DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class, + LazyConnectionDataSourceConfiguration.class }) protected static class PooledDataSourceConfiguration { @Bean diff --git a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/autoconfigure/LazyConnectionDataSourceConfiguration.java b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/autoconfigure/LazyConnectionDataSourceConfiguration.java new file mode 100644 index 000000000000..ef00f8c3faad --- /dev/null +++ b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/autoconfigure/LazyConnectionDataSourceConfiguration.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.autoconfigure; + +import javax.sql.DataSource; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.core.Ordered; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; +import org.springframework.jmx.export.MBeanExporter; +import org.springframework.jmx.support.JmxUtils; + +/** + * Replace the auto-configured {@link DataSource} by a + * {@linkplain LazyConnectionDataSourceProxy lazy proxy} that fetches the underlying JDBC + * connection as late as possible. Also make sure to register the target + * {@link DataSource} in the JMX domain if necessary. + * + * @author Stephane Nicoll + */ +@Configuration +@ConditionalOnBean(DataSource.class) +@ConditionalOnProperty(name = "spring.datasource.connection-fetch", havingValue = "lazy") +class LazyConnectionDataSourceConfiguration { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static LazyConnectionDataSourceBeanPostProcessor lazyConnectionDataSourceBeanPostProcessor( + ObjectProvider mbeanExporter) { + return new LazyConnectionDataSourceBeanPostProcessor(mbeanExporter); + } + + static class LazyConnectionDataSourceBeanPostProcessor implements BeanPostProcessor, Ordered { + + private final ObjectProvider mbeanExporter; + + LazyConnectionDataSourceBeanPostProcessor(ObjectProvider mbeanExporter) { + this.mbeanExporter = mbeanExporter; + } + + @Override + public @Nullable Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (beanName.equals("dataSource") && bean instanceof DataSource dataSource) { + this.mbeanExporter.ifAvailable((exporter) -> { + if (JmxUtils.isMBean(dataSource.getClass())) { + exporter.registerManagedResource(dataSource, beanName); + } + }); + return new LazyConnectionDataSourceProxy(dataSource); + } + return bean; + } + + @Override + public int getOrder() { + return 0; + } + + } + +} diff --git a/module/spring-boot-jdbc/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-jdbc/src/main/resources/META-INF/additional-spring-configuration-metadata.json index b15bc1f28f83..91f32a0bcd12 100644 --- a/module/spring-boot-jdbc/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-jdbc/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -6,6 +6,12 @@ "description": "Whether to enable database health check.", "defaultValue": true }, + { + "name": "spring.datasource.connection-fetch", + "type": "java.lang.String", + "description": "Whether the auto-configured DataSource should be wrapped in a proxy that fetches JDBC connections as late as possible.", + "defaultValue": "eager" + }, { "name": "spring.datasource.continue-on-error", "type": "java.lang.Boolean", @@ -118,6 +124,19 @@ } ], "hints": [ + { + "name": "spring.datasource.connection-fetch", + "values": [ + { + "value": "eager", + "description": "Fetch JDBC connections directly." + }, + { + "value": "lazy", + "description": "Fetch JDBC connections as late as possible, or not at all if no statement is executed." + } + ] + }, { "name": "spring.datasource.data", "providers": [ diff --git a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceUnwrapperTests.java b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceUnwrapperTests.java index 129b6026e62d..3b8cd79f260d 100644 --- a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceUnwrapperTests.java +++ b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceUnwrapperTests.java @@ -29,6 +29,7 @@ import org.springframework.aop.framework.ProxyFactory; import org.springframework.jdbc.datasource.DelegatingDataSource; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; import org.springframework.jdbc.datasource.SingleConnectionDataSource; import org.springframework.jdbc.datasource.SmartDataSource; @@ -73,6 +74,14 @@ void unwrapWithProxy() { .isSameAs(dataSource); } + @Test + void unwrapWithLazyConnectionDataSource() { + HikariDataSource dataSource = new HikariDataSource(); + DataSource actual = new LazyConnectionDataSourceProxy(dataSource); + assertThat(DataSourceUnwrapper.unwrap(actual, HikariConfigMXBean.class, HikariDataSource.class)) + .isSameAs(dataSource); + } + @Test void unwrapWithProxyAndDelegate() { DataSource dataSource = new HikariDataSource(); diff --git a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/autoconfigure/LazyConnectionDataSourceConfigurationTests.java b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/autoconfigure/LazyConnectionDataSourceConfigurationTests.java new file mode 100644 index 000000000000..dc99d2a0c52b --- /dev/null +++ b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/autoconfigure/LazyConnectionDataSourceConfigurationTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.autoconfigure; + +import java.util.Random; +import java.util.UUID; + +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariDataSource; +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.autoconfigure.jmx.JmxAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LazyConnectionDataSourceConfiguration}. + * + * @author Stephane Nicoll + */ +class LazyConnectionDataSourceConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.datasource.url:jdbc:h2:mem:test-" + new Random().nextInt()); + + @Test + void autoConfigurationConfiguresLazyProxyWhenEnabled() { + this.contextRunner + .withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName(), + "spring.datasource.connection-fetch=lazy") + .run((context) -> { + assertThat(context).hasSingleBean(DataSource.class).hasSingleBean(LazyConnectionDataSourceProxy.class); + DataSource dataSource = context.getBean(LazyConnectionDataSourceProxy.class); + HikariDataSource actualDataSource = dataSource.unwrap(HikariDataSource.class); + assertThat(actualDataSource.getJdbcUrl()).startsWith("jdbc:h2:mem:test-"); + }); + } + + @ParameterizedTest + @ValueSource(strings = { "eager", "lazy" }) + void autoConfigurationExposeDataSourceMBeanWhenEnabled(String connectionFetchStrategy) { + String uniqueDomain = UUID.randomUUID().toString(); + this.contextRunner.withConfiguration(AutoConfigurations.of(JmxAutoConfiguration.class)) + .withPropertyValues("spring.jmx.enabled=true", "spring.jmx.default-domain=" + uniqueDomain, + "spring.datasource.type=" + HikariDataSource.class.getName(), + "spring.datasource.connection-fetch=" + connectionFetchStrategy) + .run((context) -> { + MBeanServer mBeanServer = context.getBean(MBeanServer.class); + ObjectName objectName = new ObjectName( + "%s:type=%s,name=dataSource".formatted(uniqueDomain, HikariDataSource.class.getSimpleName())); + assertThat(mBeanServer.isRegistered(objectName)).isTrue(); + }); + } + + @Test + void autoConfigurationBacksOffWhenPropertyIsNotSet() { + this.contextRunner.withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()) + .run((context) -> assertThat(context).hasSingleBean(DataSource.class) + .hasSingleBean(HikariDataSource.class) + .doesNotHaveBean(LazyConnectionDataSourceProxy.class)); + } + + @Test + void autoConfigurationDoesNotConfigureLazyProxyWhenEager() { + this.contextRunner + .withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName(), + "spring.datasource.connection-fetch=eager") + .run((context) -> assertThat(context).hasSingleBean(DataSource.class) + .hasSingleBean(HikariDataSource.class) + .doesNotHaveBean(LazyConnectionDataSourceProxy.class)); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesDataSource() { + DataSource dataSource = mock(DataSource.class); + this.contextRunner.withBean(DataSource.class, () -> dataSource) + .withPropertyValues("spring.datasource.connection-fetch=lazy") + .run((context) -> { + assertThat(context).hasSingleBean(DataSource.class) + .doesNotHaveBean(LazyConnectionDataSourceProxy.class); + assertThat(context.getBean(DataSource.class)).isSameAs(dataSource); + }); + } + +} From cd1c2015cd644f409e396faa48021448b4d9356a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 10 Apr 2026 20:15:29 -0700 Subject: [PATCH 484/930] Upgrade to Jedis 7.4.1 Closes gh-50030 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index de8303f94a29..fbd454b3c61f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1084,7 +1084,7 @@ bom { releaseNotes("https://github.com/hunterhacker/jdom/releases/tag/JDOM-{version}") } } - library("Jedis", "7.4.0") { + library("Jedis", "7.4.1") { alignWith { property { name "jedis" From f49aa9c7da387c1cf5ef528d7be12d1b34318206 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 10 Apr 2026 20:15:33 -0700 Subject: [PATCH 485/930] Upgrade to Lettuce 7.5.1.RELEASE Closes gh-50031 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index fbd454b3c61f..638d560ca4bc 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1316,7 +1316,7 @@ bom { releaseNotes("https://github.com/Kotlin/kotlinx.serialization/releases/tag/v{version}") } } - library("Lettuce", "7.5.0.RELEASE") { + library("Lettuce", "7.5.1.RELEASE") { prohibit { contains ".BETA" because "we don't want betas" From a0c27bb4f17cd988a743986fd8c9be9205791d82 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 10 Apr 2026 20:17:37 -0700 Subject: [PATCH 486/930] Upgrade to Neo4j Java Driver 6.0.4 Closes gh-50032 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 638d560ca4bc..f943ac7e90d4 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1729,7 +1729,7 @@ bom { ] } } - library("Neo4j Java Driver", "6.0.3") { + library("Neo4j Java Driver", "6.0.4") { alignWith { version { of "org.neo4j.driver:neo4j-java-driver" From 53827d47d0802670fd53b665643aef8af4fe7bc8 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 10 Apr 2026 17:09:24 -0700 Subject: [PATCH 487/930] Move OAuth2 and SAML 2.0 documentation to a security section Closes gh-50022 --- .../antora/modules/ROOT/pages/redirect.adoc | 42 +- .../reference/pages/security/index.adoc | 7 + .../reference/pages/security/oauth2.adoc | 296 +++++++++++++ .../reference/pages/security/saml2.adoc | 107 +++++ .../reference/pages/web/spring-security.adoc | 400 +----------------- .../reference/partials/nav-reference.adoc | 4 + .../client/MyOAuthClientConfiguration.java | 17 +- .../MySamlRelyingPartyConfiguration.java | 2 +- .../client/MyOAuthClientConfiguration.kt | 2 +- .../MyWebFluxSecurityConfiguration.kt | 2 +- 10 files changed, 452 insertions(+), 427 deletions(-) create mode 100644 documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/index.adoc create mode 100644 documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/oauth2.adoc create mode 100644 documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/saml2.adoc rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/{web => }/security/oauth2/client/MyOAuthClientConfiguration.java (80%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/{web => }/security/saml2/relyingparty/MySamlRelyingPartyConfiguration.java (95%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/{web => }/security/oauth2/client/MyOAuthClientConfiguration.kt (95%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/{web => }/security/springwebflux/MyWebFluxSecurityConfiguration.kt (95%) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc index 72414df0faae..a62f294c35a5 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc @@ -1843,6 +1843,27 @@ * xref:reference:packaging/native-image/introducing-graalvm-native-images.adoc#packaging.native-image.introducing-graalvm-native-images.understanding-aot-processing.source-code-generation[#packaging.native-image.introducing-graalvm-native-images.understanding-aot-processing.source-code-generation] * xref:reference:packaging/native-image/introducing-graalvm-native-images.adoc#packaging.native-image.introducing-graalvm-native-images.understanding-aot-processing[#packaging.native-image.introducing-graalvm-native-images.understanding-aot-processing] * xref:reference:packaging/native-image/introducing-graalvm-native-images.adoc#packaging.native-image.introducing-graalvm-native-images[#native-image.introducing-graalvm-native-images] +* xref:reference:security/oauth2.adoc#security.oauth2.authorization-server[#boot-features-security-authorization-server] +* xref:reference:security/oauth2.adoc#security.oauth2.authorization-server[#features.security.authorization-server] +* xref:reference:security/oauth2.adoc#security.oauth2.authorization-server[#web.security.oauth2.authorization-server] +* xref:reference:security/oauth2.adoc#security.oauth2.client.common-providers[#boot-features-security-oauth2-common-providers] +* xref:reference:security/oauth2.adoc#security.oauth2.client.common-providers[#features.security.oauth2.client.common-providers] +* xref:reference:security/oauth2.adoc#security.oauth2.client.common-providers[#web.security.oauth2.client.common-providers] +* xref:reference:security/oauth2.adoc#security.oauth2.client[#boot-features-security-oauth2-client] +* xref:reference:security/oauth2.adoc#security.oauth2.client[#features.security.oauth2.client] +* xref:reference:security/oauth2.adoc#security.oauth2.client[#web.security.oauth2.client] +* xref:reference:security/oauth2.adoc#security.oauth2.server[#boot-features-security-oauth2-server] +* xref:reference:security/oauth2.adoc#security.oauth2.server[#features.security.oauth2.server] +* xref:reference:security/oauth2.adoc#security.oauth2.server[#web.security.oauth2.server] +* xref:reference:security/oauth2.adoc#security.oauth2[#boot-features-security-oauth2] +* xref:reference:security/oauth2.adoc#security.oauth2[#features.security.oauth2] +* xref:reference:security/oauth2.adoc#security.oauth2[#web.security.oauth2] +* xref:reference:security/saml2.adoc#security.saml2.relying-party[#boot-features-security-saml2-relying-party] +* xref:reference:security/saml2.adoc#security.saml2.relying-party[#features.security.saml2.relying-party] +* xref:reference:security/saml2.adoc#security.saml2.relying-party[#web.security.saml2.relying-party] +* xref:reference:security/saml2.adoc#security.saml2[#boot-features-security-saml] +* xref:reference:security/saml2.adoc#security.saml2[#features.security.saml2] +* xref:reference:security/saml2.adoc#security.saml2[#web.security.saml2] * xref:reference:testing/index.adoc#testing[#boot-features-testing] * xref:reference:testing/index.adoc#testing[#features.testing] * xref:reference:testing/spring-applications.adoc#testing.spring-applications[#boot-features-testing-spring-applications] @@ -2166,27 +2187,6 @@ * xref:reference:web/spring-hateoas.adoc#web.spring-hateoas[#boot-features-spring-hateoas] * xref:reference:web/spring-hateoas.adoc#web.spring-hateoas[#features.spring-hateoas] * xref:reference:web/spring-hateoas.adoc#web.spring-hateoas[#web.spring-hateoas] -* xref:reference:web/spring-security.adoc#web.security.oauth2.authorization-server[#boot-features-security-authorization-server] -* xref:reference:web/spring-security.adoc#web.security.oauth2.authorization-server[#features.security.authorization-server] -* xref:reference:web/spring-security.adoc#web.security.oauth2.authorization-server[#web.security.oauth2.authorization-server] -* xref:reference:web/spring-security.adoc#web.security.oauth2.client.common-providers[#boot-features-security-oauth2-common-providers] -* xref:reference:web/spring-security.adoc#web.security.oauth2.client.common-providers[#features.security.oauth2.client.common-providers] -* xref:reference:web/spring-security.adoc#web.security.oauth2.client.common-providers[#web.security.oauth2.client.common-providers] -* xref:reference:web/spring-security.adoc#web.security.oauth2.client[#boot-features-security-oauth2-client] -* xref:reference:web/spring-security.adoc#web.security.oauth2.client[#features.security.oauth2.client] -* xref:reference:web/spring-security.adoc#web.security.oauth2.client[#web.security.oauth2.client] -* xref:reference:web/spring-security.adoc#web.security.oauth2.server[#boot-features-security-oauth2-server] -* xref:reference:web/spring-security.adoc#web.security.oauth2.server[#features.security.oauth2.server] -* xref:reference:web/spring-security.adoc#web.security.oauth2.server[#web.security.oauth2.server] -* xref:reference:web/spring-security.adoc#web.security.oauth2[#boot-features-security-oauth2] -* xref:reference:web/spring-security.adoc#web.security.oauth2[#features.security.oauth2] -* xref:reference:web/spring-security.adoc#web.security.oauth2[#web.security.oauth2] -* xref:reference:web/spring-security.adoc#web.security.saml2.relying-party[#boot-features-security-saml2-relying-party] -* xref:reference:web/spring-security.adoc#web.security.saml2.relying-party[#features.security.saml2.relying-party] -* xref:reference:web/spring-security.adoc#web.security.saml2.relying-party[#web.security.saml2.relying-party] -* xref:reference:web/spring-security.adoc#web.security.saml2[#boot-features-security-saml] -* xref:reference:web/spring-security.adoc#web.security.saml2[#features.security.saml2] -* xref:reference:web/spring-security.adoc#web.security.saml2[#web.security.saml2] * xref:reference:web/spring-security.adoc#web.security.spring-mvc[#boot-features-security-mvc] * xref:reference:web/spring-security.adoc#web.security.spring-mvc[#features.security.spring-mvc] * xref:reference:web/spring-security.adoc#web.security.spring-mvc[#web.security.spring-mvc] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/index.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/index.adoc new file mode 100644 index 000000000000..a4392ce6b728 --- /dev/null +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/index.adoc @@ -0,0 +1,7 @@ +[[security]] += Security + +Spring Boot provides helpful support for general purpose security features such as OAuth2 and SAML 2.0. +This section covers those general purpose security concerns. + +If you're looking to secure web applications, see the xref:web/spring-security.adoc#web.security[web security section] instead of this one. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/oauth2.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/oauth2.adoc new file mode 100644 index 000000000000..f57b96a172b2 --- /dev/null +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/oauth2.adoc @@ -0,0 +1,296 @@ +[[security.oauth2]] += OAuth2 + +https://oauth.net/2/[OAuth2] is a widely used authorization framework. + + + +[[security.oauth2.client]] +== Client + +If you have `spring-security-oauth2-client` on your classpath, you can take advantage of some auto-configuration to set up OAuth2/Open ID Connect clients. +This configuration makes use of the properties under javadoc:org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties[]. +The same properties are applicable to both servlet and reactive web applications. + +Each registration must specify an OAuth 2 provider. +When set, the value of the `spring.security.oauth2.client.registration..provider` property is used to specify the registration's provider. +If the `provider` property is not set, the registration's ID is used instead. +Both approaches are shown in the following example: + +[configprops,yaml] +---- +spring: + security: + oauth2: + client: + registration: + my-client: + client-id: "abcd" + client-secret: "password" + provider: "example" + example: + client-id: "abcd" + client-secret: "password" +---- + +The registrations `my-client` and `example` will both use the provider with ID `example`. +The former will do so due to the value of the `spring.security.oauth2.client.registration.my-client.provider` property. +The latter will do so due to its ID being `example` and there being no `provider` property configured for the registration. + +The specified provider can either be a reference to a provider configured using `spring.security.oauth2.client.provider..*` properties or one of the xref:security/oauth2.adoc#security.oauth2.client.common-providers[known common providers]. + +You can register multiple OAuth2 clients and providers under the `spring.security.oauth2.client` prefix, as shown in the following example: + +[configprops,yaml] +---- +spring: + security: + oauth2: + client: + registration: + my-login-client: + client-id: "abcd" + client-secret: "password" + client-name: "Client for OpenID Connect" + provider: "my-oauth-provider" + scope: "openid,profile,email,phone,address" + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + client-authentication-method: "client_secret_basic" + authorization-grant-type: "authorization_code" + + my-client-1: + client-id: "abcd" + client-secret: "password" + client-name: "Client for user scope" + provider: "my-oauth-provider" + scope: "user" + redirect-uri: "{baseUrl}/authorized/user" + client-authentication-method: "client_secret_basic" + authorization-grant-type: "authorization_code" + + my-client-2: + client-id: "abcd" + client-secret: "password" + client-name: "Client for email scope" + provider: "my-oauth-provider" + scope: "email" + redirect-uri: "{baseUrl}/authorized/email" + client-authentication-method: "client_secret_basic" + authorization-grant-type: "authorization_code" + + provider: + my-oauth-provider: + authorization-uri: "https://my-auth-server.com/oauth2/authorize" + token-uri: "https://my-auth-server.com/oauth2/token" + user-info-uri: "https://my-auth-server.com/userinfo" + user-info-authentication-method: "header" + jwk-set-uri: "https://my-auth-server.com/oauth2/jwks" + user-name-attribute: "name" +---- + +In this example, there are three registrations. +In order of declaration, their IDs are `my-login-client`, `my-client-1`, and `my-client-2`. +There is also a single provider with ID `my-oauth-provider`. + +For OpenID Connect providers that support https://openid.net/specs/openid-connect-discovery-1_0.html[OpenID Connect discovery], the configuration can be further simplified. +The provider needs to be configured with an `issuer-uri` which is the URI that it asserts as its Issuer Identifier. +For example, if the `issuer-uri` provided is `+https://example.com+`, then an "`OpenID Provider Configuration Request`" will be made to `+https://example.com/.well-known/openid-configuration+`. +The result is expected to be an "`OpenID Provider Configuration Response`". +The following example shows how an OpenID Connect Provider can be configured with the `issuer-uri`: + +[configprops,yaml] +---- +spring: + security: + oauth2: + client: + provider: + oidc-provider: + issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/" +---- + +By default, Spring Security's javadoc:org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter[] only processes URLs matching `/login/oauth2/code/*`. +If you want to customize the `redirect-uri` to use a different pattern, you need to provide configuration to process that custom pattern. +For example, for servlet applications, you can add your own javadoc:org.springframework.security.web.SecurityFilterChain[] that resembles the following: + +include-code::MyOAuthClientConfiguration[] + +TIP: Spring Boot auto-configures an javadoc:org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService[] which is used by Spring Security for the management of client registrations. +The javadoc:org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService[] has limited capabilities and we recommend using it only for development environments. +For production environments, consider using a javadoc:org.springframework.security.oauth2.client.JdbcOAuth2AuthorizedClientService[] or creating your own implementation of javadoc:org.springframework.security.oauth2.client.OAuth2AuthorizedClientService[]. + + + +[[security.oauth2.client.common-providers]] +=== OAuth2 Client Registration for Common Providers + +For common OAuth2 and OpenID providers (Google, Github, Facebook, and Okta), we provide a set of provider defaults. +The IDs of these common provides are `google`, `github`, `facebook`, and `okta`, respectively. + +If you do not need to customize these providers, set the registration's `provider` property to the ID of one of the common providers. +Alternatively, you can xref:security/oauth2.adoc#security.oauth2.client[use a registration ID that matches the ID of the provider]. +The two configurations in the following example use the common `google` provider: + +[configprops,yaml] +---- +spring: + security: + oauth2: + client: + registration: + my-client: + client-id: "abcd" + client-secret: "password" + provider: "google" + google: + client-id: "abcd" + client-secret: "password" +---- + + + +[[security.oauth2.server]] +== Resource Server + +If you have `spring-security-oauth2-resource-server` on your classpath, Spring Boot can set up an OAuth2 Resource Server. +For JWT configuration, a JWK Set URI or OIDC Issuer URI needs to be specified, as shown in the following examples: + +[configprops,yaml] +---- +spring: + security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: "https://example.com/oauth2/default/v1/keys" +---- + +[configprops,yaml] +---- +spring: + security: + oauth2: + resourceserver: + jwt: + issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/" +---- + +NOTE: If the authorization server does not support a JWK Set URI, you can configure the resource server with the Public Key used for verifying the signature of the JWT. +This can be done using the configprop:spring.security.oauth2.resourceserver.jwt.public-key-location[] property, where the value needs to point to a file containing the public key in the PEM-encoded x509 format. + +The configprop:spring.security.oauth2.resourceserver.jwt.audiences[] property can be used to specify the expected values of the aud claim in JWTs. +For example, to require JWTs to contain an aud claim with the value `my-audience`: + +[configprops,yaml] +---- +spring: + security: + oauth2: + resourceserver: + jwt: + audiences: + - "my-audience" +---- + +The same properties are applicable for both servlet and reactive applications. +Alternatively, you can define your own javadoc:org.springframework.security.oauth2.jwt.JwtDecoder[] bean for servlet applications or a javadoc:org.springframework.security.oauth2.jwt.ReactiveJwtDecoder[] for reactive applications. + +In cases where opaque tokens are used instead of JWTs, you can configure the following properties to validate tokens through introspection: + +[configprops,yaml] +---- +spring: + security: + oauth2: + resourceserver: + opaquetoken: + introspection-uri: "https://example.com/check-token" + client-id: "my-client-id" + client-secret: "my-client-secret" +---- + +Again, the same properties are applicable for both servlet and reactive applications. + +The result is an auto-configured introspector. Either a javadoc:org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector[] or, in a reactive application, a javadoc:org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector[]. +These auto-configured introspectors can be customized using javadoc:org.springframework.boot.security.oauth2.server.resource.autoconfigure.SpringOpaqueTokenIntrospectorBuilderCustomizer[] and javadoc:org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive.SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer[] beans respectively. + +To take complete control of the introspection, define your own javadoc:org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector[] or javadoc:org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector[] bean. + + + +[[security.oauth2.authorization-server]] +== Authorization Server + +If you have `spring-security-oauth2-authorization-server` on your classpath, you can take advantage of some auto-configuration to set up a Servlet-based OAuth2 Authorization Server. + +You can register multiple OAuth2 clients under the `spring.security.oauth2.authorizationserver.client` prefix, as shown in the following example: + +[configprops,yaml] +---- +spring: + security: + oauth2: + authorizationserver: + client: + my-client-1: + registration: + client-id: "abcd" + client-secret: "{noop}secret1" + client-authentication-methods: + - "client_secret_basic" + authorization-grant-types: + - "authorization_code" + - "refresh_token" + redirect-uris: + - "https://my-client-1.com/login/oauth2/code/abcd" + - "https://my-client-1.com/authorized" + scopes: + - "openid" + - "profile" + - "email" + - "phone" + - "address" + require-authorization-consent: true + token: + authorization-code-time-to-live: 5m + access-token-time-to-live: 10m + access-token-format: "reference" + reuse-refresh-tokens: false + refresh-token-time-to-live: 30m + my-client-2: + registration: + client-id: "efgh" + client-secret: "{noop}secret2" + client-authentication-methods: + - "client_secret_jwt" + authorization-grant-types: + - "client_credentials" + scopes: + - "user.read" + - "user.write" + jwk-set-uri: "https://my-client-2.com/jwks" + token-endpoint-authentication-signing-algorithm: "RS256" +---- + +NOTE: The `client-secret` property must be in a format that can be matched by the configured javadoc:org.springframework.security.crypto.password.PasswordEncoder[]. +The default instance of javadoc:org.springframework.security.crypto.password.PasswordEncoder[] is created via `PasswordEncoderFactories.createDelegatingPasswordEncoder()`. + +The auto-configuration Spring Boot provides for Spring Authorization Server is designed for getting started quickly. +Most applications will require customization and will want to define several beans to override auto-configuration. + +The following components can be defined as beans to override auto-configuration specific to Spring Authorization Server: + +* javadoc:org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository[] +* javadoc:org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings[] +* javadoc:org.springframework.security.web.SecurityFilterChain[] +* `com.nimbusds.jose.jwk.source.JWKSource` +* javadoc:org.springframework.security.oauth2.jwt.JwtDecoder[] + +TIP: Spring Boot auto-configures an javadoc:org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository[] which is used by Spring Authorization Server for the management of registered clients. +The javadoc:org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository[] has limited capabilities and we recommend using it only for development environments. +For production environments, consider using a javadoc:org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository[] or creating your own implementation of javadoc:org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository[]. + +Additional information can be found in the {url-spring-security-docs}/servlet/oauth2/authorization-server/getting-started.html[Getting Started] chapter of Spring Security Reference Documentation. + + + diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/saml2.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/saml2.adoc new file mode 100644 index 000000000000..144a1240b9be --- /dev/null +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/saml2.adoc @@ -0,0 +1,107 @@ +[[security.saml2]] += SAML 2.0 + +https://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html[SAML v2.0] is a widely adopted framework for exchanging security information between online business partners. + + + +[[security.saml2.build]] +== Build Configuration +SAML 2.0 support builds off of the OpenSAML library that https://shibboleth.atlassian.net/wiki/spaces/DEV/pages/1123844333/Use+of+Maven+Central#Publishing-to-Maven-Central[requires an extra repository] configuration. + + + +[[security.saml2.build.maven]] +=== Using Maven +With Maven, you need to add an extra `repository` element to your POM as follows: + +[source,xml,subs="verbatim,attributes"] +---- + + + shibboleth-releases + Shibboleth Releases Repository + https://build.shibboleth.net/maven/releases + + false + + + +---- + + +[[security.saml2.build.gradle]] +=== Using Gradle +With Gradle, a repository element should be added to your build script: + +[source,gradle,subs="verbatim,attributes"] +---- +repositories { + maven { url "https://build.shibboleth.net/maven/releases" } +} +---- + + + +[[security.saml2.relying-party]] +== Relying Party + +If you have `spring-security-saml2-service-provider` on your classpath, you can take advantage of some auto-configuration to set up a SAML 2.0 Relying Party. +This configuration makes use of the properties under javadoc:org.springframework.boot.security.saml2.autoconfigure.Saml2RelyingPartyProperties[]. + +A relying party registration represents a paired configuration between an Identity Provider, IDP, and a Service Provider, SP. +You can register multiple relying parties under the `spring.security.saml2.relyingparty` prefix, as shown in the following example: + +[configprops,yaml] +---- +spring: + security: + saml2: + relyingparty: + registration: + my-relying-party1: + signing: + credentials: + - private-key-location: "path-to-private-key" + certificate-location: "path-to-certificate" + decryption: + credentials: + - private-key-location: "path-to-private-key" + certificate-location: "path-to-certificate" + singlelogout: + url: "https://myapp/logout/saml2/slo" + response-url: "https://remoteidp2.slo.url" + binding: "POST" + assertingparty: + verification: + credentials: + - certificate-location: "path-to-verification-cert" + entity-id: "remote-idp-entity-id1" + sso-url: "https://remoteidp1.sso.url" + + my-relying-party2: + signing: + credentials: + - private-key-location: "path-to-private-key" + certificate-location: "path-to-certificate" + decryption: + credentials: + - private-key-location: "path-to-private-key" + certificate-location: "path-to-certificate" + assertingparty: + verification: + credentials: + - certificate-location: "path-to-other-verification-cert" + entity-id: "remote-idp-entity-id2" + sso-url: "https://remoteidp2.sso.url" + singlelogout: + url: "https://remoteidp2.slo.url" + response-url: "https://myapp/logout/saml2/slo" + binding: "POST" +---- + +For SAML2 logout, by default, Spring Security's javadoc:org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter[] and javadoc:org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter[] only process URLs matching `/logout/saml2/slo`. +If you want to customize the `url` to which AP-initiated logout requests get sent to or the `response-url` to which an AP sends logout responses to, to use a different pattern, you need to provide configuration to process that custom pattern. +For example, for servlet applications, you can add your own javadoc:org.springframework.security.web.SecurityFilterChain[] that resembles the following: + +include-code::MySamlRelyingPartyConfiguration[] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/spring-security.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/spring-security.adoc index ccbcc6c6a27c..86c46862ea19 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/spring-security.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/spring-security.adoc @@ -88,403 +88,15 @@ include-code::MyWebFluxSecurityConfiguration[] [[web.security.oauth2]] -== OAuth2 +== [[web.security.oauth2.client]][[web.security.oauth2.client.common-providers]][[web.security.oauth2.server]][[web.security.oauth2.authorization-server]]OAuth2 -https://oauth.net/2/[OAuth2] is a widely used authorization framework that is supported by Spring. - - - -[[web.security.oauth2.client]] -=== Client - -If you have `spring-security-oauth2-client` on your classpath, you can take advantage of some auto-configuration to set up OAuth2/Open ID Connect clients. -This configuration makes use of the properties under javadoc:org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties[]. -The same properties are applicable to both servlet and reactive applications. - -Each registration must specify an OAuth 2 provider. -When set, the value of the `spring.security.oauth2.client.registration..provider` property is used to specify the registration's provider. -If the `provider` property is not set, the registration's ID is used instead. -Both approaches are shown in the following example: - -[configprops,yaml] ----- -spring: - security: - oauth2: - client: - registration: - my-client: - client-id: "abcd" - client-secret: "password" - provider: "example" - example: - client-id: "abcd" - client-secret: "password" ----- - -The registrations `my-client` and `example` will both use the provider with ID `example`. -The former will do so due to the value of the `spring.security.oauth2.client.registration.my-client.provider` property. -The latter will do so due to its ID being `example` and there being no `provider` property configured for the registration. - -The specified provider can either be a reference to a provider configured using `spring.security.oauth2.client.provider..*` properties or one of the xref:web/spring-security.adoc#web.security.oauth2.client.common-providers[known common providers]. - -You can register multiple OAuth2 clients and providers under the `spring.security.oauth2.client` prefix, as shown in the following example: - -[configprops,yaml] ----- -spring: - security: - oauth2: - client: - registration: - my-login-client: - client-id: "abcd" - client-secret: "password" - client-name: "Client for OpenID Connect" - provider: "my-oauth-provider" - scope: "openid,profile,email,phone,address" - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - client-authentication-method: "client_secret_basic" - authorization-grant-type: "authorization_code" - - my-client-1: - client-id: "abcd" - client-secret: "password" - client-name: "Client for user scope" - provider: "my-oauth-provider" - scope: "user" - redirect-uri: "{baseUrl}/authorized/user" - client-authentication-method: "client_secret_basic" - authorization-grant-type: "authorization_code" - - my-client-2: - client-id: "abcd" - client-secret: "password" - client-name: "Client for email scope" - provider: "my-oauth-provider" - scope: "email" - redirect-uri: "{baseUrl}/authorized/email" - client-authentication-method: "client_secret_basic" - authorization-grant-type: "authorization_code" - - provider: - my-oauth-provider: - authorization-uri: "https://my-auth-server.com/oauth2/authorize" - token-uri: "https://my-auth-server.com/oauth2/token" - user-info-uri: "https://my-auth-server.com/userinfo" - user-info-authentication-method: "header" - jwk-set-uri: "https://my-auth-server.com/oauth2/jwks" - user-name-attribute: "name" ----- - -In this example, there are three registrations. -In order of declaration, their IDs are `my-login-client`, `my-client-1`, and `my-client-2`. -There is also a single provider with ID `my-oauth-provider`. - -For OpenID Connect providers that support https://openid.net/specs/openid-connect-discovery-1_0.html[OpenID Connect discovery], the configuration can be further simplified. -The provider needs to be configured with an `issuer-uri` which is the URI that it asserts as its Issuer Identifier. -For example, if the `issuer-uri` provided is "https://example.com", then an "OpenID Provider Configuration Request" will be made to "https://example.com/.well-known/openid-configuration". -The result is expected to be an "OpenID Provider Configuration Response". -The following example shows how an OpenID Connect Provider can be configured with the `issuer-uri`: - -[configprops,yaml] ----- -spring: - security: - oauth2: - client: - provider: - oidc-provider: - issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/" ----- - -By default, Spring Security's javadoc:org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter[] only processes URLs matching `/login/oauth2/code/*`. -If you want to customize the `redirect-uri` to use a different pattern, you need to provide configuration to process that custom pattern. -For example, for servlet applications, you can add your own javadoc:org.springframework.security.web.SecurityFilterChain[] that resembles the following: - -include-code::MyOAuthClientConfiguration[] - -TIP: Spring Boot auto-configures an javadoc:org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService[] which is used by Spring Security for the management of client registrations. -The javadoc:org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService[] has limited capabilities and we recommend using it only for development environments. -For production environments, consider using a javadoc:org.springframework.security.oauth2.client.JdbcOAuth2AuthorizedClientService[] or creating your own implementation of javadoc:org.springframework.security.oauth2.client.OAuth2AuthorizedClientService[]. - - - -[[web.security.oauth2.client.common-providers]] -==== OAuth2 Client Registration for Common Providers - -For common OAuth2 and OpenID providers (Google, Github, Facebook, and Okta), we provide a set of provider defaults. -The IDs of these common provides are `google`, `github`, `facebook`, and `okta`, respectively. - -If you do not need to customize these providers, set the registration's `provider` property to the ID of one of the common providers. -Alternatively, you can xref:web/spring-security.adoc#web.security.oauth2.client[use a registration ID that matches the ID of the provider]. -The two configurations in the following example use the common `google` provider: - -[configprops,yaml] ----- -spring: - security: - oauth2: - client: - registration: - my-client: - client-id: "abcd" - client-secret: "password" - provider: "google" - google: - client-id: "abcd" - client-secret: "password" ----- - - - -[[web.security.oauth2.server]] -=== Resource Server - -If you have `spring-security-oauth2-resource-server` on your classpath, Spring Boot can set up an OAuth2 Resource Server. -For JWT configuration, a JWK Set URI or OIDC Issuer URI needs to be specified, as shown in the following examples: - -[configprops,yaml] ----- -spring: - security: - oauth2: - resourceserver: - jwt: - jwk-set-uri: "https://example.com/oauth2/default/v1/keys" ----- - -[configprops,yaml] ----- -spring: - security: - oauth2: - resourceserver: - jwt: - issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/" ----- - -NOTE: If the authorization server does not support a JWK Set URI, you can configure the resource server with the Public Key used for verifying the signature of the JWT. -This can be done using the configprop:spring.security.oauth2.resourceserver.jwt.public-key-location[] property, where the value needs to point to a file containing the public key in the PEM-encoded x509 format. - -The configprop:spring.security.oauth2.resourceserver.jwt.audiences[] property can be used to specify the expected values of the aud claim in JWTs. -For example, to require JWTs to contain an aud claim with the value `my-audience`: - -[configprops,yaml] ----- -spring: - security: - oauth2: - resourceserver: - jwt: - audiences: - - "my-audience" ----- - -The same properties are applicable for both servlet and reactive applications. -Alternatively, you can define your own javadoc:org.springframework.security.oauth2.jwt.JwtDecoder[] bean for servlet applications or a javadoc:org.springframework.security.oauth2.jwt.ReactiveJwtDecoder[] for reactive applications. - -In cases where opaque tokens are used instead of JWTs, you can configure the following properties to validate tokens through introspection: - -[configprops,yaml] ----- -spring: - security: - oauth2: - resourceserver: - opaquetoken: - introspection-uri: "https://example.com/check-token" - client-id: "my-client-id" - client-secret: "my-client-secret" ----- - -Again, the same properties are applicable for both servlet and reactive applications. - -The result is an auto-configured introspector. Either a javadoc:org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector[] or, in a reactive application, a javadoc:org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector[]. -These auto-configured introspectors can be customized using javadoc:org.springframework.boot.security.oauth2.server.resource.autoconfigure.SpringOpaqueTokenIntrospectorBuilderCustomizer[] and javadoc:org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive.SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer[] beans respectively. - -To take complete control of the introspection, define your own javadoc:org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector[] or javadoc:org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector[] bean. - - - -[[web.security.oauth2.authorization-server]] -=== Authorization Server - -If you have `spring-security-oauth2-authorization-server` on your classpath, you can take advantage of some auto-configuration to set up a Servlet-based OAuth2 Authorization Server. - -You can register multiple OAuth2 clients under the `spring.security.oauth2.authorizationserver.client` prefix, as shown in the following example: - -[configprops,yaml] ----- -spring: - security: - oauth2: - authorizationserver: - client: - my-client-1: - registration: - client-id: "abcd" - client-secret: "{noop}secret1" - client-authentication-methods: - - "client_secret_basic" - authorization-grant-types: - - "authorization_code" - - "refresh_token" - redirect-uris: - - "https://my-client-1.com/login/oauth2/code/abcd" - - "https://my-client-1.com/authorized" - scopes: - - "openid" - - "profile" - - "email" - - "phone" - - "address" - require-authorization-consent: true - token: - authorization-code-time-to-live: 5m - access-token-time-to-live: 10m - access-token-format: "reference" - reuse-refresh-tokens: false - refresh-token-time-to-live: 30m - my-client-2: - registration: - client-id: "efgh" - client-secret: "{noop}secret2" - client-authentication-methods: - - "client_secret_jwt" - authorization-grant-types: - - "client_credentials" - scopes: - - "user.read" - - "user.write" - jwk-set-uri: "https://my-client-2.com/jwks" - token-endpoint-authentication-signing-algorithm: "RS256" ----- - -NOTE: The `client-secret` property must be in a format that can be matched by the configured javadoc:org.springframework.security.crypto.password.PasswordEncoder[]. -The default instance of javadoc:org.springframework.security.crypto.password.PasswordEncoder[] is created via `PasswordEncoderFactories.createDelegatingPasswordEncoder()`. - -The auto-configuration Spring Boot provides for Spring Authorization Server is designed for getting started quickly. -Most applications will require customization and will want to define several beans to override auto-configuration. - -The following components can be defined as beans to override auto-configuration specific to Spring Authorization Server: - -* javadoc:org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository[] -* javadoc:org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings[] -* javadoc:org.springframework.security.web.SecurityFilterChain[] -* `com.nimbusds.jose.jwk.source.JWKSource` -* javadoc:org.springframework.security.oauth2.jwt.JwtDecoder[] - -TIP: Spring Boot auto-configures an javadoc:org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository[] which is used by Spring Authorization Server for the management of registered clients. -The javadoc:org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository[] has limited capabilities and we recommend using it only for development environments. -For production environments, consider using a javadoc:org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository[] or creating your own implementation of javadoc:org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository[]. - -Additional information can be found in the {url-spring-security-docs}/servlet/oauth2/authorization-server/getting-started.html[Getting Started] chapter of Spring Security Reference Documentation. +https://oauth.net/2/[OAuth2] is a widely used authorization framework. +For details of how to configure and use OAuth2 with your web applications, see the xref:security/oauth2.adoc#security.oauth2["`OAuth2`" section] of under "`Security`". [[web.security.saml2]] -== SAML 2.0 - - - -[[web.security.saml2.build]] -=== Build Configuration -SAML 2.0 support builds off of the OpenSAML library that https://shibboleth.atlassian.net/wiki/spaces/DEV/pages/1123844333/Use+of+Maven+Central#Publishing-to-Maven-Central[requires an extra repository] configuration. - - - -[[web.security.saml2.build.maven]] -==== Using Maven -With Maven, you need to add an extra `repository` element to your POM as follows: - -[source,xml,subs="verbatim,attributes"] ----- - - - shibboleth-releases - Shibboleth Releases Repository - https://build.shibboleth.net/maven/releases - - false - - - ----- - - -[[web.security.saml2.build.gradle]] -==== Using Gradle -With Gradle, a repository element should be added to your build script: - -[source,gradle,subs="verbatim,attributes"] ----- -repositories { - maven { url "https://build.shibboleth.net/maven/releases" } -} ----- - - - -[[web.security.saml2.relying-party]] -=== Relying Party - -If you have `spring-security-saml2-service-provider` on your classpath, you can take advantage of some auto-configuration to set up a SAML 2.0 Relying Party. -This configuration makes use of the properties under javadoc:org.springframework.boot.security.saml2.autoconfigure.Saml2RelyingPartyProperties[]. - -A relying party registration represents a paired configuration between an Identity Provider, IDP, and a Service Provider, SP. -You can register multiple relying parties under the `spring.security.saml2.relyingparty` prefix, as shown in the following example: - -[configprops,yaml] ----- -spring: - security: - saml2: - relyingparty: - registration: - my-relying-party1: - signing: - credentials: - - private-key-location: "path-to-private-key" - certificate-location: "path-to-certificate" - decryption: - credentials: - - private-key-location: "path-to-private-key" - certificate-location: "path-to-certificate" - singlelogout: - url: "https://myapp/logout/saml2/slo" - response-url: "https://remoteidp2.slo.url" - binding: "POST" - assertingparty: - verification: - credentials: - - certificate-location: "path-to-verification-cert" - entity-id: "remote-idp-entity-id1" - sso-url: "https://remoteidp1.sso.url" - - my-relying-party2: - signing: - credentials: - - private-key-location: "path-to-private-key" - certificate-location: "path-to-certificate" - decryption: - credentials: - - private-key-location: "path-to-private-key" - certificate-location: "path-to-certificate" - assertingparty: - verification: - credentials: - - certificate-location: "path-to-other-verification-cert" - entity-id: "remote-idp-entity-id2" - sso-url: "https://remoteidp2.sso.url" - singlelogout: - url: "https://remoteidp2.slo.url" - response-url: "https://myapp/logout/saml2/slo" - binding: "POST" ----- - -For SAML2 logout, by default, Spring Security's javadoc:org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter[] and javadoc:org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter[] only process URLs matching `/logout/saml2/slo`. -If you want to customize the `url` to which AP-initiated logout requests get sent to or the `response-url` to which an AP sends logout responses to, to use a different pattern, you need to provide configuration to process that custom pattern. -For example, for servlet applications, you can add your own javadoc:org.springframework.security.web.SecurityFilterChain[] that resembles the following: +== [[web.security.saml2.build]][[web.security.saml2.build.maven]][[web.security.saml2.build.gradle]][[web.security.saml2.relying-party]]SAML 2.0 -include-code::MySamlRelyingPartyConfiguration[] +https://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html[SAML v2.0] is a widely adopted framework for exchanging security information between online business partners. +For details of how to configure and use SAML 2.0 with your web applications, see the xref:security/saml2.adoc#security.saml2["`SAML 2.0`" section] under "`Security`". diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/partials/nav-reference.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/partials/nav-reference.adoc index fa7ba15090ca..3f1235532fd9 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/partials/nav-reference.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/partials/nav-reference.adoc @@ -57,6 +57,10 @@ *** xref:reference:messaging/spring-integration.adoc[] *** xref:reference:messaging/websockets.adoc[] +** xref:reference:security/index.adoc[] +*** xref:reference:security/oauth2.adoc[] +*** xref:reference:security/saml2.adoc[] + ** xref:reference:testing/index.adoc[] *** xref:reference:testing/test-modules.adoc[] *** xref:reference:testing/test-scope-dependencies.adoc[] diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/security/oauth2/client/MyOAuthClientConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/security/oauth2/client/MyOAuthClientConfiguration.java similarity index 80% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/security/oauth2/client/MyOAuthClientConfiguration.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/security/oauth2/client/MyOAuthClientConfiguration.java index f3e35ff60c5f..81aba498b9b4 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/security/oauth2/client/MyOAuthClientConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/security/oauth2/client/MyOAuthClientConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.web.security.oauth2.client; +package org.springframework.boot.docs.security.oauth2.client; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -29,15 +29,14 @@ public class MyOAuthClientConfiguration { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) { // @formatter:off - http - .authorizeHttpRequests((requests) -> requests - .anyRequest().authenticated() + http.authorizeHttpRequests((requests) -> + requests.anyRequest().authenticated() + ); + http.oauth2Login((login) -> + login.redirectionEndpoint((endpoint) -> + endpoint.baseUri("/login/oauth2/callback/*") ) - .oauth2Login((login) -> login - .redirectionEndpoint((endpoint) -> endpoint - .baseUri("/login/oauth2/callback/*") - ) - ); + ); // @formatter:on return http.build(); } diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/security/saml2/relyingparty/MySamlRelyingPartyConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/security/saml2/relyingparty/MySamlRelyingPartyConfiguration.java similarity index 95% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/security/saml2/relyingparty/MySamlRelyingPartyConfiguration.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/security/saml2/relyingparty/MySamlRelyingPartyConfiguration.java index 99514c1b389b..9928ebdc1766 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/security/saml2/relyingparty/MySamlRelyingPartyConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/security/saml2/relyingparty/MySamlRelyingPartyConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.web.security.saml2.relyingparty; +package org.springframework.boot.docs.security.saml2.relyingparty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/security/oauth2/client/MyOAuthClientConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/security/oauth2/client/MyOAuthClientConfiguration.kt similarity index 95% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/security/oauth2/client/MyOAuthClientConfiguration.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/security/oauth2/client/MyOAuthClientConfiguration.kt index e643c3b02561..4b877afa87cc 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/security/oauth2/client/MyOAuthClientConfiguration.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/security/oauth2/client/MyOAuthClientConfiguration.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.web.security.oauth2.client +package org.springframework.boot.docs.security.oauth2.client import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/security/springwebflux/MyWebFluxSecurityConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/security/springwebflux/MyWebFluxSecurityConfiguration.kt similarity index 95% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/security/springwebflux/MyWebFluxSecurityConfiguration.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/security/springwebflux/MyWebFluxSecurityConfiguration.kt index c2390c63b1ab..5036ccae239b 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/security/springwebflux/MyWebFluxSecurityConfiguration.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/security/springwebflux/MyWebFluxSecurityConfiguration.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.web.security.springwebflux +package org.springframework.boot.docs.security.springwebflux import org.springframework.boot.security.autoconfigure.web.reactive.PathRequest import org.springframework.context.annotation.Bean From 495d16b1f84bf19c7373d1284aadcee40f3269e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 8 Apr 2026 14:51:06 +0200 Subject: [PATCH 488/930] Add auto-configuration for Redis annotation-driven listeners This commit adds support for `@RedisListener` by providing the infrastructure to register those endpoints using a default `RedisMessageListenerContainer`. Users wishing to configure additional containers can benefit from `RedisMessageListenerContainerConfigurer`. The default container can be tuned using new properties in the `spring.data.redis.listener` namespace. Closes gh-49858 --- .../modules/reference/pages/data/nosql.adoc | 24 ++++ .../data/nosql/redis/receiving/MyBean.java | 30 +++++ .../nosql/redis/receiving/custom/MyBean.java | 30 +++++ .../custom/MyRedisConfiguration.java | 37 ++++++ .../docs/data/nosql/redis/receiving/MyBean.kt | 30 +++++ .../nosql/redis/receiving/custom/MyBean.kt | 30 +++++ .../receiving/custom/MyRedisConfiguration.kt | 39 ++++++ module/spring-boot-data-redis/build.gradle | 1 + ...onDrivenConfigurationIntegrationTests.java | 73 +++++++++++ ...ataRedisAnnotationDrivenConfiguration.java | 70 ++++++++++ .../DataRedisAutoConfiguration.java | 3 +- .../autoconfigure/DataRedisProperties.java | 123 ++++++++++++++++++ ...disMessageListenerContainerConfigurer.java | 76 +++++++++++ ...disAnnotationDrivenConfigurationTests.java | 122 +++++++++++++++++ .../build.gradle | 1 + 15 files changed, 688 insertions(+), 1 deletion(-) create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/nosql/redis/receiving/MyBean.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyBean.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyRedisConfiguration.java create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/redis/receiving/MyBean.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyBean.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyRedisConfiguration.kt create mode 100644 module/spring-boot-data-redis/src/dockerTest/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfigurationIntegrationTests.java create mode 100644 module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfiguration.java create mode 100644 module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisMessageListenerContainerConfigurer.java create mode 100644 module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfigurationTests.java diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc index 8bb18267aef0..1761565c5210 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc @@ -108,6 +108,30 @@ spring: +[[data.nosql.redis.receiving]] +=== Receiving a Message + +When the Redis infrastructure is present, any bean can be annotated with javadoc:org.springframework.data.redis.annotation.RedisListener[format=annotation] to create a listener endpoint. +If no javadoc:org.springframework.data.redis.listener.RedisMessageListenerContainer[] has been defined, a default one is configured automatically. + +The following component creates a listener endpoint on the `someChannel` channel: + +include-code::MyBean[] + +TIP: See the javadoc:org.springframework.data.redis.annotation.EnableRedisListeners[format=annotation] API documentation for more details. + +If you need to create more javadoc:org.springframework.data.redis.listener.RedisMessageListenerContainer[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.data.redis.autoconfigure.RedisMessageListenerContainerConfigurer[] that you can use to initialize a javadoc:org.springframework.data.redis.listener.RedisMessageListenerContainer[] with the same settings as the one that is auto-configured. + +For instance, the following example exposes another container that uses a specific javadoc:org.springframework.data.redis.connection.RedisConnectionFactory[]: + +include-code::custom/MyRedisConfiguration[] + +Then you can use the container in any javadoc:org.springframework.data.redis.annotation.RedisListener[format=annotation]-annotated method as follows: + +include-code::custom/MyBean[] + + + [[data.nosql.mongodb]] == MongoDB diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/nosql/redis/receiving/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/nosql/redis/receiving/MyBean.java new file mode 100644 index 000000000000..c8010cd955ae --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/nosql/redis/receiving/MyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.nosql.redis.receiving; + +import org.springframework.data.redis.annotation.RedisListener; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @RedisListener("someChannel") + public void processMessage(String content) { + // ... + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyBean.java new file mode 100644 index 000000000000..542b4ce37fda --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.nosql.redis.receiving.custom; + +import org.springframework.data.redis.annotation.RedisListener; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @RedisListener(topic = "someChannel", container = "myRedisMessageListenerContainer") + public void processMessage(String content) { + // ... + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyRedisConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyRedisConfiguration.java new file mode 100644 index 000000000000..13d975ad5593 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyRedisConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.nosql.redis.receiving.custom; + +import org.springframework.boot.data.redis.autoconfigure.RedisMessageListenerContainerConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +@Configuration(proxyBeanMethods = false) +public class MyRedisConfiguration { + + @Bean + public RedisMessageListenerContainer myRedisMessageListenerContainer( + RedisMessageListenerContainerConfigurer configurer, RedisConnectionFactory connectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + configurer.configure(container, connectionFactory); + // ... custom configuration + return container; + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/redis/receiving/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/redis/receiving/MyBean.kt new file mode 100644 index 000000000000..fc834fba26c4 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/redis/receiving/MyBean.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.nosql.redis.receiving + +import org.springframework.data.redis.annotation.RedisListener +import org.springframework.stereotype.Component + +@Component +class MyBean { + + @RedisListener("someChannel") + fun processMessage(content: String) { + // ... + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyBean.kt new file mode 100644 index 000000000000..7e40eaad8efe --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyBean.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.nosql.redis.receiving.custom + +import org.springframework.data.redis.annotation.RedisListener +import org.springframework.stereotype.Component + +@Component +class MyBean { + + @RedisListener(topic = "someChannel", container = "myRedisMessageListenerContainer") + fun processMessage(content: String) { + // ... + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyRedisConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyRedisConfiguration.kt new file mode 100644 index 000000000000..e53648f3b2e7 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/redis/receiving/custom/MyRedisConfiguration.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.nosql.redis.receiving.custom + +import org.springframework.boot.data.redis.autoconfigure.RedisMessageListenerContainerConfigurer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.connection.RedisConnectionFactory +import org.springframework.data.redis.listener.RedisMessageListenerContainer + +@Configuration(proxyBeanMethods = false) +class MyRedisConfiguration { + + @Bean + fun myRedisMessageListenerContainer( + configurer: RedisMessageListenerContainerConfigurer, + connectionFactory: RedisConnectionFactory + ): RedisMessageListenerContainer { + val container = RedisMessageListenerContainer() + configurer.configure(container, connectionFactory) + // ... custom configuration + return container + } + +} diff --git a/module/spring-boot-data-redis/build.gradle b/module/spring-boot-data-redis/build.gradle index 4f9843b7c543..95345e6009fe 100644 --- a/module/spring-boot-data-redis/build.gradle +++ b/module/spring-boot-data-redis/build.gradle @@ -42,6 +42,7 @@ dependencies { optional(project(":module:spring-boot-health")) optional(project(":module:spring-boot-micrometer-metrics")) optional("com.redis:testcontainers-redis") + optional("org.springframework:spring-messaging") optional("redis.clients:jedis") dockerTestImplementation(project(":core:spring-boot-test")) diff --git a/module/spring-boot-data-redis/src/dockerTest/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfigurationIntegrationTests.java b/module/spring-boot-data-redis/src/dockerTest/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfigurationIntegrationTests.java new file mode 100644 index 000000000000..fd5ca978bc5c --- /dev/null +++ b/module/spring-boot-data-redis/src/dockerTest/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfigurationIntegrationTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.data.redis.autoconfigure; + +import java.util.ArrayList; +import java.util.List; + +import com.redis.testcontainers.RedisContainer; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.container.TestImage; +import org.springframework.data.redis.annotation.RedisListener; +import org.springframework.data.redis.core.StringRedisTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +/** + * Integration tests for {@link DataRedisAnnotationDrivenConfiguration}. + * + * @author Stephane Nicoll + */ +@Testcontainers(disabledWithoutDocker = true) +class DataRedisAnnotationDrivenConfigurationIntegrationTests { + + @Container + static final RedisContainer redis = TestImage.container(RedisContainer.class); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataRedisAutoConfiguration.class)) + .withPropertyValues("spring.data.redis.host=" + redis.getHost(), + "spring.data.redis.port=" + redis.getFirstMappedPort()); + + @Test + void annotatedListenerShouldReceiveMessages() { + this.contextRunner.withUserConfiguration(TestListener.class).run((context) -> { + StringRedisTemplate redisTemplate = context.getBean(StringRedisTemplate.class); + TestListener testListener = context.getBean(TestListener.class); + redisTemplate.convertAndSend("test-channel", "hello-world"); + await().untilAsserted(() -> assertThat(testListener.messages).contains("hello-world")); + }); + } + + static class TestListener { + + private final List messages = new ArrayList<>(); + + @RedisListener("test-channel") + void process(String message) { + this.messages.add(message); + } + + } + +} diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfiguration.java new file mode 100644 index 000000000000..bfff28513ab6 --- /dev/null +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfiguration.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.data.redis.autoconfigure; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.annotation.EnableRedisListeners; +import org.springframework.data.redis.config.RedisListenerConfigUtils; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.messaging.Message; + +/** + * Configuration for Redis annotation-driven listeners. + * + * @author Stephane Nicoll + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ EnableRedisListeners.class, Message.class }) +class DataRedisAnnotationDrivenConfiguration { + + private static final String DEFAULT_MESSAGE_LISTENER_BEAN_NAME = RedisListenerConfigUtils.REDIS_MESSAGE_LISTENER_BEAN_NAME; + + private final DataRedisProperties properties; + + DataRedisAnnotationDrivenConfiguration(DataRedisProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean + RedisMessageListenerContainerConfigurer redisMessageListenerContainerConfigurer() { + return new RedisMessageListenerContainerConfigurer(this.properties); + } + + @Bean(name = DEFAULT_MESSAGE_LISTENER_BEAN_NAME) + @ConditionalOnSingleCandidate(RedisConnectionFactory.class) + @ConditionalOnMissingBean(name = DEFAULT_MESSAGE_LISTENER_BEAN_NAME) + RedisMessageListenerContainer redisMessageListenerContainer(RedisMessageListenerContainerConfigurer configurer, + RedisConnectionFactory redisConnectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + configurer.configure(container, redisConnectionFactory); + return container; + } + + @Configuration(proxyBeanMethods = false) + @EnableRedisListeners + @ConditionalOnMissingBean(name = RedisListenerConfigUtils.REDIS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME) + static class EnableRedisListenersConfiguration { + + } + +} diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAutoConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAutoConfiguration.java index c0f1d0073679..8a88b9a2708d 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAutoConfiguration.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAutoConfiguration.java @@ -48,7 +48,8 @@ @AutoConfiguration @ConditionalOnClass(RedisOperations.class) @EnableConfigurationProperties(DataRedisProperties.class) -@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) +@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class, + DataRedisAnnotationDrivenConfiguration.class }) public final class DataRedisAutoConfiguration { @Bean diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisProperties.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisProperties.java index 88045e385364..b830197093b5 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisProperties.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisProperties.java @@ -102,6 +102,8 @@ public class DataRedisProperties { private final Lettuce lettuce = new Lettuce(); + private final Listener listener = new Listener(); + public int getDatabase() { return this.database; } @@ -218,6 +220,10 @@ public Lettuce getLettuce() { return this.lettuce; } + public Listener getListener() { + return this.listener; + } + /** * Type of Redis client to use. */ @@ -580,4 +586,121 @@ public void setPeriod(@Nullable Duration period) { } + /** + * Listener properties. + */ + public static class Listener { + + /** + * Whether to start the container automatically on startup. + */ + private boolean autoStartup = true; + + /** + * Maximum amount of time to wait for the subscription to become active. + */ + private Duration subscriptionRegistrationTimeout = Duration.ofSeconds(2); + + private final Recovery recovery = new Recovery(); + + public boolean isAutoStartup() { + return this.autoStartup; + } + + public void setAutoStartup(boolean autoStartup) { + this.autoStartup = autoStartup; + } + + public Duration getSubscriptionRegistrationTimeout() { + return this.subscriptionRegistrationTimeout; + } + + public void setSubscriptionRegistrationTimeout(Duration subscriptionRegistrationTimeout) { + this.subscriptionRegistrationTimeout = subscriptionRegistrationTimeout; + } + + public Recovery getRecovery() { + return this.recovery; + } + + } + + /** + * Recovery properties. + */ + public static class Recovery { + + /** + * Maximum number of recovery attempts. + */ + private long maxRetries = Long.MAX_VALUE; + + /** + * Base delay for a recovery attempt. Can be combined with a "multiplier" to use + * an exponential back off strategy. + */ + private Duration delay = Duration.ofSeconds(5); + + /** + * Multiplier for a delay for the next retry attempt, applied to the previous + * delay, starting with the initial delay as well as to the applicable jitter for + * each attempt. Fixed delay by default. + */ + private double multiplier = 1.0; + + /** + * Maximum delay for any retry attempt, limiting how far jitter and the multiplier + * can increase the delay. + */ + private Duration maxDelay = Duration.ofSeconds(30); + + /** + * Jitter value for the base retry attempt, randomly subtracted or added to the + * calculated delay, resulting in a value between 'delay - jitter' and 'delay + + * jitter' but never below the base delay or above the max delay. + */ + private Duration jitter = Duration.ZERO; + + public long getMaxRetries() { + return this.maxRetries; + } + + public void setMaxRetries(long maxRetries) { + this.maxRetries = maxRetries; + } + + public Duration getDelay() { + return this.delay; + } + + public void setDelay(Duration delay) { + this.delay = delay; + } + + public double getMultiplier() { + return this.multiplier; + } + + public void setMultiplier(double multiplier) { + this.multiplier = multiplier; + } + + public Duration getMaxDelay() { + return this.maxDelay; + } + + public void setMaxDelay(Duration maxDelay) { + this.maxDelay = maxDelay; + } + + public Duration getJitter() { + return this.jitter; + } + + public void setJitter(Duration jitter) { + this.jitter = jitter; + } + + } + } diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisMessageListenerContainerConfigurer.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisMessageListenerContainerConfigurer.java new file mode 100644 index 000000000000..9983b0529939 --- /dev/null +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisMessageListenerContainerConfigurer.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.data.redis.autoconfigure; + +import java.time.Duration; +import java.util.function.Predicate; + +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties.Listener; +import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties.Recovery; +import org.springframework.core.retry.RetryPolicy; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.util.backoff.BackOff; + +/** + * Configure {@link RedisMessageListenerContainer} with sensible defaults tuned using + * configuration properties. + *

    + * Can be injected into application code and used to define a custom + * {@code RedisMessageListenerContainer} whose configuration is based upon that produced + * by auto-configuration. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +public class RedisMessageListenerContainerConfigurer { + + private final DataRedisProperties properties; + + public RedisMessageListenerContainerConfigurer(DataRedisProperties properties) { + this.properties = properties; + } + + /** + * Configure the specified Redis message listener container. The container can be + * further tuned and default settings can be overridden. + * @param container the {@link RedisMessageListenerContainer} instance to configure + * @param connectionFactory the {@link RedisConnectionFactory} to use + */ + public void configure(RedisMessageListenerContainer container, RedisConnectionFactory connectionFactory) { + container.setConnectionFactory(connectionFactory); + PropertyMapper map = PropertyMapper.get(); + Listener listenerProperties = this.properties.getListener(); + map.from(listenerProperties::isAutoStartup).to(container::setAutoStartup); + map.from(listenerProperties::getSubscriptionRegistrationTimeout) + .as(Duration::toMillis) + .to(container::setMaxSubscriptionRegistrationWaitingTime); + map.from(getRecoveryBackOff(listenerProperties.getRecovery())).to(container::setRecoveryBackoff); + } + + static BackOff getRecoveryBackOff(Recovery recovery) { + PropertyMapper map = PropertyMapper.get(); + RetryPolicy.Builder builder = RetryPolicy.builder().maxRetries(recovery.getMaxRetries()); + map.from(recovery.getDelay()).to(builder::delay); + map.from(recovery.getMaxDelay()).when(Predicate.not(Duration::isZero)).to(builder::maxDelay); + map.from(recovery.getMultiplier()).to(builder::multiplier); + map.from(recovery.getJitter()).when((Predicate.not(Duration::isZero))).to(builder::jitter); + return builder.build().getBackOff(); + } + +} diff --git a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfigurationTests.java b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfigurationTests.java new file mode 100644 index 000000000000..91fbb0748556 --- /dev/null +++ b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfigurationTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.data.redis.autoconfigure; + +import java.time.Duration; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties.Listener; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.config.RedisListenerConfigUtils; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.util.backoff.ExponentialBackOff; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DataRedisAnnotationDrivenConfiguration}. + * + * @author Stephane Nicoll + */ +class DataRedisAnnotationDrivenConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataRedisAnnotationDrivenConfiguration.class)) + .withUserConfiguration(TestConfiguration.class); + + @Test + void registersContainerAndAnnotationProcessor() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(RedisMessageListenerContainer.class); + assertThat(context).hasBean(RedisListenerConfigUtils.REDIS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME); + }); + } + + @Test + void backsOffWhenContainerWithDefaultNameIsDefined() { + RedisMessageListenerContainer container = mock(RedisMessageListenerContainer.class); + this.contextRunner + .withBean("redisMessageListenerContainer", RedisMessageListenerContainer.class, () -> container) + .run((context) -> assertThat(context).hasSingleBean(RedisMessageListenerContainer.class) + .getBean(RedisMessageListenerContainer.class) + .isSameAs(container)); + } + + @Test + void registerContainerWhenContainerWithCustomNameIsDefined() { + RedisMessageListenerContainer container = mock(RedisMessageListenerContainer.class); + this.contextRunner + .withBean("customRedisMessageListenerContainer", RedisMessageListenerContainer.class, () -> container) + .run((context) -> assertThat(context).getBeans(RedisMessageListenerContainer.class) + .hasSize(2) + .containsKey("customRedisMessageListenerContainer")); + } + + @Test + void containerConfigurationMatchesDefaults() { + this.contextRunner.run((context) -> { + RedisMessageListenerContainer container = context.getBean(RedisMessageListenerContainer.class); + Listener listener = new DataRedisProperties().getListener(); + assertThat(container.isAutoStartup()).isEqualTo(listener.isAutoStartup()); + assertThat(container.getMaxSubscriptionRegistrationWaitingTime()) + .isEqualTo(listener.getSubscriptionRegistrationTimeout().toMillis()); + }); + } + + @Test + void containerCanBeConfigured() { + this.contextRunner.withPropertyValues("spring.data.redis.listener.auto-startup=false", + "spring.data.redis.listener.max-subscription-registration-waiting-time=2s", + "spring.data.redis.listener.recovery.max-retries=6", "spring.data.redis.listener.recovery.delay=4s", + "spring.data.redis.listener.recovery.multiplier=1.5", + "spring.data.redis.listener.recovery.max-delay=2m", "spring.data.redis.listener.recovery.jitter=500ms") + .run((context) -> { + RedisMessageListenerContainer container = context.getBean(RedisMessageListenerContainer.class); + assertThat(container.isAutoStartup()).isFalse(); + assertThat(container.getMaxSubscriptionRegistrationWaitingTime()).isEqualTo(2000); + assertThat(container).extracting("backOff") + .asInstanceOf(InstanceOfAssertFactories.type(ExponentialBackOff.class)) + .satisfies((backOff) -> { + assertThat(backOff.getMaxAttempts()).isEqualTo(6); + assertThat(backOff.getInitialInterval()).isEqualTo(4000); + assertThat(backOff.getMultiplier()).isEqualTo(1.5); + assertThat(backOff.getMaxInterval()).isEqualTo(Duration.ofMinutes(2).toMillis()); + assertThat(backOff.getJitter()).isEqualTo(500); + }); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(DataRedisProperties.class) + static class TestConfiguration { + + @Bean + RedisConnectionFactory redisConnectionFactory() { + return mock(RedisConnectionFactory.class); + } + + } + +} diff --git a/starter/spring-boot-starter-data-redis/build.gradle b/starter/spring-boot-starter-data-redis/build.gradle index 06f6436c4a80..cddc089aea27 100644 --- a/starter/spring-boot-starter-data-redis/build.gradle +++ b/starter/spring-boot-starter-data-redis/build.gradle @@ -24,4 +24,5 @@ dependencies { api(project(":starter:spring-boot-starter")) api(project(":module:spring-boot-data-redis")) + api("org.springframework:spring-messaging") } From b1672ba0406e7135d4812d5eac1962fdc0d8871f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 13 Apr 2026 16:20:45 +0200 Subject: [PATCH 489/930] Detect duplicate property names in configuration processing This commit updates the annotation processor to fail the build if a given configuration property defines the same property name on two different elements. Closes gh-49565 --- ...ConfigurationPropertiesSourceResolver.java | 6 +++ ...onstructorParameterPropertyDescriptor.java | 6 +++ .../JavaBeanPropertyDescriptor.java | 6 +++ .../LombokPropertyDescriptor.java | 6 +++ .../ParameterPropertyDescriptor.java | 5 ++ .../PropertyDescriptor.java | 6 +++ .../PropertyDescriptorResolver.java | 19 ++++++- .../RecordParameterPropertyDescriptor.java | 6 +++ ...uctorParameterPropertyDescriptorTests.java | 12 +++++ .../JavaBeanPropertyDescriptorTests.java | 11 ++++ .../LombokPropertyDescriptorTests.java | 11 ++++ .../NameAnnotationPropertiesTests.java | 22 ++++++++ ...ateNameConstructorParameterProperties.java | 47 +++++++++++++++++ .../name/DuplicateNameJavaBeanProperties.java | 52 +++++++++++++++++++ 14 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/name/DuplicateNameConstructorParameterProperties.java create mode 100644 configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/name/DuplicateNameJavaBeanProperties.java diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationPropertiesSourceResolver.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationPropertiesSourceResolver.java index d13d386d07d5..db5d8a643d9b 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationPropertiesSourceResolver.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationPropertiesSourceResolver.java @@ -23,6 +23,7 @@ import java.util.stream.Collectors; import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.tools.FileObject; import javax.tools.StandardLocation; @@ -146,6 +147,11 @@ static class SourcePropertyDescriptor extends PropertyDescriptor { this.sourceItemHint = sourceItemHint; } + @Override + protected Element getSourceElement() { + return this.delegate.getSourceElement(); + } + @Override protected ItemHint resolveItemHint(String prefix, MetadataGenerationEnvironment environment) { return (this.sourceItemHint != null) ? this.sourceItemHint.applyPrefix(prefix) diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java index e49ef48a9a1c..f6fdf3bbaa39 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java @@ -16,6 +16,7 @@ package org.springframework.boot.configurationprocessor; +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -42,6 +43,11 @@ class ConstructorParameterPropertyDescriptor extends ParameterPropertyDescriptor this.field = field; } + @Override + protected Element getSourceElement() { + return this.field; + } + @Override protected ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment) { return resolveItemDeprecation(environment, getGetter(), this.setter, this.field); diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java index 05120a645eec..bc2df607b56a 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java @@ -16,6 +16,7 @@ package org.springframework.boot.configurationprocessor; +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -45,6 +46,11 @@ class JavaBeanPropertyDescriptor extends PropertyDescriptor { this.factoryMethod = factoryMethod; } + @Override + protected Element getSourceElement() { + return getGetter(); + } + ExecutableElement getSetter() { return this.setter; } diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptor.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptor.java index 565390d97217..09d8408be2c1 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptor.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptor.java @@ -19,6 +19,7 @@ import java.util.Map; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; @@ -59,6 +60,11 @@ class LombokPropertyDescriptor extends PropertyDescriptor { this.setter = setter; } + @Override + protected Element getSourceElement() { + return getField(); + } + VariableElement getField() { return this.field; } diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ParameterPropertyDescriptor.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ParameterPropertyDescriptor.java index 6bf5cebbac91..5fe2e7883f41 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ParameterPropertyDescriptor.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ParameterPropertyDescriptor.java @@ -47,6 +47,11 @@ abstract class ParameterPropertyDescriptor extends PropertyDescriptor { } + @Override + protected Element getSourceElement() { + return getParameter(); + } + final VariableElement getParameter() { return this.parameter; } diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java index 75531fa9aeda..97abe0dd2237 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java @@ -91,6 +91,12 @@ protected final ExecutableElement getGetter() { return this.getter; } + /** + * Return the {@link Element} that primarily defines the property. + * @return the source element + */ + protected abstract Element getSourceElement(); + /** * Resolve the {@link ItemMetadata} for this property. * @param prefix the property prefix diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java index ba4aa6ec936c..b90369a317ee 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java @@ -20,6 +20,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Stream; import javax.lang.model.element.AnnotationMirror; @@ -31,6 +32,7 @@ import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; +import javax.tools.Diagnostic.Kind; import org.springframework.boot.configurationprocessor.ConfigurationPropertiesSourceResolver.SourceMetadata; @@ -149,15 +151,30 @@ private ExecutableElement findMatchingGetter(TypeElementMembers members, List candidates, PropertyDescriptor descriptor) { - if (!candidates.containsKey(descriptor.getName()) && isCandidate(descriptor)) { + if (!isCandidate(descriptor)) { + return; + } + PropertyDescriptor existing = candidates.get(descriptor.getName()); + if (existing == null) { candidates.put(descriptor.getName(), descriptor); } + else if (isDistinctDescriptor(existing, descriptor)) { + this.environment.getMessager() + .printMessage(Kind.ERROR, + "Property name '%s' maps to distinct properties in type %s".formatted(descriptor.getName(), + this.environment.getTypeUtils().getQualifiedName(descriptor.getDeclaringElement()))); + } } private boolean isCandidate(PropertyDescriptor descriptor) { return descriptor.isProperty(this.environment) || descriptor.isNested(this.environment); } + private boolean isDistinctDescriptor(PropertyDescriptor existing, PropertyDescriptor candidate) { + return Objects.equals(existing.getClass(), candidate.getClass()) + && !Objects.equals(existing.getSourceElement(), candidate.getSourceElement()); + } + /** * Wrapper around a {@link TypeElement} that could be bound. */ diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/RecordParameterPropertyDescriptor.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/RecordParameterPropertyDescriptor.java index 492cc5a2d305..dfec811c02d8 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/RecordParameterPropertyDescriptor.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/RecordParameterPropertyDescriptor.java @@ -16,6 +16,7 @@ package org.springframework.boot.configurationprocessor; +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.RecordComponentElement; import javax.lang.model.element.TypeElement; @@ -41,6 +42,11 @@ class RecordParameterPropertyDescriptor extends ParameterPropertyDescriptor { this.recordComponent = recordComponent; } + @Override + protected Element getSourceElement() { + return this.recordComponent; + } + @Override protected ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment) { return resolveItemDeprecation(environment, getGetter()); diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java index 7652240097da..a390e2ebd333 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.List; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -55,6 +56,17 @@ void constructorParameterSimpleProperty() { }); } + @Test + void getSourceElementReturnsBackingField() { + process(ImmutableSimpleProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv.getRootElement(ImmutableSimpleProperties.class); + ConstructorParameterPropertyDescriptor property = createPropertyDescriptor(ownerElement, "theName"); + assertThat(property.getSourceElement().getKind()).isEqualTo(ElementKind.FIELD); + assertThat(property.getSourceElement().getSimpleName()).hasToString("theName"); + assertThat(property.getSourceElement()).isNotSameAs(property.getGetter()); + }); + } + @Test void constructorParameterNestedPropertySameClass() { process(ImmutableInnerClassProperties.class, (roundEnv, metadataEnv) -> { diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptorTests.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptorTests.java index e6ca9d5e2272..74bc8c2a560e 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptorTests.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptorTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.configurationprocessor; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -50,6 +51,16 @@ void javaBeanSimpleProperty() { }); } + @Test + void getSourceElementReturnsPublicGetter() { + process(SimpleTypeProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv.getRootElement(SimpleTypeProperties.class); + JavaBeanPropertyDescriptor property = createPropertyDescriptor(ownerElement, "myString"); + assertThat(property.getSourceElement().getKind()).isEqualTo(ElementKind.METHOD); + assertThat(property.getSourceElement()).isSameAs(property.getGetter()); + }); + } + @Test void javaBeanCollectionProperty() { process(SimpleCollectionProperties.class, (roundEnv, metadataEnv) -> { diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptorTests.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptorTests.java index ef5fba054596..6c2b07abd33f 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptorTests.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptorTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.configurationprocessor; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -53,6 +54,16 @@ void lombokSimpleProperty() { }); } + @Test + void getSourceElementReturnsField() { + process(LombokSimpleProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv.getRootElement(LombokSimpleProperties.class); + LombokPropertyDescriptor property = createPropertyDescriptor(ownerElement, "name"); + assertThat(property.getSourceElement().getKind()).isEqualTo(ElementKind.FIELD); + assertThat(property.getSourceElement()).isSameAs(property.getField()); + }); + } + @Test void lombokCollectionProperty() { process(LombokSimpleProperties.class, (roundEnv, metadataEnv) -> { diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/NameAnnotationPropertiesTests.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/NameAnnotationPropertiesTests.java index 440a876252a7..d0dd1e47ef32 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/NameAnnotationPropertiesTests.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/NameAnnotationPropertiesTests.java @@ -21,11 +21,15 @@ import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; import org.springframework.boot.configurationprocessor.metadata.Metadata; import org.springframework.boot.configurationsample.name.ConstructorParameterNameAnnotationProperties; +import org.springframework.boot.configurationsample.name.DuplicateNameConstructorParameterProperties; +import org.springframework.boot.configurationsample.name.DuplicateNameJavaBeanProperties; import org.springframework.boot.configurationsample.name.JavaBeanNameAnnotationProperties; import org.springframework.boot.configurationsample.name.LombokNameAnnotationProperties; import org.springframework.boot.configurationsample.name.RecordComponentNameAnnotationProperties; +import org.springframework.core.test.tools.CompilationException; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Metadata generation tests for using {@code @Name}. @@ -87,4 +91,22 @@ void lombokNameAnnotationProperties() { .withDefaultValue(true)); } + @Test + void duplicateNameOnJavaBeanPropertiesFailsCompilation() { + assertThatExceptionOfType(CompilationException.class) + .isThrownBy(() -> compile(DuplicateNameJavaBeanProperties.class)) + .withMessageContaining("Unable to compile source") + .withMessageContaining("Property name 'aaa' maps to distinct properties in type ") + .withMessageContaining(DuplicateNameJavaBeanProperties.class.getName()); + } + + @Test + void duplicateNameOnConstructorParametersFailsCompilation() { + assertThatExceptionOfType(CompilationException.class) + .isThrownBy(() -> compile(DuplicateNameConstructorParameterProperties.class)) + .withMessageContaining("Unable to compile source") + .withMessageContaining("Property name 'aaa' maps to distinct properties in type ") + .withMessageContaining(DuplicateNameConstructorParameterProperties.class.getName()); + } + } diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/name/DuplicateNameConstructorParameterProperties.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/name/DuplicateNameConstructorParameterProperties.java new file mode 100644 index 000000000000..1a4df523230a --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/name/DuplicateNameConstructorParameterProperties.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.name; + +import org.springframework.boot.configurationsample.TestConfigurationProperties; +import org.springframework.boot.configurationsample.TestName; + +/** + * Sample where two constructor parameters use the same {@code @Name} value (invalid). + * + * @author Stephane Nicoll + */ +@TestConfigurationProperties("duplicate") +public class DuplicateNameConstructorParameterProperties { + + private final String company; + + private final String name; + + public DuplicateNameConstructorParameterProperties(@TestName("aaa") String company, @TestName("aaa") String name) { + this.company = company; + this.name = name; + } + + public String getCompany() { + return this.company; + } + + public String getName() { + return this.name; + } + +} diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/name/DuplicateNameJavaBeanProperties.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/name/DuplicateNameJavaBeanProperties.java new file mode 100644 index 000000000000..c8588004834b --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/name/DuplicateNameJavaBeanProperties.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.name; + +import org.springframework.boot.configurationsample.TestConfigurationProperties; +import org.springframework.boot.configurationsample.TestName; + +/** + * Sample where two JavaBean properties use the same {@code @Name} value (invalid). + * + * @author Stephane Nicoll + */ +@TestConfigurationProperties("duplicate") +public class DuplicateNameJavaBeanProperties { + + @TestName("aaa") + private String company; + + @TestName("aaa") + private String name; + + public String getCompany() { + return this.company; + } + + public void setCompany(String company) { + this.company = company; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + +} From 83632ae3140a66cb0091c99f92d7f352c646b101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 13 Apr 2026 18:13:49 +0200 Subject: [PATCH 490/930] Upgrade to Hibernate 7.2.11.Final Closes gh-50054 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index f943ac7e90d4..ab2191814975 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -639,7 +639,7 @@ bom { releaseNotes("https://github.com/hazelcast/hazelcast/releases/tag/v{version}") } } - library("Hibernate", "7.2.10.Final") { + library("Hibernate", "7.2.11.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From 79e26952db5b7f8783b3623e29000affdc0f77b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 13 Apr 2026 18:13:54 +0200 Subject: [PATCH 491/930] Upgrade to Jackson Bom 3.1.2 Closes gh-50055 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cbb7e0faf66e..f005808091fd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ commonsCodecVersion=1.21.0 graalVersion=25 hamcrestVersion=3.0 jackson2Version=2.21.2 -jacksonVersion=3.1.1 +jacksonVersion=3.1.2 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 kotlinVersion=2.3.20 From 8ee36c6dedb11f9b5903372cce7f5435fefd4d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 13 Apr 2026 18:13:54 +0200 Subject: [PATCH 492/930] Upgrade to Micrometer 1.17.0-RC1 Closes gh-49990 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ab2191814975..eac70423dde0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1613,7 +1613,7 @@ bom { releaseNotes("https://github.com/apache/maven-war-plugin/releases/tag/maven-war-plugin-{version}") } } - library("Micrometer", "1.17.0-SNAPSHOT") { + library("Micrometer", "1.17.0-RC1") { considerSnapshots() group("io.micrometer") { modules = [ From 6faf3725d72c2481e628b34f5884568f663620ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 13 Apr 2026 18:14:00 +0200 Subject: [PATCH 493/930] Upgrade to Protobuf Maven Plugin 5.1.3 Closes gh-50056 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index eac70423dde0..e4f6de3a0f8a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1841,7 +1841,7 @@ bom { releaseNotes("https://github.com/oracle/oracle-r2dbc/releases/tag/{version}") } } - library("Protobuf Maven Plugin", "5.1.2") { + library("Protobuf Maven Plugin", "5.1.3") { group("io.github.ascopes") { plugins = [ "protobuf-maven-plugin" From 6409d8c4337c60b037bb2c568bd38652f5a76633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 13 Apr 2026 18:14:04 +0200 Subject: [PATCH 494/930] Upgrade to Selenium 4.43.0 Closes gh-50057 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e4f6de3a0f8a..8de1fb5b3a92 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2442,7 +2442,7 @@ bom { ] } } - library("Selenium", "4.41.0") { + library("Selenium", "4.43.0") { group("org.seleniumhq.selenium") { bom("selenium-bom") } From 8c47060528a835b7652ed62ece38e6054bf01b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 13 Apr 2026 18:14:08 +0200 Subject: [PATCH 495/930] Upgrade to Selenium HtmlUnit 4.43.0 Closes gh-50058 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8de1fb5b3a92..729a115bfd1e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2452,7 +2452,7 @@ bom { releaseNotes("https://github.com/SeleniumHQ/selenium/releases/tag/selenium-{version}") } } - library("Selenium HtmlUnit", "4.41.0") { + library("Selenium HtmlUnit", "4.43.0") { group("org.seleniumhq.selenium") { modules = [ "htmlunit3-driver" From 85f9e807c45b7388c54657ae1986115e8cfdfbb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 13 Apr 2026 18:14:13 +0200 Subject: [PATCH 496/930] Upgrade to Thymeleaf 3.1.4.RELEASE Closes gh-50059 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 729a115bfd1e..a41f58f5698a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2808,7 +2808,7 @@ bom { javadoc("https://javadoc.io/doc/com.redis/testcontainers-redis/{version}", "com.redis.testcontainers") } } - library("Thymeleaf", "3.1.3.RELEASE") { + library("Thymeleaf", "3.1.4.RELEASE") { group("org.thymeleaf") { modules = [ "thymeleaf", From 16632fa19e72ed4a8fe9f69bde27aa65cd72bf1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 13 Apr 2026 18:14:17 +0200 Subject: [PATCH 497/930] Upgrade to Thymeleaf Extras SpringSecurity 3.1.4.RELEASE Closes gh-50060 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a41f58f5698a..a674d2ddb629 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2829,7 +2829,7 @@ bom { ] } } - library("Thymeleaf Extras SpringSecurity", "3.1.3.RELEASE") { + library("Thymeleaf Extras SpringSecurity", "3.1.4.RELEASE") { group("org.thymeleaf.extras") { modules = [ "thymeleaf-extras-springsecurity6" From 39ee6098a6bc78b469287f945ad2da9dee5a8873 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 13 Apr 2026 17:58:52 -0700 Subject: [PATCH 498/930] Upgrade to Couchbase Client 3.11.2 Closes gh-50066 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a674d2ddb629..04836885c5b1 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -312,7 +312,7 @@ bom { site("https://commons.apache.org/proper/commons-pool") } } - library("Couchbase Client", "3.11.1") { + library("Couchbase Client", "3.11.2") { group("com.couchbase.client") { modules = [ "java-client" From 52cef1868635b48bfafad0b9c9af91f1caf76b4f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 26 Mar 2026 16:05:40 -0700 Subject: [PATCH 499/930] Support InetAddress filtering for HTTP Clients Add `InetAddressFilter` interface which can be provided by `HttpSettings` to filter out addresses in order to harden applications against SSRF attacks. Closes gh-49687 --- .../reference/pages/io/rest-client.adoc | 19 + .../inetaddressfiltering/Details.java | 21 + .../MyHttpClientConfiguration.java | 31 + .../inetaddressfiltering/MyService.java | 42 + .../inetaddressfiltering/Details.kt | 4 + .../MyHttpClientConfiguration.kt | 15 + .../inetaddressfiltering/MyService.kt | 27 + module/spring-boot-http-client/build.gradle | 1 + .../boot/http/client/FilteredAddresses.java | 85 ++ .../http/client/FilteredHostException.java | 53 ++ .../boot/http/client/HttpClientSettings.java | 51 +- ...onentsClientHttpRequestFactoryBuilder.java | 14 + .../HttpComponentsFilteredDnsResolver.java | 70 ++ .../HttpComponentsHttpAsyncClientBuilder.java | 40 +- .../HttpComponentsHttpClientBuilder.java | 41 +- .../boot/http/client/InetAddressFilter.java | 320 ++++++++ .../client/InternalInetAddressFilter.java | 84 ++ .../boot/http/client/IpAddress.java | 136 ++++ .../JdkClientHttpRequestFactoryBuilder.java | 14 + .../http/client/JdkFilteredProxySelector.java | 76 ++ .../http/client/JdkHttpClientBuilder.java | 26 +- .../JettyClientHttpRequestFactoryBuilder.java | 14 + .../JettyFilteredSocketAddressResolver.java | 80 ++ .../http/client/JettyHttpClientBuilder.java | 66 +- ...eactorClientHttpRequestFactoryBuilder.java | 17 + ...eactorFilteredResolvedAddressSelector.java | 82 ++ .../http/client/ReactorHttpClientBuilder.java | 40 +- ...onentsClientHttpRequestFactoryBuilder.java | 1 + ...SimpleClientHttpRequestFactoryBuilder.java | 7 +- .../HttpClientAutoConfiguration.java | 11 +- ...pComponentsClientHttpConnectorBuilder.java | 14 + .../JdkClientHttpConnectorBuilder.java | 14 + .../JettyClientHttpConnectorBuilder.java | 14 + .../ReactorClientHttpConnectorBuilder.java | 17 + ...tClientHttpRequestFactoryBuilderTests.java | 25 + .../http/client/FilteredAddressesTests.java | 76 ++ .../client/FilteredHostExceptionTests.java | 39 + .../http/client/HttpClientSettingsTests.java | 38 +- ...sClientHttpRequestFactoryBuilderTests.java | 24 + ...ttpComponentsFilteredDnsResolverTests.java | 114 +++ .../http/client/InetAddressFilterAssert.java | 79 ++ .../http/client/InetAddressFilterTests.java | 723 ++++++++++++++++++ .../boot/http/client/IpAddressTests.java | 197 +++++ ...kClientHttpRequestFactoryBuilderTests.java | 26 + .../client/JdkFilteredProxySelectorTests.java | 75 ++ ...yClientHttpRequestFactoryBuilderTests.java | 22 + ...ttyFilteredSocketAddressResolverTests.java | 100 +++ ...rFilteredResolvedAddressSelectorTests.java | 83 ++ ...sClientHttpRequestFactoryBuilderTests.java | 9 + ...eClientHttpRequestFactoryBuilderTests.java | 12 +- .../HttpClientAutoConfigurationTests.java | 10 + ...stractClientHttpConnectorBuilderTests.java | 24 + ...onentsClientHttpConnectorBuilderTests.java | 25 + .../JdkClientHttpConnectorBuilderTests.java | 27 + .../JettyClientHttpConnectorBuilderTests.java | 33 +- ...eactorClientHttpConnectorBuilderTests.java | 27 + 56 files changed, 3272 insertions(+), 63 deletions(-) create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/Details.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyHttpClientConfiguration.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyService.java create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/Details.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyHttpClientConfiguration.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyService.kt create mode 100644 module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/FilteredAddresses.java create mode 100644 module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/FilteredHostException.java create mode 100644 module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsFilteredDnsResolver.java create mode 100644 module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/InetAddressFilter.java create mode 100644 module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/InternalInetAddressFilter.java create mode 100644 module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/IpAddress.java create mode 100644 module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkFilteredProxySelector.java create mode 100644 module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyFilteredSocketAddressResolver.java create mode 100644 module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorFilteredResolvedAddressSelector.java create mode 100644 module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/FilteredAddressesTests.java create mode 100644 module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/FilteredHostExceptionTests.java create mode 100644 module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpComponentsFilteredDnsResolverTests.java create mode 100644 module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/InetAddressFilterAssert.java create mode 100644 module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/InetAddressFilterTests.java create mode 100644 module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/IpAddressTests.java create mode 100644 module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JdkFilteredProxySelectorTests.java create mode 100644 module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JettyFilteredSocketAddressResolverTests.java create mode 100644 module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReactorFilteredResolvedAddressSelectorTests.java diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc index 2309a1edbcb8..bb068499d0e5 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc @@ -446,3 +446,22 @@ spring: read-timeout: 1s redirects: dont-follow ---- + + + +[[io.rest-client.global-configuration.inetaddress-filtering]] +=== InetAddress Filtering and SSRF Protection + +It's sometimes useful to limit the remote addresses that an HTTP client is permitted to call. +This technique can be especially useful when hardening your application against Server-Side Request Forgery (SSRF) attacks. + +To limit the address that a client can call, you can use an javadoc:org.springframework.boot.http.client.InetAddressFilter[] which will only allow outgoing calls to addresses that match the filter. +The filter is a functional interface and you can either create your own implementation, or use one of the convenient factory methods. + +Filters may be applied to the javadoc:org.springframework.boot.http.client.HttpClientSettings[] you used when building a client: + +include-code::MyService[] + +Or you can also define one as a javadoc:org.springframework.context.annotation.Bean[format=annotation] if you want to apply it to all auto-confugured HTTP client builders: + +include-code::MyHttpClientConfiguration[] diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/Details.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/Details.java new file mode 100644 index 000000000000..cf5d98f2730a --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/Details.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.globalconfiguration.inetaddressfiltering; + +public class Details { + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyHttpClientConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyHttpClientConfiguration.java new file mode 100644 index 000000000000..6b88328ff440 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyHttpClientConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.globalconfiguration.inetaddressfiltering; + +import org.springframework.boot.http.client.InetAddressFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyHttpClientConfiguration { + + @Bean + public InetAddressFilter httpClientInetAddressFilter() { + return InetAddressFilter.of("192.168.1.0/24").andNot("192.168.1.1", "192.168.1.10"); + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyService.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyService.java new file mode 100644 index 000000000000..a8a8002875ef --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyService.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.globalconfiguration.inetaddressfiltering; + +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.InetAddressFilter; +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() { + InetAddressFilter onlyExternalAddresses = InetAddressFilter.externalAddresses(); + HttpClientSettings settings = HttpClientSettings.defaults().withInetAddressFilter(onlyExternalAddresses); + ClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder.jdk().build(settings); + this.restClient = RestClient.builder().requestFactory(requestFactory).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/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/Details.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/Details.kt new file mode 100644 index 000000000000..da14b832eec7 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/Details.kt @@ -0,0 +1,4 @@ +package org.springframework.boot.docs.io.restclient.globalconfiguration.inetaddressfiltering + +class Details { +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyHttpClientConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyHttpClientConfiguration.kt new file mode 100644 index 000000000000..b6aaa99cfe74 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyHttpClientConfiguration.kt @@ -0,0 +1,15 @@ +package org.springframework.boot.docs.io.restclient.globalconfiguration.inetaddressfiltering + +import org.springframework.boot.http.client.InetAddressFilter +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration(proxyBeanMethods = false) +class MyHttpClientConfiguration { + + @Bean + fun httpClientInetAddressFilter(): InetAddressFilter { + return InetAddressFilter.of("192.168.1.0/24").andNot("192.168.1.1", "192.168.1.10") + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyService.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyService.kt new file mode 100644 index 000000000000..6895f32dac34 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/globalconfiguration/inetaddressfiltering/MyService.kt @@ -0,0 +1,27 @@ +package org.springframework.boot.docs.io.restclient.globalconfiguration.inetaddressfiltering + +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder +import org.springframework.boot.http.client.HttpClientSettings +import org.springframework.boot.http.client.InetAddressFilter +import org.springframework.http.client.ClientHttpRequestFactory +import org.springframework.stereotype.Service +import org.springframework.web.client.RestClient + +@Service +class MyService { + + private val restClient: RestClient + + init { + val onlyExternalAddresses = InetAddressFilter.externalAddresses() + val settings = HttpClientSettings.defaults().withInetAddressFilter(onlyExternalAddresses) + val requestFactory: ClientHttpRequestFactory = ClientHttpRequestFactoryBuilder.jdk().build(settings) + restClient = RestClient.builder().requestFactory(requestFactory).baseUrl("https://example.org").build() + } + + fun someRestCall(name: String?): Details { + return restClient.get().uri("/{name}/details", name) + .retrieve().body(Details::class.java)!! + } + +} diff --git a/module/spring-boot-http-client/build.gradle b/module/spring-boot-http-client/build.gradle index 22aebdd62d7a..f287b94acb29 100644 --- a/module/spring-boot-http-client/build.gradle +++ b/module/spring-boot-http-client/build.gradle @@ -42,6 +42,7 @@ dependencies { testImplementation(project(":module:spring-boot-tomcat")) testImplementation(project(":test-support:spring-boot-test-support")) testImplementation("org.springframework:spring-webflux") + testImplementation("org.springframework.security:spring-security-core") testImplementation("io.micrometer:micrometer-observation-test") testCompileOnly("com.google.code.findbugs:jsr305") diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/FilteredAddresses.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/FilteredAddresses.java new file mode 100644 index 000000000000..05b6f4ad66ef --- /dev/null +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/FilteredAddresses.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.util.List; +import java.util.Objects; +import java.util.function.IntFunction; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.ObjectUtils; + +/** + * Internal utility to handle filtering of addresses and possibly throwing a + * {@link FilteredHostException}. + * + * @param the address type + * @author Phillip Webb + */ +final class FilteredAddresses { + + private final Stream stream; + + private FilteredAddresses(Stream stream) { + this.stream = stream; + } + + Filtered> toList() { + return new Filtered<>(this.stream.toList(), List::isEmpty); + } + + Filtered toArray(IntFunction generator) { + return new Filtered<>(this.stream.toArray(generator), ObjectUtils::isEmpty); + } + + Filtered get() { + return new Filtered<>(this.stream.findAny().orElse(null), Objects::isNull); + } + + static FilteredAddresses of(Stream stream, Predicate predicate) { + return new FilteredAddresses<>(stream.filter(Objects::nonNull).filter(predicate)); + } + + static final class Filtered { + + private final @Nullable T result; + + private final Predicate check; + + Filtered(@Nullable T result, Predicate check) { + this.result = result; + this.check = check; + } + + T orElseThrow(String host, InetAddressFilter filter) { + return orElseThrow(() -> host, filter); + } + + T orElseThrow(Supplier host, InetAddressFilter filter) { + if (this.result == null || this.check.test(this.result)) { + throw new FilteredHostException(host.get(), filter); + } + return this.result; + } + + } + +} diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/FilteredHostException.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/FilteredHostException.java new file mode 100644 index 000000000000..489336705ace --- /dev/null +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/FilteredHostException.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +/** + * Exception thrown when a host was not used due to an {@link InetAddressFilter}. + * + * @author Phillip Webb + * @since 4.1.0 + */ +public class FilteredHostException extends RuntimeException { + + private final String host; + + private final InetAddressFilter filter; + + FilteredHostException(String host, InetAddressFilter filter) { + super("Filtered host '%s'".formatted(host)); + this.host = host; + this.filter = filter; + } + + /** + * Return the host that was not matched. + * @return the unmatched host + */ + public String getHost() { + return this.host; + } + + /** + * Return the filter that was used. + * @return the filter that didn't match + */ + public InetAddressFilter getFilter() { + return this.filter; + } + +} diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java index b6966fd9bee7..d73187318dd1 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpClientSettings.java @@ -32,11 +32,13 @@ * @param connectTimeout the connect timeout * @param readTimeout the read timeout * @param sslBundle the SSL bundle providing SSL configuration + * @param inetAddressFilter the inetAddress filter used to filter out matching requests * @author Phillip Webb * @since 3.5.0 */ public record HttpClientSettings(@Nullable HttpCookieHandling cookieHandling, @Nullable HttpRedirects redirects, - @Nullable Duration connectTimeout, @Nullable Duration readTimeout, @Nullable SslBundle sslBundle) { + @Nullable Duration connectTimeout, @Nullable Duration readTimeout, @Nullable SslBundle sslBundle, + @Nullable InetAddressFilter inetAddressFilter) { /** * Create a new {@link HttpClientSettings} instance. @@ -53,7 +55,22 @@ public HttpClientSettings(@Nullable HttpRedirects redirects, @Nullable Duration this(null, redirects, connectTimeout, readTimeout, sslBundle); } - private static final HttpClientSettings defaults = new HttpClientSettings(null, null, null, null, null); + /** + * Create a new {@link HttpClientSettings} instance. + * @param cookieHandling the cookie handling strategy to use or null to use the + * underlying library's default + * @param redirects the follow redirect strategy to use + * @param connectTimeout the connect timeout + * @param readTimeout the read timeout + * @param sslBundle the SSL bundle providing SSL configuration + * @since 3.5.0 + */ + public HttpClientSettings(@Nullable HttpCookieHandling cookieHandling, @Nullable HttpRedirects redirects, + @Nullable Duration connectTimeout, @Nullable Duration readTimeout, @Nullable SslBundle sslBundle) { + this(cookieHandling, redirects, connectTimeout, readTimeout, sslBundle, null); + } + + private static final HttpClientSettings defaults = new HttpClientSettings(null, null, null, null, null, null); /** * Return a new {@link HttpClientSettings} instance with an updated cookie handling @@ -64,7 +81,7 @@ public HttpClientSettings(@Nullable HttpRedirects redirects, @Nullable Duration */ public HttpClientSettings withCookieHandling(@Nullable HttpCookieHandling cookieHandling) { return new HttpClientSettings(cookieHandling, this.redirects, this.connectTimeout, this.readTimeout, - this.sslBundle); + this.sslBundle, this.inetAddressFilter); } /** @@ -76,7 +93,7 @@ public HttpClientSettings withCookieHandling(@Nullable HttpCookieHandling cookie */ public HttpClientSettings withConnectTimeout(@Nullable Duration connectTimeout) { return new HttpClientSettings(this.cookieHandling, this.redirects, connectTimeout, this.readTimeout, - this.sslBundle); + this.sslBundle, this.inetAddressFilter); } /** @@ -88,7 +105,7 @@ public HttpClientSettings withConnectTimeout(@Nullable Duration connectTimeout) */ public HttpClientSettings withReadTimeout(@Nullable Duration readTimeout) { return new HttpClientSettings(this.cookieHandling, this.redirects, this.connectTimeout, readTimeout, - this.sslBundle); + this.sslBundle, this.inetAddressFilter); } /** @@ -100,7 +117,8 @@ public HttpClientSettings withReadTimeout(@Nullable Duration readTimeout) { * @since 4.0.0 */ public HttpClientSettings withTimeouts(@Nullable Duration connectTimeout, @Nullable Duration readTimeout) { - return new HttpClientSettings(this.cookieHandling, this.redirects, connectTimeout, readTimeout, this.sslBundle); + return new HttpClientSettings(this.cookieHandling, this.redirects, connectTimeout, readTimeout, this.sslBundle, + this.inetAddressFilter); } /** @@ -112,7 +130,7 @@ public HttpClientSettings withTimeouts(@Nullable Duration connectTimeout, @Nulla */ public HttpClientSettings withSslBundle(@Nullable SslBundle sslBundle) { return new HttpClientSettings(this.cookieHandling, this.redirects, this.connectTimeout, this.readTimeout, - sslBundle); + sslBundle, this.inetAddressFilter); } /** @@ -123,7 +141,19 @@ public HttpClientSettings withSslBundle(@Nullable SslBundle sslBundle) { */ public HttpClientSettings withRedirects(@Nullable HttpRedirects redirects) { return new HttpClientSettings(this.cookieHandling, redirects, this.connectTimeout, this.readTimeout, - this.sslBundle); + this.sslBundle, this.inetAddressFilter); + } + + /** + * Return a new {@link HttpClientSettings} instance with an updated inetAddress + * filter. + * @param inetAddressFilter the new inetAddress filter + * @return a new {@link HttpClientSettings} instance + * @since 4.1.0 + */ + public HttpClientSettings withInetAddressFilter(@Nullable InetAddressFilter inetAddressFilter) { + return new HttpClientSettings(this.cookieHandling, this.redirects, this.connectTimeout, this.readTimeout, + this.sslBundle, inetAddressFilter); } /** @@ -142,7 +172,10 @@ public HttpClientSettings orElse(@Nullable HttpClientSettings other) { Duration connectTimeout = (connectTimeout() != null) ? connectTimeout() : other.connectTimeout(); Duration readTimeout = (readTimeout() != null) ? readTimeout() : other.readTimeout(); SslBundle sslBundle = (sslBundle() != null) ? sslBundle() : other.sslBundle(); - return new HttpClientSettings(cookieHandling, redirects, connectTimeout, readTimeout, sslBundle); + InetAddressFilter inetAddressFilter = (inetAddressFilter() != null) ? inetAddressFilter() + : other.inetAddressFilter(); + return new HttpClientSettings(cookieHandling, redirects, connectTimeout, readTimeout, sslBundle, + inetAddressFilter); } /** diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilder.java index d0645da06c17..0f16fa2d50e4 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilder.java @@ -21,6 +21,7 @@ import java.util.function.Consumer; import java.util.function.UnaryOperator; +import org.apache.hc.client5.http.DnsResolver; import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.RequestConfig; @@ -157,6 +158,19 @@ public HttpComponentsClientHttpRequestFactoryBuilder withDefaultRequestConfigCus this.httpClientBuilder.withDefaultRequestConfigCustomizer(defaultRequestConfigCustomizer)); } + /** + * Return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} with a + * replacement {@link DnsResolver}. + * @param dnsResolver the new DNS resolver + * @return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} instance + * @since 4.1.0 + */ + public HttpComponentsClientHttpRequestFactoryBuilder withDnsResolver(DnsResolver dnsResolver) { + Assert.notNull(dnsResolver, "'dnsResolver' must not be null"); + return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(), + this.httpClientBuilder.withDnsResolver(dnsResolver)); + } + /** * Return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} that applies the * given customizer. This can be useful for applying pre-packaged customizations. diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsFilteredDnsResolver.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsFilteredDnsResolver.java new file mode 100644 index 000000000000..10444a510d66 --- /dev/null +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsFilteredDnsResolver.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.List; + +import org.apache.hc.client5.http.DnsResolver; + +import org.springframework.util.ObjectUtils; + +/** + * HTTP Components {@link DnsResolver} that filters using a {@link InetAddressFilter}. + * + * @author Phillip Webb + */ +class HttpComponentsFilteredDnsResolver implements DnsResolver { + + private final DnsResolver delegate; + + private final InetAddressFilter filter; + + HttpComponentsFilteredDnsResolver(DnsResolver delegate, InetAddressFilter filter) { + this.delegate = delegate; + this.filter = filter; + } + + @Override + public InetAddress[] resolve(String host) throws UnknownHostException { + InetAddress[] resolved = this.delegate.resolve(host); + if (ObjectUtils.isEmpty(resolved)) { + return resolved; + } + return FilteredAddresses.of(Arrays.stream(resolved), this.filter::matches) + .toArray(InetAddress[]::new) + .orElseThrow(host, this.filter); + } + + @Override + public List resolve(String host, int port) throws UnknownHostException { + List resolved = this.delegate.resolve(host, port); + if (resolved.isEmpty()) { + return resolved; + } + return FilteredAddresses.of(resolved.stream(), this.filter::matches).toList().orElseThrow(host, this.filter); + } + + @Override + public String resolveCanonicalHostname(String host) throws UnknownHostException { + return this.delegate.resolveCanonicalHostname(host); + } + +} diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpAsyncClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpAsyncClientBuilder.java index 609860599546..9c509ae34fd8 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpAsyncClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpAsyncClientBuilder.java @@ -21,6 +21,8 @@ import java.util.function.Consumer; import java.util.function.Function; +import org.apache.hc.client5.http.DnsResolver; +import org.apache.hc.client5.http.SystemDefaultDnsResolver; import org.apache.hc.client5.http.async.HttpAsyncClient; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.RequestConfig; @@ -57,21 +59,24 @@ public final class HttpComponentsHttpAsyncClientBuilder { private final Function<@Nullable SslBundle, @Nullable TlsStrategy> tlsStrategyFactory; + private final DnsResolver dnsResolver; + public HttpComponentsHttpAsyncClientBuilder() { this(Empty.consumer(), Empty.consumer(), Empty.consumer(), Empty.consumer(), - HttpComponentsSslBundleTlsStrategy::get); + HttpComponentsSslBundleTlsStrategy::get, SystemDefaultDnsResolver.INSTANCE); } private HttpComponentsHttpAsyncClientBuilder(Consumer customizer, Consumer connectionManagerCustomizer, Consumer connectionConfigCustomizer, Consumer defaultRequestConfigCustomizer, - Function<@Nullable SslBundle, @Nullable TlsStrategy> tlsStrategyFactory) { + Function<@Nullable SslBundle, @Nullable TlsStrategy> tlsStrategyFactory, DnsResolver dnsResolver) { this.customizer = customizer; this.connectionManagerCustomizer = connectionManagerCustomizer; this.connectionConfigCustomizer = connectionConfigCustomizer; this.defaultRequestConfigCustomizer = defaultRequestConfigCustomizer; this.tlsStrategyFactory = tlsStrategyFactory; + this.dnsResolver = dnsResolver; } /** @@ -84,7 +89,7 @@ public HttpComponentsHttpAsyncClientBuilder withCustomizer(Consumer tlsStrategyFactory) { Assert.notNull(tlsStrategyFactory, "'tlsStrategyFactory' must not be null"); return new HttpComponentsHttpAsyncClientBuilder(this.customizer, this.connectionManagerCustomizer, - this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer, tlsStrategyFactory); + this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer, tlsStrategyFactory, + this.dnsResolver); } /** @@ -143,7 +149,22 @@ public HttpComponentsHttpAsyncClientBuilder withDefaultRequestConfigCustomizer( Assert.notNull(defaultRequestConfigCustomizer, "'defaultRequestConfigCustomizer' must not be null"); return new HttpComponentsHttpAsyncClientBuilder(this.customizer, this.connectionManagerCustomizer, this.connectionConfigCustomizer, - this.defaultRequestConfigCustomizer.andThen(defaultRequestConfigCustomizer), this.tlsStrategyFactory); + this.defaultRequestConfigCustomizer.andThen(defaultRequestConfigCustomizer), this.tlsStrategyFactory, + this.dnsResolver); + } + + /** + * Return a new {@link HttpComponentsHttpAsyncClientBuilder} with a replacement + * {@link DnsResolver}. + * @param dnsResolver the new DNS resolver + * @return a new {@link HttpComponentsHttpAsyncClientBuilder} instance + * @since 4.1.0 + */ + public HttpComponentsHttpAsyncClientBuilder withDnsResolver(DnsResolver dnsResolver) { + Assert.notNull(dnsResolver, "'dnsResolver' must not be null"); + return new HttpComponentsHttpAsyncClientBuilder(this.customizer, this.connectionManagerCustomizer, + this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer, this.tlsStrategyFactory, + dnsResolver); } /** @@ -168,6 +189,11 @@ private PoolingAsyncClientConnectionManager createConnectionManager(HttpClientSe PropertyMapper map = PropertyMapper.get(); builder.setDefaultConnectionConfig(createConnectionConfig(settings)); map.from(settings::sslBundle).as(this.tlsStrategyFactory::apply).to(builder::setTlsStrategy); + DnsResolver dnsResolver = this.dnsResolver; + if (settings.inetAddressFilter() != null) { + dnsResolver = new HttpComponentsFilteredDnsResolver(dnsResolver, settings.inetAddressFilter()); + } + builder.setDnsResolver(dnsResolver); this.connectionManagerCustomizer.accept(builder); return builder.build(); } diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java index c14ecf3a3d9f..a2e7e1857025 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java @@ -21,6 +21,8 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import org.apache.hc.client5.http.DnsResolver; +import org.apache.hc.client5.http.SystemDefaultDnsResolver; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.cookie.StandardCookieSpec; @@ -60,9 +62,11 @@ public final class HttpComponentsHttpClientBuilder { private final TlsSocketStrategyFactory tlsSocketStrategyFactory; + private final DnsResolver dnsResolver; + public HttpComponentsHttpClientBuilder() { this(Empty.consumer(), Empty.consumer(), Empty.consumer(), Empty.consumer(), Empty.consumer(), - HttpComponentsSslBundleTlsStrategy::get); + HttpComponentsSslBundleTlsStrategy::get, SystemDefaultDnsResolver.INSTANCE); } private HttpComponentsHttpClientBuilder(Consumer customizer, @@ -70,13 +74,14 @@ private HttpComponentsHttpClientBuilder(Consumer customizer, Consumer socketConfigCustomizer, Consumer connectionConfigCustomizer, Consumer defaultRequestConfigCustomizer, - TlsSocketStrategyFactory tlsSocketStrategyFactory) { + TlsSocketStrategyFactory tlsSocketStrategyFactory, DnsResolver dnsResolver) { this.customizer = customizer; this.connectionManagerCustomizer = connectionManagerCustomizer; this.socketConfigCustomizer = socketConfigCustomizer; this.connectionConfigCustomizer = connectionConfigCustomizer; this.defaultRequestConfigCustomizer = defaultRequestConfigCustomizer; this.tlsSocketStrategyFactory = tlsSocketStrategyFactory; + this.dnsResolver = dnsResolver; } /** @@ -89,7 +94,7 @@ public HttpComponentsHttpClientBuilder withCustomizer(Consumer + * Allow HTTP clients to offer Server-Side Request Forgery (SSRF) mitigation features, for + * example by only allowing local addresses to be called. + *

    + * Filters are typically built using the static factory methods on this interface, + * optionally combined with one or more of the logic methods. For example: + *

    + * InetAddressFilter.of("192.168.0.0/24")
    + * 	.andNot("192.168.0.1");
    + * 
    + * + * @author Rossen Stoyanchev + * @author Rob Winch + * @author Phillip Webb + * @since 4.1.0 + * @see FilteredHostException + */ +@FunctionalInterface +public interface InetAddressFilter { + + /** + * Determine whether the given socket address matches. + * @param address the socket address string to check + * @return if the address matches + */ + default boolean matches(InetSocketAddress address) { + Assert.notNull(address, "'address' must not be null"); + return matches(address.getAddress()); + } + + /** + * Whether the given address matches. + * @param address the address to check + * @return if the address matches + */ + boolean matches(InetAddress address); + + /** + * Return a composed filter that represents a short-circuiting logical AND of this + * filter and other IP addresses. + * @param addresses the addresses that will be logically-ANDed with this filter in any + * form supported by {@link #of(String...)} + * @return a new composed filter instance + */ + default InetAddressFilter and(String... addresses) { + return and(Arrays.stream(addresses).map(IpAddress::of).map(IpAddress::filter).toList()); + } + + /** + * Return a composed filter that represents a short-circuiting logical AND of this + * filter and other filters. + * @param filters the filters that will be logically-ANDed with this filter + * @return a new composed filter instance + */ + default InetAddressFilter and(InetAddressFilter... filters) { + return and(Arrays.asList(filters)); + } + + /** + * Return a composed filter that represents a short-circuiting logical AND of this + * filter and other filters. + * @param filters the filters that will be logically-ANDed with this filter + * @return a new composed filter instance + */ + default InetAddressFilter and(Collection filters) { + InetAddressFilter result = this; + for (InetAddressFilter filter : filters) { + InetAddressFilter ours = result; + result = (address) -> ours.matches(address) && filter.matches(address); + } + return result; + } + + /** + * Return a composed filter that represents a short-circuiting logical AND of this + * filter and other {@link #negate() negated} IP addresses. + * @param addresses the addresses that will be {@link #negate() negated} and + * logically-ANDed with this filter in any form supported by {@link #of(String...)} + * @return a new composed filter instance + */ + default InetAddressFilter andNot(String... addresses) { + return andNot(Arrays.stream(addresses).map(IpAddress::of).map(IpAddress::filter).toList()); + } + + /** + * Return a composed filter that represents a short-circuiting logical AND of this + * filter and other {@link #negate() negated} filters. + * @param filters the filters that will be {@link #negate() negated} and + * logically-ANDed with this filter + * @return a new composed filter instance + */ + default InetAddressFilter andNot(InetAddressFilter... filters) { + return andNot(Arrays.asList(filters)); + } + + /** + * Return a composed filter that represents a short-circuiting logical AND of this + * filter and other {@link #negate() negated} filters. + * @param filters the filters that will be {@link #negate() negated} and + * logically-ANDed with this filter + * @return a new composed filter instance + */ + default InetAddressFilter andNot(Collection filters) { + InetAddressFilter result = this; + for (InetAddressFilter filter : filters) { + InetAddressFilter ours = result; + result = (address) -> ours.matches(address) && !filter.matches(address); + } + return result; + } + + /** + * Return a composed filter that represents a short-circuiting logical OR of this + * filter and other IP addresses. + * @param addresses the addresses that will be logically-ORed with this filter in any + * form supported by {@link #of(String...)} + * @return a new composed filter instance + */ + default InetAddressFilter or(String... addresses) { + return or(Arrays.stream(addresses).map(IpAddress::of).map(IpAddress::filter).toList()); + } + + /** + * Return a composed filter that represents a short-circuiting logical OR of this + * filter and other filters. + * @param filters the matchers that will be logically-ORed with this filter + * @return a new composed filter instance + */ + default InetAddressFilter or(InetAddressFilter... filters) { + return or(Arrays.asList(filters)); + } + + /** + * Return a composed filter that represents a short-circuiting logical OR of this + * filter and other filters. + * @param filters the filters that will be logically-ORed with this filter + * @return a new composed filter instance + */ + default InetAddressFilter or(Collection filters) { + InetAddressFilter result = this; + for (InetAddressFilter matcher : filters) { + InetAddressFilter ours = result; + result = (address) -> ours.matches(address) || matcher.matches(address); + } + return result; + } + + /** + * Return a new filter that represents the logical negation of this filter. + * @return the negated filter + */ + default InetAddressFilter negate() { + return (address) -> !matches(address); + } + + /** + * Return a filter that will match external (non-private) IP addresses. External + * addresses are all non-{@link #internalAddresses() internal addresses} + * @return a filter for external IP addresses + * @see #internalAddresses() + */ + static InetAddressFilter externalAddresses() { + return routable().andNot(InternalInetAddressFilter.instance); + } + + /** + * Return a filter that will match internal (private) IP addresses. + *

    + * Internal addresses include loopback addresses ({@code 127.0.0.0/8} for IPv4, + * {@code ::1} for IPv6), private IPv4 address ranges ({@code 10.0.0.0/8}, + * {@code 172.16.0.0/12}, {@code 192.168.0.0/16}), and IPv6 Unique Local Addresses + * ({@code fc00::/7}). + * @return a filter for external IP addresses + * @see #externalAddresses() + */ + static InetAddressFilter internalAddresses() { + return routable().and(InternalInetAddressFilter.instance); + } + + /** + * Returns a filter that will match all routable addresses (not all zeros). + * @return a filter for routable IP addresses + */ + static InetAddressFilter routable() { + return (address) -> { + Assert.notNull(address, "'address' must not be null"); + byte[] bytes = address.getAddress(); + for (byte b : bytes) { + if (b != 0) { + return true; + } + } + return false; + }; + } + + /** + * Return a filter that is the negation of all the given addresses. + * @param addresses the addresses to negate in any form supported by + * {@link #of(String...)} + * @return a negated filter + * @see #negate() + */ + static InetAddressFilter not(String... addresses) { + return all().andNot(addresses); + } + + /** + * Return a filter that is the negation of all the given matchers. + * @param filters the filters to negate + * @return a negated filter + * @see #negate() + */ + static InetAddressFilter not(InetAddressFilter... filters) { + return all().andNot(filters); + } + + /** + * Return a filter that is the negation of all the given filters. + * @param filters the filters to negate + * @return a negated filter + * @see #negate() + */ + static InetAddressFilter not(Collection filters) { + return all().andNot(filters); + } + + /** + * Return a filter that matches any of the given IP addresses. Address may be either a + * full IP address (e.g. {@code 192.168.1.1}) or an IP address block spcified using + * CIDR notations (for example {@code 192.168.1.0/24}). Both IPv4 and IPv6 addresses + * are supported. + * @param addresses the IP addresses to match + * @return a filter that matches any of the given addresses + */ + static InetAddressFilter of(String... addresses) { + return none().or(addresses); + } + + /** + * Return a filter that matches any of the given filters. + * @param filters the filters to include + * @return a filter that matches any of the filters + */ + static InetAddressFilter of(InetAddressFilter... filters) { + return none().or(filters); + } + + /** + * Return a filter that matches any of the given filters. + * @param filters the filters to include + * @return a filter that matches any of the filters + */ + static InetAddressFilter of(Collection filters) { + return none().or(filters); + } + + /** + * Adapt the given {@link Predicate} into an {@link InetAddressFilter}. + * @param predicate the predicate to adapt + * @return a filter that matches using the predicate + */ + static InetAddressFilter adapt(Predicate<@Nullable InetAddress> predicate) { + Assert.notNull(predicate, "'predicate' must not be null"); + return (address) -> address != null && predicate.test(address); + } + + /** + * Return a filter that matches all addresses. + * @return a filter that matches all + */ + static InetAddressFilter all() { + return (address) -> { + Assert.notNull(address, "'address' must not be null"); + return true; + }; + } + + /** + * Return a filter that matches no addresses. + * @return a filter that matches none + */ + static InetAddressFilter none() { + return (address) -> { + Assert.notNull(address, "'address' must not be null"); + return false; + }; + } + +} diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/InternalInetAddressFilter.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/InternalInetAddressFilter.java new file mode 100644 index 000000000000..2182c5961537 --- /dev/null +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/InternalInetAddressFilter.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; + +import org.springframework.util.Assert; + +/** + * An {@link InetAddressFilter} that matches internal (private) addresses. + *

    + * Internal addresses include loopback addresses (127.0.0.0/8 for IPv4, ::1 for IPv6), + * private IPv4 address ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and IPv6 + * Unique Local Addresses (fc00::/7). + * + * @author Gábor Vaspöri + * @author Kian Jamali + * @author Rossen Stoyanchev + * @author Rob Winch + * @author Phillip Webb + */ +final class InternalInetAddressFilter implements InetAddressFilter { + + private static final byte[] NAT64_PREFIX = { (byte) 0x00, (byte) 0x64, (byte) 0xff, (byte) 0x9b }; + + static final InternalInetAddressFilter instance = new InternalInetAddressFilter(); + + private InternalInetAddressFilter() { + } + + @Override + public boolean matches(InetAddress address) { + Assert.notNull(address, "'address' must not be null"); + return isLocal(address) || isSiteLocalIpv6Address(address.getAddress()); + } + + /** + * Check for Unique Local IPv6 Addresses. We cannot rely on + * {@code Inet6Address.isSiteLocalAddress()} because the JVM implementation dictates + * that {@code fec0::/10} is the only site-local IPv6 address space, based on the + * outdated RFC 2373. That RFC was deprecated by the IETF in 2004 in favor of + * {@code fc00::/7} (RFC 4193). To keep our private network checking accurate to + * modern subnets, we maintain manual parsing. + * @param address the address to check + * @return if the addess is site local + */ + private boolean isSiteLocalIpv6Address(byte[] address) { + return (address.length == 16) + && (address[0] == (byte) 0xfc || address[0] == (byte) 0xfd || isNat64Local(address)); + } + + private boolean isNat64Local(byte[] address) { + if (!Arrays.equals(address, 0, NAT64_PREFIX.length, NAT64_PREFIX, 0, NAT64_PREFIX.length)) { + return false; + } + try { // IPv4/IPv6 translation, 64:ff9b + return isLocal(InetAddress.getByAddress(Arrays.copyOfRange(address, 12, 16))); + } + catch (UnknownHostException ex) { + return false; // Should not happen for 4-byte array + } + } + + private boolean isLocal(InetAddress address) { + return address.isLoopbackAddress() || address.isLinkLocalAddress() || address.isSiteLocalAddress(); + } + +} diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/IpAddress.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/IpAddress.java new file mode 100644 index 000000000000..538c0074d7a9 --- /dev/null +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/IpAddress.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * An IP address and optional mask as used in Classless Inter-Domain Routing (CIDR). + * + * @author Luke Taylor + * @author Steve Riesenberg + * @author Andrey Litvitski + * @author Rob Winch + * @author Phillip Webb + */ +final class IpAddress { + + private static Pattern IPV4 = Pattern.compile("^\\d{1,3}(?:\\.\\d{1,3}){0,3}(?:/\\d{1,2})?$"); + + private final InetAddress address; + + private int maskBitSize; + + private IpAddress(InetAddress address, int maskBitSize) { + Assert.notNull(address, "'address' must not be null"); + Assert.isTrue(maskBitSize >= -1, "'maskBitSize' must be positive or -1"); + Assert.isTrue(address.getAddress().length * 8 >= maskBitSize, () -> String + .format("IP address %s is too short for bitmask of length %d", address.getHostAddress(), maskBitSize)); + this.address = address; + this.maskBitSize = maskBitSize; + } + + InetAddressFilter filter() { + return (address) -> { + Assert.notNull(address, "'address' must not be null"); + if (this.maskBitSize == -1) { + return this.address.equals(address); + } + if (this.maskBitSize == 0) { + return true; + } + byte[] ours = this.address.getAddress(); + byte[] theirs = address.getAddress(); + return (ours.length == theirs.length) && matchesMasked(ours, theirs); + }; + } + + private boolean matchesMasked(byte[] ours, byte[] theirs) { + boolean result = true; + for (int i = 0; i < ours.length; i++) { + int remain = Math.max(this.maskBitSize - (i * 8), 0); + byte mask = (byte) ((remain < 8) ? 0xFF << (8 - remain) : 0xFF); + result = result && (ours[i] & mask) == (theirs[i] & mask); + } + return result; + } + + @Override + public String toString() { + String hostAddress = this.address.getHostAddress(); + String suffix = (this.maskBitSize != -1) ? "/" + this.maskBitSize : ""; + return hostAddress + suffix; + } + + /** + * Factory method to create a new {@link IpAddress} from a string. + * @param address the IP address (plain or in CIDR notation) + * @return a new {@link IpAddress} instance + */ + static IpAddress of(String address) { + Assert.hasText(address, "'address' must not be empty"); + int slash = address.indexOf('/'); + if (slash == -1) { + return new IpAddress(parseInetAddress(address), -1); + } + InetAddress parsedAddress = parseInetAddress(address.substring(0, slash)); + Assert.state(parsedAddress != null, "'address' [%s] did not parse".formatted(address)); + int parseMaskBitSize = parseMaskBitSize(address.substring(slash + 1)); + return new IpAddress(parsedAddress, parseMaskBitSize); + + } + + private static int parseMaskBitSize(String maskBitSize) { + try { + return Integer.parseInt(maskBitSize); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException("'address' subnet mask must be a number", ex); + } + } + + static InetAddress parseInetAddress(String address) { + Assert.isTrue(isLikelyIpAddress(address), + () -> "'address' [%s] must be an IP address and not a host name".formatted(address)); + try { + return InetAddress.getByName(address); + } + catch (UnknownHostException ex) { + throw new IllegalArgumentException("'address' [%s] must be parsable to an InetAddress".formatted(address), + ex); + } + } + + private static boolean isLikelyIpAddress(String address) { + return StringUtils.hasText(address) && (IPV4.matcher(address).matches() || isLikelyIpv6Address(address)); + } + + private static boolean isLikelyIpv6Address(String address) { + char firstChar = address.charAt(0); + return (firstChar == '[' || firstChar == ':') || (isHexDigit(firstChar) && address.contains(":")); + } + + private static boolean isHexDigit(char ch) { + return Character.digit(ch, 16) != -1; + } + +} diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java index b292371feffd..314b17e58e35 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java @@ -16,6 +16,7 @@ package org.springframework.boot.http.client; +import java.net.ProxySelector; import java.net.http.HttpClient; import java.util.Collection; import java.util.List; @@ -89,6 +90,19 @@ public JdkClientHttpRequestFactoryBuilder withHttpClientCustomizer( this.httpClientBuilder.withCustomizer(httpClientCustomizer)); } + /** + * Return a new {@link JdkClientHttpRequestFactoryBuilder} with a replacement + * {@link ProxySelector}. + * @param proxySelector the new proxy selector + * @return a new {@link JdkClientHttpRequestFactoryBuilder} instance + * @since 4.1.0 + */ + public JdkClientHttpRequestFactoryBuilder withProxySelector(ProxySelector proxySelector) { + Assert.notNull(proxySelector, "'proxySelector' must not be null"); + return new JdkClientHttpRequestFactoryBuilder(getCustomizers(), + this.httpClientBuilder.withProxySelector(proxySelector)); + } + /** * Return a new {@link JdkClientHttpRequestFactoryBuilder} that applies the given * customizer. This can be useful for applying pre-packaged customizations. diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkFilteredProxySelector.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkFilteredProxySelector.java new file mode 100644 index 000000000000..84ec8e9091a7 --- /dev/null +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkFilteredProxySelector.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.util.List; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; + +/** + * JDK {@link ProxySelector} to check a URL is not filtered by a + * {@link InetAddressFilter}. + * + * @author Phillip Webb + */ +class JdkFilteredProxySelector extends ProxySelector { + + private final ProxySelector delegate; + + private final InetAddressFilter filter; + + JdkFilteredProxySelector(ProxySelector delegate, InetAddressFilter filter) { + this.delegate = delegate; + this.filter = filter; + } + + @Override + public List select(URI uri) { + String host = uri.getHost(); + FilteredAddresses.of(Stream.of(host), this::matchesResolvedHost).get().orElseThrow(host, this.filter); + return this.delegate.select(uri); + } + + private boolean matchesResolvedHost(String host) { + InetAddress resolved = resolve(host); + return (resolved != null) && this.filter.matches(resolved); + } + + private @Nullable InetAddress resolve(String host) { + try { + // We follow the same resolution logic as + // jdk.internal.net.http.HttpRequestImpl.getAddress() + return InetAddress.getByName(host); + } + catch (UnknownHostException ex) { + return null; + } + } + + @Override + public void connectFailed(URI uri, SocketAddress address, IOException ex) { + this.delegate.connectFailed(uri, address, ex); + } + +} diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkHttpClientBuilder.java index 13e1f6d89702..cd88d9d884af 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkHttpClientBuilder.java @@ -18,6 +18,7 @@ import java.net.CookieHandler; import java.net.CookieManager; +import java.net.ProxySelector; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; import java.util.concurrent.Executor; @@ -44,12 +45,15 @@ public final class JdkHttpClientBuilder { private final Consumer customizer; + private final ProxySelector proxySelector; + public JdkHttpClientBuilder() { - this(Empty.consumer()); + this(Empty.consumer(), ProxySelector.getDefault()); } - private JdkHttpClientBuilder(Consumer customizer) { + private JdkHttpClientBuilder(Consumer customizer, ProxySelector proxySelector) { this.customizer = customizer; + this.proxySelector = proxySelector; } /** @@ -72,7 +76,18 @@ public JdkHttpClientBuilder withExecutor(Executor executor) { */ public JdkHttpClientBuilder withCustomizer(Consumer customizer) { Assert.notNull(customizer, "'customizer' must not be null"); - return new JdkHttpClientBuilder(this.customizer.andThen(customizer)); + return new JdkHttpClientBuilder(this.customizer.andThen(customizer), this.proxySelector); + } + + /** + * Return a new {@link JdkHttpClientBuilder} with a replacement {@link ProxySelector}. + * @param proxySelector the new proxy selector + * @return a new {@link JdkHttpClientBuilder} instance + * @since 4.1.0 + */ + public JdkHttpClientBuilder withProxySelector(ProxySelector proxySelector) { + Assert.notNull(proxySelector, "'proxySelector' must not be null"); + return new JdkHttpClientBuilder(this.customizer, proxySelector); } /** @@ -90,6 +105,7 @@ public HttpClient build(@Nullable HttpClientSettings settings) { map.from(settings::connectTimeout).to(builder::connectTimeout); map.from(settings::sslBundle).as(SslBundle::createSslContext).to(builder::sslContext); map.from(settings::sslBundle).as(this::asSslParameters).to(builder::sslParameters); + map.from(proxySelector(settings.inetAddressFilter())).to(builder::proxy); this.customizer.accept(builder); return builder.build(); } @@ -119,4 +135,8 @@ private Redirect asHttpClientRedirect(@Nullable HttpRedirects redirects) { }; } + private @Nullable ProxySelector proxySelector(@Nullable InetAddressFilter filter) { + return (filter != null) ? new JdkFilteredProxySelector(this.proxySelector, filter) : this.proxySelector; + } + } diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java index c65ef61e3524..662023a051fc 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClientTransport; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.SocketAddressResolver; import org.jspecify.annotations.Nullable; import org.springframework.boot.context.properties.PropertyMapper; @@ -119,6 +120,19 @@ public JettyClientHttpRequestFactoryBuilder withClientConnectorCustomizerCustomi this.httpClientBuilder.withClientConnectorCustomizerCustomizer(clientConnectorCustomizerCustomizer)); } + /** + * Return a new {@link JettyClientHttpRequestFactoryBuilder} with a replacement + * {@link SocketAddressResolver}. + * @param socketAddressResolver the new socket address resolver + * @return a new {@link JettyClientHttpRequestFactoryBuilder} instance + * @since 4.1.0 + */ + public JettyClientHttpRequestFactoryBuilder withSocketAddressResolver(SocketAddressResolver socketAddressResolver) { + Assert.notNull(socketAddressResolver, "'socketAddressResolver' must not be null"); + return new JettyClientHttpRequestFactoryBuilder(getCustomizers(), + this.httpClientBuilder.withSocketAddressResolver(socketAddressResolver)); + } + /** * Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies the given * customizer. This can be useful for applying pre-packaged customizations. diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyFilteredSocketAddressResolver.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyFilteredSocketAddressResolver.java new file mode 100644 index 000000000000..0c545e7025d7 --- /dev/null +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyFilteredSocketAddressResolver.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; + +import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.SocketAddressResolver; + +/** + * Jetty {@link SocketAddressResolver} that filters using a {@link InetAddressFilter}. + * + * @author Phillip Webb + */ +class JettyFilteredSocketAddressResolver implements SocketAddressResolver { + + private final SocketAddressResolver delegate; + + private final InetAddressFilter filter; + + JettyFilteredSocketAddressResolver(SocketAddressResolver delegate, InetAddressFilter filter) { + this.delegate = delegate; + this.filter = filter; + } + + @Override + public void resolve(String host, int port, Map context, Promise> promise) { + this.delegate.resolve(host, port, context, new FilteredPromise(host, promise)); + } + + class FilteredPromise implements Promise> { + + private final String host; + + private final Promise> delegate; + + FilteredPromise(String host, Promise> delegate) { + this.host = host; + this.delegate = delegate; + } + + @Override + public void succeeded(List result) { + try { + this.delegate.succeeded(filter(result)); + } + catch (FilteredHostException ex) { + failed(ex); + } + } + + private List filter(List result) { + InetAddressFilter filter = JettyFilteredSocketAddressResolver.this.filter; + return FilteredAddresses.of(result.stream(), filter::matches).toList().orElseThrow(this.host, filter); + } + + @Override + public void failed(Throwable ex) { + this.delegate.failed(ex); + } + + } + +} diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java index e01647be3d3e..3dd83d6471ae 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java @@ -30,6 +30,7 @@ import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; import org.eclipse.jetty.http.HttpCookieStore; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.SocketAddressResolver; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.jspecify.annotations.Nullable; @@ -56,18 +57,23 @@ public final class JettyHttpClientBuilder { private final Consumer clientConnectorCustomizerCustomizer; + private final @Nullable SocketAddressResolver socketAddressResolver; + public JettyHttpClientBuilder() { - this(Empty.consumer(), JettyHttpClientBuilder::createHttpClientTransport, Empty.consumer(), Empty.consumer()); + this(Empty.consumer(), JettyHttpClientBuilder::createHttpClientTransport, Empty.consumer(), Empty.consumer(), + null); } private JettyHttpClientBuilder(Consumer customizer, Function httpClientTransportFactory, Consumer httpClientTransportCustomizer, - Consumer clientConnectorCustomizerCustomizer) { + Consumer clientConnectorCustomizerCustomizer, + @Nullable SocketAddressResolver socketAddressResolver) { this.customizer = customizer; this.httpClientTransportFactory = httpClientTransportFactory; this.httpClientTransportCustomizer = httpClientTransportCustomizer; this.clientConnectorCustomizerCustomizer = clientConnectorCustomizerCustomizer; + this.socketAddressResolver = socketAddressResolver; } private static HttpClientTransport createHttpClientTransport(ClientConnector connector) { @@ -84,7 +90,8 @@ private static HttpClientTransport createHttpClientTransport(ClientConnector con public JettyHttpClientBuilder withCustomizer(Consumer customizer) { Assert.notNull(customizer, "'customizer' must not be null"); return new JettyHttpClientBuilder(this.customizer.andThen(customizer), this.httpClientTransportFactory, - this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer); + this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer, + this.socketAddressResolver); } /** @@ -98,7 +105,8 @@ public JettyHttpClientBuilder withHttpClientTransportFactory( Function httpClientTransportFactory) { Assert.notNull(httpClientTransportFactory, "'httpClientTransportFactory' must not be null"); return new JettyHttpClientBuilder(this.customizer, httpClientTransportFactory, - this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer); + this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer, + this.socketAddressResolver); } /** @@ -112,7 +120,7 @@ public JettyHttpClientBuilder withHttpClientTransportCustomizer( Assert.notNull(httpClientTransportCustomizer, "'httpClientTransportCustomizer' must not be null"); return new JettyHttpClientBuilder(this.customizer, this.httpClientTransportFactory, this.httpClientTransportCustomizer.andThen(httpClientTransportCustomizer), - this.clientConnectorCustomizerCustomizer); + this.clientConnectorCustomizerCustomizer, this.socketAddressResolver); } /** @@ -126,7 +134,21 @@ public JettyHttpClientBuilder withClientConnectorCustomizerCustomizer( Assert.notNull(clientConnectorCustomizerCustomizer, "'clientConnectorCustomizerCustomizer' must not be null"); return new JettyHttpClientBuilder(this.customizer, this.httpClientTransportFactory, this.httpClientTransportCustomizer, - this.clientConnectorCustomizerCustomizer.andThen(clientConnectorCustomizerCustomizer)); + this.clientConnectorCustomizerCustomizer.andThen(clientConnectorCustomizerCustomizer), + this.socketAddressResolver); + } + + /** + * Return a new {@link JettyHttpClientBuilder} with a replacement + * {@link SocketAddressResolver}. + * @param socketAddressResolver the new socket address resolver + * @return a new {@link JettyHttpClientBuilder} instance + * @since 4.1.0 + */ + public JettyHttpClientBuilder withSocketAddressResolver(SocketAddressResolver socketAddressResolver) { + Assert.notNull(socketAddressResolver, "'socketAddressResolver' must not be null"); + return new JettyHttpClientBuilder(this.customizer, this.httpClientTransportFactory, + this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer, socketAddressResolver); } /** @@ -138,18 +160,19 @@ public HttpClient build(@Nullable HttpClientSettings settings) { settings = (settings != null) ? settings : HttpClientSettings.defaults(); HttpClientTransport transport = createTransport(settings); this.httpClientTransportCustomizer.accept(transport); - HttpClient httpClient = createHttpClient(settings.readTimeout(), transport); + HttpClient httpClient = createHttpClient(settings, transport); PropertyMapper map = PropertyMapper.get(); map.from(settings::connectTimeout).as(Duration::toMillis).to(httpClient::setConnectTimeout); map.from(settings::cookieHandling).as(this::asCookieStore).to(httpClient::setHttpCookieStore); map.from(settings::redirects).always().as(this::followRedirects).to(httpClient::setFollowRedirects); + map.from(this.socketAddressResolver).to(httpClient::setSocketAddressResolver); this.customizer.accept(httpClient); return httpClient; } - private HttpClient createHttpClient(@Nullable Duration readTimeout, HttpClientTransport transport) { - return (readTimeout != null) ? new HttpClientWithReadTimeout(transport, readTimeout) - : new HttpClient(transport); + private HttpClient createHttpClient(HttpClientSettings settings, HttpClientTransport transport) { + return (settings.readTimeout() != null || settings.inetAddressFilter() != null) + ? new CustomizedHttpClient(transport, settings) : new HttpClient(transport); } private HttpClientTransport createTransport(HttpClientSettings settings) { @@ -202,21 +225,32 @@ private boolean followRedirects(@Nullable HttpRedirects redirects) { } /** - * {@link HttpClient} subclass that sets the read timeout. + * {@link HttpClient} subclass to support customization. */ - static class HttpClientWithReadTimeout extends HttpClient { + static class CustomizedHttpClient extends HttpClient { - private final Duration readTimeout; + private final HttpClientSettings settings; - HttpClientWithReadTimeout(HttpClientTransport transport, Duration readTimeout) { + CustomizedHttpClient(HttpClientTransport transport, HttpClientSettings settings) { super(transport); - this.readTimeout = readTimeout; + this.settings = settings; + } + + @Override + public void setSocketAddressResolver(SocketAddressResolver resolver) { + if (this.settings.inetAddressFilter() != null) { + Assert.notNull(resolver, "'resolver' must not be null when addresses are filtered"); + resolver = new JettyFilteredSocketAddressResolver(resolver, this.settings.inetAddressFilter()); + } + super.setSocketAddressResolver(resolver); } @Override public org.eclipse.jetty.client.Request newRequest(java.net.URI uri) { Request request = super.newRequest(uri); - request.timeout(this.readTimeout.toMillis(), TimeUnit.MILLISECONDS); + if (this.settings.readTimeout() != null) { + request.timeout(this.settings.readTimeout().toMillis(), TimeUnit.MILLISECONDS); + } return request; } diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java index c35cb485ae43..35d3d61304eb 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java @@ -25,6 +25,8 @@ import org.jspecify.annotations.Nullable; import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClientConfig; +import reactor.netty.transport.ClientTransport.ResolvedAddressSelector; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.http.client.ReactorClientHttpRequestFactory; @@ -107,6 +109,21 @@ public ReactorClientHttpRequestFactoryBuilder withHttpClientCustomizer( this.httpClientBuilder.withHttpClientCustomizer(httpClientCustomizer)); } + /** + * Return a new {@link ReactorHttpClientBuilder} that uses the + * {@link ResolvedAddressSelector}. This method should be used in favor of a + * customizer so that {@link HttpClientSettings#inetAddressFilter()} can be applied. + * @param resolvedAddressSelector the resolved address selector to use + * @return a new {@link ReactorHttpClientBuilder} instance + * @since 4.1.0 + */ + public ReactorClientHttpRequestFactoryBuilder withResolvedAddressSelector( + ResolvedAddressSelector resolvedAddressSelector) { + Assert.notNull(resolvedAddressSelector, "'resolvedAddressSelector' must not be null"); + return new ReactorClientHttpRequestFactoryBuilder(getCustomizers(), + this.httpClientBuilder.withResolvedAddressSelector(resolvedAddressSelector)); + } + /** * Return a new {@link ReactorClientHttpRequestFactoryBuilder} that applies the given * customizer. This can be useful for applying pre-packaged customizations. diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorFilteredResolvedAddressSelector.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorFilteredResolvedAddressSelector.java new file mode 100644 index 000000000000..7dd0439bb5c9 --- /dev/null +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorFilteredResolvedAddressSelector.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.List; + +import org.jspecify.annotations.Nullable; +import reactor.netty.transport.ClientTransport.ResolvedAddressSelector; + +import org.springframework.util.CollectionUtils; + +/** + * Reactor Netty {@link ResolvedAddressSelector} that filters using a + * {@link InetAddressFilter}. + * + * @param the client configuration implementation + * @author Phillip Webb + */ +class ReactorFilteredResolvedAddressSelector implements ResolvedAddressSelector { + + private final @Nullable ResolvedAddressSelector delegate; + + private final InetAddressFilter filter; + + ReactorFilteredResolvedAddressSelector(@Nullable ResolvedAddressSelector delegate, + InetAddressFilter filter) { + this.delegate = delegate; + this.filter = filter; + } + + @Override + public @Nullable List apply(C config, List resolvedAddresses) { + return filter((this.delegate != null) ? this.delegate.apply(config, resolvedAddresses) : resolvedAddresses); + } + + private @Nullable List filter(@Nullable List resolvedAddresses) { + if (CollectionUtils.isEmpty(resolvedAddresses)) { + return resolvedAddresses; + } + return FilteredAddresses.of(resolvedAddresses.stream(), this::matches) + .toList() + .orElseThrow(() -> hostString(resolvedAddresses), this.filter); + } + + private boolean matches(SocketAddress address) { + return (address instanceof InetSocketAddress socketAddress) ? this.filter.matches(socketAddress) : true; + } + + private String hostString(List resolvedAddresses) { + List hosts = resolvedAddresses.stream() + .filter(InetSocketAddress.class::isInstance) + .map(InetSocketAddress.class::cast) + .map(InetSocketAddress::getAddress) + .map(InetAddress::getHostAddress) + .toList(); + if (hosts.isEmpty()) { + return "unknown"; + } + if (hosts.size() == 1) { + return hosts.get(0); + } + return hosts.toString(); + } + +} diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java index c004e316210e..c5dc7329c035 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java @@ -26,7 +26,9 @@ import io.netty.handler.ssl.SslContextBuilder; import org.jspecify.annotations.Nullable; import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClientConfig; import reactor.netty.tcp.SslProvider.SslContextSpec; +import reactor.netty.transport.ClientTransport.ResolvedAddressSelector; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.ssl.SslBundle; @@ -50,13 +52,17 @@ public final class ReactorHttpClientBuilder { private final UnaryOperator customizer; + private final @Nullable ResolvedAddressSelector resolvedAddressSelector; + public ReactorHttpClientBuilder() { - this(HttpClient::create, UnaryOperator.identity()); + this(HttpClient::create, UnaryOperator.identity(), null); } - private ReactorHttpClientBuilder(Supplier httpClientFactory, UnaryOperator customizer) { + private ReactorHttpClientBuilder(Supplier httpClientFactory, UnaryOperator customizer, + @Nullable ResolvedAddressSelector resolvedAddressSelector) { this.factory = httpClientFactory; this.customizer = customizer; + this.resolvedAddressSelector = resolvedAddressSelector; } /** @@ -68,7 +74,8 @@ private ReactorHttpClientBuilder(Supplier httpClientFactory, UnaryOp public ReactorHttpClientBuilder withReactorResourceFactory(ReactorResourceFactory reactorResourceFactory) { Assert.notNull(reactorResourceFactory, "'reactorResourceFactory' must not be null"); return new ReactorHttpClientBuilder(() -> HttpClient.create(reactorResourceFactory.getConnectionProvider()), - (httpClient) -> this.customizer.apply(httpClient).runOn(reactorResourceFactory.getLoopResources())); + (httpClient) -> this.customizer.apply(httpClient).runOn(reactorResourceFactory.getLoopResources()), + this.resolvedAddressSelector); } /** @@ -79,7 +86,7 @@ public ReactorHttpClientBuilder withReactorResourceFactory(ReactorResourceFactor */ public ReactorHttpClientBuilder withHttpClientFactory(Supplier factory) { Assert.notNull(factory, "'factory' must not be null"); - return new ReactorHttpClientBuilder(factory, this.customizer); + return new ReactorHttpClientBuilder(factory, this.customizer, this.resolvedAddressSelector); } /** @@ -91,7 +98,21 @@ public ReactorHttpClientBuilder withHttpClientFactory(Supplier facto public ReactorHttpClientBuilder withHttpClientCustomizer(UnaryOperator customizer) { Assert.notNull(customizer, "'customizer' must not be null"); return new ReactorHttpClientBuilder(this.factory, - (httpClient) -> customizer.apply(this.customizer.apply(httpClient))); + (httpClient) -> customizer.apply(this.customizer.apply(httpClient)), this.resolvedAddressSelector); + } + + /** + * Return a new {@link ReactorHttpClientBuilder} that uses the + * {@link ResolvedAddressSelector}. This method should be used in favor of a + * customizer so that {@link HttpClientSettings#inetAddressFilter()} can be applied. + * @param resolvedAddressSelector the resolved address selector to use + * @return a new {@link ReactorHttpClientBuilder} instance + * @since 4.1.0 + */ + public ReactorHttpClientBuilder withResolvedAddressSelector( + ResolvedAddressSelector resolvedAddressSelector) { + Assert.notNull(resolvedAddressSelector, "'resolvedAddressSelector' must not be null"); + return new ReactorHttpClientBuilder(this.factory, this.customizer, resolvedAddressSelector); } /** @@ -113,9 +134,18 @@ public HttpClient build(@Nullable HttpClientSettings settings) { throw new IllegalArgumentException("Reactor Netty HTTP client does not support cookie handling"); } httpClient = map.from(settings::sslBundle).to(httpClient, this::secure); + httpClient = map.from(resolvedAddressSelector(settings.inetAddressFilter())) + .to(httpClient, HttpClient::resolvedAddressesSelector); return this.customizer.apply(httpClient); } + private @Nullable ResolvedAddressSelector resolvedAddressSelector( + @Nullable InetAddressFilter inetAddressFilter) { + return (inetAddressFilter != null) + ? new ReactorFilteredResolvedAddressSelector<>(this.resolvedAddressSelector, inetAddressFilter) + : this.resolvedAddressSelector; + } + HttpClient applyDefaults(HttpClient httpClient) { // Aligns with Spring Framework defaults return httpClient.compress(true); diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilder.java index 25ae283d228a..e5706ab5e822 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilder.java @@ -82,6 +82,7 @@ private void configure(ClientHttpRequestFactory requestFactory, HttpClientSettin settings.cookieHandling() == null || settings.cookieHandling() == HttpCookieHandling.ENABLE_WHEN_POSSIBLE, "Unable to set HTTP cookie handling using reflection"); + Assert.state(settings.inetAddressFilter() == null, "Unable to set InetAddress filter using reflection"); ClientHttpRequestFactory unwrapped = unwrapRequestFactoryIfNecessary(requestFactory); PropertyMapper map = PropertyMapper.get(); map.from(settings::connectTimeout).to((connectTimeout) -> setConnectTimeout(unwrapped, connectTimeout)); diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilder.java index fb9040a46acb..f78002d8b001 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilder.java @@ -78,9 +78,10 @@ public SimpleClientHttpRequestFactoryBuilder with(UnaryOperator sslBundles, HttpClientsProperties properties) { + HttpClientSettings httpClientSettings(HttpClientsProperties properties, + ObjectProvider sslBundlesProvider, + ObjectProvider inetAddressFilterProvider) { + InetAddressFilter inetAddressFilter = inetAddressFilterProvider.getIfAvailable(); + HttpClientSettings settings = (inetAddressFilter != null) + ? HttpClientSettings.defaults().withInetAddressFilter(inetAddressFilter) + : HttpClientSettings.defaults(); HttpClientSettingsPropertyMapper propertyMapper = new HttpClientSettingsPropertyMapper( - sslBundles.getIfAvailable(), null); + sslBundlesProvider.getIfAvailable(), settings); return propertyMapper.map(properties); } diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/HttpComponentsClientHttpConnectorBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/HttpComponentsClientHttpConnectorBuilder.java index d47f8c424eda..33b2b2189e67 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/HttpComponentsClientHttpConnectorBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/HttpComponentsClientHttpConnectorBuilder.java @@ -21,6 +21,7 @@ import java.util.function.Function; import java.util.function.UnaryOperator; +import org.apache.hc.client5.http.DnsResolver; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; @@ -128,6 +129,19 @@ public HttpComponentsClientHttpConnectorBuilder withDefaultRequestConfigCustomiz this.httpClientBuilder.withDefaultRequestConfigCustomizer(defaultRequestConfigCustomizer)); } + /** + * Return a new {@link HttpComponentsClientHttpConnectorBuilder} with a replacement + * {@link DnsResolver}. + * @param dnsResolver the new DNS resolver + * @return a new {@link HttpComponentsClientHttpConnectorBuilder} instance + * @since 4.1.0 + */ + public HttpComponentsClientHttpConnectorBuilder withDnsResolver(DnsResolver dnsResolver) { + Assert.notNull(dnsResolver, "'dnsResolver' must not be null"); + return new HttpComponentsClientHttpConnectorBuilder(getCustomizers(), + this.httpClientBuilder.withDnsResolver(dnsResolver)); + } + /** * Return a new {@link HttpComponentsClientHttpConnectorBuilder} that applies the * given customizer. This can be useful for applying pre-packaged customizations. diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/JdkClientHttpConnectorBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/JdkClientHttpConnectorBuilder.java index 92cab4d5772d..58aaa4a98181 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/JdkClientHttpConnectorBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/JdkClientHttpConnectorBuilder.java @@ -16,6 +16,7 @@ package org.springframework.boot.http.client.reactive; +import java.net.ProxySelector; import java.net.http.HttpClient; import java.util.Collection; import java.util.List; @@ -85,6 +86,19 @@ public JdkClientHttpConnectorBuilder withHttpClientCustomizer(Consumer resolvedAddressSelector) { + Assert.notNull(resolvedAddressSelector, "'resolvedAddressSelector' must not be null"); + return new ReactorClientHttpConnectorBuilder(getCustomizers(), + this.httpClientBuilder.withResolvedAddressSelector(resolvedAddressSelector)); + } + /** * Return a new {@link ReactorClientHttpConnectorBuilder} that applies the given * customizer. This can be useful for applying pre-packaged customizations. diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilderTests.java index 2b13595e11e9..e2930eaec5a9 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilderTests.java @@ -53,6 +53,7 @@ import org.springframework.util.StreamUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** @@ -188,6 +189,26 @@ protected final void testRedirect(@Nullable HttpClientSettings settings, HttpMet } } + @Test + void filteredInetAddress() throws Exception { + TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0); + WebServer webServer = webServerFactory + .getWebServer((context) -> context.addServlet("test", TestServlet.class).addMapping("/")); + try { + webServer.start(); + int port = webServer.getPort(); + URI uri = new URI("http://localhost:%s".formatted(port) + "/redirect"); + ClientHttpRequestFactory requestFactory = this.builder + .build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses())); + ClientHttpRequest request = requestFactory.createRequest(uri, HttpMethod.GET); + assertThatException().isThrownBy(request::execute) + .matches((ex) -> ex instanceof FilteredHostException || ex.getCause() instanceof FilteredHostException); + } + finally { + webServer.stop(); + } + } + private ClientHttpRequest request(ClientHttpRequestFactory factory, URI uri, String method) throws IOException { return factory.createRequest(uri, HttpMethod.valueOf(method)); } @@ -222,6 +243,10 @@ protected HttpStatus getExpectedRedirect(HttpMethod httpMethod) { protected abstract long readTimeout(T requestFactory); + protected final ClientHttpRequestFactoryBuilder getBuilder() { + return this.builder; + } + public static class TestServlet extends HttpServlet { @Override diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/FilteredAddressesTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/FilteredAddressesTests.java new file mode 100644 index 000000000000..e5e5e4da0ee6 --- /dev/null +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/FilteredAddressesTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.util.stream.Stream; + +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.mockito.Mockito.mock; + +/** + * Tests for {@link FilteredAddresses}. + * + * @author Phillip Webb + */ +class FilteredAddressesTests { + + @Test + void toListOrElseThrowWhenNotEmptyReturnsResult() { + FilteredAddresses result = FilteredAddresses.of(Stream.of("127.0.0.1"), (address) -> true); + assertThat(result.toList().orElseThrow("localhost", mock())).containsExactly("127.0.0.1"); + } + + @Test + void toListOrElseThrowWhenEmptyThrowsException() { + FilteredAddresses result = FilteredAddresses.of(Stream.of("127.0.0.1"), (address) -> false); + assertThatExceptionOfType(FilteredHostException.class) + .isThrownBy(() -> result.toList().orElseThrow("localhost", mock())) + .withMessage("Filtered host 'localhost'"); + } + + @Test + void toArrayOrElseThrowWhenNotEmptyReturnsResult() { + FilteredAddresses result = FilteredAddresses.of(Stream.of("127.0.0.1"), (address) -> true); + assertThat(result.toArray(String[]::new).orElseThrow("localhost", mock())).containsExactly("127.0.0.1"); + } + + @Test + void toArrayOrElseThrowWhenEmptyThrowsException() { + FilteredAddresses result = FilteredAddresses.of(Stream.of("127.0.0.1"), (address) -> false); + assertThatExceptionOfType(FilteredHostException.class) + .isThrownBy(() -> result.toArray(String[]::new).orElseThrow("localhost", mock())) + .withMessage("Filtered host 'localhost'"); + } + + @Test + void getOrElseThrowWhenNotEmptyReturnsResult() { + FilteredAddresses result = FilteredAddresses.of(Stream.of("127.0.0.1"), (address) -> true); + assertThat(result.get().orElseThrow("localhost", mock())).isEqualTo("127.0.0.1"); + } + + @Test + void getOrElseThrowWhenEmptyThrowsException() { + FilteredAddresses result = FilteredAddresses.of(Stream.of("127.0.0.1"), (address) -> false); + assertThatExceptionOfType(FilteredHostException.class) + .isThrownBy(() -> result.get().orElseThrow("localhost", mock())) + .withMessage("Filtered host 'localhost'"); + } + +} diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/FilteredHostExceptionTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/FilteredHostExceptionTests.java new file mode 100644 index 000000000000..500ef011e018 --- /dev/null +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/FilteredHostExceptionTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link FilteredHostException}. + * + * @author Phillip Webb + */ +class FilteredHostExceptionTests { + + @Test + void create() { + InetAddressFilter matcher = (address) -> false; + FilteredHostException exception = new FilteredHostException("localhost", matcher); + assertThat(exception).hasMessage("Filtered host 'localhost'"); + assertThat(exception.getHost()).isEqualTo("localhost"); + assertThat(exception.getFilter()).isSameAs(matcher); + } + +} diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpClientSettingsTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpClientSettingsTests.java index 58d55443466f..e26ed8fc0396 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpClientSettingsTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpClientSettingsTests.java @@ -43,16 +43,29 @@ void defaults() { assertThat(settings.connectTimeout()).isNull(); assertThat(settings.readTimeout()).isNull(); assertThat(settings.sslBundle()).isNull(); + assertThat(settings.inetAddressFilter()).isNull(); } @Test void createWithNulls() { + HttpClientSettings settings = new HttpClientSettings(null, null, null, null, null, null); + assertThat(settings.cookieHandling()).isNull(); + assertThat(settings.redirects()).isNull(); + assertThat(settings.connectTimeout()).isNull(); + assertThat(settings.readTimeout()).isNull(); + assertThat(settings.sslBundle()).isNull(); + assertThat(settings.inetAddressFilter()).isNull(); + } + + @Test + void createWithNullsWhenSpringBoot41() { HttpClientSettings settings = new HttpClientSettings(null, null, null, null, null); assertThat(settings.cookieHandling()).isNull(); assertThat(settings.redirects()).isNull(); assertThat(settings.connectTimeout()).isNull(); assertThat(settings.readTimeout()).isNull(); assertThat(settings.sslBundle()).isNull(); + assertThat(settings.inetAddressFilter()).isNull(); } @Test @@ -62,6 +75,7 @@ void withConnectTimeoutReturnsInstanceWithUpdatedConnectionTimeout() { assertThat(settings.connectTimeout()).isEqualTo(ONE_SECOND); assertThat(settings.readTimeout()).isNull(); assertThat(settings.sslBundle()).isNull(); + assertThat(settings.inetAddressFilter()).isNull(); } @Test @@ -71,6 +85,7 @@ void withReadTimeoutReturnsInstanceWithUpdatedReadTimeout() { assertThat(settings.connectTimeout()).isNull(); assertThat(settings.readTimeout()).isEqualTo(ONE_SECOND); assertThat(settings.sslBundle()).isNull(); + assertThat(settings.inetAddressFilter()).isNull(); } @Test @@ -81,6 +96,7 @@ void withSslBundleReturnsInstanceWithUpdatedSslBundle() { assertThat(settings.connectTimeout()).isNull(); assertThat(settings.readTimeout()).isNull(); assertThat(settings.sslBundle()).isSameAs(sslBundle); + assertThat(settings.inetAddressFilter()).isNull(); } @Test @@ -91,6 +107,7 @@ void withCookieHandlingReturnsInstanceWithUpdatedCookieHandling() { assertThat(settings.connectTimeout()).isNull(); assertThat(settings.readTimeout()).isNull(); assertThat(settings.sslBundle()).isNull(); + assertThat(settings.inetAddressFilter()).isNull(); } @Test @@ -100,19 +117,33 @@ void withRedirectsReturnsInstanceWithUpdatedRedirect() { assertThat(settings.connectTimeout()).isNull(); assertThat(settings.readTimeout()).isNull(); assertThat(settings.sslBundle()).isNull(); + assertThat(settings.inetAddressFilter()).isNull(); + } + + @Test + void withInetAddressMatcherReturnsInstanceWithUpdatedInetAddressMatcher() { + InetAddressFilter inetAddressMatcher = mock(); + HttpClientSettings settings = HttpClientSettings.defaults().withInetAddressFilter(inetAddressMatcher); + assertThat(settings.redirects()).isNull(); + assertThat(settings.connectTimeout()).isNull(); + assertThat(settings.readTimeout()).isNull(); + assertThat(settings.sslBundle()).isNull(); + assertThat(settings.inetAddressFilter()).isEqualTo(inetAddressMatcher); } @Test void orElseReturnsNewInstanceWithUpdatedValues() { - SslBundle sslBundle = mock(SslBundle.class); - HttpClientSettings settings = new HttpClientSettings(null, null, ONE_SECOND, null, null) + SslBundle sslBundle = mock(); + InetAddressFilter inetAddressMatcher = mock(); + HttpClientSettings settings = new HttpClientSettings(null, null, ONE_SECOND, null, null, null) .orElse(new HttpClientSettings(HttpCookieHandling.ENABLE, HttpRedirects.FOLLOW_WHEN_POSSIBLE, TWO_SECONDS, - TWO_SECONDS, sslBundle)); + TWO_SECONDS, sslBundle, inetAddressMatcher)); assertThat(settings.cookieHandling()).isEqualTo(HttpCookieHandling.ENABLE); assertThat(settings.redirects()).isEqualTo(HttpRedirects.FOLLOW_WHEN_POSSIBLE); assertThat(settings.connectTimeout()).isEqualTo(ONE_SECOND); assertThat(settings.readTimeout()).isEqualTo(TWO_SECONDS); assertThat(settings.sslBundle()).isEqualTo(sslBundle); + assertThat(settings.inetAddressFilter()).isEqualTo(inetAddressMatcher); } @Test @@ -123,6 +154,7 @@ void ofSslBundleCreatesNewSettings() { assertThat(settings.connectTimeout()).isNull(); assertThat(settings.readTimeout()).isNull(); assertThat(settings.sslBundle()).isSameAs(sslBundle); + assertThat(settings.inetAddressFilter()).isNull(); } } diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilderTests.java index ba2ef777ebf7..87720a737908 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilderTests.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; +import org.apache.hc.client5.http.DnsResolver; import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.RequestConfig; @@ -35,10 +36,12 @@ import org.springframework.boot.http.client.HttpComponentsHttpClientBuilder.TlsSocketStrategyFactory; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.classpath.resources.WithPackageResources; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link HttpComponentsClientHttpRequestFactoryBuilder} and @@ -105,6 +108,27 @@ void with() { customizer.assertCalled(); } + @Test + void withDnsResolver() { + DnsResolver dnsResolver = mock(); + ClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.httpComponents() + .withDnsResolver(dnsResolver) + .build(); + assertThat(factory).extracting("httpClient.connManager.connectionOperator.dnsResolver").isSameAs(dnsResolver); + } + + @Test + void withDnsResolverWhenHasInetAddressMatcher() { + DnsResolver dnsResolver = mock(); + ClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.httpComponents() + .withDnsResolver(dnsResolver) + .build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses())); + assertThat(factory).extracting("httpClient.connManager.connectionOperator.dnsResolver") + .matches((resolver) -> resolver.getClass().getName().contains("HttpComponentsFiltered")); + assertThat(factory).extracting("httpClient.connManager.connectionOperator.dnsResolver.delegate") + .isSameAs(dnsResolver); + } + @Test void defaultCookieHandling() { HttpComponentsClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.httpComponents() diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpComponentsFilteredDnsResolverTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpComponentsFilteredDnsResolverTests.java new file mode 100644 index 000000000000..f4fe182205fa --- /dev/null +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpComponentsFilteredDnsResolverTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.List; + +import org.apache.hc.client5.http.DnsResolver; +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.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HttpComponentsFilteredDnsResolver}. + * + * @author Phillip Webb + */ +class HttpComponentsFilteredDnsResolverTests { + + @Test + void resolveHostWhenMatchedOnlyElement() throws Exception { + DnsResolver delegate = mock(); + InetAddress localhost = InetAddress.getLocalHost(); + given(delegate.resolve("localhost")).willReturn(new InetAddress[] { localhost }); + HttpComponentsFilteredDnsResolver dnsResolver = new HttpComponentsFilteredDnsResolver(delegate, + InetAddressFilter.internalAddresses()); + assertThat(dnsResolver.resolve("localhost")).containsExactly(localhost); + } + + @Test + void resolveHostWhenMatchedOneOfManyElements() throws Exception { + DnsResolver delegate = mock(); + InetAddress localhost = InetAddress.getLocalHost(); + InetAddress remote = InetAddress.getByName("8.8.8.8"); + given(delegate.resolve("localhost")).willReturn(new InetAddress[] { localhost, remote }); + HttpComponentsFilteredDnsResolver dnsResolver = new HttpComponentsFilteredDnsResolver(delegate, + InetAddressFilter.internalAddresses()); + assertThat(dnsResolver.resolve("localhost")).containsExactly(localhost); + } + + @Test + void resolveHostWhenMatchedNoElements() throws UnknownHostException { + DnsResolver delegate = mock(); + InetAddress localhost = InetAddress.getLocalHost(); + InetAddress remote = InetAddress.getByName("8.8.8.8"); + given(delegate.resolve("localhost")).willReturn(new InetAddress[] { localhost, remote }); + HttpComponentsFilteredDnsResolver dnsResolver = new HttpComponentsFilteredDnsResolver(delegate, + InetAddressFilter.externalAddresses().andNot("8.8.8.8")); + assertThatExceptionOfType(FilteredHostException.class).isThrownBy(() -> dnsResolver.resolve("localhost")); + } + + @Test + void resolveHostAndPortWhenMatchedOnlyElement() throws Exception { + DnsResolver delegate = mock(); + InetAddress localhost = InetAddress.getLocalHost(); + given(delegate.resolve("localhost", 8080)).willReturn(List.of(new InetSocketAddress(localhost, 8080))); + HttpComponentsFilteredDnsResolver dnsResolver = new HttpComponentsFilteredDnsResolver(delegate, + InetAddressFilter.internalAddresses()); + assertThat(dnsResolver.resolve("localhost", 8080)).containsExactly(new InetSocketAddress(localhost, 8080)); + } + + @Test + void resolveHostAndPortWhenMatchedOneOfManyElements() throws Exception { + DnsResolver delegate = mock(); + InetAddress localhost = InetAddress.getLocalHost(); + InetAddress remote = InetAddress.getByName("8.8.8.8"); + given(delegate.resolve("localhost", 8080)) + .willReturn(List.of(new InetSocketAddress(localhost, 8080), new InetSocketAddress(remote, 8080))); + HttpComponentsFilteredDnsResolver dnsResolver = new HttpComponentsFilteredDnsResolver(delegate, + InetAddressFilter.internalAddresses()); + assertThat(dnsResolver.resolve("localhost", 8080)).containsExactly(new InetSocketAddress(localhost, 8080)); + } + + @Test + void resolveHostAndPortWhenMatchedNoElements() throws UnknownHostException { + DnsResolver delegate = mock(); + InetAddress localhost = InetAddress.getLocalHost(); + InetAddress remote = InetAddress.getByName("8.8.8.8"); + given(delegate.resolve("localhost", 8080)) + .willReturn(List.of(new InetSocketAddress(localhost, 8080), new InetSocketAddress(remote, 8080))); + HttpComponentsFilteredDnsResolver dnsResolver = new HttpComponentsFilteredDnsResolver(delegate, + InetAddressFilter.externalAddresses().andNot("8.8.8.8")); + assertThatExceptionOfType(FilteredHostException.class).isThrownBy(() -> dnsResolver.resolve("localhost", 8080)); + } + + @Test + void resolveCanonicalHostnameDelegates() throws Exception { + DnsResolver delegate = mock(); + given(delegate.resolveCanonicalHostname("spring")).willReturn("boot"); + HttpComponentsFilteredDnsResolver dnsResolver = new HttpComponentsFilteredDnsResolver(delegate, + InetAddressFilter.internalAddresses()); + assertThat(dnsResolver.resolveCanonicalHostname("spring")).isEqualTo("boot"); + } + +} diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/InetAddressFilterAssert.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/InetAddressFilterAssert.java new file mode 100644 index 000000000000..4ea213af1451 --- /dev/null +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/InetAddressFilterAssert.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +import org.assertj.core.api.AbstractObjectAssert; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Assertions for an {@link InetAddressFilter}. + * + * @author Phillip Webb + */ +class InetAddressFilterAssert extends AbstractObjectAssert { + + InetAddressFilterAssert(InetAddressFilter actual) { + super(actual, InetAddressFilterAssert.class); + } + + InetAddressFilterAssert matches(String address) { + return matches(asInetAddress(address)); + } + + InetAddressFilterAssert matches(InetAddress address) { + isNotNull(); + assertThat(this.actual.matches(address)).as("Matches address %s", address).isTrue(); + return this; + } + + InetAddressFilterAssert matches(InetSocketAddress address) { + isNotNull(); + assertThat(this.actual.matches(address)).as("Matches socket address %s", address).isTrue(); + return this; + } + + InetAddressFilterAssert doesNotMatch(String address) { + return doesNotMatch(asInetAddress(address)); + } + + InetAddressFilterAssert doesNotMatch(InetAddress address) { + isNotNull(); + assertThat(this.actual.matches(address)).as("Does not match address %s", address).isFalse(); + return this; + } + + InetAddressFilterAssert doesNotMatch(InetSocketAddress address) { + isNotNull(); + assertThat(this.actual.matches(address)).as("Does not match socket address %s", address).isFalse(); + return this; + } + + private InetAddress asInetAddress(String address) { + try { + return InetAddress.getByName(address); + } + catch (UnknownHostException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/InetAddressFilterTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/InetAddressFilterTests.java new file mode 100644 index 000000000000..3eb8d4089138 --- /dev/null +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/InetAddressFilterTests.java @@ -0,0 +1,723 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.List; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.security.util.matcher.InetAddressMatcher; +import org.springframework.security.util.matcher.InetAddressMatchers; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link InetAddressFilter} and {@link InternalInetAddressFilter}. + * + * @author Rob Winch + * @author Phillip Webb + */ +class InetAddressFilterTests { + + private static InetAddressFilterAssert assertThat(InetAddressFilter filter) { + return new InetAddressFilterAssert(filter); + } + + @Nested + class MatchesSocketAddressTests { + + @Test + void whenIpv4() { + InetAddressFilter filter = InetAddressFilter.of("192.168.1.1"); + assertThat(filter).matches(new InetSocketAddress("192.168.1.1", 8080)); + assertThat(filter).doesNotMatch(new InetSocketAddress("192.168.1.2", 8080)); + } + + @Test + void whenIpv6() { + InetAddressFilter filter = InetAddressFilter.of("fe80:0:0:0:21f:5bff:fe33:bd68"); + assertThat(filter).matches(new InetSocketAddress("fe80::21f:5bff:fe33:bd68", 8080)); + assertThat(filter).doesNotMatch(new InetSocketAddress("fe90::21f:5bff:fe33:bd68", 8080)); + } + + @Test + @SuppressWarnings("NullAway") // Test null check + void whenNull() { + InetAddressFilter filter = (address) -> address != null; + assertThatIllegalArgumentException().isThrownBy(() -> filter.matches((InetSocketAddress) null)) + .withMessage("'address' must not be null"); + } + + @Test + void whenLambda() { + InetAddressFilter filter = (address) -> address.getHostAddress().startsWith("192.168"); + assertThat(filter).matches(new InetSocketAddress("192.168.1.1", 8080)); + assertThat(filter).matches(new InetSocketAddress("192.168.100.200", 8080)); + assertThat(filter).doesNotMatch(new InetSocketAddress("10.0.0.1", 8080)); + } + + } + + @Nested + class AndTests { + + @Test + void stringsWhenEmpty() { + InetAddressFilter originalFilter = (address) -> true; + InetAddressFilter filter = originalFilter.and(new String[] {}); + assertThat(filter).isSameAs(originalFilter); + } + + @Test + void stringsWhenSingle() { + InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1/24"); + InetAddressFilter filter = originalFilter.and("192.168.1.1"); + assertThat(originalFilter).matches("192.168.1.1"); + assertThat(originalFilter).matches("192.168.1.2"); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).doesNotMatch("192.168.1.2"); + } + + @Test + void stringsWhenMultiple() { + InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1/16"); + InetAddressFilter filter = originalFilter.and("192.168.1.1/24", "192.168.1.1"); + assertThat(originalFilter).matches("192.168.1.1"); + assertThat(originalFilter).matches("192.168.1.2"); + assertThat(originalFilter).matches("192.168.2.1"); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).doesNotMatch("192.168.1.2"); + assertThat(filter).doesNotMatch("192.168.2.1"); + } + + @Test + void filters() { + InetAddressFilter startsWithTen = (address) -> address.getHostAddress().startsWith("10."); + InetAddressFilter endsWithOne = (address) -> address.getHostAddress().endsWith(".1"); + InetAddressFilter filter = startsWithTen.and(endsWithOne); + assertThat(filter).matches("10.0.0.1"); + assertThat(filter).doesNotMatch("10.0.0.2"); + assertThat(filter).doesNotMatch("192.168.1.1"); + } + + @Test + void collection() { + InetAddressFilter originalfilter = InetAddressFilter.of("192.168.1.1/24"); + InetAddressFilter filter = originalfilter.and(List.of(InetAddressFilter.of("192.168.1.1"))); + assertThat(originalfilter).matches("192.168.1.1"); + assertThat(originalfilter).matches("192.168.1.2"); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).doesNotMatch("192.168.1.2"); + } + + } + + @Nested + class AndNotTests { + + @Test + void stringsWhenEmpty() { + InetAddressFilter originalFilter = (address) -> true; + InetAddressFilter filter = originalFilter.andNot(new String[] {}); + assertThat(filter).isSameAs(originalFilter); + } + + @Test + void stringsWhenSingle() { + InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1/24"); + InetAddressFilter filter = originalFilter.andNot("192.168.1.1"); + assertThat(originalFilter).matches("192.168.1.1"); + assertThat(originalFilter).matches("192.168.1.2"); + assertThat(filter).doesNotMatch("192.168.1.1"); + assertThat(filter).matches("192.168.1.2"); + } + + @Test + void stringsWhenMultiple() { + InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1/24"); + InetAddressFilter filter = originalFilter.andNot("192.168.1.1", "192.168.1.2"); + assertThat(originalFilter).matches("192.168.1.1"); + assertThat(originalFilter).matches("192.168.1.2"); + assertThat(originalFilter).matches("192.168.1.3"); + assertThat(filter).doesNotMatch("192.168.1.1"); + assertThat(filter).doesNotMatch("192.168.1.2"); + assertThat(filter).matches("192.168.1.3"); + } + + @Test + void filters() { + InetAddressFilter startsWithTen = (address) -> address.getHostAddress().startsWith("10."); + InetAddressFilter endsWithOne = (address) -> address.getHostAddress().endsWith(".1"); + InetAddressFilter filter = startsWithTen.andNot(endsWithOne); + assertThat(filter).doesNotMatch("10.0.0.1"); + assertThat(filter).matches("10.0.0.2"); + assertThat(filter).doesNotMatch("192.168.1.1"); + } + + @Test + void collection() { + InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1/24"); + InetAddressFilter filter = originalFilter.andNot(List.of(InetAddressFilter.of("192.168.1.1"))); + assertThat(originalFilter).matches("192.168.1.1"); + assertThat(originalFilter).matches("192.168.1.2"); + assertThat(filter).doesNotMatch("192.168.1.1"); + assertThat(filter).matches("192.168.1.2"); + } + + } + + @Nested + class OrTests { + + @Test + void stringsWhenEmpty() { + InetAddressFilter originalFilter = (address) -> true; + InetAddressFilter filter = originalFilter.or(new String[] {}); + assertThat(filter).isSameAs(originalFilter); + } + + @Test + void stringsWhenSingle() { + InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1"); + InetAddressFilter filter = originalFilter.or("192.168.1.2"); + assertThat(originalFilter).matches("192.168.1.1"); + assertThat(originalFilter).doesNotMatch("192.168.1.2"); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).matches("192.168.1.2"); + } + + @Test + void stringsWhenMultiple() { + InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1"); + InetAddressFilter filter = originalFilter.or("192.168.1.2", "192.168.1.3"); + assertThat(originalFilter).matches("192.168.1.1"); + assertThat(originalFilter).doesNotMatch("192.168.1.2"); + assertThat(originalFilter).doesNotMatch("192.168.1.3"); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).matches("192.168.1.2"); + assertThat(filter).matches("192.168.1.3"); + } + + @Test + void filter() { + InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1"); + InetAddressFilter filter = originalFilter.or(InetAddressFilter.of("192.168.1.2")); + assertThat(originalFilter).matches("192.168.1.1"); + assertThat(originalFilter).doesNotMatch("192.168.1.2"); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).matches("192.168.1.2"); + } + + @Test + void collection() { + InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1"); + InetAddressFilter filter = originalFilter.or(List.of(InetAddressFilter.of("192.168.1.2"))); + assertThat(originalFilter).matches("192.168.1.1"); + assertThat(originalFilter).doesNotMatch("192.168.1.2"); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).matches("192.168.1.2"); + } + + } + + @Nested + class NegateTests { + + @Test + void negate() { + InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1"); + InetAddressFilter filter = originalFilter.negate(); + assertThat(originalFilter).matches("192.168.1.1"); + assertThat(originalFilter).doesNotMatch("192.168.1.2"); + assertThat(filter).doesNotMatch("192.168.1.1"); + assertThat(filter).matches("192.168.1.2"); + } + + } + + @Nested + class ExternalAddressesTests { + + @Test + @SuppressWarnings("NullAway") // Test null check + void nullInetAddress() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThatIllegalArgumentException().isThrownBy(() -> filter.matches((InetAddress) null)) + .withMessage("'address' must not be null"); + } + + @Test + void ipv4Public() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).matches("8.8.8.8"); + assertThat(filter).matches("1.1.1.1"); + } + + @Test + void ipv6Public() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).matches("2001:4860:4860::8888"); + } + + @Test + void ipv4Private() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("192.168.1.1"); + assertThat(filter).doesNotMatch("10.0.0.1"); + assertThat(filter).doesNotMatch("172.16.0.1"); + } + + @Test + void ipv4Loopback() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("127.0.0.1"); + assertThat(filter).doesNotMatch("127.1.1.1"); + } + + @Test + void ipv4LinkLocal() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("169.254.0.0"); + assertThat(filter).doesNotMatch("169.254.169.254"); + assertThat(filter).doesNotMatch("169.254.255.255"); + } + + @Test + void ipv6Loopback() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("::1"); + assertThat(filter).doesNotMatch("0000::1"); + } + + @Test + void ipv6UniqueLocal() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("fc00::1"); + assertThat(filter).doesNotMatch("fd00::1"); + } + + @Test + void ipv4NonRoutable() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("0.0.0.0"); + } + + @Test + void ipv6NonRoutable() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("0000:0000:0000:0000:0000:0000:0000:0000"); + assertThat(filter).doesNotMatch("::"); + } + + } + + @Nested + class InternalAddresses { + + @Test + @SuppressWarnings("NullAway") // Test null check + void nullInetAddress() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThatIllegalArgumentException().isThrownBy(() -> filter.matches((InetAddress) null)) + .withMessage("'address' must not be null"); + } + + @Test + void ipv4Loopback() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).matches("127.0.0.1"); + assertThat(filter).matches("127.1.1.1"); + assertThat(filter).matches("127.0.0.255"); + } + + @Test + void ipv6Loopback() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).matches("::1"); + assertThat(filter).matches("0000::1"); + } + + @Test + void ipv4PrivateClass10() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).matches("10.0.0.1"); + assertThat(filter).matches("10.255.255.255"); + } + + @Test + void ipv4PrivateClass192() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).matches("192.168.0.1"); + assertThat(filter).matches("192.168.255.255"); + } + + @Test + void ipv4LinkLocal() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).matches("169.254.0.0"); + assertThat(filter).matches("169.254.169.254"); + assertThat(filter).matches("169.254.255.255"); + } + + @Test + void ipv4PrivateClass172() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).matches("172.16.0.1"); + assertThat(filter).matches("172.16.255.255"); + assertThat(filter).matches("172.17.1.1"); + assertThat(filter).matches("172.31.255.255"); + } + + @Test + void ipv4MappedIpv6Internal() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).matches("::ffff:127.0.0.1"); + assertThat(filter).matches("::ffff:192.168.1.1"); + assertThat(filter).matches("::ffff:169.254.169.254"); + assertThat(filter).matches("::ffff:10.0.0.1"); + } + + @Test + void ipv6UniqueLocal() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).matches("fc00::1"); + assertThat(filter).matches("fd00::1"); + } + + @Test + void ipv6TranslationWithInternalIpv4() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).matches("64:ff9b::10.0.0.1"); + assertThat(filter).matches("64:ff9b::127.0.0.1"); + assertThat(filter).matches("64:ff9b::192.168.1.1"); + assertThat(filter).matches("64:ff9b::172.16.0.1"); + } + + @Test + void ipv6TranslationWithIpv4StartsWith192ButNot168() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).doesNotMatch("64:ff9b::192.0.2.1"); + assertThat(filter).doesNotMatch("64:ff9b::192.167.1.1"); + } + + @Test + void ipv6TranslationWithIpv4StartsWith172And16() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).matches("64:ff9b::172.16.0.1"); + assertThat(filter).matches("64:ff9b::172.16.255.255"); + } + + @Test + @ValueSource(strings = {}) + void ipv6TranslationWithExternalIpv4() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).doesNotMatch("64:ff9b::8.8.8.8"); + assertThat(filter).doesNotMatch("64:ff9b::1.1.1.1"); + } + + @Test + void ppv6NonTranslationPrefixByte0() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).doesNotMatch("65:ff9b::10.0.0.1"); + } + + @Test + void ipv6NonTranslationPrefixByte1() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).doesNotMatch("64:fe9b::10.0.0.1"); + } + + @Test + void ipv6NonTranslationPrefixByte2() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).doesNotMatch("64:ff9a::10.0.0.1"); + } + + @Test + void ipv6NonTranslationPrefixByte3() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).doesNotMatch("64:ff9c::10.0.0.1"); + } + + @Test + void ipv4Public() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).doesNotMatch("8.8.8.8"); + assertThat(filter).doesNotMatch("1.1.1.1"); + } + + @Test + void ipv4StartsWith192ButNot168() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).doesNotMatch("192.0.2.1"); + assertThat(filter).doesNotMatch("192.167.1.1"); + assertThat(filter).doesNotMatch("192.169.1.1"); + } + + @Test + void ipv4StartsWith172ButNotPrivate16To31() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).doesNotMatch("172.15.1.1"); + assertThat(filter).doesNotMatch("172.32.1.1"); + } + + @Test + void ipv6Public() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).doesNotMatch("2001:4860:4860::8888"); + } + + @Test + void ipv4NonRoutable() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).doesNotMatch("0.0.0.0"); + } + + @Test + void ipv6NonRoutable() { + InetAddressFilter filter = InetAddressFilter.internalAddresses(); + assertThat(filter).doesNotMatch("0000:0000:0000:0000:0000:0000:0000:0000"); + assertThat(filter).doesNotMatch("::"); + } + + } + + @Nested + class Routable { + + @Test + void nonRoutable() { + InetAddressFilter filter = InetAddressFilter.routable(); + assertThat(filter).doesNotMatch("0.0.0.0"); + assertThat(filter).doesNotMatch("0000:0000:0000:0000:0000:0000:0000:0000"); + assertThat(filter).doesNotMatch("::"); + + } + + @Test + void routable() { + InetAddressFilter filter = InetAddressFilter.routable(); + assertThat(filter).matches("0.0.0.1"); + assertThat(filter).matches("0000:0000:0000:0000:0000:0000:0000:0001"); + } + + } + + @Nested + class NotTests { + + @Test + void stringsWhenEmpty() { + InetAddressFilter filter = InetAddressFilter.not(new String[] {}); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).matches("8.8.8.8"); + } + + @Test + void stringsWhenSingle() { + InetAddressFilter filter = InetAddressFilter.not("192.168.1.1"); + assertThat(filter).doesNotMatch("192.168.1.1"); + assertThat(filter).matches("192.168.1.2"); + } + + @Test + void stringsWhenMultiple() { + InetAddressFilter filter = InetAddressFilter.not("192.168.1.1", "10.0.0.1"); + assertThat(filter).doesNotMatch("192.168.1.1"); + assertThat(filter).doesNotMatch("10.0.0.1"); + assertThat(filter).matches("8.8.8.8"); + } + + @Test + void stringsWhenCidr() { + InetAddressFilter filter = InetAddressFilter.not("192.168.1.0/24"); + assertThat(filter).matches("192.168.2.1"); + assertThat(filter).doesNotMatch("192.168.1.1"); + assertThat(filter).doesNotMatch("192.168.1.255"); + } + + @Test + void filters() { + InetAddressFilter filter = InetAddressFilter.not(InetAddressFilter.of("192.168.1.1")); + assertThat(filter).doesNotMatch("192.168.1.1"); + assertThat(filter).matches("192.168.1.2"); + } + + @Test + void collection() { + InetAddressFilter filter = InetAddressFilter.not(List.of(InetAddressFilter.of("192.168.1.1"))); + assertThat(filter).doesNotMatch("192.168.1.1"); + assertThat(filter).matches("192.168.1.2"); + } + + } + + @Nested + class OfTests { + + @Test + void stringsWhenEmpty() { + InetAddressFilter filter = InetAddressFilter.of(new String[] {}); + assertThat(filter).doesNotMatch("192.168.1.1"); + assertThat(filter).doesNotMatch("8.8.8.8"); + } + + @Test + void stringsWhenSingle() { + InetAddressFilter filter = InetAddressFilter.of("192.168.1.1"); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).doesNotMatch("192.168.1.2"); + } + + @Test + void stringsWhenMultiple() { + InetAddressFilter filter = InetAddressFilter.of("192.168.1.1", "10.0.0.1"); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).matches("10.0.0.1"); + assertThat(filter).doesNotMatch("8.8.8.8"); + } + + @Test + void stringsWhenCidr() { + InetAddressFilter filter = InetAddressFilter.of("192.168.1.0/24"); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).matches("192.168.1.255"); + assertThat(filter).doesNotMatch("192.168.2.1"); + } + + @Test + void filter() { + InetAddressFilter originalfilter = (address) -> address.getHostAddress().startsWith("10."); + InetAddressFilter filter = InetAddressFilter.of(originalfilter); + assertThat(filter).matches("10.0.0.1"); + assertThat(filter).doesNotMatch("192.168.1.1"); + } + + @Test + void collection() { + InetAddressFilter originalfilter = (address) -> address.getHostAddress().startsWith("10."); + InetAddressFilter filter = InetAddressFilter.of(List.of(originalfilter)); + assertThat(filter).matches("10.0.0.1"); + assertThat(filter).doesNotMatch("192.168.1.1"); + } + + } + + @Nested + class CompositeTests { + + @Test + void ofAndNot() { + InetAddressFilter filter = InetAddressFilter.of("192.168.1.0/24").andNot("192.168.1.100"); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).doesNotMatch("192.168.1.100"); + assertThat(filter).doesNotMatch("192.168.2.1"); + } + + @Test + void ofOr() { + InetAddressFilter filter = InetAddressFilter.of("192.168.1.100").or("192.168.1.101"); + assertThat(filter).matches("192.168.1.100"); + assertThat(filter).matches("192.168.1.101"); + assertThat(filter).doesNotMatch("192.168.1.102"); + } + + @Test + void ofAnd() { + InetAddressFilter filter = InetAddressFilter.of("192.168.1.0/24") + .and((address) -> address.getHostAddress().endsWith(".1")); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).doesNotMatch("192.168.1.2"); + } + + @Test + void ofInternalAddressOrAndNot() { + InetAddressFilter filter = InetAddressFilter.internalAddresses() + .or("8.8.8.8", "8.8.4.4") + .andNot("192.168.2.0/24"); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).matches("8.8.8.8"); + assertThat(filter).matches("8.8.4.4"); + assertThat(filter).doesNotMatch("192.168.2.1"); + } + + } + + @Nested + class AllTests { + + @Test + void all() { + InetAddressFilter filter = InetAddressFilter.all(); + assertThat(filter).matches("192.168.1.1"); + assertThat(filter).matches("8.8.8.8"); + } + + @Test + @SuppressWarnings("NullAway") // Test null check + void allWhenNull() { + InetAddressFilter filter = InetAddressFilter.all(); + assertThatIllegalArgumentException().isThrownBy(() -> filter.matches((InetAddress) null)) + .withMessage("'address' must not be null"); + } + + } + + @Nested + class NoneTests { + + @Test + void none() { + InetAddressFilter filter = InetAddressFilter.none(); + assertThat(filter).doesNotMatch("192.168.1.1"); + assertThat(filter).doesNotMatch("8.8.8.8"); + } + + @Test + @SuppressWarnings("NullAway") // Test null check + void noneWhenNull() { + InetAddressFilter filter = InetAddressFilter.none(); + assertThatIllegalArgumentException().isThrownBy(() -> filter.matches((InetAddress) null)) + .withMessage("'address' must not be null"); + } + + } + + @Nested + class AdaptTests { + + @Test + void adaptsSpringSecurity() { + InetAddressMatcher securityMatcher = InetAddressMatchers.matchInternal().build(); + InetAddressFilter filter = InetAddressFilter.adapt(securityMatcher::matches); + assertThat(filter).matches("127.0.0.1"); + assertThat(filter).doesNotMatch("8.8.8.8"); + assertThatFilterDoesNotMatchNull(filter); + } + + @SuppressWarnings("NullAway") // Test null check + private void assertThatFilterDoesNotMatchNull(InetAddressFilter filter) { + assertThat(filter).doesNotMatch((InetAddress) null); + } + + } + +} diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/IpAddressTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/IpAddressTests.java new file mode 100644 index 000000000000..06681e11190d --- /dev/null +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/IpAddressTests.java @@ -0,0 +1,197 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.net.InetAddress; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link IpAddress}. + * + * @author Rob Winch + * @author Phillip Webb + */ +class IpAddressTests { + + @Test + @SuppressWarnings("NullAway") // Test null check + void ofWhenAddressIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> IpAddress.of(null)) + .withMessage("'address' must not be empty"); + } + + @Test + void ofWhenAddressIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> IpAddress.of("")) + .withMessage("'address' must not be empty"); + } + + @Test + void ofWithMaskWhenAddressIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> IpAddress.of("192.168.1.1/")) + .withMessage("'address' subnet mask must be a number"); + } + + @Test + void ofWhenUnmaskedIpAddress() throws Exception { + IpAddress address = IpAddress.of("192.168.1.1"); + assertThat(address).extracting("address").isEqualTo(InetAddress.getByName("192.168.1.1")); + assertThat(address).extracting("maskBitSize").isEqualTo(-1); + } + + @Test + void ofWhenMaskedIpAddress() throws Exception { + IpAddress address = IpAddress.of("192.168.1.1/24"); + assertThat(address).extracting("address").isEqualTo(InetAddress.getByName("192.168.1.1")); + assertThat(address).extracting("maskBitSize").isEqualTo(24); + } + + @Test + void parseInetAddressWhenIpv4() throws Exception { + InetAddress parsed = IpAddress.parseInetAddress("192.168.1.1"); + assertThat(parsed).isEqualTo(InetAddress.getByName("192.168.1.1")); + } + + @Test + void parseInetAddressWhenIpv6InUrl() { + InetAddress parsed = IpAddress.parseInetAddress("[::1]"); + assertThat(parsed.isLoopbackAddress()).isTrue(); + } + + @Test + void parseInetAddressWhenIpv6Shortcut() { + InetAddress parsed = IpAddress.parseInetAddress("::1"); + assertThat(parsed.isLoopbackAddress()).isTrue(); + } + + @Test + void parseInetAddressWhenLikelyHost() { + String message = "must be an IP address and not a host name"; + assertThatIllegalArgumentException().isThrownBy(() -> IpAddress.parseInetAddress("https://example.com")) + .withMessageContaining(message); + assertThatIllegalArgumentException().isThrownBy(() -> IpAddress.parseInetAddress("192.168.1.2.3")) + .withMessageContaining(message); + assertThatIllegalArgumentException() + .isThrownBy(() -> IpAddress.parseInetAddress("G001:0db8:0000:0000:0000:0000:0000:0000")) + .withMessageContaining(message); + } + + @Test + void parseInetAddressWhenCannotBeParsed() { + assertThatIllegalArgumentException() + .isThrownBy(() -> IpAddress.parseInetAddress("2001:0db8:0000:0000:0000:0000:0000:000G")) + .withMessageContaining("must be parsable to an InetAddress"); + } + + @Test + void ofWithHostnameThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> IpAddress.of("example.com")) + .withMessage("'address' [example.com] must be an IP address and not a host name"); + } + + @Test + void matcherWhenUnmaskedIpv4() { + IpAddress address = IpAddress.of("192.168.1.1"); + assertThatFilter(address).matches("192.168.1.1"); + assertThatFilter(address).doesNotMatch("192.168.1.2"); + } + + @Test + void matcherWhenUnmaskedIpv6() { + IpAddress address = IpAddress.of("fe80::21f:5bff:fe33:bd68"); + assertThatFilter(address).matches("fe80::21f:5bff:fe33:bd68"); + assertThatFilter(address).doesNotMatch("fe80::21f:5bff:fe33:bd69"); + } + + @Test + void matcherWhenMaskedIpv4() { + IpAddress address = IpAddress.of("192.168.1.0/24"); + assertThatFilter(address).doesNotMatch("192.168.2.1"); + assertThatFilter(address).doesNotMatch("192.168.0.255"); + } + + @Test + void matcherWhenMaskedWithZero() { + IpAddress address = IpAddress.of("192.168.1.0/0"); + assertThatFilter(address).matches("192.168.1.1"); + assertThatFilter(address).matches("192.168.1.255"); + assertThatFilter(address).matches("8.8.8.8"); + } + + @Test + void matcherWhenMaskedIpv6() { + IpAddress address = IpAddress.of("2001:db8::/48"); + assertThatFilter(address).matches("2001:db8:0:0:0:0:0:0"); + assertThatFilter(address).matches("2001:db8:0:ffff:ffff:ffff:ffff:ffff"); + assertThatFilter(address).doesNotMatch("2001:db8:1:0:0:0:0:0"); + } + + @Test + void matcherWhenMaskedIpv4OutsideOfByteBoundary() { + IpAddress address = IpAddress.of("192.168.1.0/30"); + assertThatFilter(address).matches("192.168.1.0"); + assertThatFilter(address).matches("192.168.1.1"); + assertThatFilter(address).matches("192.168.1.2"); + assertThatFilter(address).matches("192.168.1.3"); + assertThatFilter(address).doesNotMatch("192.168.1.4"); + } + + @Test + void matcherWhenIpv4DoesNotMatchIpv6() { + assertThatFilter(IpAddress.of("192.168.1.1")).doesNotMatch("fe80::21f:5bff:fe33:bd68"); + assertThatFilter(IpAddress.of("8.8.8.8")).doesNotMatch("0808:0808::"); + } + + @Test + void matcherWhenIpv6DoesNotMatchIpv4() { + assertThatFilter(IpAddress.of("fe80::21f:5bff:fe33:bd68")).doesNotMatch("192.168.1.1"); + assertThatFilter(IpAddress.of("0808:0808::/32")).doesNotMatch("8.8.8.8"); + } + + @Test + @SuppressWarnings("NullAway") // Test null check + void matcherWhenCheckingNullThrowsException() { + IpAddress address = IpAddress.of("192.168.1.1"); + assertThatIllegalArgumentException().isThrownBy(() -> address.filter().matches((InetAddress) null)) + .withMessage("'address' must not be null"); + } + + @Test + void addressesInIpRangeMatch() { + for (int i = 0; i < 255; i++) { + assertThatFilter(IpAddress.of("192.168.1.0/24")).matches("192.168.1." + i); + } + assertThatFilter(IpAddress.of("192.168.1.0/25")).matches("192.168.1.127").doesNotMatch("192.168.1.128"); + assertThatFilter(IpAddress.of("192.168.1.128/25")).matches("192.168.1.255"); + assertThatFilter(IpAddress.of("192.168.1.192/26")).matches("192.168.1.255"); + assertThatFilter(IpAddress.of("192.168.1.224/27")).matches("192.168.1.255"); + assertThatFilter(IpAddress.of("192.168.1.240/27")).matches("192.168.1.255"); + assertThatFilter(IpAddress.of("192.168.1.255/32")).matches("192.168.1.255"); + assertThatFilter(IpAddress.of("202.24.0.0/14")).matches("202.24.199.127"); + assertThatFilter(IpAddress.of("202.24.0.0/14")).matches("202.25.179.135"); + assertThatFilter(IpAddress.of("202.24.0.0/14")).matches("202.26.179.135"); + } + + private static InetAddressFilterAssert assertThatFilter(IpAddress address) { + return new InetAddressFilterAssert(address.filter()); + } + +} diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilderTests.java index 2f240492cc23..3a183f8e76a1 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilderTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.http.client; +import java.net.ProxySelector; import java.net.http.HttpClient; import java.time.Duration; import java.util.concurrent.Executor; @@ -29,6 +30,7 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link JdkClientHttpRequestFactoryBuilder} and {@link JdkHttpClientBuilder}. @@ -70,6 +72,30 @@ void with() { customizer.assertCalled(); } + @Test + void withProxySelector() { + ProxySelector proxySelector = mock(); + JdkClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.jdk() + .withProxySelector(proxySelector) + .build(); + HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(factory, "httpClient"); + assertThat(httpClient).isNotNull(); + assertThat(httpClient.proxy()).contains(proxySelector); + } + + @Test + void withProxySelectorWhenHasInetAddressMatcher() { + ProxySelector proxySelector = mock(); + JdkClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.jdk() + .withProxySelector(proxySelector) + .build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses())); + HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(factory, "httpClient"); + assertThat(httpClient).isNotNull(); + ProxySelector actual = httpClient.proxy().get(); + assertThat(actual).matches((proxy) -> proxy.getClass().getName().contains("JdkFiltered")); + assertThat(actual).extracting("delegate").isEqualTo(proxySelector); + } + @Test void defaultCookieHandling() { JdkClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.jdk() diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JdkFilteredProxySelectorTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JdkFilteredProxySelectorTests.java new file mode 100644 index 000000000000..c177c8de68db --- /dev/null +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JdkFilteredProxySelectorTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.io.IOException; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +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.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JdkFilteredProxySelector}. + * + * @author Phillip Webb + */ +class JdkFilteredProxySelectorTests { + + @Test + void selectWhenMatchesResolvedHost() throws Exception { + URI localhost = new URI("http://localhost"); + ProxySelector delegate = mock(ProxySelector.class); + Proxy proxy = mock(); + given(delegate.select(localhost)).willReturn(List.of(proxy)); + JdkFilteredProxySelector proxySelector = new JdkFilteredProxySelector(delegate, + InetAddressFilter.internalAddresses()); + assertThat(proxySelector.select(localhost)).containsExactly(proxy); + } + + @Test + void selectWhenDoesNotMatchResolvedHost() throws Exception { + URI localhost = new URI("http://localhost"); + ProxySelector delegate = mock(ProxySelector.class); + Proxy proxy = mock(); + given(delegate.select(localhost)).willReturn(List.of(proxy)); + JdkFilteredProxySelector proxySelector = new JdkFilteredProxySelector(delegate, + InetAddressFilter.externalAddresses()); + assertThatExceptionOfType(FilteredHostException.class).isThrownBy(() -> proxySelector.select(localhost)); + } + + @Test + void connectFailDelegates() throws Exception { + URI localhost = new URI("http://localhost"); + ProxySelector delegate = mock(ProxySelector.class); + JdkFilteredProxySelector proxySelector = new JdkFilteredProxySelector(delegate, + InetAddressFilter.externalAddresses()); + SocketAddress address = mock(); + IOException ex = new IOException(); + proxySelector.connectFailed(localhost, address, ex); + then(delegate).should().connectFailed(localhost, address, ex); + } + +} diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java index 66b8636b551b..f7ad4c2c7a54 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java @@ -21,6 +21,7 @@ import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; import org.eclipse.jetty.http.HttpCookieStore; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.SocketAddressResolver; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -29,6 +30,7 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link JettyClientHttpRequestFactoryBuilder} and @@ -78,6 +80,26 @@ void withHttpClientTransportFactory() { .isInstanceOf(TestHttpClientTransport.class); } + @Test + void withSocketAddressResolver() { + SocketAddressResolver socketAddressResolver = mock(); + JettyClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.jetty() + .withSocketAddressResolver(socketAddressResolver) + .build(); + assertThat(factory).extracting("httpClient.resolver").isSameAs(socketAddressResolver); + } + + @Test + void withSocketAddressResolverWhenHasInetAddressMatcher() { + SocketAddressResolver socketAddressResolver = mock(); + JettyClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.jetty() + .withSocketAddressResolver(socketAddressResolver) + .build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses())); + assertThat(factory).extracting("httpClient.resolver") + .matches((resolver) -> resolver.getClass().getName().contains("JettyFiltered")); + assertThat(factory).extracting("httpClient.resolver.delegate").isSameAs(socketAddressResolver); + } + @Test void defaultCookieHandling() { JettyClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.jetty() diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JettyFilteredSocketAddressResolverTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JettyFilteredSocketAddressResolverTests.java new file mode 100644 index 000000000000..473df5c38c16 --- /dev/null +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JettyFilteredSocketAddressResolverTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.SocketAddressResolver; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JettyFilteredSocketAddressResolver}. + * + * @author Phillip Webb + */ +class JettyFilteredSocketAddressResolverTests { + + @Test + void resolveWhenMatchedOnlyElement() { + InetSocketAddress localhost = inetSocketAddress("localhost", 8080); + SocketAddressResolver delegate = (host, port, context, promise) -> promise.succeeded(List.of(localhost)); + JettyFilteredSocketAddressResolver resolver = new JettyFilteredSocketAddressResolver(delegate, + InetAddressFilter.internalAddresses()); + Promise> promise = mock(); + resolver.resolve("localhost", 8080, Collections.emptyMap(), promise); + then(promise).should().succeeded(List.of(localhost)); + } + + @Test + void resolveWhenMatchedOneOfManyElements() { + InetSocketAddress localhost = inetSocketAddress("localhost", 8080); + InetSocketAddress remote = inetSocketAddress("8.8.8.8", 8080); + SocketAddressResolver delegate = (host, port, context, promise) -> promise + .succeeded(List.of(localhost, remote)); + JettyFilteredSocketAddressResolver resolver = new JettyFilteredSocketAddressResolver(delegate, + InetAddressFilter.internalAddresses()); + Promise> promise = mock(); + resolver.resolve("localhost", 8080, Collections.emptyMap(), promise); + then(promise).should().succeeded(List.of(localhost)); + } + + @Test + void resolveWhenMatchedNoElements() { + InetSocketAddress localhost = inetSocketAddress("localhost", 8080); + InetSocketAddress remote = inetSocketAddress("8.8.8.8", 8080); + SocketAddressResolver delegate = (host, port, context, promise) -> promise + .succeeded(List.of(localhost, remote)); + JettyFilteredSocketAddressResolver resolver = new JettyFilteredSocketAddressResolver(delegate, + InetAddressFilter.externalAddresses().andNot("8.8.8.8")); + Promise> promise = mock(); + resolver.resolve("localhost", 8080, Collections.emptyMap(), promise); + ArgumentCaptor failure = ArgumentCaptor.captor(); + then(promise).should().failed(failure.capture()); + assertThat(failure.getValue()).isInstanceOf(FilteredHostException.class); + } + + @Test + void resolveWhenDelegateFails() { + Throwable ex = new RuntimeException(); + SocketAddressResolver delegate = (host, port, context, promise) -> promise.failed(ex); + JettyFilteredSocketAddressResolver resolver = new JettyFilteredSocketAddressResolver(delegate, + InetAddressFilter.externalAddresses()); + Promise> promise = mock(); + resolver.resolve("localhost", 8080, Collections.emptyMap(), promise); + then(promise).should().failed(ex); + } + + private InetSocketAddress inetSocketAddress(String host, int port) { + try { + return new InetSocketAddress(InetAddress.getByName(host), port); + } + catch (UnknownHostException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReactorFilteredResolvedAddressSelectorTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReactorFilteredResolvedAddressSelectorTests.java new file mode 100644 index 000000000000..514b63d1179a --- /dev/null +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReactorFilteredResolvedAddressSelectorTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.http.client; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.List; + +import org.junit.jupiter.api.Test; +import reactor.netty.http.client.HttpClientConfig; +import reactor.netty.transport.ClientTransport.ResolvedAddressSelector; + +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 ReactorFilteredResolvedAddressSelector}. + * + * @author Phillip Webb + */ +class ReactorFilteredResolvedAddressSelectorTests { + + @Test + void applyWhenMatchedOnlyElement() { + InetSocketAddress localhost = inetSocketAddress("localhost", 8080); + ResolvedAddressSelector delegate = (config, resolvedAddresses) -> resolvedAddresses; + HttpClientConfig config = mock(); + ReactorFilteredResolvedAddressSelector addressSelector = new ReactorFilteredResolvedAddressSelector<>( + delegate, InetAddressFilter.internalAddresses()); + assertThat(addressSelector.apply(config, List.of(localhost))).isEqualTo(List.of(localhost)); + } + + @Test + void applyWhenMatchedOneOfManyElements() { + InetSocketAddress localhost = inetSocketAddress("localhost", 8080); + InetSocketAddress remote = inetSocketAddress("8.8.8.8", 8080); + ResolvedAddressSelector delegate = (config, resolvedAddresses) -> resolvedAddresses; + HttpClientConfig config = mock(); + ReactorFilteredResolvedAddressSelector addressSelector = new ReactorFilteredResolvedAddressSelector<>( + delegate, InetAddressFilter.internalAddresses()); + assertThat(addressSelector.apply(config, List.of(localhost, remote))).isEqualTo(List.of(localhost)); + } + + @Test + void applyWhenMatchedNoElements() { + InetSocketAddress localhost = inetSocketAddress("localhost", 8080); + InetSocketAddress remote = inetSocketAddress("8.8.8.8", 8080); + ResolvedAddressSelector delegate = (config, resolvedAddresses) -> resolvedAddresses; + HttpClientConfig config = mock(); + ReactorFilteredResolvedAddressSelector addressSelector = new ReactorFilteredResolvedAddressSelector<>( + delegate, InetAddressFilter.externalAddresses().andNot("8.8.8.8")); + assertThatExceptionOfType(FilteredHostException.class) + .isThrownBy(() -> addressSelector.apply(config, List.of(localhost, remote))) + .withMessage("Filtered host '[127.0.0.1, 8.8.8.8]'"); + + } + + private InetSocketAddress inetSocketAddress(String host, int port) { + try { + return new InetSocketAddress(InetAddress.getByName(host), port); + } + catch (UnknownHostException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java index f328d0af04dc..a18cfa639194 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java @@ -46,6 +46,15 @@ class ReflectiveComponentsClientHttpRequestFactoryBuilderTests super(ClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.of(JettyClientHttpRequestFactory::new)); } + @Test + @Override + void filteredInetAddress() throws Exception { + assertThatIllegalStateException() + .isThrownBy(() -> getBuilder() + .build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses()))) + .withMessage("Unable to set InetAddress filter using reflection"); + } + @Override void connectWithSslBundle(String httpMethod) throws Exception { HttpClientSettings settings = HttpClientSettings.ofSslBundle(sslBundle()); diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.java index f23d1f91fcc7..79c5b1e03ab0 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.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.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** @@ -83,9 +82,18 @@ void redirectDontFollow(String httpMethod) throws Exception { super.redirectDontFollow(httpMethod); } + @Test + @Override + void filteredInetAddress() throws Exception { + assertThatIllegalStateException() + .isThrownBy(() -> getBuilder() + .build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses()))) + .withMessage("Simple HTTP request factory does not support InetAddress filtering"); + } + @Test void throwsWhenCookieHandlingEnabled() { - assertThatIllegalArgumentException().isThrownBy(() -> ClientHttpRequestFactoryBuilder.simple() + assertThatIllegalStateException().isThrownBy(() -> ClientHttpRequestFactoryBuilder.simple() .build(HttpClientSettings.defaults().withCookieHandling(HttpCookieHandling.ENABLE))); } diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfigurationTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfigurationTests.java index 9bc5844f677c..9f003368896b 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfigurationTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfigurationTests.java @@ -24,11 +24,13 @@ import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpRedirects; +import org.springframework.boot.http.client.InetAddressFilter; 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 HttpClientAutoConfiguration}. @@ -62,6 +64,14 @@ void doesNotReplaceUserProvidedHttpClientSettings() { .isEqualTo(new HttpClientSettings(null, null, Duration.ofSeconds(1), Duration.ofSeconds(2), null))); } + @Test + void injectsInetAddressMatcher() { + InetAddressFilter matcher = mock(); + this.contextRunner.withBean(InetAddressFilter.class, () -> matcher) + .run((context) -> assertThat(context.getBean(HttpClientSettings.class).inetAddressFilter()) + .isEqualTo(matcher)); + } + @Configuration(proxyBeanMethods = false) static class TestHttpClientConfiguration { diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/AbstractClientHttpConnectorBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/AbstractClientHttpConnectorBuilderTests.java index 0ebdfb443e1d..f0c4097a2898 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/AbstractClientHttpConnectorBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/AbstractClientHttpConnectorBuilderTests.java @@ -34,8 +34,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.http.client.FilteredHostException; import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpRedirects; +import org.springframework.boot.http.client.InetAddressFilter; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundleKey; import org.springframework.boot.ssl.SslOptions; @@ -55,6 +57,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.assertThatExceptionOfType; /** @@ -192,6 +195,27 @@ protected final void testRedirect(@Nullable HttpClientSettings settings, HttpMet } } + @Test + void filteredInetAddress() throws Exception { + TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0); + WebServer webServer = webServerFactory + .getWebServer((context) -> context.addServlet("test", TestServlet.class).addMapping("/")); + try { + webServer.start(); + int port = webServer.getPort(); + URI uri = new URI("http://localhost:%s".formatted(port) + "/redirect"); + ClientHttpConnector connector = this.builder + .build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses())); + ClientRequest request = createRequest("GET", uri); + assertThatException().isThrownBy(() -> getResponse(connector, request)) + .matches((ex) -> ex instanceof FilteredHostException || ex.getCause() instanceof FilteredHostException); + + } + finally { + webServer.stop(); + } + } + private ClientRequest createRequest(String httpMethod, URI uri) { return createRequest(HttpMethod.valueOf(httpMethod), uri); } diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/HttpComponentsClientHttpConnectorBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/HttpComponentsClientHttpConnectorBuilderTests.java index 0b5a7857478f..6797eb8a5dff 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/HttpComponentsClientHttpConnectorBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/HttpComponentsClientHttpConnectorBuilderTests.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.function.Function; +import org.apache.hc.client5.http.DnsResolver; import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.async.HttpAsyncClient; import org.apache.hc.client5.http.config.ConnectionConfig; @@ -33,12 +34,14 @@ import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpComponentsHttpAsyncClientBuilder; +import org.springframework.boot.http.client.InetAddressFilter; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.classpath.resources.WithPackageResources; import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link HttpComponentsClientHttpConnectorBuilder} and @@ -104,6 +107,28 @@ void with() { customizer.assertCalled(); } + @Test + void withDnsResolver() { + DnsResolver dnsResolver = mock(); + HttpComponentsClientHttpConnector connector = ClientHttpConnectorBuilder.httpComponents() + .withDnsResolver(dnsResolver) + .build(); + assertThat(connector).extracting("client.manager.connectionOperator.sessionRequester.dnsResolver") + .isSameAs(dnsResolver); + } + + @Test + void withDnsResolverWhenHasInetAddressMatcher() { + DnsResolver dnsResolver = mock(); + HttpComponentsClientHttpConnector connector = ClientHttpConnectorBuilder.httpComponents() + .withDnsResolver(dnsResolver) + .build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses())); + assertThat(connector).extracting("client.manager.connectionOperator.sessionRequester.dnsResolver") + .matches((resolver) -> resolver.getClass().getName().contains("HttpComponentsFiltered")); + assertThat(connector).extracting("client.manager.connectionOperator.sessionRequester.dnsResolver.delegate") + .isSameAs(dnsResolver); + } + @Override protected long connectTimeout(HttpComponentsClientHttpConnector connector) { return getConnectorConfig(connector).getConnectTimeout().toMilliseconds(); diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/JdkClientHttpConnectorBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/JdkClientHttpConnectorBuilderTests.java index 4af086153a53..54681bb376d7 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/JdkClientHttpConnectorBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/JdkClientHttpConnectorBuilderTests.java @@ -16,18 +16,22 @@ package org.springframework.boot.http.client.reactive; +import java.net.ProxySelector; import java.net.http.HttpClient; import java.time.Duration; import java.util.concurrent.Executor; import org.junit.jupiter.api.Test; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.InetAddressFilter; import org.springframework.boot.http.client.JdkHttpClientBuilder; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.http.client.reactive.JdkClientHttpConnector; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link JdkClientHttpConnectorBuilder} and {@link JdkHttpClientBuilder}. @@ -68,6 +72,29 @@ void with() { customizer.assertCalled(); } + @Test + void withProxySelector() { + ProxySelector proxySelector = mock(); + JdkClientHttpConnector connector = ClientHttpConnectorBuilder.jdk().withProxySelector(proxySelector).build(); + HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient"); + assertThat(httpClient).isNotNull(); + assertThat(httpClient.proxy()).contains(proxySelector); + } + + @Test + void withProxySelectorWhenHasInetAddressMatcher() { + ProxySelector proxySelector = mock(); + JdkClientHttpConnector connector = ClientHttpConnectorBuilder.jdk() + .withProxySelector(proxySelector) + .build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses())); + HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient"); + assertThat(httpClient).isNotNull(); + assertThat(httpClient.proxy()).isNotNull(); + ProxySelector actual = httpClient.proxy().get(); + assertThat(actual).matches((proxy) -> proxy.getClass().getName().contains("JdkFiltered")); + assertThat(actual).extracting("delegate").isEqualTo(proxySelector); + } + @Override protected long connectTimeout(JdkClientHttpConnector connector) { HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient"); diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/JettyClientHttpConnectorBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/JettyClientHttpConnectorBuilderTests.java index ef4ae1e67091..892cb3e98c3d 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/JettyClientHttpConnectorBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/JettyClientHttpConnectorBuilderTests.java @@ -16,19 +16,21 @@ package org.springframework.boot.http.client.reactive; -import java.time.Duration; - import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClientTransport; import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.SocketAddressResolver; import org.junit.jupiter.api.Test; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.InetAddressFilter; import org.springframework.boot.http.client.JettyHttpClientBuilder; import org.springframework.http.client.reactive.JettyClientHttpConnector; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link JettyClientHttpConnectorBuilder} and {@link JettyHttpClientBuilder}. @@ -76,6 +78,26 @@ void withHttpClientTransportFactory() { .isInstanceOf(TestHttpClientTransport.class); } + @Test + void withSocketAddressResolver() { + SocketAddressResolver socketAddressResolver = mock(); + JettyClientHttpConnector connector = ClientHttpConnectorBuilder.jetty() + .withSocketAddressResolver(socketAddressResolver) + .build(); + assertThat(connector).extracting("httpClient.resolver").isSameAs(socketAddressResolver); + } + + @Test + void withSocketAddressResolverWhenHasInetAddressMatcher() { + SocketAddressResolver socketAddressResolver = mock(); + JettyClientHttpConnector connector = ClientHttpConnectorBuilder.jetty() + .withSocketAddressResolver(socketAddressResolver) + .build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses())); + assertThat(connector).extracting("httpClient.resolver") + .matches((resolver) -> resolver.getClass().getName().contains("JettyFiltered")); + assertThat(connector).extracting("httpClient.resolver.delegate").isSameAs(socketAddressResolver); + } + @Override protected long connectTimeout(JettyClientHttpConnector connector) { HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient"); @@ -87,9 +109,10 @@ protected long connectTimeout(JettyClientHttpConnector connector) { protected long readTimeout(JettyClientHttpConnector connector) { HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient"); assertThat(httpClient).isNotNull(); - Object field = ReflectionTestUtils.getField(httpClient, "readTimeout"); - assertThat(field).isNotNull(); - return ((Duration) field).toMillis(); + HttpClientSettings settings = (HttpClientSettings) ReflectionTestUtils.getField(httpClient, "settings"); + assertThat(settings).isNotNull(); + assertThat(settings.readTimeout()).isNotNull(); + return settings.readTimeout().toMillis(); } static class TestHttpClientTransport extends HttpClientTransportOverHTTP { diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilderTests.java index f35af8af49ab..e48bb4c3ecb3 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilderTests.java @@ -25,7 +25,11 @@ import io.netty.channel.ChannelOption; import org.junit.jupiter.api.Test; import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClientConfig; +import reactor.netty.transport.ClientTransport.ResolvedAddressSelector; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.InetAddressFilter; import org.springframework.boot.http.client.ReactorHttpClientBuilder; import org.springframework.http.client.ReactorResourceFactory; import org.springframework.http.client.reactive.ReactorClientHttpConnector; @@ -33,6 +37,7 @@ 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; /** @@ -98,6 +103,28 @@ void with() { assertThat(called).containsExactly(true); } + @Test + void withResolvedAddressSelector() { + ResolvedAddressSelector resolvedAddressSelector = mock(); + ReactorClientHttpConnector connector = ClientHttpConnectorBuilder.reactor() + .withResolvedAddressSelector(resolvedAddressSelector) + .build(); + assertThat(connector).extracting("httpClient.config.resolvedAddressesSelector") + .isEqualTo(resolvedAddressSelector); + } + + @Test + void withResolvedAddressSelectorWhenHasInetAddressFilter() { + ResolvedAddressSelector resolvedAddressSelector = mock(); + ReactorClientHttpConnector connector = ClientHttpConnectorBuilder.reactor() + .withResolvedAddressSelector(resolvedAddressSelector) + .build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses())); + assertThat(connector).extracting("httpClient.config.resolvedAddressesSelector") + .matches((selector) -> selector.getClass().getName().contains("ReactorFiltered")); + assertThat(connector).extracting("httpClient.config.resolvedAddressesSelector.delegate") + .isEqualTo(resolvedAddressSelector); + } + @Override protected long connectTimeout(ReactorClientHttpConnector connector) { HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient"); From 0318c4461f82babfed9de6c586096e015a1fccd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 14 Apr 2026 10:07:21 +0200 Subject: [PATCH 500/930] Upgrade to Micrometer Tracing 1.7.0-RC1 Closes gh-49991 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 04836885c5b1..5ec9fc11208d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1636,7 +1636,7 @@ bom { releaseNotes("https://github.com/micrometer-metrics/micrometer/releases/tag/v{version}") } } - library("Micrometer Tracing", "1.7.0-SNAPSHOT") { + library("Micrometer Tracing", "1.7.0-RC1") { considerSnapshots() group("io.micrometer") { bom("micrometer-tracing-bom") From c72f692f509ca55e60653bff490f5d766a675bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 14 Apr 2026 10:07:25 +0200 Subject: [PATCH 501/930] Upgrade to SQLite JDBC 3.53.0.0 Closes gh-50068 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 5ec9fc11208d..3ce6075b98b3 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2754,7 +2754,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ws/releases/tag/v{version}") } } - library("SQLite JDBC", "3.51.3.0") { + library("SQLite JDBC", "3.53.0.0") { group("org.xerial") { modules = [ "sqlite-jdbc" From 257d6ba14878f803c3a4a36893fffdc0b99bd760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 14 Apr 2026 13:04:36 +0200 Subject: [PATCH 502/930] Upgrade to Reactor Bom 2025.0.5 Closes gh-49992 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3ce6075b98b3..e486a7049b63 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2080,7 +2080,7 @@ bom { ] } } - library("Reactor Bom", "2025.0.5-SNAPSHOT") { + library("Reactor Bom", "2025.0.5") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From ad2878c68c95cef08f22e740541a26375cb4827d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 14 Apr 2026 14:34:54 +0200 Subject: [PATCH 503/930] Fix changelog link for Protobuf Maven Plugin Closes gh-50061 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e486a7049b63..942230a2a4c0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1849,7 +1849,7 @@ bom { } links { site("https://github.com/ascopes/protobuf-maven-plugin") - releaseNotes("https://github.com/ascopes/protobuf-maven-plugin/tag/v{version}") + releaseNotes("https://github.com/ascopes/protobuf-maven-plugin/releases/tag/v{version}") } } library("Pooled JMS", "3.2.2") { From 2ed90a938fb971681e74fcdbfd57ecb8dce72d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 15 Apr 2026 15:25:30 +0200 Subject: [PATCH 504/930] Upgrade to Neo4j Java Driver 6.0.5 Closes gh-50076 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 942230a2a4c0..6fb5890fa754 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1729,7 +1729,7 @@ bom { ] } } - library("Neo4j Java Driver", "6.0.4") { + library("Neo4j Java Driver", "6.0.5") { alignWith { version { of "org.neo4j.driver:neo4j-java-driver" From 2f73d15e353912368787d2e57cc14d1fea4a5c01 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 15 Apr 2026 14:52:14 +0100 Subject: [PATCH 505/930] Update distribution properties to support long task timers Previously, LongTaskTimers were ignored when configuring a distribution's min and max expected values and SLO boundaries. This commit adds support, aligning them with the existing support for Timers. Fixes gh-49190 --- .../metrics/autoconfigure/MeterValue.java | 6 ++--- .../autoconfigure/MetricsProperties.java | 15 ++++++----- .../autoconfigure/MeterValueTests.java | 19 ++++++++++++- .../PropertiesMeterFilterTests.java | 27 +++++++++++++++++++ 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterValue.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterValue.java index 440a38f5b859..5afa8f4d39cd 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterValue.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterValue.java @@ -27,8 +27,8 @@ /** * A meter value that is used when configuring micrometer. Can be a String representation - * of either a {@link Double} (applicable to timers and distribution summaries) or a - * {@link Duration} (applicable to only timers). + * of either a {@link Double} (applicable to timers, long task timers, and distribution + * summaries) or a {@link Duration} (applicable to timers and long task timers). * * @author Phillip Webb * @author Stephane Nicoll @@ -55,7 +55,7 @@ public final class MeterValue { if (meterType == Type.DISTRIBUTION_SUMMARY) { return getDistributionSummaryValue(); } - if (meterType == Type.TIMER) { + if (meterType == Type.TIMER || meterType == Type.LONG_TASK_TIMER) { Long timerValue = getTimerValue(); if (timerValue != null) { return timerValue.doubleValue(); diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsProperties.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsProperties.java index ecf700966590..e5b4017723fa 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsProperties.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsProperties.java @@ -203,22 +203,25 @@ public static class Distribution { /** * Specific service-level objective boundaries for meter IDs starting with the * specified name. The longest match wins. Counters will be published for each - * specified boundary. Values can be specified as a double or as a Duration value - * (for timer meters, defaulting to ms if no unit specified). + * specified boundary. Values can be specified as a double or, for timer and + * long-task timer meters, as a Duration value defaulting to ms if no unit + * specified. */ private final Map slo = new LinkedHashMap<>(); /** * Minimum value that meter IDs starting with the specified name are expected to - * observe. The longest match wins. Values can be specified as a double or as a - * Duration value (for timer meters, defaulting to ms if no unit specified). + * observe. The longest match wins. Values can be specified as a double or, for + * timer and long-task timer meters, as a Duration value defaulting to ms if no + * unit specified. */ private final Map minimumExpectedValue = new LinkedHashMap<>(); /** * Maximum value that meter IDs starting with the specified name are expected to - * observe. The longest match wins. Values can be specified as a double or as a - * Duration value (for timer meters, defaulting to ms if no unit specified). + * observe. The longest match wins. Values can be specified as a double or, for + * timer and long-task timer meters, as a Duration value defaulting to ms if no + * unit specified. */ private final Map maximumExpectedValue = new LinkedHashMap<>(); diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterValueTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterValueTests.java index c5d04c0ed489..b65b755577f8 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterValueTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterValueTests.java @@ -82,12 +82,29 @@ void getValueForTimerWhenFromDurationStringShouldReturnDurationNanos() { assertThat(meterValue.getValue(Type.TIMER)).isEqualTo(123000000); } + @Test + void getValueForLongTaskTimerWhenFromNumberShouldReturnMsToNanosValue() { + MeterValue meterValue = MeterValue.valueOf(123d); + assertThat(meterValue.getValue(Type.LONG_TASK_TIMER)).isEqualTo(123000000); + } + + @Test + void getValueForLongTaskTimerWhenFromNumberStringShouldReturnMsToNanosValue() { + MeterValue meterValue = MeterValue.valueOf("123"); + assertThat(meterValue.getValue(Type.LONG_TASK_TIMER)).isEqualTo(123000000); + } + + @Test + void getValueForLongTaskTimerWhenFromDurationStringShouldReturnDurationNanos() { + MeterValue meterValue = MeterValue.valueOf("123ms"); + assertThat(meterValue.getValue(Type.LONG_TASK_TIMER)).isEqualTo(123000000); + } + @Test void getValueForOthersShouldReturnNull() { MeterValue meterValue = MeterValue.valueOf("123"); assertThat(meterValue.getValue(Type.COUNTER)).isNull(); assertThat(meterValue.getValue(Type.GAUGE)).isNull(); - assertThat(meterValue.getValue(Type.LONG_TASK_TIMER)).isNull(); assertThat(meterValue.getValue(Type.OTHER)).isNull(); } diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/PropertiesMeterFilterTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/PropertiesMeterFilterTests.java index fcd71145e148..35bc86d84047 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/PropertiesMeterFilterTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/PropertiesMeterFilterTests.java @@ -213,6 +213,15 @@ void configureWhenTimerMeterHasSloShouldSetSloToValue() { .getServiceLevelObjectiveBoundaries()).containsExactly(1000000, 2000000, 3000000); } + @Test + void configureWhenLongTaskTimerMeterHasSloShouldSetSloToValue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.slo.spring.boot=1,2,3")); + assertThat(filter + .configure(createMeterId("spring.boot", Meter.Type.LONG_TASK_TIMER), DistributionStatisticConfig.DEFAULT) + .getServiceLevelObjectiveBoundaries()).containsExactly(1000000, 2000000, 3000000); + } + @Test void configureWhenDistributionSummaryMeterHasSloShouldSetSloToValue() { PropertiesMeterFilter filter = new PropertiesMeterFilter(createProperties("distribution.slo.example=1,2,3")); @@ -244,6 +253,15 @@ void configureWhenTimerMeterHasMinimumExpectedValueShouldSetMinimumExpectedToVal .getMinimumExpectedValueAsDouble()).isEqualTo(Duration.ofMillis(10).toNanos()); } + @Test + void configureWhenLongTaskTimerMeterHasMinimumExpectedValueShouldSetMinimumExpectedToValue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.minimum-expected-value.spring.boot=10")); + assertThat(filter + .configure(createMeterId("spring.boot", Meter.Type.LONG_TASK_TIMER), DistributionStatisticConfig.DEFAULT) + .getMinimumExpectedValueAsDouble()).isEqualTo(Duration.ofMillis(10).toNanos()); + } + @Test void configureWhenDistributionSummaryMeterHasMinimumExpectedValueShouldSetMinimumExpectedToValue() { PropertiesMeterFilter filter = new PropertiesMeterFilter( @@ -278,6 +296,15 @@ void configureWhenTimerMeterHasMaximumExpectedValueShouldSetMaximumExpectedToVal .getMaximumExpectedValueAsDouble()).isEqualTo(Duration.ofMillis(5000).toNanos()); } + @Test + void configureWhenLongTaskTimerMeterHasMaximumExpectedValueShouldSetMaximumExpectedToValue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.maximum-expected-value.spring.boot=5000")); + assertThat(filter + .configure(createMeterId("spring.boot", Meter.Type.LONG_TASK_TIMER), DistributionStatisticConfig.DEFAULT) + .getMaximumExpectedValueAsDouble()).isEqualTo(Duration.ofMillis(5000).toNanos()); + } + @Test void configureWhenDistributionSummaryMeterHasMaximumExpectedValueShouldSetMaximumExpectedToValue() { PropertiesMeterFilter filter = new PropertiesMeterFilter( From 9cad1a090620f719571d2d2519e12172c663b97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 16 Apr 2026 08:01:30 +0200 Subject: [PATCH 506/930] Upgrade to Flyway 12.4.0 Closes gh-50079 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6fb5890fa754..e3d2366e2bea 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -425,7 +425,7 @@ bom { javadoc("elasticsearch-java", version -> "https://javadoc.io/doc/co.elastic.clients/elasticsearch-java/%s/index.html".formatted(version), "co.elastic.clients.elasticsearch", "co.elastic.clients.transport") } } - library("Flyway", "12.3.0") { + library("Flyway", "12.4.0") { group("org.flywaydb") { modules = [ "flyway-command-test-connection", From f73e8b4707671ad677e33d80de47cd5152d510ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 16 Apr 2026 08:01:34 +0200 Subject: [PATCH 507/930] Upgrade to Protobuf Common Protos 2.70.0 Closes gh-50080 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e3d2366e2bea..cb3a3a020eac 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1918,7 +1918,7 @@ bom { releaseNotes("https://qpid.apache.org/releases/qpid-protonj2-{version}/release-notes.html") } } - library("Protobuf Common Protos", "2.67.0") { + library("Protobuf Common Protos", "2.70.0") { group("com.google.api.grpc") { modules = [ "proto-google-common-protos" From 48bbafff7658e50f22dd1f6e713ccfca7afd027c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 16 Apr 2026 08:01:39 +0200 Subject: [PATCH 508/930] Upgrade to Rabbit AMQP Client 5.30.0 Closes gh-50081 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index cb3a3a020eac..04e37e8c4d7a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2050,7 +2050,7 @@ bom { releaseNotes("https://github.com/r2dbc/r2dbc-spi/releases/tag/v{version}") } } - library("Rabbit AMQP Client", "5.29.0") { + library("Rabbit AMQP Client", "5.30.0") { group("com.rabbitmq") { modules = [ "amqp-client" From 17bdd6c3bdd0038ec890ad02d40314649292b5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 16 Apr 2026 08:01:43 +0200 Subject: [PATCH 509/930] Upgrade to Rabbit Stream Client 1.6.0 Closes gh-50082 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 04e37e8c4d7a..e908c6dfad88 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2062,7 +2062,7 @@ bom { releaseNotes("https://github.com/rabbitmq/rabbitmq-java-client/releases/tag/v{version}") } } - library("Rabbit Stream Client", "1.5.0") { + library("Rabbit Stream Client", "1.6.0") { group("com.rabbitmq") { modules = [ "stream-client" From 05d6159512617118a183e90787022829e2d0737b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 16 Apr 2026 09:39:53 +0100 Subject: [PATCH 510/930] Narrow icons pattern of StaticResourceLocation.FAVICON Closes gh-50084 --- .../security/autoconfigure/web/StaticResourceLocation.java | 4 ++-- .../web/reactive/StaticResourceRequestTests.java | 1 + .../autoconfigure/web/servlet/StaticResourceRequestTests.java | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/web/StaticResourceLocation.java b/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/web/StaticResourceLocation.java index b64e6e3e8418..2b7d36444623 100644 --- a/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/web/StaticResourceLocation.java +++ b/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/web/StaticResourceLocation.java @@ -49,9 +49,9 @@ public enum StaticResourceLocation { /** * The {@code "favicon.ico"} resource and resources matching - * "/*/icon-*". + * "/icons/icon-*". */ - FAVICON("/favicon.*", "/*/icon-*"), + FAVICON("/favicon.*", "/icons/icon-*"), /** * Resources under {@code "/fonts"}. diff --git a/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/web/reactive/StaticResourceRequestTests.java b/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/web/reactive/StaticResourceRequestTests.java index 012748e8de7d..3843f23b239f 100644 --- a/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/web/reactive/StaticResourceRequestTests.java +++ b/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/web/reactive/StaticResourceRequestTests.java @@ -60,6 +60,7 @@ void atCommonLocationsShouldMatchCommonLocations() { assertMatcher(matcher).matches("/icons/icon-48x48.png"); assertMatcher(matcher).matches("/fonts/font.woff2"); assertMatcher(matcher).doesNotMatch("/bar"); + assertMatcher(matcher).doesNotMatch("/elsewhere/icon-48x48.png"); } @Test diff --git a/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/web/servlet/StaticResourceRequestTests.java b/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/web/servlet/StaticResourceRequestTests.java index 384490b75c63..aa5c35333ba1 100644 --- a/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/web/servlet/StaticResourceRequestTests.java +++ b/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/web/servlet/StaticResourceRequestTests.java @@ -54,6 +54,7 @@ void atCommonLocationsShouldMatchCommonLocations() { assertMatcher(matcher).matches("/icons/icon-48x48.png"); assertMatcher(matcher).matches("/fonts/font.woff2"); assertMatcher(matcher).doesNotMatch("/bar"); + assertMatcher(matcher).doesNotMatch("/elsewhere/icon-48x48.png"); } @Test From 0d4b989225fdad8b0c76f85357f7fec13df77265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 17 Apr 2026 08:52:10 +0200 Subject: [PATCH 511/930] Upgrade to Elasticsearch Client 9.3.4 Closes gh-50099 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e908c6dfad88..0733ce935eae 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -401,7 +401,7 @@ bom { releaseNotes("https://github.com/ehcache/ehcache3/releases/tag/v{version}") } } - library("Elasticsearch Client", "9.3.3") { + library("Elasticsearch Client", "9.3.4") { prohibit { contains "-alpha" contains "-beta" From 4e5a539217e175b239299fe46555d79856cc3c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 18 Apr 2026 08:18:24 +0200 Subject: [PATCH 512/930] Upgrade to Jaxen 2.0.1 Closes gh-50106 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0733ce935eae..194fea332336 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1039,7 +1039,7 @@ bom { ] } } - library("Jaxen", "2.0.0") { + library("Jaxen", "2.0.1") { group("jaxen") { modules = [ "jaxen" From fb652548f52f4a27b2d0c790ea357c1699825d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 18 Apr 2026 08:18:30 +0200 Subject: [PATCH 513/930] Upgrade to jOOQ 3.21.2 Closes gh-50107 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 194fea332336..d7d58d568514 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1143,7 +1143,7 @@ bom { ] } } - library("jOOQ", "3.21.1") { + library("jOOQ", "3.21.2") { group("org.jooq") { bom("jooq-bom") plugins = [ From a97e738783be19b6c3663b8de378aee181a2b11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 18 Apr 2026 08:18:30 +0200 Subject: [PATCH 514/930] Upgrade to Spring Data Bom 2026.0.0-RC1 Closes gh-49994 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d7d58d568514..a15c0808603c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2536,7 +2536,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-batch/releases/tag/v{version}") } } - library("Spring Data Bom", "2026.0.0-SNAPSHOT") { + library("Spring Data Bom", "2026.0.0-RC1") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From a8fa58498d3c01fcdde71dc1f20ec4a79dc36e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 18 Apr 2026 08:18:30 +0200 Subject: [PATCH 515/930] Upgrade to Spring Framework 7.0.7 Closes gh-49995 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f005808091fd..15138efffe95 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ mockitoVersion=5.23.0 nativeBuildToolsVersion=1.0.0 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.6 -springFrameworkVersion=7.0.7-SNAPSHOT +springFrameworkVersion=7.0.7 springFramework60xVersion=6.0.23 tomcatVersion=11.0.21 protobufGradlePluginVersion=0.9.6 From 964a8bd1a97f4402327f3bdeb0579c00b784ec85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 18 Apr 2026 08:18:34 +0200 Subject: [PATCH 516/930] Upgrade to Spring HATEOAS 3.1.0-RC1 Closes gh-50108 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a15c0808603c..8acd68cc9967 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2601,7 +2601,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-grpc/releases/tag/v{version}") } } - library("Spring HATEOAS", "3.1.0-M1") { + library("Spring HATEOAS", "3.1.0-RC1") { considerSnapshots() group("org.springframework.hateoas") { modules = [ From 2db1f7557abf15d73693e45401d292a184730d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 18 Apr 2026 08:18:34 +0200 Subject: [PATCH 517/930] Upgrade to Spring LDAP 4.1.0-RC1 Closes gh-49999 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8acd68cc9967..d44b18263ffe 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2651,7 +2651,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-kafka/releases/tag/v{version}") } } - library("Spring LDAP", "4.1.0-SNAPSHOT") { + library("Spring LDAP", "4.1.0-RC1") { considerSnapshots() group("org.springframework.ldap") { modules = [ From 1b712e043444a7209a26349cf8124238488261e9 Mon Sep 17 00:00:00 2001 From: itsmevichu Date: Sat, 18 Apr 2026 15:53:10 +0530 Subject: [PATCH 518/930] Add support for docker.elastic.co/elasticsearch/elasticsearch See gh-50119 Signed-off-by: itsmevichu --- .../ConnectionNamePredicateTests.java | 21 ++++++++++--------- ...nectionDetailsFactoryIntegrationTests.java | 3 ++- ...DockerComposeConnectionDetailsFactory.java | 2 +- .../boot/testsupport/container/TestImage.java | 5 +++++ 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java b/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java index a506e48f7958..aee7d90122d3 100644 --- a/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java +++ b/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java @@ -39,16 +39,17 @@ class ConnectionNamePredicateTests { @Test void official() { - assertThat(predicateOf("elasticsearch")).accepts(sourceOf("elasticsearch")); - assertThat(predicateOf("elasticsearch")).accepts(sourceOf("library/elasticsearch")); - assertThat(predicateOf("elasticsearch")).accepts(sourceOf("docker.io/library/elasticsearch")); - assertThat(predicateOf("elasticsearch")).accepts(sourceOf("docker.io/elasticsearch")); - assertThat(predicateOf("elasticsearch")).accepts(sourceOf("docker.io/elasticsearch:latest")); - assertThat(predicateOf("elasticsearch")).rejects(sourceOf("redis")); - assertThat(predicateOf("elasticsearch")).rejects(sourceOf("library/redis")); - assertThat(predicateOf("elasticsearch")).rejects(sourceOf("docker.io/library/redis")); - assertThat(predicateOf("elasticsearch")).rejects(sourceOf("docker.io/redis")); - assertThat(predicateOf("elasticsearch")).rejects(sourceOf("docker.io/redis")); + assertThat(predicateOf("elasticsearch", "elasticsearch/elasticsearch")).accepts(sourceOf("elasticsearch")) + .accepts(sourceOf("library/elasticsearch")) + .accepts(sourceOf("docker.io/library/elasticsearch")) + .accepts(sourceOf("docker.io/library/elasticsearch")) + .accepts(sourceOf("docker.io/elasticsearch:latest")) + .accepts(sourceOf("docker.elastic.co/elasticsearch/elasticsearch:latest")) + .accepts(sourceOf("docker.elastic.co/elasticsearch/elasticsearch")) + .rejects(sourceOf("redis")) + .rejects(sourceOf("library/redis")) + .rejects(sourceOf("docker.io/library/redis")) + .rejects(sourceOf("docker.io/redis")); assertThat(predicateOf("zipkin")).rejects(sourceOf("openzipkin/zipkin")); } diff --git a/module/spring-boot-elasticsearch/src/dockerTest/java/org/springframework/boot/elasticsearch/docker/compose/ElasticsearchDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-elasticsearch/src/dockerTest/java/org/springframework/boot/elasticsearch/docker/compose/ElasticsearchDockerComposeConnectionDetailsFactoryIntegrationTests.java index 7fca9023e96c..4bf343fe8ed6 100644 --- a/module/spring-boot-elasticsearch/src/dockerTest/java/org/springframework/boot/elasticsearch/docker/compose/ElasticsearchDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-elasticsearch/src/dockerTest/java/org/springframework/boot/elasticsearch/docker/compose/ElasticsearchDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -41,7 +41,8 @@ void runCreatesConnectionDetails(ElasticsearchConnectionDetails connectionDetail assertThat(connectionDetails.getSslBundle()).isNull(); } - @DockerComposeTest(composeFile = "elasticsearch-ssl-compose.yaml", image = TestImage.ELASTICSEARCH_9, + @DockerComposeTest(composeFile = "elasticsearch-ssl-compose.yaml", + image = TestImage.ELASTICSEARCH_ELASTIC_REGISTRY_9, additionalResources = { "ca.crt", "server.crt", "server.key", "client.crt", "client.key" }) void runWithSslCreatesConnectionDetails(ElasticsearchConnectionDetails connectionDetails) { assertConnectionDetails(connectionDetails, Protocol.HTTPS); diff --git a/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/docker/compose/ElasticsearchDockerComposeConnectionDetailsFactory.java b/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/docker/compose/ElasticsearchDockerComposeConnectionDetailsFactory.java index 428719d2d5ba..19bd3be5fc32 100644 --- a/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/docker/compose/ElasticsearchDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-elasticsearch/src/main/java/org/springframework/boot/elasticsearch/docker/compose/ElasticsearchDockerComposeConnectionDetailsFactory.java @@ -42,7 +42,7 @@ class ElasticsearchDockerComposeConnectionDetailsFactory private static final int ELASTICSEARCH_PORT = 9200; protected ElasticsearchDockerComposeConnectionDetailsFactory() { - super("elasticsearch"); + super(new String[] { "elasticsearch", "elasticsearch/elasticsearch" }); } @Override diff --git a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java index 6d7d83ed93ff..6fc763b3e744 100644 --- a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java +++ b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java @@ -126,6 +126,11 @@ public enum TestImage { */ ELASTICSEARCH_9("elasticsearch", "9.0.2"), + /** + * A container image from Elastic Registry suitable for testing Elasticsearch 9. + */ + ELASTICSEARCH_ELASTIC_REGISTRY_9("docker.elastic.co/elasticsearch/elasticsearch", "9.0.2"), + /** * A container image suitable for testing Grafana OTel LGTM. */ From 02df21cc07b11f53086ffc16322760e4f6cbded2 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 20 Apr 2026 08:58:54 +0200 Subject: [PATCH 519/930] Polish "Add support for docker.elastic.co/elasticsearch/elasticsearch" See gh-50119 --- .../connection/ConnectionNamePredicateTests.java | 2 +- .../modules/reference/pages/features/dev-services.adoc | 2 +- ...omposeConnectionDetailsFactoryIntegrationTests.java | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java b/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java index aee7d90122d3..7450bb47f828 100644 --- a/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java +++ b/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java @@ -42,7 +42,7 @@ void official() { assertThat(predicateOf("elasticsearch", "elasticsearch/elasticsearch")).accepts(sourceOf("elasticsearch")) .accepts(sourceOf("library/elasticsearch")) .accepts(sourceOf("docker.io/library/elasticsearch")) - .accepts(sourceOf("docker.io/library/elasticsearch")) + .accepts(sourceOf("docker.io/elasticsearch")) .accepts(sourceOf("docker.io/elasticsearch:latest")) .accepts(sourceOf("docker.elastic.co/elasticsearch/elasticsearch:latest")) .accepts(sourceOf("docker.elastic.co/elasticsearch/elasticsearch")) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc index 6a031505e173..f18d9d9f9dec 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc @@ -93,7 +93,7 @@ The following service connections are currently supported: | Containers named "cassandra" | javadoc:org.springframework.boot.elasticsearch.autoconfigure.ElasticsearchConnectionDetails[] -| Containers named "elasticsearch" +| Containers named "elasticsearch" or "elasticsearch/elasticsearch" | javadoc:org.springframework.boot.hazelcast.autoconfigure.HazelcastConnectionDetails[] | Containers named "hazelcast/hazelcast". diff --git a/module/spring-boot-elasticsearch/src/dockerTest/java/org/springframework/boot/elasticsearch/docker/compose/ElasticsearchDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-elasticsearch/src/dockerTest/java/org/springframework/boot/elasticsearch/docker/compose/ElasticsearchDockerComposeConnectionDetailsFactoryIntegrationTests.java index 4bf343fe8ed6..e80614050240 100644 --- a/module/spring-boot-elasticsearch/src/dockerTest/java/org/springframework/boot/elasticsearch/docker/compose/ElasticsearchDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-elasticsearch/src/dockerTest/java/org/springframework/boot/elasticsearch/docker/compose/ElasticsearchDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -41,10 +41,18 @@ void runCreatesConnectionDetails(ElasticsearchConnectionDetails connectionDetail assertThat(connectionDetails.getSslBundle()).isNull(); } + @DockerComposeTest(composeFile = "elasticsearch-ssl-compose.yaml", image = TestImage.ELASTICSEARCH_9, + additionalResources = { "ca.crt", "server.crt", "server.key", "client.crt", "client.key" }) + void runWithSslCreatesConnectionDetails(ElasticsearchConnectionDetails connectionDetails) { + assertConnectionDetails(connectionDetails, Protocol.HTTPS); + SslBundle sslBundle = connectionDetails.getSslBundle(); + assertThat(sslBundle).isNotNull(); + } + @DockerComposeTest(composeFile = "elasticsearch-ssl-compose.yaml", image = TestImage.ELASTICSEARCH_ELASTIC_REGISTRY_9, additionalResources = { "ca.crt", "server.crt", "server.key", "client.crt", "client.key" }) - void runWithSslCreatesConnectionDetails(ElasticsearchConnectionDetails connectionDetails) { + void runWithSslCreatesConnectionDetailsForElasticRegistryImage(ElasticsearchConnectionDetails connectionDetails) { assertConnectionDetails(connectionDetails, Protocol.HTTPS); SslBundle sslBundle = connectionDetails.getSslBundle(); assertThat(sslBundle).isNotNull(); From 6121d25f0e4af4fa8948ef23fe62239ab7b78cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Apr 2026 20:36:55 +0200 Subject: [PATCH 520/930] Upgrade to Hibernate 7.2.12.Final Closes gh-50136 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d44b18263ffe..ec96c8e8175c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -639,7 +639,7 @@ bom { releaseNotes("https://github.com/hazelcast/hazelcast/releases/tag/v{version}") } } - library("Hibernate", "7.2.11.Final") { + library("Hibernate", "7.2.12.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From a907880930964a6c677f5883d13dc6e257312dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Apr 2026 20:36:59 +0200 Subject: [PATCH 521/930] Upgrade to HttpClient5 5.6.1 Closes gh-50137 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ec96c8e8175c..277e1e477f34 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -725,7 +725,7 @@ bom { ] } } - library("HttpClient5", "5.6") { + library("HttpClient5", "5.6.1") { prohibit { contains "-alpha" contains "-beta" From b08b754f0e75225a6b757bcef859ad6b552ce6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Apr 2026 20:37:04 +0200 Subject: [PATCH 522/930] Upgrade to Kotlin Serialization 1.11.0 Closes gh-50138 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 277e1e477f34..6c55373aa1de 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1307,7 +1307,7 @@ bom { releaseNotes("https://github.com/Kotlin/kotlinx.coroutines/releases/tag/{version}") } } - library("Kotlin Serialization", "1.10.0") { + library("Kotlin Serialization", "1.11.0") { group("org.jetbrains.kotlinx") { bom("kotlinx-serialization-bom") } From 483a3c2f72de2ad2985c7f8bf414742bbf601387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Apr 2026 20:37:04 +0200 Subject: [PATCH 523/930] Upgrade to Spring AMQP 4.1.0-RC1 Closes gh-49993 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6c55373aa1de..2e097a177ab0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2506,7 +2506,7 @@ bom { ] } } - library("Spring AMQP", "4.1.0-SNAPSHOT") { + library("Spring AMQP", "4.1.0-RC1") { considerSnapshots() group("org.springframework.amqp") { bom("spring-amqp-bom") From 8a62746790a87be7a230ac06d4c3b791b17e1324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Apr 2026 20:37:04 +0200 Subject: [PATCH 524/930] Upgrade to Spring Kafka 4.1.0-RC1 Closes gh-49998 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 2e097a177ab0..05c5454204ae 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2633,7 +2633,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-integration/releases/tag/v{version}") } } - library("Spring Kafka", "4.1.0-SNAPSHOT") { + library("Spring Kafka", "4.1.0-RC1") { considerSnapshots() group("org.springframework.kafka") { modules = [ From 03430f0a1bb40cbf921c02259863d77fca954b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Apr 2026 20:37:09 +0200 Subject: [PATCH 525/930] Upgrade to Testcontainers 2.0.5 Closes gh-50139 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 05c5454204ae..235a0af082b9 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2765,7 +2765,7 @@ bom { releaseNotes("https://github.com/xerial/sqlite-jdbc/releases/tag/{version}") } } - library("Testcontainers", "2.0.4") { + library("Testcontainers", "2.0.5") { group("org.testcontainers") { bom("testcontainers-bom") } From d562e90fc8954e42a49f3c6420ab2c119e0a8164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 21 Apr 2026 06:04:59 +0200 Subject: [PATCH 526/930] Upgrade to Spring Pulsar 2.0.5 Closes gh-50000 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 235a0af082b9..538b4dd44a85 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2671,7 +2671,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ldap/releases/tag/{version}") } } - library("Spring Pulsar", "2.0.5-SNAPSHOT") { + library("Spring Pulsar", "2.0.5") { considerSnapshots() group("org.springframework.pulsar") { bom("spring-pulsar-bom") From c1471eaaca016b1f9a52cdaa503cf8bd53112bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 21 Apr 2026 06:04:59 +0200 Subject: [PATCH 527/930] Upgrade to Spring Security 7.1.0-RC1 Closes gh-50001 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 538b4dd44a85..c8891ed08402 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2701,7 +2701,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-restdocs/releases/tag/v{version}") } } - library("Spring Security", "7.1.0-SNAPSHOT") { + library("Spring Security", "7.1.0-RC1") { considerSnapshots() group("org.springframework.security") { bom("spring-security-bom") From f169bf8a485c4a56f4e4ec59c42ec9bfcce90e98 Mon Sep 17 00:00:00 2001 From: bbbbooo Date: Wed, 18 Mar 2026 21:14:01 +0900 Subject: [PATCH 528/930] Monitor truststore certificates in SslMeterBinder See gh-49641 Signed-off-by: bbbbooo --- .../reference/pages/actuator/metrics.adoc | 5 +- .../autoconfigure/ssl/SslMeterBinder.java | 25 ++++-- .../ssl/SslMeterBinderTests.java | 88 ++++++++++++++++++- 3 files changed, 109 insertions(+), 9 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc index 0cec2d64890c..9b20630a8c64 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc @@ -815,7 +815,7 @@ To customize the tags, provide a javadoc:org.springframework.context.annotation. === SSL Bundle Metrics Spring Boot Actuator publishes expiry metrics about SSL bundles. -The metric `ssl.chain.expiry` gauges the expiry date of each certificate chain in seconds. +The metric `ssl.chain.expiry` gauges the expiry date of each certificate chain in key stores and trust stores in seconds. This number will be negative if the chain has already expired. This metric is tagged with the following information: @@ -830,6 +830,9 @@ This metric is tagged with the following information: | `chain` | The name of the certificate chain. + +| `source` +| Whether the certificate chain comes from the key store (`keystore`) or trust store (`truststore`) |=== diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinder.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinder.java index db3565bd17d4..9474cc8f5a0f 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinder.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinder.java @@ -52,6 +52,12 @@ class SslMeterBinder implements MeterBinder { private static final String CHAIN_EXPIRY_METRIC_NAME = "ssl.chain.expiry"; + private static final String SOURCE_TAG_NAME = "source"; + + private static final String KEY_STORE_SOURCE_TAG_VALUE = "keystore"; + + private static final String TRUST_STORE_SOURCE_TAG_VALUE = "truststore"; + private final Clock clock; private final SslInfo sslInfo; @@ -95,16 +101,23 @@ public void bindTo(MeterRegistry meterRegistry) { private void createOrUpdateBundleMetrics(MeterRegistry meterRegistry, BundleInfo bundle) { MultiGauge multiGauge = this.bundleMetrics.getGauge(bundle, meterRegistry); List> rows = new ArrayList<>(); - for (CertificateChainInfo chain : bundle.getCertificateChains()) { - Row row = createRowForChain(bundle, chain); + addRows(rows, bundle, bundle.getCertificateChains(), KEY_STORE_SOURCE_TAG_VALUE); + addRows(rows, bundle, bundle.getTrustStoreCertificateChains(), TRUST_STORE_SOURCE_TAG_VALUE); + multiGauge.register(rows, true); + } + + private void addRows(List> rows, BundleInfo bundle, List chains, + String source) { + for (CertificateChainInfo chain : chains) { + Row row = createRowForChain(bundle, chain, source); if (row != null) { rows.add(row); } } - multiGauge.register(rows, true); } - private @Nullable Row createRowForChain(BundleInfo bundle, CertificateChainInfo chain) { + private @Nullable Row createRowForChain(BundleInfo bundle, CertificateChainInfo chain, + String source) { CertificateInfo leastValidCertificate = chain.getCertificates() .stream() .filter((c) -> c.getValidityEnds() != null) @@ -114,8 +127,8 @@ private void createOrUpdateBundleMetrics(MeterRegistry meterRegistry, BundleInfo return null; } String serialNumber = leastValidCertificate.getSerialNumber(); - Tags tags = Tags.of("chain", chain.getAlias(), "bundle", bundle.getName(), "certificate", - (serialNumber != null) ? serialNumber : ""); + Tags tags = Tags.of("chain", chain.getAlias(), "bundle", bundle.getName(), SOURCE_TAG_NAME, source, + "certificate", (serialNumber != null) ? serialNumber : ""); return Row.of(tags, leastValidCertificate, this::getChainExpiry); } diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinderTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinderTests.java index a85e1f5bad01..e8c8151ea570 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinderTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinderTests.java @@ -64,6 +64,44 @@ void shouldRegisterChainExpiryMetrics() { .hasDays(36889); } + @Test + void shouldRegisterTrustStoreChainExpiryMetrics() { + DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); + sslBundleRegistry.registerBundle("test-0", + SslBundle.of(createTrustStoreBundle("classpath:certificates/chains.p12"))); + MeterRegistry meterRegistry = bindToRegistry(sslBundleRegistry); + assertThat(Duration + .ofSeconds(findExpiryGauge(meterRegistry, "truststore", "ca", "419224ce190242b2c44069dd3c560192b3b669f3"))) + .hasDays(1095); + assertThat(Duration.ofSeconds(findExpiryGauge(meterRegistry, "truststore", "intermediary", + "60f79365fc46bf69149754d377680192b3b6bcf5"))) + .hasDays(730); + assertThat(Duration.ofSeconds( + findExpiryGauge(meterRegistry, "truststore", "server", "504c45129526ac050abb11459b1f0192b3b70fe9"))) + .hasDays(365); + assertThat(Duration.ofSeconds( + findExpiryGauge(meterRegistry, "truststore", "expired", "562bc5dcf4f26bb179abb13068180192b3bb53dc"))) + .hasDays(-386); + assertThat(Duration.ofSeconds(findExpiryGauge(meterRegistry, "truststore", "not-yet-valid", + "7df79335f274e2cfa7467fd5f9ce0192b3bcf4aa"))) + .hasDays(36889); + } + + @Test + void shouldDifferentiateKeyStoreAndTrustStoreMetrics() { + DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); + sslBundleRegistry.registerBundle("test-0", + SslBundle.of(createKeyAndTrustStoreBundle("classpath:certificates/chains.p12", + "classpath:certificates/chains.p12"))); + MeterRegistry meterRegistry = bindToRegistry(sslBundleRegistry); + assertThat(Duration + .ofSeconds(findExpiryGauge(meterRegistry, "keystore", "ca", "419224ce190242b2c44069dd3c560192b3b669f3"))) + .hasDays(1095); + assertThat(Duration + .ofSeconds(findExpiryGauge(meterRegistry, "truststore", "ca", "419224ce190242b2c44069dd3c560192b3b669f3"))) + .hasDays(1095); + } + @Test void shouldWatchUpdatesForBundlesRegisteredAfterConstruction() { DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); @@ -90,6 +128,35 @@ void shouldWatchUpdatesForBundlesRegisteredAfterConstruction() { .hasDays(36889); } + @Test + void shouldWatchTrustStoreUpdatesForBundlesRegisteredAfterConstruction() { + DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); + sslBundleRegistry.registerBundle("dummy", + SslBundle.of(createTrustStoreBundle("classpath:certificates/chains2.p12"))); + MeterRegistry meterRegistry = bindToRegistry(sslBundleRegistry); + sslBundleRegistry.registerBundle("test-0", + SslBundle.of(createTrustStoreBundle("classpath:certificates/chains2.p12"))); + sslBundleRegistry.updateBundle("test-0", + SslBundle.of(createTrustStoreBundle("classpath:certificates/chains.p12"))); + assertThat(meterRegistry.find("ssl.chain.expiry").tags("bundle", "test-0", "source", "truststore").meters()) + .hasSize(5); + assertThat(Duration + .ofSeconds(findExpiryGauge(meterRegistry, "truststore", "ca", "419224ce190242b2c44069dd3c560192b3b669f3"))) + .hasDays(1095); + assertThat(Duration.ofSeconds(findExpiryGauge(meterRegistry, "truststore", "intermediary", + "60f79365fc46bf69149754d377680192b3b6bcf5"))) + .hasDays(730); + assertThat(Duration.ofSeconds( + findExpiryGauge(meterRegistry, "truststore", "server", "504c45129526ac050abb11459b1f0192b3b70fe9"))) + .hasDays(365); + assertThat(Duration.ofSeconds( + findExpiryGauge(meterRegistry, "truststore", "expired", "562bc5dcf4f26bb179abb13068180192b3bb53dc"))) + .hasDays(-386); + assertThat(Duration.ofSeconds(findExpiryGauge(meterRegistry, "truststore", "not-yet-valid", + "7df79335f274e2cfa7467fd5f9ce0192b3bcf4aa"))) + .hasDays(36889); + } + @Test void shouldRegisterMetricsIfNoBundleExistsAtBindTime() { DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); @@ -100,8 +167,14 @@ void shouldRegisterMetricsIfNoBundleExistsAtBindTime() { } private long findExpiryGauge(MeterRegistry meterRegistry, String chain, String certificateSerialNumber) { + return findExpiryGauge(meterRegistry, "keystore", chain, certificateSerialNumber); + } + + private long findExpiryGauge(MeterRegistry meterRegistry, String source, String chain, + String certificateSerialNumber) { return (long) meterRegistry.get("ssl.chain.expiry") .tag("bundle", "test-0") + .tag("source", source) .tag("chain", chain) .tag("certificate", certificateSerialNumber) .gauge() @@ -117,8 +190,19 @@ private SimpleMeterRegistry bindToRegistry(SslBundles sslBundles) { } private SslStoreBundle createSslStoreBundle(String location) { - JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation(location).withPassword("secret"); - return new JksSslStoreBundle(keyStoreDetails, null); + return new JksSslStoreBundle(createStoreDetails(location), null); + } + + private SslStoreBundle createTrustStoreBundle(String location) { + return new JksSslStoreBundle(null, createStoreDetails(location)); + } + + private SslStoreBundle createKeyAndTrustStoreBundle(String keyStoreLocation, String trustStoreLocation) { + return new JksSslStoreBundle(createStoreDetails(keyStoreLocation), createStoreDetails(trustStoreLocation)); + } + + private JksSslStoreDetails createStoreDetails(String location) { + return JksSslStoreDetails.forLocation(location).withPassword("secret"); } private DefaultSslBundleRegistry createSslBundleRegistry(String... locations) { From b21ade191c91d19f5fbec2d6eb1bbe1b9b8d7053 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 21 Apr 2026 12:33:44 +0200 Subject: [PATCH 529/930] Polish "Monitor truststore certificates in SslMeterBinder" See gh-49641 --- .../metrics/autoconfigure/ssl/SslMeterBinder.java | 6 +++--- .../metrics/autoconfigure/ssl/SslMeterBinderTests.java | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinder.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinder.java index 9474cc8f5a0f..97564f084f12 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinder.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinder.java @@ -31,7 +31,6 @@ import io.micrometer.core.instrument.MultiGauge; import io.micrometer.core.instrument.MultiGauge.Row; import io.micrometer.core.instrument.Tags; -import io.micrometer.core.instrument.TimeGauge; import io.micrometer.core.instrument.binder.MeterBinder; import org.jspecify.annotations.Nullable; @@ -44,7 +43,8 @@ /** * {@link MeterBinder} which registers the SSL chain expiry (soonest to expire certificate - * in the chain) as a {@link TimeGauge}. + * in the chain) for certificate chains in the bundle's key store and trust store as a + * {@link MultiGauge}. * * @author Moritz Halbritter */ @@ -199,7 +199,7 @@ Gauges withBundle(BundleInfo bundle) { private MultiGauge createGauge(MeterRegistry meterRegistry) { return MultiGauge.builder(CHAIN_EXPIRY_METRIC_NAME) .baseUnit("seconds") - .description("SSL chain expiry") + .description("SSL certificate chain expiry for key store and trust store chains") .register(meterRegistry); } diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinderTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinderTests.java index e8c8151ea570..53ebb7505621 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinderTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinderTests.java @@ -112,6 +112,8 @@ void shouldWatchUpdatesForBundlesRegisteredAfterConstruction() { SslBundle.of(createSslStoreBundle("classpath:certificates/chains2.p12"))); sslBundleRegistry.updateBundle("test-0", SslBundle.of(createSslStoreBundle("classpath:certificates/chains.p12"))); + assertThat(meterRegistry.find("ssl.chain.expiry").tags("bundle", "test-0", "source", "keystore").meters()) + .hasSize(5); assertThat(Duration.ofSeconds(findExpiryGauge(meterRegistry, "ca", "419224ce190242b2c44069dd3c560192b3b669f3"))) .hasDays(1095); assertThat(Duration From 6f53ac29361f4d8750e35c296ee1fc25d0f78485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 21 Apr 2026 20:44:55 +0200 Subject: [PATCH 530/930] Upgrade to Spring GraphQL 2.0.3 Closes gh-49996 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c8891ed08402..6e47ab54c68f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2566,7 +2566,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-framework/releases/tag/v{version}") } } - library("Spring GraphQL", "2.0.3-SNAPSHOT") { + library("Spring GraphQL", "2.0.3") { considerSnapshots() group("org.springframework.graphql") { modules = [ From 92b1e49d69ac85934fa43f8103b5bbce1fee6fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 21 Apr 2026 20:45:00 +0200 Subject: [PATCH 531/930] Upgrade to Spring Session 4.1.0-RC1 Closes gh-50144 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6e47ab54c68f..736c00b553f9 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2716,7 +2716,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-security/releases/tag/{version}") } } - library("Spring Session", "4.1.0-M1") { + library("Spring Session", "4.1.0-RC1") { considerSnapshots() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) From 752be34e1c1aa0a25f7f31b13107d44ea1a8c6f5 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 21 Apr 2026 17:56:57 -0700 Subject: [PATCH 532/930] Fix GrpcDisableCsrfHttpConfigurer logic and add metadata Fix the `GrpcDisableCsrfHttpConfigurer` logic so that the property `spring.grpc.server.security.csrf.enabled` is used correctly. Also add missing metadata. Fixes gh-50145 --- .../GrpcDisableCsrfHttpConfigurer.java | 4 ++-- ...itional-spring-configuration-metadata.json | 7 ++++++ .../GrpcDisableCsrfHttpConfigurerTests.java | 23 +++++++++++++++---- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java index 1b889389bd24..249d9a28fe04 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java @@ -50,7 +50,7 @@ class GrpcDisableCsrfHttpConfigurer extends AbstractHttpConfigurer type) { } private boolean isCsrfEnabled(ApplicationContext context) { - return context.getEnvironment().getProperty("spring.grpc.server.security.csrf.enabled", Boolean.class, true); + return context.getEnvironment().getProperty("spring.grpc.server.security.csrf.enabled", Boolean.class, false); } private void disable(CsrfConfigurer csrf) { diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json index a49d405d6c24..eee3a0253bb2 100644 --- a/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -24,6 +24,13 @@ "type": "java.lang.Boolean", "description": "Whether to enable Reflection on the gRPC server.", "defaultValue": true + }, + { + "name": "spring.grpc.server.security.csrf.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable CSRF protection.", + "defaultValue": false } + ] } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java index 7d60d3f1c3c5..921bc9b45e7d 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java @@ -112,12 +112,12 @@ void initWhenNoGrpcServletRegistrationBeanDoesNothing() { } @Test - void initWhenEnabledPropertyFalseDoesNothing() { + void initWhenEnabledPropertyTrueDoesNothing() { ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity(); AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); StaticApplicationContext applicationContext = addApplicationContext(http); - TestPropertyValues.of("spring.grpc.server.security.csrf.enabled=false").applyTo(applicationContext); + TestPropertyValues.of("spring.grpc.server.security.csrf.enabled=true").applyTo(applicationContext); addServiceDiscoverer(applicationContext); addGrpcServletRegistration(applicationContext); CsrfConfigurer csrf = addCsrf(http); @@ -126,12 +126,27 @@ void initWhenEnabledPropertyFalseDoesNothing() { } @Test - void initWhenEnabledPropertyTrueDisablesCsrf() { + void initWhenEnabledPropertyFalseDisablesCsrf() { + ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity(); + AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); + HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); + StaticApplicationContext applicationContext = addApplicationContext(http); + TestPropertyValues.of("spring.grpc.server.security.csrf.enabled=false").applyTo(applicationContext); + addServiceDiscoverer(applicationContext); + addGrpcServletRegistration(applicationContext); + CsrfConfigurer csrf = addCsrf(http); + this.configurer.init(http); + ArgumentCaptor matcher = ArgumentCaptor.captor(); + then(csrf).should().requireCsrfProtectionMatcher(matcher.capture()); + assertThat(matcher.getValue()).isSameAs(GrpcCsrfRequestMatcher.INSTANCE); + } + + @Test + void initWhenEnabledPropertyMissingDisablesCsrf() { ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity(); AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); StaticApplicationContext applicationContext = addApplicationContext(http); - TestPropertyValues.of("spring.grpc.server.security.csrf.enabled=true").applyTo(applicationContext); addServiceDiscoverer(applicationContext); addGrpcServletRegistration(applicationContext); CsrfConfigurer csrf = addCsrf(http); From 9bf698d56436368f39f61e9349c5671759561e3e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 21 Apr 2026 17:39:57 -0700 Subject: [PATCH 533/930] Add missing gRPC test starters Add `spring-boot-starter-grpc-client-test` and `spring-boot-starter-grpc-server-test` to align with the client and server modules. Also refine dependencies for common test usage patterns. `spring-boot-starter-grpc-server-test` has been removed. Closes gh-49690 --- .../spring-boot-dependencies/build.gradle | 3 +- settings.gradle | 3 +- .../build.gradle | 2 +- .../build.gradle | 4 +-- .../build.gradle | 4 +-- .../build.gradle | 2 +- .../build.gradle | 3 +- .../build.gradle | 30 +++++++++++++++++++ 8 files changed, 40 insertions(+), 11 deletions(-) rename starter/{spring-boot-starter-grpc-test => spring-boot-starter-grpc-client-test}/build.gradle (89%) create mode 100644 starter/spring-boot-starter-grpc-server-test/build.gradle diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 736c00b553f9..73cdab167d7b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2296,8 +2296,9 @@ bom { "spring-boot-starter-graphql", "spring-boot-starter-graphql-test", "spring-boot-starter-grpc-client", + "spring-boot-starter-grpc-client-test", "spring-boot-starter-grpc-server", - "spring-boot-starter-grpc-test", + "spring-boot-starter-grpc-server-test", "spring-boot-starter-groovy-templates", "spring-boot-starter-groovy-templates-test", "spring-boot-starter-gson", diff --git a/settings.gradle b/settings.gradle index a1521d6b2e59..57017885a6c9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -271,8 +271,9 @@ include "starter:spring-boot-starter-freemarker-test" include "starter:spring-boot-starter-graphql" include "starter:spring-boot-starter-graphql-test" include "starter:spring-boot-starter-grpc-client" +include "starter:spring-boot-starter-grpc-client-test" include "starter:spring-boot-starter-grpc-server" -include "starter:spring-boot-starter-grpc-test" +include "starter:spring-boot-starter-grpc-server-test" include "starter:spring-boot-starter-groovy-templates" include "starter:spring-boot-starter-groovy-templates-test" include "starter:spring-boot-starter-gson" diff --git a/smoke-test/spring-boot-smoke-test-grpc-client-test/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-client-test/build.gradle index 5c328e0c1e83..5df78a9f5f42 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-client-test/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-client-test/build.gradle @@ -25,7 +25,7 @@ description = "Spring Boot gRPC client and test smoke test" dependencies { implementation(project(":starter:spring-boot-starter-grpc-client")) - testImplementation(project(":starter:spring-boot-starter-grpc-test")) + testImplementation(project(":starter:spring-boot-starter-grpc-client-test")) } def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom") diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle index 7584bb955041..c8bfb5ef95a9 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle @@ -27,10 +27,8 @@ dependencies { implementation(project(":starter:spring-boot-starter-oauth2-resource-server")) implementation(project(":starter:spring-boot-starter-grpc-server")) - testImplementation(project(":starter:spring-boot-starter-grpc-client")) - testImplementation(project(":starter:spring-boot-starter-grpc-test")) + testImplementation(project(":starter:spring-boot-starter-grpc-server-test")) testImplementation(project(":starter:spring-boot-starter-oauth2-client")) - testImplementation(project(":starter:spring-boot-starter-test")) } def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom") diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle index 1249a7fece36..93ab9e7af052 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle @@ -26,9 +26,7 @@ dependencies { implementation(project(":starter:spring-boot-starter-grpc-server")) implementation(project(":starter:spring-boot-starter-security")) - testImplementation(project(":starter:spring-boot-starter-grpc-client")) - testImplementation(project(":starter:spring-boot-starter-grpc-test")) - testImplementation(project(":starter:spring-boot-starter-test")) + testImplementation(project(":starter:spring-boot-starter-grpc-server-test")) } def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom") diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-test/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-test/build.gradle index 9f4f475ce295..e068f9019acc 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-test/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-server-test/build.gradle @@ -25,7 +25,7 @@ description = "Spring Boot gRPC server and test smoke test" dependencies { implementation(project(":starter:spring-boot-starter-grpc-server")) - testImplementation(project(":starter:spring-boot-starter-grpc-test")) + testImplementation(project(":starter:spring-boot-starter-grpc-server-test")) } def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom") diff --git a/starter/spring-boot-starter-grpc-test/build.gradle b/starter/spring-boot-starter-grpc-client-test/build.gradle similarity index 89% rename from starter/spring-boot-starter-grpc-test/build.gradle rename to starter/spring-boot-starter-grpc-client-test/build.gradle index d0f2bf871d4c..c928576a2314 100644 --- a/starter/spring-boot-starter-grpc-test/build.gradle +++ b/starter/spring-boot-starter-grpc-client-test/build.gradle @@ -18,11 +18,12 @@ plugins { id "org.springframework.boot.starter" } -description = "Starter for testing gRPC" +description = "Starter for testing gRPC client" dependencies { api(project(":starter:spring-boot-starter-test")) + api(project(":module:spring-boot-grpc-server")) api(project(":module:spring-boot-grpc-test")) api("io.grpc:grpc-inprocess") api("io.grpc:grpc-stub") diff --git a/starter/spring-boot-starter-grpc-server-test/build.gradle b/starter/spring-boot-starter-grpc-server-test/build.gradle new file mode 100644 index 000000000000..ea5d616f8ae3 --- /dev/null +++ b/starter/spring-boot-starter-grpc-server-test/build.gradle @@ -0,0 +1,30 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for testing gRPC server" + +dependencies { + api(project(":starter:spring-boot-starter-test")) + + api(project(":module:spring-boot-grpc-client")) + api(project(":module:spring-boot-grpc-test")) + api("io.grpc:grpc-inprocess") + api("io.grpc:grpc-stub") +} From e62a0409c97763d8dfad572ad028936fcf000e3c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 21 Apr 2026 18:30:40 -0700 Subject: [PATCH 534/930] Add `@GrpcAdvice` exception handling support Closes gh-49053 --- .../GrpcServerAutoConfiguration.java | 33 +++++++++++++- .../GrpcServerAutoConfigurationTests.java | 45 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java index c4b872c2a10f..a22d6294c3a6 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java @@ -38,6 +38,10 @@ import org.springframework.grpc.server.GlobalServerInterceptor; import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.grpc.server.advice.GrpcAdvice; +import org.springframework.grpc.server.advice.GrpcAdviceDiscoverer; +import org.springframework.grpc.server.advice.GrpcAdviceExceptionHandler; +import org.springframework.grpc.server.advice.GrpcExceptionHandlerMethodResolver; import org.springframework.grpc.server.exception.CompositeGrpcExceptionHandler; import org.springframework.grpc.server.exception.GrpcExceptionHandler; import org.springframework.grpc.server.exception.GrpcExceptionHandlerInterceptor; @@ -91,7 +95,8 @@ DefaultGrpcServiceDiscoverer grpcServiceDiscoverer(ApplicationContext applicatio @Bean @GlobalServerInterceptor @ConditionalOnMissingBean - GrpcExceptionHandlerInterceptor globalExceptionHandlerInterceptor(List exceptionHandlers) { + GrpcExceptionHandlerInterceptor grpcGlobalExceptionHandlerInterceptor( + List exceptionHandlers) { CompositeGrpcExceptionHandler compositeHandler = new CompositeGrpcExceptionHandler( exceptionHandlers.toArray(GrpcExceptionHandler[]::new)); return new GrpcExceptionHandlerInterceptor(compositeHandler); @@ -104,4 +109,30 @@ static class ReactiveStubConfiguration { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(annotation = GrpcAdvice.class) + static class GrpcAdviceConfiguration { + + @Bean + @ConditionalOnMissingBean + GrpcAdviceDiscoverer grpcAdviceDiscoverer(ApplicationContext applicationContext) { + return new GrpcAdviceDiscoverer(applicationContext); + } + + @Bean + @ConditionalOnMissingBean + GrpcExceptionHandlerMethodResolver grpcExceptionHandlerMethodResolver( + GrpcAdviceDiscoverer grpcAdviceDiscoverer) { + return new GrpcExceptionHandlerMethodResolver(grpcAdviceDiscoverer); + } + + @Bean + @ConditionalOnMissingBean + GrpcAdviceExceptionHandler grpcAdviceExceptionHandler( + GrpcExceptionHandlerMethodResolver grpcExceptionHandlerMethodResolver) { + return new GrpcAdviceExceptionHandler(grpcExceptionHandlerMethodResolver); + } + + } + } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java index 4c83859b1dff..f0721b1e2fe3 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java @@ -21,6 +21,8 @@ import io.grpc.BindableService; import io.grpc.ServerServiceDefinition; import io.grpc.ServiceDescriptor; +import io.grpc.Status; +import io.grpc.StatusException; import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.internal.GrpcUtil; import io.grpc.netty.NettyServerBuilder; @@ -36,6 +38,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.grpc.server.GrpcServletRegistration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration.GrpcAdviceConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider; import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner; @@ -51,6 +54,11 @@ import org.springframework.grpc.server.NettyGrpcServerFactory; import org.springframework.grpc.server.ServerBuilderCustomizer; import org.springframework.grpc.server.ShadedNettyGrpcServerFactory; +import org.springframework.grpc.server.advice.GrpcAdvice; +import org.springframework.grpc.server.advice.GrpcAdviceDiscoverer; +import org.springframework.grpc.server.advice.GrpcAdviceExceptionHandler; +import org.springframework.grpc.server.advice.GrpcExceptionHandler; +import org.springframework.grpc.server.advice.GrpcExceptionHandlerMethodResolver; import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer; import org.springframework.grpc.server.service.GrpcServiceDiscoverer; @@ -469,6 +477,43 @@ void whenHttp2EnabledPropertyMissingAndValidationDisabled() { } + @Nested + class GrpcAdviceAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = GrpcServerAutoConfigurationTests.this.contextRunner; + + @Test + void whenNoAdviceDoesNotCreateBeans() { + this.contextRunner.run((context) -> { + assertThat(context).doesNotHaveBean(GrpcAdviceConfiguration.class); + assertThat(context).doesNotHaveBean(GrpcAdviceDiscoverer.class); + assertThat(context).doesNotHaveBean(GrpcExceptionHandlerMethodResolver.class); + assertThat(context).doesNotHaveBean(GrpcAdviceExceptionHandler.class); + }); + } + + @Test + void whenHasAdviceCreatesBeans() { + this.contextRunner.withBean(GrpAdviceComponent.class).run((context) -> { + assertThat(context).hasSingleBean(GrpcAdviceConfiguration.class); + assertThat(context).hasSingleBean(GrpcAdviceDiscoverer.class); + assertThat(context).hasSingleBean(GrpcExceptionHandlerMethodResolver.class); + assertThat(context).hasSingleBean(GrpcAdviceExceptionHandler.class); + }); + } + + @GrpcAdvice + static class GrpAdviceComponent { + + @GrpcExceptionHandler + StatusException onIllegalStateException(IllegalStateException ex) { + return Status.FAILED_PRECONDITION.withDescription(ex.getMessage()).withCause(ex).asException(); + } + + } + + } + @Configuration(proxyBeanMethods = false) static class ServerBuilderCustomizersConfig { From 3bafd4994e2fb890f6984ab4ae55c43c29426931 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 21 Apr 2026 17:57:15 -0700 Subject: [PATCH 535/930] Document gRPC support Closes gh-49291 --- .../antora/AntoraAsciidocAttributes.java | 3 + .../antora-asciidoc-attributes.properties | 4 + .../antora/AntoraAsciidocAttributesTests.java | 1 + documentation/spring-boot-docs/build.gradle | 2 + .../modules/reference/pages/io/grpc.adoc | 743 ++++++++++++++++++ .../reference/partials/nav-reference.adoc | 1 + .../pages/first-application/index.adoc | 4 +- .../docs/io/grpc/client/HelloWorldGrpc.java | 25 + .../docs/io/grpc/client/MyApplication.java | 31 + .../MyGrpcConfiguration.java | 33 + .../io/grpc/client/stubbeans/HelloReply.java | 35 + .../grpc/client/stubbeans/HelloRequest.java | 35 + .../grpc/client/stubbeans/HelloWorldGrpc.java | 27 + .../client/stubbeans/MyApplicationRunner.java | 40 + .../boot/docs/io/grpc/server/HelloReply.java | 35 + .../docs/io/grpc/server/HelloRequest.java | 35 + .../docs/io/grpc/server/HelloWorldGrpc.java | 29 + .../io/grpc/server/MyHelloWorldService.java | 34 + .../servlet/MySecurityConfiguration.java | 38 + .../testing/localserverport/HelloReply.java | 35 + .../testing/localserverport/HelloRequest.java | 35 + .../localserverport/HelloWorldGrpc.java | 33 + .../MyGrpcIntegrationTests.java | 49 ++ .../testing/testtransport/HelloReply.java | 35 + .../testing/testtransport/HelloRequest.java | 35 + .../testing/testtransport/HelloWorldGrpc.java | 27 + .../testing/testtransport/MyGrpcTests.java | 43 + .../docs/io/grpc/client/HelloWorldGrpc.kt | 9 + .../boot/docs/io/grpc/client/MyApplication.kt | 14 + .../MyGrpcConfiguration.kt | 20 + .../io/grpc/client/stubbeans/HelloReply.kt | 23 + .../io/grpc/client/stubbeans/HelloRequest.kt | 23 + .../grpc/client/stubbeans/HelloWorldGrpc.kt | 11 + .../client/stubbeans/MyApplicationRunner.kt | 15 + .../boot/docs/io/grpc/server/HelloReply.kt | 23 + .../boot/docs/io/grpc/server/HelloRequest.kt | 23 + .../docs/io/grpc/server/HelloWorldGrpc.kt | 14 + .../io/grpc/server/MyHelloWorldService.kt | 16 + .../servlet/MySecurityConfiguration.kt | 21 + .../testing/localserverport/HelloReply.kt | 23 + .../testing/localserverport/HelloRequest.kt | 23 + .../testing/localserverport/HelloWorldGrpc.kt | 19 + .../localserverport/MyGrpcIntegrationTests.kt | 28 + .../grpc/testing/testtransport/HelloReply.kt | 23 + .../testing/testtransport/HelloRequest.kt | 23 + .../testing/testtransport/HelloWorldGrpc.kt | 19 + .../grpc/testing/testtransport/MyGrpcTests.kt | 23 + .../spring-boot-dependencies/build.gradle | 2 +- .../build.gradle | 2 +- .../build.gradle | 2 +- .../grpcserver/HelloWorldService.java | 4 +- 51 files changed, 1843 insertions(+), 7 deletions(-) create mode 100644 documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/HelloWorldGrpc.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/MyApplication.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/channelcustomization/MyGrpcConfiguration.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloReply.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloRequest.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloWorldGrpc.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/MyApplicationRunner.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/HelloReply.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/HelloRequest.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/HelloWorldGrpc.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/MyHelloWorldService.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/security/servlet/MySecurityConfiguration.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloReply.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloRequest.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloWorldGrpc.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/MyGrpcIntegrationTests.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloReply.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloRequest.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloWorldGrpc.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/MyGrpcTests.java create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/HelloWorldGrpc.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/MyApplication.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/channelcustomization/MyGrpcConfiguration.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloReply.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloRequest.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloWorldGrpc.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/MyApplicationRunner.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/HelloReply.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/HelloRequest.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/HelloWorldGrpc.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/MyHelloWorldService.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/security/servlet/MySecurityConfiguration.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloReply.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloRequest.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloWorldGrpc.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/MyGrpcIntegrationTests.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloReply.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloRequest.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloWorldGrpc.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/MyGrpcTests.kt diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraAsciidocAttributes.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraAsciidocAttributes.java index 39ee2273c9d6..9b1a6e59c4ce 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraAsciidocAttributes.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraAsciidocAttributes.java @@ -161,6 +161,9 @@ private void addVersionAttributes(Map attributes, Map mockDependencyVersions(String version) { addMockJacksonCoreVersion(versions, "jackson-core", version); addMockJacksonCoreVersion(versions, "jackson-databind", version); addMockJacksonCoreVersion(versions, "jackson-databind", version); + versions.put("io.grpc:grpc-api", version); versions.put("org.apache.pulsar:pulsar-client-api", version); versions.put("tools.jackson.dataformat:jackson-dataformat-xml", version); return versions; diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index 2d739b45907c..cf6ab55c3022 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -149,6 +149,8 @@ dependencies { implementation("ch.qos.logback:logback-classic") implementation("com.redis:testcontainers-redis") implementation("com.zaxxer:HikariCP") + implementation("io.grpc:grpc-stub") + implementation("io.grpc:grpc-netty") implementation("io.micrometer:micrometer-jakarta9") implementation("io.micrometer:micrometer-tracing") implementation("io.micrometer:micrometer-registry-graphite") diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc new file mode 100644 index 000000000000..1289ead94743 --- /dev/null +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc @@ -0,0 +1,743 @@ +[[io.grpc]] += gRPC + +Google Remote Procedure Call (gRPC) is a high-performance RPC framework that enables client-server communication using binary messages. +Spring Boot include support for developing and testing both client and server gRPC applications. + +The underling message format used by gRPC is Protocol Buffers which allow messages to be created and consumed by a wide variety of programming languages. + + + +[[io.grpc.servicedefinitions]] +== Service Definitions + +To develop a gRPC application you first need a Protocol Buffers service definition file. +A `.proto` file defines the services and messages that your application can consume or provide. + +Here's an example of a typical `.proto` file that uses https://protobuf.dev/programming-guides/proto3/[the `proto3` revision] of the protocol buffers language: + +[,protobuf] +---- +syntax = "proto3"; + +option java_package = "com.example.grpc.proto"; +option java_multiple_files = true; + +service HelloWorld { + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +---- + +This file defines a `HelloWorld` service with a single method that accepts a `HelloReqest` message and return a `HelloReply` message. +The `HelloReqest` message contains a `name` string field. +The `HelloReply` message contains a `message` string field. + +With the exception of a the `java_package` and `java_multiple_files` options, there is nothing in the `.proto` file that is specific to the Java programming langage. + + + +[[io.grpc.servicedefinitions.generatingjavacode]] +=== Generating Java Code + +Since `.proto` files are language agnostic, we need a process to convert them into usable Java code. +We can then use the generated code to either make a remote procedure call to running service, or implement the service ourselves so that others may call it. + +The exact process you use to generate code will depend on your build system. +Spring Boot supports for both Maven and Gradle protobuf plugins, but you are free to use whatever solution works best for you. + + + +[[io.grpc.servicedefinitions.generatingjavacode.maven]] +==== Using the Maven Plugin + +Spring Boot include dependency management for the `io.github.ascopes:protobuf-maven-plugin` Maven plugin. +If you are using the the `spring-boot-starter-parent` POM, you'll also get sensible out-of-the-box configuration. + +The following shows a typical Maven POM file that uses the plugin: + +[source,xml,subs="verbatim,attributes"] +---- + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + {version-spring-boot} + + com.example + myproject + 0.0.1-SNAPSHOT + + + + + io.github.ascopes + protobuf-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + + +ifeval::["{build-and-artifact-release-type}" == "opensource-snapshot"] + + + + spring-snapshots + https://repo.spring.io/snapshot + + true + + + + + + spring-snapshots + https://repo.spring.io/snapshot + + true + + + +endif::[] + +---- + +Since POM above extends `spring-boot-starter-parent`, you'll get the following: + +* Configuration of the `protoc` version. +* Configuration of the `binary-maven` plugin. +* Execution configuration for the `generate` goal. + +The `.proto` files should be added to `src/main/proto`. + +TIP: If you don't use `spring-boot-starter-parent`, or you want to configure the plugin directly, refer to the https://ascopes.github.io/protobuf-maven-plugin[protobuf-maven-plugin documentation]. +If you use Spring Boot's dependency management the `${protobuf-java.version}` and `${grpc-java.version}` properties will be useful. + + + +[[io.grpc.servicedefinitions.generatingjavacode.gradle]] +==== Using the Gradle Plugin + +Spring Boot include dependency management for the `com.google.protobuf:protobuf-gradle-plugin` Gradle plugin. +In addition, the `spring-boot-gradle-plugin` will react to the presence of the protobuf plugin and configure it appropriately. + +The following shows a typical Gradle file that uses the plugin: + +[source,gradle,subs="verbatim,attributes"] +---- +plugins { + id 'java' + id 'org.springframework.boot' version '{version-spring-boot}' + id 'io.spring.dependency-management' version '{version-dependency-management-plugin}' + id 'com.google.protobuf' version '{version-protobuf-gradle-plugin}' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + mavenCentral() +ifeval::["{build-and-artifact-release-type}" == "opensource-snapshot"] + // you don't need this if you are using a release version + maven { url 'https://repo.spring.io/snapshot' } +endif::[] +} +---- + +Since this gradle file uses both the `org.springframework.boot` and `com.google.protobuf` plugins, you'll get the following: + +* Configuration of the `protoc` version. +* Configuration of the `protoc-gen-grpc-java` version. + +The `.proto` files should be added to `src/main/proto`. + +TIP: If you don't use `org.springframework.boot` plugin, or you want to configure the plugin directly, refer to the https://github.com/google/protobuf-gradle-plugin[protobuf-gradle-plugin documentation]. + + + +[[io.grpc.server]] +== Writing a gRPC Server Application + +Spring Boot provides a `spring-boot-grpc-server` module and a `spring-boot-starter-grpc-server` starter POM that you can use for server applications. + +In order to write the actual server code, you'll need to extended one or more of base classes generated from your `.proto` file and expose them as Spring beans. +Spring gRPC will automatically expose any bean that implements javadoc:io.grpc.BindableService[] as a gRPC server. +Since all `.proto` generated classes implement javadoc:io.grpc.BindableService[], adding them as beans is enough to expose them over gRPC. + +TIP: For more details see {url-spring-grpc-docs}/server.html#_create_a_grpc_service[the Spring gRPC documentation]. + +The following example shows how the `HelloWorld` service from the `.proto` file above could be implemented. +In this example, we're using the javadoc:org.springframework.grpc.server.service.GrpcService[format=annotation] annotation and assuming that the code is in a package that will be picked up by component scanning: + +include-code::MyHelloWorldService[] + +If the application makes used of `spring-boot-starter-grpc-server`, then Netty will be used as the server implementation listening on port `9090`. + +You can test your application using https://github.com/fullstorydev/grpcurl[grpcurl]: + +[source,shell] +---- +$ grpcurl -d '{"name":"Spring"}' -plaintext localhost:9090 HelloWorld.SayHello +---- + +[source,json] +---- +{ + "message": "Hello 'Spring'" +} +---- + + + +[[io.grpc.server.netty-shaded]] +=== Switching to a Netty Shaded Server + +If you find that the version of Netty provided by the `spring-boot-starter-grpc-server` starter POM isn't compatible with other libraries you use, you can switch to a "`shaded`" version. + +To switch, you can excluded `io.grpc:grpc-netty` and include `io.grpc:grpc-netty-shaded`. +For example: + + +[tabs] +====== + +Maven:: ++ +[source,xml] +---- + + org.springframework.boot + spring-boot-starter-grpc-server + + + + io.grpc + grpc-netty + + + + + + io.grpc + grpc-netty-shaded + +---- ++ + +Gradle:: ++ +[source,gradle] +---- +dependencies { + implementation('org.springframework.boot:spring-boot-starter-grpc-server') { + // Exclude the gRPC Netty dependency + exclude group: 'io.grpc', module: 'grpc-netty' + } + // Use gRPC Netty Shaded instead + implementation "io.grpc:grpc-netty-shaded" +} +---- ++ +====== + + + +[[io.grpc.server.servlet]] +=== Switching to a Servlet Container + +It's possible to expose gRPC services using a regular Servlet Container such as Tomcat rather than using Netty. +To do so, your Servlet Container must be configured to support HTTP/2. + +To switch to the Servlet gRPC implementation, you can exclude `io.grpc:grpc-netty` and include `io.grpc:grpc-servlet-jakarta`. +For example: + +[tabs] +====== + +Maven:: ++ +[source,xml] +---- + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-grpc-server + + + + io.grpc + grpc-netty + + + + + + io.grpc + grpc-servlet-jakarta + +---- ++ + +Gradle:: ++ +[source,gradle] +---- +dependencies { + implementation('org.springframework.boot:spring-boot-starter-webmvc') { + implementation('org.springframework.boot:spring-boot-starter-grpc-server') { + // Exclude the gRPC Netty dependency + exclude group: 'io.grpc', module: 'grpc-netty' + } + // Use gRPC Servlet Jakarta instead + implementation "io.grpc:grpc-servlet-jakarta" +} +---- ++ +====== + +TIP: Remember to include a Servlet Container dependency, for example using `spring-boot-starter-tomcat`, and to set `server.http2.enabled` to `true`. + +NOTE: When using a servlet container, certain gRPC server configuration properties are not relevant and will be ignored. +For example, configprop:spring.grpc.server.port[] is ignored since configprop:server.port[] used used to set a web server port. + + + +[[io.grpc.server.ssl]] +=== SSL Support + +SSL can be configured for both `grpc-netty` and `grpc-netty-shaded` servers using SSL bundles. +See the xref:features/ssl.adoc[SSL core documentation] for details on how to declare an SSL bundle. + +Once your bundle has been defined, you can use the following properties in your gRPC server application to use it: + +[configprops,yaml] +---- +spring: + grpc: + server: + ssl: + bundle: mysslbundle +---- + +Client authentication can also be configured by setting configprop:spring.grpc.server.ssl.client-auth[] to `optional` or `require`. + +TIP: To temporarily disable server SSL support, for example to aid with testing, you can set configprop:spring.grpc.server.ssl.enabled[] to `false`. + + + +[[io.grpc.server.in-process]] +=== Using an In-Process Server + +You can run an in-process server by including the `io.grpc:grpc-inprocess` dependency on your classpath and defining a configprop:spring.grpc.server.inprocess.name[] property. +In this mode, the in-process server factory is auto-configured in addition to the regular server factory. + +The name you provide can be used as a client channel target using the form `in-process:`. + + + +[[io.grpc.server.reflection]] +=== Reflection + +When it's available, Spring Boot will auto-configure the https://grpc.io/docs/guides/reflection/[gRPC Reflection service]. +This allows clients to browse the metadata of your services and download their `.proto` files. + +The reflection service resides in the `io.grpc:grpc-services` library, which is an optional dependency. +You will need to add the dependency to your project in order for auto-configuration to apply. + +TIP: If you have the `io.grpc:grpc-services` library but prefer that reflection isn't auto-configured, you can set configprop:spring.grpc.server.reflection.enabled[] to `false`. + + + +[[io.grpc.server.health]] +=== Server Health + +A gRPC server can provide health information using a standard service API (https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto[health/v1]). +This allows clients to check on the health of your server services a route traffic appropriately. + +Spring Boot provides a bridge between its own `spring-boot-health` module and the standard gRPC health service. +Health information is provided whenever the `io.grpc:grpc-services` and `org.springframework.boot:spring-boot-health` modules are on your classpath. + +TIP: If you don't want health indicators to be exposed, you can set configprop:spring.grpc.server.health.enabled[] to `false`. + + + +[[io.grpc.server.health.service-mappings]] +==== Service Specific Health Mappings + +By default, health information is provided for the overall server status (`""`) using all available health indicators. + +It is also possible to provide fine-grained health information for specific services by including only a sub-set of health indicators. +Custom mapping and ordering rules can also be defined on a per-service basis. + +For example, the following configuration will provide health for "`myservice`" using only the `db` and `redis` indicators. + +[configprops,yaml] +---- +spring: + grpc: + server: + health: + service: + myservice: + include: + - db + - redis +---- + +TIP: You can set configprop:spring.grpc.server.health.include-overall-health[] to `false` to disable the overall server status health if you only want to provide service-specific health. + + + +[[io.grpc.server.health.push]] +==== Push Configuration + +Unlike web-based health checks, gRPC health information is periodically pushed rather than pulled. +By default, the first health push happens 5 seconds after the application starts and then every subsequent 5 seconds. + +To fine-tune this, you can use the following properties: + +[configprops,yaml] +---- +spring: + grpc: + server: + health: + schedule: + period: 5m + delay: 2s +---- + +TIP: You can also set configprop:spring.grpc.server.health.schedule.enabled[] to `false` if want to send health updates in some other way. + + + +[[io.grpc.server.security]] +=== Securing gRPC Server Applications + + + +[[io.grpc.server.security.netty]] +==== Netty Based Servers + +Spring gRPC includes features that allow you to secure your Netty based server applications declaratively using Spring Security. +This follows similar patterns to those you would use to secure a regular web application. + +Spring Boot provides auto-configuration for both javadoc:org.springframework.grpc.server.security.GrpcSecurity[] and javadoc:org.springframework.grpc.server.security.SecurityGrpcExceptionHandler[] beans. +Typically gRPC application are then secured using javadoc:org.springframework.security.access.prepost.PreAuthorize[format=annotation] annotations on your gRPC service beans, or a javadoc:org.springframework.grpc.server.security.AuthenticationProcessInterceptor[] bean. + +For more details, please see {url-spring-grpc-docs}/server.html#_declarative_security_with_spring_security[the Spring gRPC documentation]. + + + +[[io.grpc.server.security.servlet]] +==== Servlet Container Based Servers + +If your gRPC server is running xref:web/servlet.adoc[within a standard Servlet Container], you can use typical xref:web/spring-security.adoc#web.security.spring-mvc[web security configuration] to secure your application. +Spring Boot will auto-configre javadoc:org.springframework.boot.grpc.server.autoconfigure.GrpcServerExecutorProvider[] and javadoc:org.springframework.grpc.server.security.SecurityContextServerInterceptor[] beans to ensure that Spring Security works correctly. + +Cross-Site Request Forgery (CSRF) protection is incompatible with the gRPC protocol and will be disabled by default for all gRPC requests. +If you prefer to configure your own CSRF protection, you can switch this off by setting configprop:spring.grpc.server.security.csrf.enabled[] to `false`. + +To help with manual Security configuration, Spring Boot provides request matchers for gRPC services. +Matches are available for both servlet and reactive stacks. +For example, the following will include all gRPC services with the exception of "`special`". + +include-code::MySecurityConfiguration[] + +NOTE: For reactive matches use javadoc:org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest[format=full]. + + + +[[io.grpc.server.security.oauth-resource-server]] +==== OAuth2 Resource Server + +https://oauth.net/2/[OAuth2] is a widely used authorization framework. +Spring Boot's OAuth2 Resource Server support in compatible with gRPC and may be configured in the usual way. + +For details of how to configure an OAuth2 Resource Server to use with your gRPC server application, see the xref:security/oauth2.adoc#security.oauth2.server["`OAuth2`" section] of under "`Security`". + + + +[[io.grpc.client]] +== Writing a gRPC Client Application + +Spring Boot provides a `spring-boot-grpc-client` module and a `spring-boot-starter-grpc-client` starter POM that you can use for client applications. + +Clients can call remote gRPC services by importing one or more of the "`stub`" classes generated from their `.proto` file. +You can use the javadoc:org.springframework.grpc.client.ImportGrpcClients[format=annotation] annotation to import the stub classes you want to use. + +Each import includes a `target` which can either be a logical channel name, or the base URL of the remote server. +We typically recommend using channel names rather than hard-coding targets. + +Here's a typical example: + +include-code::MyApplication[] + +NOTE: If you don't specify a `target` then "`default`" is used. + +TIP: You can use the `basePackageClasses` or `basePackages` attribute of javadoc:org.springframework.grpc.client.ImportGrpcClients[format=annotation] to import all stubs in given package. + + + +[[io.grpc.client.channel-properties]] +=== Channel Properties + +When the `target` attribute of javadoc:org.springframework.grpc.client.ImportGrpcClients[format=annotation] uses a logical channel name, you'll need to provide some properties so that Spring Boot can find the actual gRPC server to call. + +To do that, you can add an entry using `spring.grpc.channel..*` properties. +Typically, you'll configure the actual real `target`, along with any other settings that are unique to the channel. + +For example, the following will configure `myservice` to use the real target of `static://grpc.example.com:9090`. +It also changes the keep-alive timeout and the maximum message size permitted: + +[configprops,yaml] +---- +spring: + grpc: + client: + channel: + myservice: + target: static://grpc.example.com:9090 + inbound: + keepalive: + timeout: 40s + message: + max-size: 8MB +---- + + +[[io.grpc.client.stub-beans]] +=== Using Stub Beans + +With your javadoc:org.springframework.grpc.client.ImportGrpcClients[format=annotation] annotations in place, and your properties written, you can use stubs as you would any other bean. + +For example, here's the `HelloWorldStub` being injected into a `ApplicationRunner` bean: + +include-code::MyApplicationRunner[] + + + +[[io.grpc.client.netty-shaded]] +=== Switching to Netty Shaded Client Transport + +Under the hood, remote gRPC network calls are made using Netty. +If you find that the version of Netty provided by the `spring-boot-starter-grpc-client` starter POM isn't compatible with other libraries you use, you can switch to a "`shaded`" version. + +To switch, you can excluded `io.grpc:grpc-netty` and include `io.grpc:grpc-netty-shaded`. +For example: + + +[tabs] +====== + +Maven:: ++ +[source,xml] +---- + + org.springframework.boot + spring-boot-starter-grpc-client + + + + io.grpc + grpc-netty + + + + + + io.grpc + grpc-netty-shaded + +---- ++ + +Gradle:: ++ +[source,gradle] +---- +dependencies { + implementation('org.springframework.boot:spring-boot-starter-grpc-client') { + // Exclude the gRPC Netty dependency + exclude group: 'io.grpc', module: 'grpc-netty' + } + // Use gRPC Netty Shaded instead + implementation "io.grpc:grpc-netty-shaded" +} +---- ++ +====== + + +[[io.grpc.client.ssl]] +=== SSL Support + +Client gRPC applications can connect to gRPC services using SSL/TSL encrypted connections. +You can configure gRPC connections to use standard one-way-TLS, or mutual TLS + + + +[[io.grpc.client.ssl.one-way]] +==== Standard one-way TLS + +To use standard one-way TLS, you can set the `ssl.enabled` property to `true` in your channel properties. +For example, the following will enabled an SSL/TLS connection for the `myservice` channel: + +[configprops,yaml] +---- +spring: + grpc: + client: + channel: + myservice: + target: static://grpc.example.com:9090 + ssl: + enabled: true +---- + + + +[[io.grpc.client.ssl.mutual]] +==== Mutual TLS + +Mutual TLS (mTLS) is a security protocol that requires both the client and the server to present certificates to each other. +To use mutual TLS, you can set the `ssl.bundle` property in your channel properties. +See the xref:features/ssl.adoc[SSL core features documentation] for details on how to declare an SSL bundle. + +Here is an example the configures the `myservice` channel to use the `mybundle` bundle for mutual TLS: + +[configprops,yaml] +---- +spring: + grpc: + client: + channel: + myservice: + target: static://grpc.example.com:9090 + ssl: + bundle: mybundle +---- + +[TIP] +==== +To temporarily disable client SSL support, for example to aid with testing, you can set `bypass-certificate-validation` to `true` on your channel config: + +[configprops,yaml] +---- +spring: + grpc: + client: + channel: + myservice: + bypass-certificate-validation: true +---- +==== + + + +[[io.grpc.client.in-process]] +=== Using In-Process Channels + +You can communicate with an xref:io/grpc.adoc#io.grpc.server.in-process[in-process server] (i.e. not listening on a network port) by including the `io.grpc.grpc-inprocess` dependency on your classpath. + +In this mode, the in-process channel factory is auto-configured in addition to the regular channel factories (e.g. Netty). +To prevent users from having to deal with multiple channel factories, a composite channel factory is configured as the primary channel factory bean. +The composite consults its composed factories to find the first one that supports the channel target. + +To use the in-process server the channel target must be set to `in-process:` + +TIP: To disable the in-process channel factory, you can set the configprop:spring.grpc.client.inprocess.enabled[] property to `false`. + + + +[[io.grpc.client.observability]] +=== Observability + +Spring Boot provides auto-configuration of the javadoc:io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor[] whenever Micrometer is available. +This interceptor provides observability into your gRPC client applications. + +TIP: If you use Micrometer, but prefer to not to use it for gRPC, you can set configprop:spring.grpc.client.observation.enabled[] to `false`. + + + +[[io.grpc.client.channel-customization]] +=== Channel Customization + +If you need to customize your gRPC channel beyond the basic properties, you can use a javadoc:org.springframework.grpc.client.GrpcChannelBuilderCustomizer[]. +Each customizer is called with the logical target name and the javadoc:io.grpc.ManagedChannelBuilder[] that will build the channel. +There's also a convenient `matching(String pattern)` factory method that will limit customizations to targets that match the given regex pattern. + +A common customizer use-case is to add {url-spring-grpc-docs}/client.html#_http_headers[security interceptors] to the builder. +For example, here we're adding the javadoc:org.springframework.grpc.client.interceptor.security.BearerTokenAuthenticationInterceptor[] to the target matching "`hello`": + +include-code::MyGrpcConfiguration[] + + + +[[io.grpc.testing]] +== Testing gRPC Applications + +To help test your gRPC client and server applications you can use the `spring-boot-grpc-test` module or the `spring-boot-starter-grpc-client-test` / `spring-boot-starter-grpc-server-test` starter POMs. + + +[[io.grpc.testing.test-transport]] +=== Using In-Process Test Transport + +The javadoc:org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport[format=annotation] annotation allows you to quickly replace gRPC communication channels with in-process channels specifically designed for testing. +Unlike regular in-process channels, these test channels to not require any configuration. + +Using test gRPC transport means that you don't need to actually listen on a network port to start your application. +This allows your tests to run quickly, whilst still ensuring that your application works as expected. + +By default, using javadoc:org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport[format=annotation] will: + +* Configure test javadoc:org.springframework.grpc.server.GrpcServerFactory[] / javadoc:org.springframework.grpc.client.GrpcChannelFactory[] beans +* Disable any gRPC servelt registration. +* Disable javadoc:org.springframework.grpc.server.GrpcServerFactory[] bean auto-configuration. +* Disable javadoc:org.springframework.grpc.client.GrpcChannelFactory[] bean auto-configuration. + +The following example shows how you can use javadoc:org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport[format=annotation] to test a gRPC server application: + +include-code::MyGrpcTests[] + + + +[[io.grpc.testing.local-server-port]] +=== Testing With a Running Server + +If you prefer to test your gRPC application by starting the real server and using the actual network connection, we recommend that you use random ports. +This will ensure that you can run your tests in any environment, and that you won't accidentally call real services. + +To start a gRPC server using a random port, set configprop:spring.grpc.server.port[] to `0`. +You can use the javadoc:org.springframework.boot.grpc.test.autoconfigure.LocalGrpcServerPort[format=annotation] annotation to obtain the actual port that the server started on. + +Here's an example test: + +include-code::MyGrpcIntegrationTests[] + + + +// FIXME https://github.com/spring-projects/spring-grpc/issues/397 \ No newline at end of file diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/partials/nav-reference.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/partials/nav-reference.adoc index 3f1235532fd9..8b7e069f3cd3 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/partials/nav-reference.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/partials/nav-reference.adoc @@ -40,6 +40,7 @@ ** xref:reference:io/index.adoc[] *** xref:reference:io/caching.adoc[] *** xref:reference:io/spring-batch.adoc[] +*** xref:reference:io/grpc.adoc[] *** xref:reference:io/hazelcast.adoc[] *** xref:reference:io/quartz.adoc[] *** xref:reference:io/email.adoc[] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/tutorial/pages/first-application/index.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/tutorial/pages/first-application/index.adoc index 97b9bfe20c7d..f21ceb0be5fc 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/tutorial/pages/first-application/index.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/tutorial/pages/first-application/index.adoc @@ -155,7 +155,7 @@ Open your favorite text editor and add the following: plugins { id 'java' id 'org.springframework.boot' version '{version-spring-boot}' - id 'io.spring.dependency-management' version '{version-dependency-management-plugin}' + id 'io.spring.dependency-management' version '{version-dependency-management-plugin}' } group = 'com.example' @@ -170,7 +170,7 @@ java { repositories { mavenCentral() ifeval::["{build-and-artifact-release-type}" == "opensource-snapshot"] - // you don't need this if you are using a release version + // you don't need this if you are using a release version maven { url 'https://repo.spring.io/snapshot' } endif::[] } diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/HelloWorldGrpc.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/HelloWorldGrpc.java new file mode 100644 index 000000000000..a9d29503cdc8 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/HelloWorldGrpc.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.client; + +class HelloWorldGrpc { + + interface HelloWorldBlockingStub { + + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/MyApplication.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/MyApplication.java new file mode 100644 index 000000000000..ed692948df85 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/MyApplication.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.client; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.grpc.client.ImportGrpcClients; + +@SpringBootApplication(proxyBeanMethods = false) +@ImportGrpcClients(target = "hello", types = HelloWorldGrpc.HelloWorldBlockingStub.class) +public class MyApplication { + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/channelcustomization/MyGrpcConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/channelcustomization/MyGrpcConfiguration.java new file mode 100644 index 000000000000..5d64753c6091 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/channelcustomization/MyGrpcConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.client.channelcustomization; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; +import org.springframework.grpc.client.interceptor.security.BasicAuthenticationInterceptor; + +@Configuration(proxyBeanMethods = false) +public class MyGrpcConfiguration { + + @Bean + GrpcChannelBuilderCustomizer helloChannelCustomizer() { + return GrpcChannelBuilderCustomizer.matching("hello", + (builder) -> builder.intercept(new BasicAuthenticationInterceptor("user", "password"))); + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloReply.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloReply.java new file mode 100644 index 000000000000..5fd2437dda26 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloReply.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.client.stubbeans; + +interface HelloReply { + + String getMessage(); + + static Builder newBuilder() { + throw new IllegalStateException(); + } + + interface Builder { + + Builder setMessage(String message); + + HelloReply build(); + + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloRequest.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloRequest.java new file mode 100644 index 000000000000..baf2f88e52df --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloRequest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.client.stubbeans; + +interface HelloRequest { + + String getName(); + + static Builder newBuilder() { + throw new IllegalStateException(); + } + + interface Builder { + + Builder setName(String name); + + HelloRequest build(); + + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloWorldGrpc.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloWorldGrpc.java new file mode 100644 index 000000000000..f86094051b87 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloWorldGrpc.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.client.stubbeans; + +class HelloWorldGrpc { + + interface HelloWorldBlockingStub { + + HelloReply sayHello(HelloRequest request); + + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/MyApplicationRunner.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/MyApplicationRunner.java new file mode 100644 index 000000000000..772749a648e4 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/client/stubbeans/MyApplicationRunner.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.client.stubbeans; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.docs.io.grpc.client.stubbeans.HelloWorldGrpc.HelloWorldBlockingStub; +import org.springframework.stereotype.Component; + +@Component +class MyApplicationRunner implements ApplicationRunner { + + private final HelloWorldBlockingStub helloStub; + + MyApplicationRunner(HelloWorldGrpc.HelloWorldBlockingStub helloStub) { + this.helloStub = helloStub; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + HelloRequest request = HelloRequest.newBuilder().setName("Spring").build(); + HelloReply reply = this.helloStub.sayHello(request); + System.out.println(reply.getMessage()); + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/HelloReply.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/HelloReply.java new file mode 100644 index 000000000000..d951f2b874fc --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/HelloReply.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.server; + +interface HelloReply { + + String getMessage(); + + static Builder newBuilder() { + throw new IllegalStateException(); + } + + interface Builder { + + Builder setMessage(String message); + + HelloReply build(); + + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/HelloRequest.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/HelloRequest.java new file mode 100644 index 000000000000..f0bb4dcf6df3 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/HelloRequest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.server; + +interface HelloRequest { + + String getName(); + + static Builder newBuilder() { + throw new IllegalStateException(); + } + + interface Builder { + + Builder setName(String name); + + HelloRequest build(); + + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/HelloWorldGrpc.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/HelloWorldGrpc.java new file mode 100644 index 000000000000..de1595401c82 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/HelloWorldGrpc.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.server; + +import io.grpc.stub.StreamObserver; + +class HelloWorldGrpc { + + abstract static class HelloWorldImplBase { + + abstract void sayHello(HelloRequest request, StreamObserver responseObserver); + + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/MyHelloWorldService.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/MyHelloWorldService.java new file mode 100644 index 000000000000..70de5ff2d983 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/MyHelloWorldService.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.server; + +import io.grpc.stub.StreamObserver; + +import org.springframework.grpc.server.service.GrpcService; + +@GrpcService +public class MyHelloWorldService extends HelloWorldGrpc.HelloWorldImplBase { + + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + String message = "Hello '%s'".formatted(request.getName()); + HelloReply reply = HelloReply.newBuilder().setMessage(message).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/security/servlet/MySecurityConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/security/servlet/MySecurityConfiguration.java new file mode 100644 index 000000000000..d2d9a64929f4 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/server/security/servlet/MySecurityConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.server.security.servlet; + +import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration(proxyBeanMethods = false) +public class MySecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) { + http.securityMatcher(GrpcRequest.toAnyService().excluding("special")); + http.authorizeHttpRequests((requests) -> requests.anyRequest().hasRole("GRPC_ADMIN")); + http.httpBasic(withDefaults()); + return http.build(); + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloReply.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloReply.java new file mode 100644 index 000000000000..46ff563e8594 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloReply.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.testing.localserverport; + +interface HelloReply { + + String getMessage(); + + static Builder newBuilder() { + throw new IllegalStateException(); + } + + interface Builder { + + Builder setMessage(String message); + + HelloReply build(); + + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloRequest.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloRequest.java new file mode 100644 index 000000000000..b2ed9386480e --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloRequest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.testing.localserverport; + +interface HelloRequest { + + String getName(); + + static Builder newBuilder() { + throw new IllegalStateException(); + } + + interface Builder { + + Builder setName(String name); + + HelloRequest build(); + + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloWorldGrpc.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloWorldGrpc.java new file mode 100644 index 000000000000..db8994d4aad1 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloWorldGrpc.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.testing.localserverport; + +import io.grpc.ManagedChannel; + +interface HelloWorldGrpc { + + static HelloWorldBlockingStub newBlockingStub(ManagedChannel channel) { + throw new IllegalStateException(); + } + + interface HelloWorldBlockingStub { + + HelloReply sayHello(HelloRequest request); + + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/MyGrpcIntegrationTests.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/MyGrpcIntegrationTests.java new file mode 100644 index 000000000000..5fa4304daf3c --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/localserverport/MyGrpcIntegrationTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.testing.localserverport; + +import io.grpc.ManagedChannel; +import io.grpc.netty.NettyChannelBuilder; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docs.io.grpc.testing.localserverport.HelloWorldGrpc.HelloWorldBlockingStub; +import org.springframework.boot.grpc.test.autoconfigure.LocalGrpcServerPort; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = "spring.grpc.server.port=0") +class MyGrpcIntegrationTests { + + @LocalGrpcServerPort + private int port; + + @Test + void sayHello() { + String target = "localhost:%s".formatted(this.port); + ManagedChannel channel = NettyChannelBuilder.forTarget(target).usePlaintext().build(); + try { + HelloWorldBlockingStub hello = HelloWorldGrpc.newBlockingStub(channel); + HelloRequest request = HelloRequest.newBuilder().setName("Spring").build(); + assertThat(hello.sayHello(request).getMessage()).isEqualTo("Hello 'Spring'"); + } + finally { + channel.shutdown(); + } + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloReply.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloReply.java new file mode 100644 index 000000000000..4a15ad45cea8 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloReply.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.testing.testtransport; + +interface HelloReply { + + String getMessage(); + + static Builder newBuilder() { + throw new IllegalStateException(); + } + + interface Builder { + + Builder setMessage(String message); + + HelloReply build(); + + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloRequest.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloRequest.java new file mode 100644 index 000000000000..8d520fc66050 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloRequest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.testing.testtransport; + +interface HelloRequest { + + String getName(); + + static Builder newBuilder() { + throw new IllegalStateException(); + } + + interface Builder { + + Builder setName(String name); + + HelloRequest build(); + + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloWorldGrpc.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloWorldGrpc.java new file mode 100644 index 000000000000..b5d680b5fc7d --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloWorldGrpc.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.testing.testtransport; + +class HelloWorldGrpc { + + interface HelloWorldBlockingStub { + + HelloReply sayHello(HelloRequest request); + + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/MyGrpcTests.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/MyGrpcTests.java new file mode 100644 index 000000000000..178192a47167 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/grpc/testing/testtransport/MyGrpcTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.grpc.testing.testtransport; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.grpc.client.ImportGrpcClients; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@AutoConfigureTestGrpcTransport +@ImportGrpcClients(types = HelloWorldGrpc.HelloWorldBlockingStub.class) +class MyGrpcTests { + + @Autowired + private HelloWorldGrpc.HelloWorldBlockingStub helloStub; + + @Test + void sayHello() { + HelloRequest request = HelloRequest.newBuilder().setName("Spring").build(); + HelloReply reply = this.helloStub.sayHello(request); + assertThat(reply.getMessage()).isEqualTo("Hello 'Spring'"); + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/HelloWorldGrpc.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/HelloWorldGrpc.kt new file mode 100644 index 000000000000..f4da5896b391 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/HelloWorldGrpc.kt @@ -0,0 +1,9 @@ +package org.springframework.boot.docs.io.grpc.client + +class HelloWorldGrpc { + + interface HelloWorldBlockingStub { + + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/MyApplication.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/MyApplication.kt new file mode 100644 index 000000000000..7b5c0a369f33 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/MyApplication.kt @@ -0,0 +1,14 @@ +package org.springframework.boot.docs.io.grpc.client + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.docs.features.springapplication.MyApplication +import org.springframework.boot.runApplication +import org.springframework.grpc.client.ImportGrpcClients + +@SpringBootApplication(proxyBeanMethods = false) +@ImportGrpcClients(target = "hello", types = [HelloWorldGrpc.HelloWorldBlockingStub::class]) +class MyApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/channelcustomization/MyGrpcConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/channelcustomization/MyGrpcConfiguration.kt new file mode 100644 index 000000000000..36c019233b42 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/channelcustomization/MyGrpcConfiguration.kt @@ -0,0 +1,20 @@ +package org.springframework.boot.docs.io.grpc.client.channelcustomization + +import io.grpc.ManagedChannelBuilder +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer +import org.springframework.grpc.client.interceptor.security.BasicAuthenticationInterceptor +import java.util.function.Consumer + +@Configuration(proxyBeanMethods = false) +class MyGrpcConfiguration { + + @Bean + fun helloChannelCustomizer(): GrpcChannelBuilderCustomizer<*> { + return GrpcChannelBuilderCustomizer.matching("hello", { builder -> + builder.intercept(BasicAuthenticationInterceptor("user", "password")) + }) + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloReply.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloReply.kt new file mode 100644 index 000000000000..b9e136cb17c1 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloReply.kt @@ -0,0 +1,23 @@ +package org.springframework.boot.docs.io.grpc.client.stubbeans + +interface HelloReply { + + fun getMessage(): String + + companion object { + + fun newBuilder(): Builder { + throw IllegalStateException() + } + + } + + interface Builder { + + fun setMessage(message: String): Builder + + fun build(): HelloReply + + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloRequest.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloRequest.kt new file mode 100644 index 000000000000..70c921637bc5 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloRequest.kt @@ -0,0 +1,23 @@ +package org.springframework.boot.docs.io.grpc.client.stubbeans + +interface HelloRequest { + + fun getName(): String + + companion object { + + fun newBuilder(): Builder { + throw IllegalStateException() + } + + } + + interface Builder { + + fun setName(name: String): Builder + + fun build(): HelloRequest + + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloWorldGrpc.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloWorldGrpc.kt new file mode 100644 index 000000000000..2bb2103ab8d6 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/HelloWorldGrpc.kt @@ -0,0 +1,11 @@ +package org.springframework.boot.docs.io.grpc.client.stubbeans + +class HelloWorldGrpc { + + interface HelloWorldBlockingStub { + + fun sayHello(request: HelloRequest): HelloReply + + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/MyApplicationRunner.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/MyApplicationRunner.kt new file mode 100644 index 000000000000..f441809cdc4c --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/client/stubbeans/MyApplicationRunner.kt @@ -0,0 +1,15 @@ +package org.springframework.boot.docs.io.grpc.client.stubbeans + +import org.springframework.boot.ApplicationArguments +import org.springframework.boot.ApplicationRunner + + +class MyApplicationRunner(val helloStub: HelloWorldGrpc.HelloWorldBlockingStub) : ApplicationRunner { + + override fun run(args: ApplicationArguments) { + val request = HelloRequest.newBuilder().setName("Spring").build() + val reply: HelloReply = helloStub.sayHello(request) + println(reply.getMessage()) + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/HelloReply.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/HelloReply.kt new file mode 100644 index 000000000000..6ea3efc39c0a --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/HelloReply.kt @@ -0,0 +1,23 @@ +package org.springframework.boot.docs.io.grpc.server + +interface HelloReply { + + fun getMessage(): String + + companion object { + + fun newBuilder(): Builder { + throw IllegalStateException() + } + + } + + interface Builder { + + fun setMessage(message: String): Builder + + fun build(): HelloReply + + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/HelloRequest.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/HelloRequest.kt new file mode 100644 index 000000000000..2fedd32fa64a --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/HelloRequest.kt @@ -0,0 +1,23 @@ +package org.springframework.boot.docs.io.grpc.server + +interface HelloRequest { + + fun getName(): String + + companion object { + + fun newBuilder(): Builder { + throw IllegalStateException() + } + + } + + interface Builder { + + fun setName(name: String): Builder + + fun build(): HelloRequest + + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/HelloWorldGrpc.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/HelloWorldGrpc.kt new file mode 100644 index 000000000000..858880344fea --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/HelloWorldGrpc.kt @@ -0,0 +1,14 @@ +package org.springframework.boot.docs.io.grpc.server + +import io.grpc.stub.StreamObserver + +class HelloWorldGrpc { + + abstract class HelloWorldImplBase { + abstract fun sayHello( + request: HelloRequest, + responseObserver: StreamObserver + ) + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/MyHelloWorldService.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/MyHelloWorldService.kt new file mode 100644 index 000000000000..5a6d57d41a43 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/MyHelloWorldService.kt @@ -0,0 +1,16 @@ +package org.springframework.boot.docs.io.grpc.server + +import io.grpc.stub.StreamObserver +import org.springframework.grpc.server.service.GrpcService + +@GrpcService +class MyHelloWorldService : HelloWorldGrpc.HelloWorldImplBase() { + + override fun sayHello(request: HelloRequest, responseObserver: StreamObserver) { + val message = "Hello '${request.getName()}'" + val reply = HelloReply.newBuilder().setMessage(message).build() + responseObserver.onNext(reply) + responseObserver.onCompleted() + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/security/servlet/MySecurityConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/security/servlet/MySecurityConfiguration.kt new file mode 100644 index 000000000000..d8af99d1b9b9 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/server/security/servlet/MySecurityConfiguration.kt @@ -0,0 +1,21 @@ +package org.springframework.boot.docs.io.grpc.server.security.servlet + +import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.Customizer.withDefaults +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.web.SecurityFilterChain + +@Configuration(proxyBeanMethods = false) +class MySecurityConfiguration { + + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http.securityMatcher(GrpcRequest.toAnyService().excluding("special")) + http.authorizeHttpRequests { requests -> requests.anyRequest().hasRole("GRPC_ADMIN") } + http.httpBasic(withDefaults()) + return http.build() + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloReply.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloReply.kt new file mode 100644 index 000000000000..fc4df9a7d982 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloReply.kt @@ -0,0 +1,23 @@ +package org.springframework.boot.docs.io.grpc.testing.localserverport + +interface HelloReply { + + fun getMessage(): String + + companion object { + + fun newBuilder(): Builder { + throw IllegalStateException() + } + + } + + interface Builder { + + fun setMessage(message: String): Builder + + fun build(): HelloReply + + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloRequest.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloRequest.kt new file mode 100644 index 000000000000..0dec210254a9 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloRequest.kt @@ -0,0 +1,23 @@ +package org.springframework.boot.docs.io.grpc.testing.localserverport + +interface HelloRequest { + + fun getName(): String + + companion object { + + fun newBuilder(): Builder { + throw IllegalStateException() + } + + } + + interface Builder { + + fun setName(name: String): Builder + + fun build(): HelloRequest + + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloWorldGrpc.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloWorldGrpc.kt new file mode 100644 index 000000000000..d19c4cadf936 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/HelloWorldGrpc.kt @@ -0,0 +1,19 @@ +package org.springframework.boot.docs.io.grpc.testing.localserverport + +import io.grpc.ManagedChannel + +class HelloWorldGrpc { + + interface HelloWorldBlockingStub { + + fun sayHello(request: HelloRequest): HelloReply + + } + + companion object { + fun newBlockingStub(channel: ManagedChannel): HelloWorldBlockingStub { + throw IllegalStateException() + } + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/MyGrpcIntegrationTests.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/MyGrpcIntegrationTests.kt new file mode 100644 index 000000000000..f53cbaccdb26 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/localserverport/MyGrpcIntegrationTests.kt @@ -0,0 +1,28 @@ +package org.springframework.boot.docs.io.grpc.testing.localserverport + +import io.grpc.ManagedChannel +import io.grpc.netty.NettyChannelBuilder +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.boot.grpc.test.autoconfigure.LocalGrpcServerPort +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest(properties = ["spring.grpc.server.port=0"]) +class MyGrpcIntegrationTests { + + @LocalGrpcServerPort + var port = 0 + + @Test + fun sayHello() { + val target = "localhost:${port}" + val channel: ManagedChannel = NettyChannelBuilder.forTarget(target).usePlaintext().build() + try { + val hello: HelloWorldGrpc.HelloWorldBlockingStub = HelloWorldGrpc.newBlockingStub(channel) + val request = HelloRequest.newBuilder().setName("Spring").build() + assertThat(hello.sayHello(request).getMessage()).isEqualTo("Hello 'Spring'") + } finally { + channel.shutdown() + } + } +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloReply.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloReply.kt new file mode 100644 index 000000000000..44a56925ac57 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloReply.kt @@ -0,0 +1,23 @@ +package org.springframework.boot.docs.io.grpc.testing.testtransport + +interface HelloReply { + + fun getMessage(): String + + companion object { + + fun newBuilder(): Builder { + throw IllegalStateException() + } + + } + + interface Builder { + + fun setMessage(message: String): Builder + + fun build(): HelloReply + + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloRequest.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloRequest.kt new file mode 100644 index 000000000000..718318c7c95d --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloRequest.kt @@ -0,0 +1,23 @@ +package org.springframework.boot.docs.io.grpc.testing.testtransport + +interface HelloRequest { + + fun getName(): String + + companion object { + + fun newBuilder(): Builder { + throw IllegalStateException() + } + + } + + interface Builder { + + fun setName(name: String): Builder + + fun build(): HelloRequest + + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloWorldGrpc.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloWorldGrpc.kt new file mode 100644 index 000000000000..24fe86509b14 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/HelloWorldGrpc.kt @@ -0,0 +1,19 @@ +package org.springframework.boot.docs.io.grpc.testing.testtransport + +import io.grpc.ManagedChannel + +class HelloWorldGrpc { + + interface HelloWorldBlockingStub { + + fun sayHello(request: HelloRequest): HelloReply + + } + + companion object { + fun newBlockingStub(channel: ManagedChannel): HelloWorldBlockingStub { + throw IllegalStateException() + } + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/MyGrpcTests.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/MyGrpcTests.kt new file mode 100644 index 000000000000..3a89499cc618 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/grpc/testing/testtransport/MyGrpcTests.kt @@ -0,0 +1,23 @@ +package org.springframework.boot.docs.io.grpc.testing.testtransport + +import org.assertj.core.api.Assertions.assertThat +import org.jooq.DSLContext +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.grpc.client.ImportGrpcClients + +@SpringBootTest +@AutoConfigureTestGrpcTransport +@ImportGrpcClients(types = [HelloWorldGrpc.HelloWorldBlockingStub::class]) +class MyGrpcTests(@Autowired val helloStub: HelloWorldGrpc.HelloWorldBlockingStub) { + + @Test + fun sayHello() { + val request = HelloRequest.newBuilder().setName("Spring").build() + val reply = helloStub.sayHello(request) + assertThat(reply.getMessage()).isEqualTo("Hello 'Spring'") + } + +} diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 73cdab167d7b..0849893be569 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2597,7 +2597,7 @@ bom { github("https://github.com/spring-projects/spring-grpc") javadoc(version -> "https://docs.spring.io/spring-grpc/docs/%s/api" .formatted(version.forMajorMinorGeneration()), "org.springframework.grpc") - docs(version -> "https://docs.spring.io/spring-grpc/docs/%s/reference" + docs(version -> "https://docs.spring.io/spring-grpc/reference/%s" .formatted(version.forMajorMinorGeneration())) releaseNotes("https://github.com/spring-projects/spring-grpc/releases/tag/v{version}") } diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/build.gradle index b647f93a8729..0a79058693fd 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/build.gradle @@ -24,7 +24,7 @@ description = "Spring Boot gRPC server netty shaded smoke test" dependencies { implementation(project(":starter:spring-boot-starter-grpc-server")) { - exclude(group: "io.grpc", module: "grpc-netty-shaded") + exclude(group: "io.grpc", module: "grpc-netty") } implementation("io.grpc:grpc-netty-shaded") diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-servlet/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/build.gradle index 4d7901ea6326..6b71b0d18909 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-servlet/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-server-servlet/build.gradle @@ -25,7 +25,7 @@ description = "Spring Boot gRPC server servlet smoke test" dependencies { implementation(project(":starter:spring-boot-starter-grpc-server")) { - exclude(group: "io.grpc", module: "grpc-netty-shaded") + exclude(group: "io.grpc", module: "grpc-netty") } implementation(project(":starter:spring-boot-starter-tomcat")) implementation("io.grpc:grpc-servlet-jakarta") diff --git a/smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/HelloWorldService.java b/smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/HelloWorldService.java index e8d5de4c01e5..eabf6ba5ef8c 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/HelloWorldService.java +++ b/smoke-test/spring-boot-smoke-test-grpc-server/src/main/java/smoketest/grpcserver/HelloWorldService.java @@ -23,10 +23,10 @@ import smoketest.grpcserver.proto.HelloRequest; import smoketest.grpcserver.proto.HelloWorldGrpc; -import org.springframework.stereotype.Service; +import org.springframework.grpc.server.service.GrpcService; import org.springframework.util.Assert; -@Service +@GrpcService public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase { private static Log logger = LogFactory.getLog(HelloWorldService.class); From f651030f9bdf2dd8e97f5511e23bcc43e58faaeb Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 21 Apr 2026 22:25:14 -0700 Subject: [PATCH 536/930] Align HttpClient defaults with Spring Framework and provide an opt-out Update default for `ReactorClientHttpRequestFactoryBuilder` and `ReactorClientHttpConnectorBuilder` to align with Spring Framework. Also provide method of opting out in case proxyWithSystemProperties is not wanted. Closes gh-49950 --- ...eactorClientHttpRequestFactoryBuilder.java | 23 ++++++++ .../http/client/ReactorHttpClientBuilder.java | 54 ++++++++++++++----- .../ReactorClientHttpConnectorBuilder.java | 23 ++++++++ ...rClientHttpRequestFactoryBuilderTests.java | 24 +++++++++ ...eactorClientHttpConnectorBuilderTests.java | 21 ++++++++ 5 files changed, 133 insertions(+), 12 deletions(-) diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java index 35d3d61304eb..cc2baedde8a8 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java @@ -96,6 +96,29 @@ public ReactorClientHttpRequestFactoryBuilder withHttpClientFactory(Supplier factoryDefaults) { + return new ReactorClientHttpRequestFactoryBuilder(getCustomizers(), + this.httpClientBuilder.withHttpClientDefaults(factoryDefaults)); + } + /** * Return a new {@link ReactorClientHttpRequestFactoryBuilder} that applies additional * customization to the underlying {@link HttpClient}. diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java index c5dc7329c035..4dabc5670871 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/ReactorHttpClientBuilder.java @@ -50,17 +50,21 @@ public final class ReactorHttpClientBuilder { private final Supplier factory; + private final UnaryOperator factoryDefaults; + private final UnaryOperator customizer; private final @Nullable ResolvedAddressSelector resolvedAddressSelector; public ReactorHttpClientBuilder() { - this(HttpClient::create, UnaryOperator.identity(), null); + this(HttpClient::create, ReactorHttpClientBuilder::applySpringDefaults, UnaryOperator.identity(), null); } - private ReactorHttpClientBuilder(Supplier httpClientFactory, UnaryOperator customizer, + private ReactorHttpClientBuilder(Supplier factory, UnaryOperator factoryDefaults, + UnaryOperator customizer, @Nullable ResolvedAddressSelector resolvedAddressSelector) { - this.factory = httpClientFactory; + this.factory = factory; + this.factoryDefaults = factoryDefaults; this.customizer = customizer; this.resolvedAddressSelector = resolvedAddressSelector; } @@ -74,6 +78,7 @@ private ReactorHttpClientBuilder(Supplier httpClientFactory, UnaryOp public ReactorHttpClientBuilder withReactorResourceFactory(ReactorResourceFactory reactorResourceFactory) { Assert.notNull(reactorResourceFactory, "'reactorResourceFactory' must not be null"); return new ReactorHttpClientBuilder(() -> HttpClient.create(reactorResourceFactory.getConnectionProvider()), + this.factoryDefaults, (httpClient) -> this.customizer.apply(httpClient).runOn(reactorResourceFactory.getLoopResources()), this.resolvedAddressSelector); } @@ -86,7 +91,31 @@ public ReactorHttpClientBuilder withReactorResourceFactory(ReactorResourceFactor */ public ReactorHttpClientBuilder withHttpClientFactory(Supplier factory) { Assert.notNull(factory, "'factory' must not be null"); - return new ReactorHttpClientBuilder(factory, this.customizer, this.resolvedAddressSelector); + return new ReactorHttpClientBuilder(factory, this.factoryDefaults, this.customizer, + this.resolvedAddressSelector); + } + + /** + * Return a new {@link ReactorHttpClientBuilder} that does not apply any defaults when + * first creating the {@link HttpClient}. + * @return a new {@link ReactorHttpClientBuilder} instance + * @since 4.1.0 + */ + public ReactorHttpClientBuilder withoutHttpClientDefaults() { + return withHttpClientDefaults(null); + } + + /** + * Return a new {@link ReactorHttpClientBuilder} that applies the given factory + * defaults when first creating the {@link HttpClient}. + * @param factoryDefaults the factory to use + * @return a new {@link ReactorHttpClientBuilder} instance + * @since 4.1.0 + */ + public ReactorHttpClientBuilder withHttpClientDefaults(@Nullable UnaryOperator factoryDefaults) { + return new ReactorHttpClientBuilder(this.factory, + (factoryDefaults != null) ? factoryDefaults : UnaryOperator.identity(), this.customizer, + this.resolvedAddressSelector); } /** @@ -97,7 +126,7 @@ public ReactorHttpClientBuilder withHttpClientFactory(Supplier facto */ public ReactorHttpClientBuilder withHttpClientCustomizer(UnaryOperator customizer) { Assert.notNull(customizer, "'customizer' must not be null"); - return new ReactorHttpClientBuilder(this.factory, + return new ReactorHttpClientBuilder(this.factory, this.factoryDefaults, (httpClient) -> customizer.apply(this.customizer.apply(httpClient)), this.resolvedAddressSelector); } @@ -112,7 +141,8 @@ public ReactorHttpClientBuilder withHttpClientCustomizer(UnaryOperator resolvedAddressSelector) { Assert.notNull(resolvedAddressSelector, "'resolvedAddressSelector' must not be null"); - return new ReactorHttpClientBuilder(this.factory, this.customizer, resolvedAddressSelector); + return new ReactorHttpClientBuilder(this.factory, this.factoryDefaults, this.customizer, + resolvedAddressSelector); } /** @@ -122,7 +152,7 @@ public ReactorHttpClientBuilder withResolvedAddressSelector( */ public HttpClient build(@Nullable HttpClientSettings settings) { settings = (settings != null) ? settings : HttpClientSettings.defaults(); - HttpClient httpClient = applyDefaults(this.factory.get()); + HttpClient httpClient = this.factoryDefaults.apply(this.factory.get()); PropertyMapper map = PropertyMapper.get(); httpClient = map.from(settings::connectTimeout).to(httpClient, this::setConnectTimeout); httpClient = map.from(settings::readTimeout).to(httpClient, HttpClient::responseTimeout); @@ -146,11 +176,6 @@ public HttpClient build(@Nullable HttpClientSettings settings) { : this.resolvedAddressSelector; } - HttpClient applyDefaults(HttpClient httpClient) { - // Aligns with Spring Framework defaults - return httpClient.compress(true); - } - private HttpClient setConnectTimeout(HttpClient httpClient, Duration timeout) { return httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) timeout.toMillis()); } @@ -177,4 +202,9 @@ private void configureSsl(SslContextSpec spec, SslBundle sslBundle) throws SSLEx spec.sslContext(builder.build()); } + static HttpClient applySpringDefaults(HttpClient httpClient) { + // Aligns with Spring Framework defaults in ReactorClientHttpRequestFactory + return httpClient.compress(true).proxyWithSystemProperties(); + } + } diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilder.java index e8ab415456e5..4105ee1c2684 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilder.java @@ -90,6 +90,29 @@ public ReactorClientHttpConnectorBuilder withHttpClientFactory(Supplier factoryDefaults) { + return new ReactorClientHttpConnectorBuilder(getCustomizers(), + this.httpClientBuilder.withHttpClientDefaults(factoryDefaults)); + } + /** * Return a new {@link ReactorClientHttpConnectorBuilder} that applies additional * customization to the underlying {@link HttpClient}. diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java index 2a60d2cd6b89..7bb64d95c3df 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.params.provider.EnumSource; import reactor.netty.http.client.HttpClient; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ReactorClientHttpRequestFactory; import org.springframework.http.client.ReactorResourceFactory; import org.springframework.test.util.ReflectionTestUtils; @@ -63,6 +64,29 @@ void withHttpClientFactory() { assertThat(called).containsExactly(true); } + @Test + void springHttpClientDefaults() { + ClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.reactor().build(); + assertThat(factory).extracting("httpClient.config.acceptGzip").isEqualTo(true); + } + + @Test + void withoutHttpClientDefaults() { + ClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.reactor() + .withoutHttpClientDefaults() + .build(); + assertThat(factory).extracting("httpClient.config.acceptGzip").isEqualTo(false); + } + + @Test + void withHttpClientDefaults() { + ClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.reactor() + .withHttpClientDefaults((httpClient) -> httpClient.baseUrl("test")) + .build(); + assertThat(factory).extracting("httpClient.config.acceptGzip").isEqualTo(false); + assertThat(factory).extracting("httpClient.config.baseUrl").isEqualTo("test"); + } + @Test void withReactorResourceFactory() { ReactorResourceFactory resourceFactory = spy(new ReactorResourceFactory()); diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilderTests.java index e48bb4c3ecb3..eb44ca46e960 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/ReactorClientHttpConnectorBuilderTests.java @@ -64,6 +64,27 @@ void withHttpClientFactory() { assertThat(called).containsExactly(true); } + @Test + void springHttpClientDefaults() { + ReactorClientHttpConnector connector = ClientHttpConnectorBuilder.reactor().build(); + assertThat(connector).extracting("httpClient.config.acceptGzip").isEqualTo(true); + } + + @Test + void withoutHttpClientDefaults() { + ReactorClientHttpConnector connector = ClientHttpConnectorBuilder.reactor().withoutHttpClientDefaults().build(); + assertThat(connector).extracting("httpClient.config.acceptGzip").isEqualTo(false); + } + + @Test + void withHttpClientDefaults() { + ReactorClientHttpConnector connector = ClientHttpConnectorBuilder.reactor() + .withHttpClientDefaults((httpClient) -> httpClient.baseUrl("test")) + .build(); + assertThat(connector).extracting("httpClient.config.acceptGzip").isEqualTo(false); + assertThat(connector).extracting("httpClient.config.baseUrl").isEqualTo("test"); + } + @Test void withReactorResourceFactory() { ReactorResourceFactory resourceFactory = spy(new ReactorResourceFactory()); From d246793ab53a2a095d8c00c396a6ea3a01cd63b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 22 Apr 2026 07:22:35 +0200 Subject: [PATCH 537/930] Upgrade to Lombok 1.18.46 Closes gh-50154 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0849893be569..8e2361fd6ce7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1385,7 +1385,7 @@ bom { javadoc("https://logback.qos.ch/apidocs/ch.qos.logback.core", "ch.qos.logback") } } - library("Lombok", "1.18.44") { + library("Lombok", "1.18.46") { group("org.projectlombok") { modules = [ "lombok" From d032100fb6e47237661b54201b5fd37f71262f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 22 Apr 2026 07:22:35 +0200 Subject: [PATCH 538/930] Upgrade to Spring Integration 7.1.0-RC1 Closes gh-49997 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8e2361fd6ce7..6241cb782812 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2619,7 +2619,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-hateoas/releases/tag/{version}") } } - library("Spring Integration", "7.1.0-SNAPSHOT") { + library("Spring Integration", "7.1.0-RC1") { considerSnapshots() group("org.springframework.integration") { bom("spring-integration-bom") From c9e71adffb81d341809a54ebefd416f992232b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 22 Apr 2026 07:22:41 +0200 Subject: [PATCH 539/930] Upgrade to Thymeleaf 3.1.5.RELEASE Closes gh-50155 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6241cb782812..ac7f5cd03518 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2809,7 +2809,7 @@ bom { javadoc("https://javadoc.io/doc/com.redis/testcontainers-redis/{version}", "com.redis.testcontainers") } } - library("Thymeleaf", "3.1.4.RELEASE") { + library("Thymeleaf", "3.1.5.RELEASE") { group("org.thymeleaf") { modules = [ "thymeleaf", From cfa329f2be4aa515a20740111e04d23bc8541460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 22 Apr 2026 07:22:46 +0200 Subject: [PATCH 540/930] Upgrade to Thymeleaf Extras SpringSecurity 3.1.5.RELEASE Closes gh-50156 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ac7f5cd03518..074832729501 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2830,7 +2830,7 @@ bom { ] } } - library("Thymeleaf Extras SpringSecurity", "3.1.4.RELEASE") { + library("Thymeleaf Extras SpringSecurity", "3.1.5.RELEASE") { group("org.thymeleaf.extras") { modules = [ "thymeleaf-extras-springsecurity6" From 3170c25f6d775034ca204e926c79b3c3cbf702cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 22 Apr 2026 07:40:11 +0200 Subject: [PATCH 541/930] Upgrade to Spring gRPC 1.1.0-RC1 Closes gh-50157 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 074832729501..f752f44a599f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2585,7 +2585,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-graphql/releases/tag/v{version}") } } - library("Spring gRPC", "1.1.0-M1") { + library("Spring gRPC", "1.1.0-RC1") { considerSnapshots() group("org.springframework.grpc") { modules = [ From 011ddcd30def1434fd7904dce5eedc7d147f836d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 22 Apr 2026 19:04:20 +0200 Subject: [PATCH 542/930] Upgrade to MySQL 9.7.0 Closes gh-50160 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index f752f44a599f..b7bed21293a2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1697,7 +1697,7 @@ bom { .formatted(version.toString().replace(".jre11", ""))) } } - library("MySQL", "9.6.0") { + library("MySQL", "9.7.0") { upgradePolicy = "same-major-version" group("com.mysql") { modules = [ From 618d31eb67c41769453c84382bd03e4674433f39 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Apr 2026 13:31:05 +0100 Subject: [PATCH 543/930] Upgrade to Native Build Tools Plugin 1.1.0 Closes gh-48968 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 15138efffe95..2ab97ad352f3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ junitJupiterVersion=6.0.3 kotlinVersion=2.3.20 mavenVersion=3.9.13 mockitoVersion=5.23.0 -nativeBuildToolsVersion=1.0.0 +nativeBuildToolsVersion=1.1.0 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.6 springFrameworkVersion=7.0.7 From eac712596d765d6e95a5edf2b61971e573fb5c1e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 24 Apr 2026 20:09:47 -0700 Subject: [PATCH 544/930] Add public constructor that accepts a cause Add a public constructor to `InvalidConfigurationPropertyValueException` that accepts a cause. Closes gh-50211 --- .../InvalidConfigurationPropertyValueException.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/InvalidConfigurationPropertyValueException.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/InvalidConfigurationPropertyValueException.java index ef9612366db0..51a6370444f6 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/InvalidConfigurationPropertyValueException.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/InvalidConfigurationPropertyValueException.java @@ -48,7 +48,18 @@ public InvalidConfigurationPropertyValueException(String name, @Nullable Object this(name, value, reason, null); } - InvalidConfigurationPropertyValueException(String name, @Nullable Object value, @Nullable String reason, + /** + * Creates a new instance for the specified property {@code name} and {@code value}, + * including a {@code reason} why the value is invalid. + * @param name the name of the property in canonical format + * @param value the value of the property, can be {@code null} + * @param reason a human-readable text that describes why the reason is invalid. + * Starts with an upper-case and ends with a dot. Several sentences and carriage + * returns are allowed. + * @param cause the cause of the exception or {@code null} + * @since 4.1.0 + */ + public InvalidConfigurationPropertyValueException(String name, @Nullable Object value, @Nullable String reason, @Nullable Throwable cause) { super("Property " + name + " with value '" + value + "' is invalid: " + reason, cause); Assert.notNull(name, "'name' must not be null"); From 4a12662a7e39871b0899e007f941b8b4c156c08c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Sat, 25 Apr 2026 22:19:39 +0200 Subject: [PATCH 545/930] Add missing dependency management for Spring Batch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See gh-50223 Signed-off-by: Eddú Meléndez Gonzales --- platform/spring-boot-dependencies/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b7bed21293a2..02f23c93a886 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2244,7 +2244,10 @@ bom { "spring-boot-starter-artemis-test", "spring-boot-starter-aspectj", "spring-boot-starter-aspectj-test", + "spring-boot-batch-data-mongodb", "spring-boot-starter-batch", + "spring-boot-starter-batch-data-mongodb", + "spring-boot-starter-batch-data-mongodb-test", "spring-boot-starter-batch-jdbc", "spring-boot-starter-batch-jdbc-test", "spring-boot-starter-batch-test", From 56396666edf4d4a3faf05b88e01c61045b8e8ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sun, 26 Apr 2026 21:20:15 +0200 Subject: [PATCH 546/930] Polish ordering --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 02f23c93a886..355f9fd5ed53 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2125,6 +2125,7 @@ bom { "spring-boot-autoconfigure-classic-modules", "spring-boot-autoconfigure-processor", "spring-boot-batch", + "spring-boot-batch-data-mongodb", "spring-boot-batch-jdbc", "spring-boot-buildpack-platform", "spring-boot-cache", @@ -2244,7 +2245,6 @@ bom { "spring-boot-starter-artemis-test", "spring-boot-starter-aspectj", "spring-boot-starter-aspectj-test", - "spring-boot-batch-data-mongodb", "spring-boot-starter-batch", "spring-boot-starter-batch-data-mongodb", "spring-boot-starter-batch-data-mongodb-test", From 9ad6722f0098158491f514517ee928679b49092a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sun, 26 Apr 2026 21:20:35 +0200 Subject: [PATCH 547/930] Add dependency management for spring-boot-web-server-test Closes gh-50224 --- platform/spring-boot-dependencies/build.gradle | 1 + settings.gradle | 1 + 2 files changed, 2 insertions(+) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 355f9fd5ed53..6324cabc39b9 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2390,6 +2390,7 @@ bom { "spring-boot-starter-validation", "spring-boot-starter-validation-test", "spring-boot-starter-web", + "spring-boot-starter-web-server-test", "spring-boot-starter-web-services", "spring-boot-starter-webclient", "spring-boot-starter-webclient-test", diff --git a/settings.gradle b/settings.gradle index 57017885a6c9..98e47e25b13f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -363,6 +363,7 @@ include "starter:spring-boot-starter-tomcat-runtime" include "starter:spring-boot-starter-validation" include "starter:spring-boot-starter-validation-test" include "starter:spring-boot-starter-web" +include "starter:spring-boot-starter-web-server-test" include "starter:spring-boot-starter-web-services" include "starter:spring-boot-starter-webclient" include "starter:spring-boot-starter-webclient-test" From 56059cf6a1d0d8478a234ad2cd418c00d8e7c532 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 27 Apr 2026 11:52:39 +0200 Subject: [PATCH 548/930] Fix typo --- .../src/docs/antora/modules/reference/pages/io/rest-client.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc index 250bc7077612..c5c5b20d222d 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc @@ -459,6 +459,6 @@ Filters may be applied to the javadoc:org.springframework.boot.http.client.HttpC include-code::MyService[] -Or you can also define one as a javadoc:org.springframework.context.annotation.Bean[format=annotation] if you want to apply it to all auto-confugured HTTP client builders: +Or you can also define one as a javadoc:org.springframework.context.annotation.Bean[format=annotation] if you want to apply it to all auto-configured HTTP client builders: include-code::MyHttpClientConfiguration[] From 72b176e85b497d076af750634977844d80ec6115 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 30 Apr 2026 10:58:38 -0700 Subject: [PATCH 549/930] Fix Spring gRPC documentation links --- platform/spring-boot-dependencies/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6324cabc39b9..79cc4e59d3d5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2599,10 +2599,10 @@ bom { links { site("https://spring.io/projects/spring-grpc") github("https://github.com/spring-projects/spring-grpc") - javadoc(version -> "https://docs.spring.io/spring-grpc/docs/%s/api" - .formatted(version.forMajorMinorGeneration()), "org.springframework.grpc") + javadoc(version -> "https://docs.spring.io/spring-grpc/reference/%s/api/java/" + .formatted(version.forAntora()), "org.springframework.grpc") docs(version -> "https://docs.spring.io/spring-grpc/reference/%s" - .formatted(version.forMajorMinorGeneration())) + .formatted(version.forAntora())) releaseNotes("https://github.com/spring-projects/spring-grpc/releases/tag/v{version}") } } From 2fffebe214d249547bcd4828b66c9835100b9fcd Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 28 Apr 2026 15:44:23 -0700 Subject: [PATCH 550/930] Reduce garbage of repeated WritableJson.toByteArray() calls Update the default implementation of `WritableJson.toByteArray()` to reduce the amount of garbage created from repeated calls. Prior to this commit, each call would create a new `ByteArrayOutputStream` and `OutputStreamWriter` to create the byte array. Writing structured JSON results in many calls to the `toByteArray()` method, which means we repeatedly create and destroy the `ByteArrayOutputStream` and `OutputStreamWriter` objects. Furthermore, both contain buffers that are often expanded and will overlap with each other. The updated implementation uses a custom `Appendable` implementation that uses a single `ByteBuffer` buffer. It also has a `ThreadLocal` cache so that repeated calls from the same thread can reuse the buffer. The cache uses a `SoftReference` to ensure that the JVM can reclaim space if needed (for example, if a large JSON line was written). Closes gh-49428 --- .../boot/json/AppendableByteArray.java | 133 ++++++++++++++++++ .../boot/json/WritableJson.java | 8 +- .../boot/json/AppendableByteArrayTests.java | 91 ++++++++++++ 3 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java create mode 100644 core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java diff --git a/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java b/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java new file mode 100644 index 000000000000..dfdf1da4105c --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java @@ -0,0 +1,133 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.json; + +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; + +import org.springframework.util.Assert; + +/** + * {@link Appendable} implementation that can be used to return a byte array. Designed to + * reduce memory pressure for {@link WritableJson#toByteArray(Charset)} by using a single + * cached buffer scoped to the thread. + * + * @author Phillip Webb + */ +class AppendableByteArray implements Appendable { + + private static ThreadLocal> cache = new ThreadLocal<>(); + + private static final int DEFAULT_INITIAL_SIZE = 8192; + + private static final int DEFAULT_EXPANSION_SIZE = 8192; + + private static final byte[] NO_BYTES = {}; + + private final Charset charset; + + private final CharsetEncoder encoder; + + private final int expansionSize; + + private ByteBuffer out; + + AppendableByteArray(Charset charset) { + this(charset, DEFAULT_INITIAL_SIZE, DEFAULT_EXPANSION_SIZE); + } + + AppendableByteArray(Charset charset, int initialSize, int expansionSize) { + this.charset = charset; + this.encoder = charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + this.out = ByteBuffer.allocate(initialSize); + this.expansionSize = expansionSize; + } + + @Override + public AppendableByteArray append(CharSequence charSequence, int start, int end) throws IOException { + return append(((charSequence != null) ? charSequence : "null").subSequence(start, end)); + } + + @Override + public AppendableByteArray append(CharSequence charSequence) throws IOException { + return append(String.valueOf(charSequence).toCharArray()); + } + + @Override + public AppendableByteArray append(char ch) throws IOException { + return append(new char[] { ch }); + } + + private AppendableByteArray append(char[] chars) throws IOException { + return (chars.length != 0) ? append(CharBuffer.wrap(chars)) : this; + } + + private AppendableByteArray append(CharBuffer in) throws IOException { + CoderResult result = this.encoder.encode(in, this.out, false); + if (result.isUnderflow()) { + return this; + } + if (result.isOverflow()) { + ByteBuffer out = this.out; + this.out = ByteBuffer.allocate(out.capacity() + this.expansionSize); + out.flip(); + this.out.put(out); + return append(in); + } + result.throwException(); + return this; + } + + byte[] toByteArray() { + this.out.flip(); + int limit = this.out.limit(); + int position = this.out.position(); + int size = limit - position; + if (size <= 0) { + return NO_BYTES; + } + byte[] result = new byte[size]; + System.arraycopy(this.out.array(), this.out.arrayOffset() + position, result, 0, size); + reset(); + return result; + } + + private void reset() { + this.out.clear(); + this.encoder.reset(); + } + + static AppendableByteArray get(Charset charset) { + Assert.notNull(charset, "'charset' must not be null"); + SoftReference cached = cache.get(); + AppendableByteArray result = (cached != null) ? cached.get() : null; + if (result == null || !result.charset.equals(charset)) { + result = new AppendableByteArray(charset); + cache.set(new SoftReference<>(result)); + } + return result; + } + +} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/json/WritableJson.java b/core/spring-boot/src/main/java/org/springframework/boot/json/WritableJson.java index 16932268ee46..0593bbc0960d 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/json/WritableJson.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/json/WritableJson.java @@ -16,7 +16,6 @@ package org.springframework.boot.json; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; @@ -76,9 +75,10 @@ default byte[] toByteArray() { */ default byte[] toByteArray(Charset charset) { Assert.notNull(charset, "'charset' must not be null"); - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - toWriter(new OutputStreamWriter(out, charset)); - return out.toByteArray(); + try { + AppendableByteArray appendable = AppendableByteArray.get(charset); + to(appendable); + return appendable.toByteArray(); } catch (IOException ex) { throw new UncheckedIOException(ex); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java b/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java new file mode 100644 index 000000000000..fb901bfcacad --- /dev/null +++ b/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.json; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.function.ThrowingConsumer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AppendableByteArray}. + * + * @author Phillip Webb + */ +class AppendableByteArrayTests { + + private static String string = """ + This is a long(ish) string. + At least it's longer that the initial size and the overflow size. + We can write it out and test if the bytes match. + """; + + @Test + void writesLargeStringWithExpandingBuffer() throws Exception { + assertByteArray(StandardCharsets.UTF_8, (appendable) -> appendable.append(string)); + assertByteArray(StandardCharsets.UTF_16, (appendable) -> appendable.append(string)); + } + + @Test + void writesLargeStringWithLargeBuffer() throws Exception { + assertByteArray(string.length() * 10, 10, StandardCharsets.UTF_8, (appendable) -> appendable.append(string)); + assertByteArray(string.length() * 10, 10, StandardCharsets.UTF_16, (appendable) -> appendable.append(string)); + } + + @Test + void writesMultipleSmallStrings() throws Exception { + assertByteArray(StandardCharsets.UTF_8, (appendable) -> appendable.append("{").append("hello").append("}")); + } + + @Test + void writeUsingCache() throws IOException { + assertByteArray(StandardCharsets.UTF_8, AppendableByteArray::get, (appendable) -> appendable.append(string)); + assertByteArray(StandardCharsets.UTF_8, AppendableByteArray::get, (appendable) -> appendable.append(string)); + assertByteArray(StandardCharsets.UTF_16, AppendableByteArray::get, (appendable) -> appendable.append(string)); + assertByteArray(StandardCharsets.UTF_16, AppendableByteArray::get, (appendable) -> appendable.append(string)); + assertByteArray(StandardCharsets.UTF_8, AppendableByteArray::get, (appendable) -> appendable.append(string)); + } + + private void assertByteArray(Charset charset, ThrowingConsumer action) throws Exception { + assertByteArray(4, 4, charset, action); + } + + private void assertByteArray(int initialSize, int expansionSize, Charset charset, + ThrowingConsumer action) throws IOException { + assertByteArray(charset, (cs) -> new AppendableByteArray(charset, initialSize, expansionSize), action); + } + + private void assertByteArray(Charset charset, Function factory, + ThrowingConsumer action) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (OutputStreamWriter writer = new OutputStreamWriter(out, charset)) { + action.accept(writer); + } + AppendableByteArray appendableByteArray = factory.apply(charset); + action.accept(appendableByteArray); + assertThat(appendableByteArray.toByteArray()).isEqualTo(out.toByteArray()); + } + +} From c5cd580073341f3807140f2cb50a38c397f69be9 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 30 Apr 2026 21:25:57 -0700 Subject: [PATCH 551/930] Support GrpcServerHealthScheduler in servlet environments Refine `GrpcServerHealthScheduler` so that it will start when gRPC is running behind a servlet. Closes gh-50209 --- .../health/GrpcServerHealthScheduler.java | 78 +++++++++++++++---- ...erverHealthSchedulerAutoConfiguration.java | 11 +-- .../GrpcServerHealthSchedulerTests.java | 67 +++++++++++++--- 3 files changed, 128 insertions(+), 28 deletions(-) diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java index e2b361882883..8b0c34dea40a 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java @@ -19,18 +19,22 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; -import java.util.concurrent.ScheduledFuture; import io.grpc.protobuf.services.HealthStatusManager; +import jakarta.servlet.ServletContext; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.boot.grpc.server.GrpcServletRegistration; import org.springframework.boot.grpc.server.health.GrpcServerHealth; -import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.log.LogMessage; import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent; import org.springframework.scheduling.TaskScheduler; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.function.SingletonSupplier; +import org.springframework.web.context.WebApplicationContext; /** * Schedules gRPC health updates once the gRPC server has been started. @@ -38,32 +42,78 @@ * @author Phillip Webb * @author Chris Bono */ -class GrpcServerHealthScheduler implements ApplicationListener { +class GrpcServerHealthScheduler { private static final Log logger = LogFactory.getLog(GrpcServerHealthScheduler.class); - private final SingletonSupplier> scheduleHealth; - - GrpcServerHealthScheduler(GrpcServerHealth grpcServerHealth, HealthStatusManager grpcServerHealthStatusManager, - TaskScheduler taskScheduler, Duration period, Duration delay) { - this(Clock.systemDefaultZone(), grpcServerHealth, grpcServerHealthStatusManager, taskScheduler, period, delay); + GrpcServerHealthScheduler(ConfigurableApplicationContext applicationContext, GrpcServerHealth grpcServerHealth, + HealthStatusManager grpcServerHealthStatusManager, TaskScheduler taskScheduler, Duration period, + Duration delay) { + this(applicationContext, grpcServerHealth, grpcServerHealthStatusManager, taskScheduler, period, delay, + Clock.systemDefaultZone()); } - GrpcServerHealthScheduler(Clock clock, GrpcServerHealth grpcServerHealth, + GrpcServerHealthScheduler(ConfigurableApplicationContext applicationContext, GrpcServerHealth grpcServerHealth, HealthStatusManager grpcServerHealthStatusManager, TaskScheduler taskScheduler, Duration period, - Duration delay) { - this.scheduleHealth = SingletonSupplier.of(() -> { + Duration delay, Clock clock) { + SingletonSupplier scheduleHealth = SingletonSupplier.of(() -> { logger.debug(LogMessage .of(() -> "Scheduling gRPC server health updates every %s seconds (after a delay of %s seconds)" .formatted((period.toMillis() / 1000.0), delay.toMillis() / 1000.0))); Runnable task = () -> grpcServerHealth.update(grpcServerHealthStatusManager); return taskScheduler.scheduleAtFixedRate(task, Instant.now(clock).plus(delay), period); }); + if (ClassUtils.isPresent("jakarta.servlet.Servlet", null) + && ClassUtils.isPresent("org.springframework.web.context.WebApplicationContext", null)) { + ServletTrigger.apply(applicationContext, scheduleHealth); + } + GrpcServerTrigger.apply(applicationContext, scheduleHealth); } - @Override - public void onApplicationEvent(GrpcServerStartedEvent event) { - this.scheduleHealth.get(); + /** + * Trigger for servlet environments. + */ + static class ServletTrigger { + + static void apply(ConfigurableApplicationContext applicationContext, SingletonSupplier scheduleHealth) { + if (hasGrpcServletRegistration(applicationContext)) { + scheduleHealth.get(); + return; + } + } + + private static boolean hasGrpcServletRegistration(ConfigurableApplicationContext applicationContext) { + if (!ObjectUtils.isEmpty(applicationContext.getBeanNamesForType(GrpcServletRegistration.class))) { + return true; + } + if (applicationContext instanceof WebApplicationContext webApplicationContext) { + ServletContext servletContext = webApplicationContext.getServletContext(); + if (servletContext != null && servletContext.getServletRegistrations() + .values() + .stream() + .anyMatch((servletRegistration) -> "io.grpc.servlet.jakarta.GrpcServlet" + .equals(servletRegistration.getClassName()))) { + return true; + } + } + return false; + } + + } + + /** + * Trigger for regular gRPC servers. + */ + static class GrpcServerTrigger { + + static void apply(ConfigurableApplicationContext applicationContext, SingletonSupplier scheduleHealth) { + applicationContext.addApplicationListener((event) -> { + if (event instanceof GrpcServerStartedEvent) { + scheduleHealth.get(); + } + }); + } + } } diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerAutoConfiguration.java index 972d4a79d3fa..8666226b0fda 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerAutoConfiguration.java @@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.task.DefaultTaskSchedulerConfiguration; import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthProperties.Schedule; import org.springframework.boot.grpc.server.health.GrpcServerHealth; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.grpc.server.GrpcServerFactory; @@ -47,12 +48,12 @@ public final class GrpcServerHealthSchedulerAutoConfiguration { @Bean @ConditionalOnBean({ TaskScheduler.class, GrpcServerHealth.class }) - GrpcServerHealthScheduler grpcServerHealthScheduler(GrpcServerHealth grpcServerHealth, - HealthStatusManager grpcServerHealthStatusManager, TaskScheduler taskScheduler, - GrpcServerHealthProperties properties) { + GrpcServerHealthScheduler grpcServerHealthScheduler(ConfigurableApplicationContext applicationContext, + GrpcServerHealth grpcServerHealth, HealthStatusManager grpcServerHealthStatusManager, + TaskScheduler taskScheduler, GrpcServerHealthProperties properties) { Schedule schedule = properties.getSchedule(); - return new GrpcServerHealthScheduler(grpcServerHealth, grpcServerHealthStatusManager, taskScheduler, - schedule.getPeriod(), schedule.getDelay()); + return new GrpcServerHealthScheduler(applicationContext, grpcServerHealth, grpcServerHealthStatusManager, + taskScheduler, schedule.getPeriod(), schedule.getDelay()); } } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerTests.java index c054fc87663f..a9d7b607e395 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerTests.java @@ -20,17 +20,26 @@ import java.time.Duration; import java.time.Instant; import java.time.ZoneId; +import java.util.Map; import io.grpc.protobuf.services.HealthStatusManager; +import io.grpc.servlet.jakarta.GrpcServlet; +import jakarta.servlet.ServletRegistration; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.springframework.boot.grpc.server.GrpcServletRegistration; import org.springframework.boot.grpc.server.health.GrpcServerHealth; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent; +import org.springframework.mock.web.MockServletContext; import org.springframework.scheduling.TaskScheduler; +import org.springframework.web.context.support.GenericWebApplicationContext; 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; import static org.mockito.Mockito.never; @@ -44,17 +53,18 @@ class GrpcServerHealthSchedulerTests { @Test - void onApplicationEventWhenEventIsGrpcStartStartsHealth() { + void onGrpcServerStartedEventWhenEventIsGrpcStartStartsHealth() { Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); GrpcServerHealth serverHealth = mock(); HealthStatusManager statusManager = mock(); TaskScheduler taskScheduler = mock(); Duration period = Duration.ofSeconds(10); Duration delay = Duration.ofSeconds(30); - GrpcServerHealthScheduler healthScheduler = new GrpcServerHealthScheduler(clock, serverHealth, statusManager, - taskScheduler, period, delay); + ConfigurableApplicationContext context = new GenericApplicationContext(); + context.refresh(); + new GrpcServerHealthScheduler(context, serverHealth, statusManager, taskScheduler, period, delay, clock); then(serverHealth).should(never()).update(statusManager); - healthScheduler.onApplicationEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 123)); + context.publishEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 123)); Instant startTime = Instant.now(clock).plus(delay); ArgumentCaptor runnable = ArgumentCaptor.captor(); then(taskScheduler).should().scheduleAtFixedRate(runnable.capture(), eq(startTime), eq(period)); @@ -64,16 +74,55 @@ void onApplicationEventWhenEventIsGrpcStartStartsHealth() { } @Test - void onApplicationEventWhenEventIsGrpcStartAndCalledTwiceStartsHealthOnlyOnce() { + void onGrpcServerStartedEventWhenEventIsGrpcStartAndCalledTwiceStartsHealthOnlyOnce() { GrpcServerHealth serverHealth = mock(); HealthStatusManager statusManager = mock(); TaskScheduler taskScheduler = mock(); - GrpcServerHealthScheduler healthScheduler = new GrpcServerHealthScheduler(serverHealth, statusManager, - taskScheduler, Duration.ofSeconds(10), Duration.ofSeconds(30)); + ConfigurableApplicationContext context = new GenericApplicationContext(); + context.refresh(); + new GrpcServerHealthScheduler(context, serverHealth, statusManager, taskScheduler, Duration.ofSeconds(10), + Duration.ofSeconds(30)); then(serverHealth).should(never()).update(statusManager); - healthScheduler.onApplicationEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 123)); - healthScheduler.onApplicationEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 345)); + context.publishEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 123)); + context.publishEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 345)); then(taskScheduler).should(times(1)).scheduleAtFixedRate(any(), any(), any()); } + @Test + void webApplicationWithGrpcServletRegistrationBeanStartsHealth() { + GrpcServerHealth serverHealth = mock(); + HealthStatusManager statusManager = mock(); + TaskScheduler taskScheduler = mock(); + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("registartion", GrpcServletRegistration.class, () -> mock()); + context.refresh(); + new GrpcServerHealthScheduler(context, serverHealth, statusManager, taskScheduler, Duration.ofSeconds(10), + Duration.ofSeconds(30)); + then(taskScheduler).should(times(1)).scheduleAtFixedRate(any(), any(), any()); + } + + @Test + void webApplicationWithGrpcServletRegistrationStartsHealth() { + GrpcServerHealth serverHealth = mock(); + HealthStatusManager statusManager = mock(); + TaskScheduler taskScheduler = mock(); + MockServletContext servletContext = new MockServletContext() { + + @Override + public java.util.Map getServletRegistrations() { + ServletRegistration registration = mock(); + given(registration.getClassName()).willReturn(GrpcServlet.class.getName()); + return Map.of("grpc", registration); + } + + }; + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.setServletContext(servletContext); + context.refresh(); + new GrpcServerHealthScheduler(context, serverHealth, statusManager, taskScheduler, Duration.ofSeconds(10), + Duration.ofSeconds(30)); + then(taskScheduler).should(times(1)).scheduleAtFixedRate(any(), any(), any()); + + } + } From 460318e569b5d1fdeffc23b3aecf3f7af75269e5 Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Fri, 1 May 2026 10:21:36 +0200 Subject: [PATCH 552/930] Handle lazy connection proxy datasource in DataSourceBuilder See gh-50271 Signed-off-by: Vedran Pavic --- .../boot/jdbc/DataSourceBuilder.java | 5 +++++ .../boot/jdbc/DataSourceBuilderTests.java | 15 +++++++++++++++ .../LiquibaseAutoConfigurationTests.java | 16 ++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java index ecaa0296fdea..30e459a03465 100644 --- a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java +++ b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java @@ -41,6 +41,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.core.ResolvableType; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.lang.Contract; import org.springframework.util.Assert; @@ -241,6 +242,10 @@ public static DataSourceBuilder derivedFrom(DataSource dataSource) { } private static DataSource unwrap(DataSource dataSource) { + if ((dataSource instanceof LazyConnectionDataSourceProxy dataSourceProxy) + && (dataSourceProxy.getTargetDataSource() != null)) { + dataSource = dataSourceProxy.getTargetDataSource(); + } try { while (dataSource.isWrapperFor(DataSource.class)) { DataSource unwrapped = dataSource.unwrap(DataSource.class); diff --git a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java index ab90f0a35d56..82891618719e 100644 --- a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java +++ b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java @@ -46,6 +46,7 @@ import org.vibur.dbcp.ViburDBCPDataSource; import org.springframework.jdbc.datasource.AbstractDataSource; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; @@ -61,6 +62,7 @@ * @author Stephane Nicoll * @author Fabio Grassi * @author Phillip Webb + * @author Vedran Pavic */ class DataSourceBuilderTests { @@ -427,6 +429,19 @@ void buildWhenDerivedFromWrappedDataSource() { assertThat(built.getJdbcUrl()).isEqualTo("jdbc:h2:test"); } + @Test + void buildWhenDerivedFromLazyConnectionDataSourceProxy() { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setUsername("test"); + dataSource.setPassword("secret"); + dataSource.setJdbcUrl("jdbc:h2:test"); + DataSourceBuilder builder = DataSourceBuilder.derivedFrom(new LazyConnectionDataSourceProxy(dataSource)); + HikariDataSource built = (HikariDataSource) builder.username("test2").password("secret2").build(); + assertThat(built.getUsername()).isEqualTo("test2"); + assertThat(built.getPassword()).isEqualTo("secret2"); + assertThat(built.getJdbcUrl()).isEqualTo("jdbc:h2:test"); + } + @Test // gh-26644 void buildWhenDerivedFromExistingDatabaseWithTypeChange() { HikariDataSource dataSource = new HikariDataSource(); diff --git a/module/spring-boot-liquibase/src/test/java/org/springframework/boot/liquibase/autoconfigure/LiquibaseAutoConfigurationTests.java b/module/spring-boot-liquibase/src/test/java/org/springframework/boot/liquibase/autoconfigure/LiquibaseAutoConfigurationTests.java index bdcff998ca11..70db2902df73 100644 --- a/module/spring-boot-liquibase/src/test/java/org/springframework/boot/liquibase/autoconfigure/LiquibaseAutoConfigurationTests.java +++ b/module/spring-boot-liquibase/src/test/java/org/springframework/boot/liquibase/autoconfigure/LiquibaseAutoConfigurationTests.java @@ -51,6 +51,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; import org.springframework.boot.jdbc.autoconfigure.EmbeddedDataSourceConfiguration; import org.springframework.boot.jdbc.autoconfigure.JdbcConnectionDetails; import org.springframework.boot.jdbc.autoconfigure.JdbcTemplateAutoConfiguration; @@ -90,6 +91,7 @@ * @author Moritz Halbritter * @author Phillip Webb * @author Ahmed Ashour + * @author Vedran Pavic */ @ExtendWith(OutputCaptureExtension.class) class LiquibaseAutoConfigurationTests { @@ -373,6 +375,20 @@ void overrideUser() { })); } + @Test + @WithDbChangelogMasterYamlResource + void lazyConnectionDataSource() { + String jdbcUrl = "jdbc:h2:mem:liquibase-" + UUID.randomUUID(); + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.datasource.url:" + jdbcUrl, "spring.datasource.username:not-sa", + "spring.datasource.connection-fetch:lazy", "spring.liquibase.user:sa") + .run(assertLiquibase((liquibase) -> { + SimpleDriverDataSource dataSource = (SimpleDriverDataSource) liquibase.getDataSource(); + assertThat(dataSource.getUrl()).isEqualTo(jdbcUrl); + assertThat(dataSource.getUsername()).isEqualTo("sa"); + })); + } + @Test @WithDbChangelogMasterYamlResource void overrideUserWhenCustom() { From 627ac0451d9ad685d0e172959e330e4031e4a749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 4 May 2026 11:30:01 +0200 Subject: [PATCH 553/930] Polish "Handle lazy connection proxy datasource in DataSourceBuilder" See gh-50271 --- .../boot/jdbc/DataSourceBuilder.java | 23 +--------- .../boot/jdbc/DataSourceUnwrapper.java | 43 +++++++++++++++++++ .../boot/jdbc/DataSourceUnwrapperTests.java | 34 +++++++++++++++ 3 files changed, 78 insertions(+), 22 deletions(-) diff --git a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java index 30e459a03465..2730af59225d 100644 --- a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java +++ b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java @@ -41,7 +41,6 @@ import org.springframework.beans.BeanUtils; import org.springframework.core.ResolvableType; -import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.lang.Contract; import org.springframework.util.Assert; @@ -238,27 +237,7 @@ public static DataSourceBuilder create(@Nullable ClassLoader classLoader) { * @return a new {@link DataSource} builder */ public static DataSourceBuilder derivedFrom(DataSource dataSource) { - return new DataSourceBuilder<>(unwrap(dataSource)); - } - - private static DataSource unwrap(DataSource dataSource) { - if ((dataSource instanceof LazyConnectionDataSourceProxy dataSourceProxy) - && (dataSourceProxy.getTargetDataSource() != null)) { - dataSource = dataSourceProxy.getTargetDataSource(); - } - try { - while (dataSource.isWrapperFor(DataSource.class)) { - DataSource unwrapped = dataSource.unwrap(DataSource.class); - if (unwrapped == dataSource) { - return unwrapped; - } - dataSource = unwrapped; - } - } - catch (SQLException ex) { - // Try to continue with the existing, potentially still wrapped, DataSource - } - return dataSource; + return new DataSourceBuilder<>(DataSourceUnwrapper.unwrapRoot(dataSource)); } /** diff --git a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceUnwrapper.java b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceUnwrapper.java index af4cda88066b..de1b3e01440b 100644 --- a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceUnwrapper.java +++ b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceUnwrapper.java @@ -93,6 +93,37 @@ private DataSourceUnwrapper() { return unwrap(dataSource, target, target); } + /** + * Return the root {@link DataSource} by recursively unwrapping all + * {@link org.springframework.jdbc.datasource.DelegatingDataSource delegating}, proxy, + * and {@link java.sql.Wrapper} layers until no further unwrapping is possible. + * @param dataSource the datasource to unwrap + * @return the root {@link DataSource} + * @since 4.1.0 + */ + public static DataSource unwrapRoot(DataSource dataSource) { + if (DELEGATING_DATA_SOURCE_PRESENT) { + DataSource targetDataSource = DelegatingDataSourceUnwrapper.getTargetDataSource(dataSource); + if (targetDataSource != null) { + return unwrapRoot(targetDataSource); + } + } + if (AopUtils.isAopProxy(dataSource)) { + Object proxyTarget = AopProxyUtils.getSingletonTarget(dataSource); + if (proxyTarget instanceof DataSource proxyDataSource) { + return unwrapRoot(proxyDataSource); + } + } + DataSource unwrapped = safeUnwrap(dataSource); + if (unwrapped != null) { + if (unwrapped == dataSource) { + return unwrapped; + } + return unwrapRoot(unwrapped); + } + return dataSource; + } + private static @Nullable S safeUnwrap(Wrapper wrapper, Class target) { try { if (target.isInterface() && wrapper.isWrapperFor(target)) { @@ -105,6 +136,18 @@ private DataSourceUnwrapper() { return null; } + private static @Nullable DataSource safeUnwrap(DataSource dataSource) { + try { + if (dataSource.isWrapperFor(DataSource.class)) { + return dataSource.unwrap(DataSource.class); + } + } + catch (Exception ex) { + // continue + } + return null; + } + private static final class DelegatingDataSourceUnwrapper { private static @Nullable DataSource getTargetDataSource(DataSource dataSource) { diff --git a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceUnwrapperTests.java b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceUnwrapperTests.java index 3b8cd79f260d..9a00a8dae54d 100644 --- a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceUnwrapperTests.java +++ b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceUnwrapperTests.java @@ -106,6 +106,40 @@ void unwrapDataSourceProxy() { .isSameAs(dataSource); } + @Test + void unwrapRootWithPlainDataSource() { + DataSource dataSource = new HikariDataSource(); + assertThat(DataSourceUnwrapper.unwrapRoot(dataSource)).isSameAs(dataSource); + } + + @Test + void unwrapRootWithDelegate() { + DataSource dataSource = new HikariDataSource(); + DataSource actual = wrapInDelegate(wrapInDelegate(dataSource)); + assertThat(DataSourceUnwrapper.unwrapRoot(actual)).isSameAs(dataSource); + } + + @Test + void unwrapRootWithProxy() { + DataSource dataSource = new HikariDataSource(); + DataSource actual = wrapInProxy(wrapInProxy(dataSource)); + assertThat(DataSourceUnwrapper.unwrapRoot(actual)).isSameAs(dataSource); + } + + @Test + void unwrapRootWithLazyConnectionDataSource() { + DataSource dataSource = new HikariDataSource(); + DataSource actual = new LazyConnectionDataSourceProxy(dataSource); + assertThat(DataSourceUnwrapper.unwrapRoot(actual)).isSameAs(dataSource); + } + + @Test + void unwrapRootWithSeveralLevelOfWrapping() { + DataSource dataSource = new HikariDataSource(); + DataSource actual = wrapInProxy(wrapInDelegate(wrapInDelegate(wrapInProxy(wrapInDelegate(dataSource))))); + assertThat(DataSourceUnwrapper.unwrapRoot(actual)).isSameAs(dataSource); + } + @Test void unwrappingIsNotAttemptedWhenTargetIsNotAnInterface() { DataSource dataSource = mock(DataSource.class); From bcf4c32426f0f84b1f4c30a047d7138100dccee7 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 4 May 2026 15:38:33 -0700 Subject: [PATCH 554/930] Call setAllowBeanDefinitionOverriding before initializer Update initialization order in `SpringApplication` to ensure `setAllowBeanDefinitionOverriding` is called before initializiers. This prevents an initializer from accidentially overriding a bean. Closes gh-50264 --- .../springframework/boot/SpringApplication.java | 14 +++++++------- .../boot/SpringApplicationTests.java | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 661edccbe3ef..c5d5af28bae5 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -383,6 +383,13 @@ private void prepareContext(DefaultBootstrapContext bootstrapContext, Configurab context.setEnvironment(environment); postProcessApplicationContext(context); addAotGeneratedInitializerIfNecessary(this.initializers); + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) { + autowireCapableBeanFactory.setAllowCircularReferences(this.properties.isAllowCircularReferences()); + if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) { + listableBeanFactory.setAllowBeanDefinitionOverriding(this.properties.isAllowBeanDefinitionOverriding()); + } + } applyInitializers(context); listeners.contextPrepared(context); bootstrapContext.close(context); @@ -391,17 +398,10 @@ private void prepareContext(DefaultBootstrapContext bootstrapContext, Configurab logStartupProfileInfo(context); } // Add boot specific singleton beans - ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); beanFactory.registerSingleton("springApplicationArguments", applicationArguments); if (printedBanner != null) { beanFactory.registerSingleton("springBootBanner", printedBanner); } - if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) { - autowireCapableBeanFactory.setAllowCircularReferences(this.properties.isAllowCircularReferences()); - if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) { - listableBeanFactory.setAllowBeanDefinitionOverriding(this.properties.isAllowBeanDefinitionOverriding()); - } - } if (this.properties.isLazyInitialization()) { context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor()); } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/core/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index a6d80f3f1597..d4ac0bf3224c 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -49,6 +49,7 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanCurrentlyInCreationException; import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanRegistrar; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.UnsatisfiedDependencyException; import org.springframework.beans.factory.annotation.Autowired; @@ -100,6 +101,7 @@ import org.springframework.context.event.SimpleApplicationEventMulticaster; import org.springframework.context.event.SmartApplicationListener; import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -1139,6 +1141,21 @@ void beanDefinitionOverridingIsDisabledByDefault() { .isThrownBy(() -> new SpringApplication(ExampleConfig.class, OverrideConfig.class).run()); } + @Test + void beanDefinitionOverridingIsAppliedToInitializer() { // gh-50264 + assertThatExceptionOfType(BeanDefinitionOverrideException.class).isThrownBy(() -> { + BeanRegistrar registrar = (registry, env) -> { + registry.registerBean("someBean", String.class); + registry.registerBean("someBean", String.class); + }; + ApplicationContextInitializer initializer = (context) -> context + .register(registrar); + SpringApplication application = new SpringApplication(Example.class); + application.setInitializers(List.of(initializer)); + application.run(); + }); + } + @Test void beanDefinitionOverridingCanBeEnabled() { assertThat(new SpringApplication(ExampleConfig.class, OverrideConfig.class) From 84a021e22fb783b072619c1a4bfce08a6a0c4cc8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 10:40:41 +0100 Subject: [PATCH 555/930] Prohibit upgrades to Jetty 12.1.9 Closes gh-50336 --- platform/spring-boot-dependencies/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 79cc4e59d3d5..7eef8d544948 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1124,6 +1124,10 @@ bom { contains ".alpha" because "we don't want alpha dependencies" } + prohibit { + versionRange "[12.1.9]" + because "it contains a regression (https://github.com/jetty/jetty.project/issues/15021)" + } group("org.eclipse.jetty.ee11") { bom("jetty-ee11-bom") } From 335060159159310db1bbfb7c1136dbfe9ac3af2a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:17:22 +0100 Subject: [PATCH 556/930] Upgrade to ActiveMQ 6.2.5 Closes gh-50337 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7eef8d544948..85647154fd98 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -31,7 +31,7 @@ bom { ] } } - library("ActiveMQ", "6.2.4") { + library("ActiveMQ", "6.2.5") { group("org.apache.activemq") { modules = [ "activemq-console", From bbf5c7a2ad4ddfaedbb91393e0ef3b7b4e529cd1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:17:26 +0100 Subject: [PATCH 557/930] Upgrade to Caffeine 3.2.4 Closes gh-50338 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 85647154fd98..eefdb2d1ee48 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -215,7 +215,7 @@ bom { releaseNotes("https://github.com/cache2k/cache2k/releases/tag/v{version}") } } - library("Caffeine", "3.2.3") { + library("Caffeine", "3.2.4") { group("com.github.ben-manes.caffeine") { modules = [ "caffeine", From 8b79b045e636e00fa9e0ffcd75d8a320cf48c83d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:17:31 +0100 Subject: [PATCH 558/930] Upgrade to Glassfish JAXB 4.0.8 Closes gh-50339 --- platform/spring-boot-dependencies/build.gradle | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index eefdb2d1ee48..54de6f71ffc6 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -484,11 +484,7 @@ bom { releaseNotes("https://github.com/git-commit-id/git-commit-id-maven-plugin/releases/tag/v{version}") } } - library("Glassfish JAXB", "4.0.6") { - prohibit { - versionRange "[4.0.7]" - because "it contains a regression (https://github.com/spring-projects/spring-boot/issues/49607#issuecomment-4072451614)" - } + library("Glassfish JAXB", "4.0.8") { alignWith { dependencyManagementDeclaredIn("org.glassfish.jaxb:jaxb-bom") { excluding { candidate -> candidate.classifier().equals("sources") } From f542e656ca7a050f6f876dde5589633415deffc0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:17:35 +0100 Subject: [PATCH 559/930] Upgrade to Groovy 5.0.6 Closes gh-50340 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 54de6f71ffc6..cde841177dd8 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -546,7 +546,7 @@ bom { releaseNotes("https://github.com/graphql-java/graphql-java/releases/tag/v{version}") } } - library("Groovy", "5.0.5") { + library("Groovy", "5.0.6") { prohibit { contains "-alpha-" because "we don't want alpha dependencies" From 311d10d5ee0e6b4936c2c7615c6dd1e8bd8fff1a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:17:40 +0100 Subject: [PATCH 560/930] Upgrade to Hibernate 7.2.13.Final Closes gh-50341 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index cde841177dd8..cfa1e3f5b339 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -635,7 +635,7 @@ bom { releaseNotes("https://github.com/hazelcast/hazelcast/releases/tag/v{version}") } } - library("Hibernate", "7.2.12.Final") { + library("Hibernate", "7.2.13.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From 92efcea6f69ce11d1195dc946d94a6447e07190a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:17:45 +0100 Subject: [PATCH 561/930] Upgrade to Infinispan 16.1.4 Closes gh-50342 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index cfa1e3f5b339..e98f1024dff0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -754,7 +754,7 @@ bom { ] } } - library("Infinispan", "16.1.3") { + library("Infinispan", "16.1.4") { group("org.infinispan") { bom("infinispan-bom") } From c1d017ef7eb0c721a8d0df6de10a1383f73de282 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:17:50 +0100 Subject: [PATCH 562/930] Upgrade to Jackson 2 Bom 2.21.3 Closes gh-50343 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2ab97ad352f3..47d8e8182e51 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ checkstyleToolVersion=10.12.4 commonsCodecVersion=1.21.0 graalVersion=25 hamcrestVersion=3.0 -jackson2Version=2.21.2 +jackson2Version=2.21.3 jacksonVersion=3.1.2 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 From b99aeb101c87a9f81ebf74f057c09ece099739e4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:17:55 +0100 Subject: [PATCH 563/930] Upgrade to Jackson Bom 3.1.3 Closes gh-50344 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 47d8e8182e51..7774630dc2a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ commonsCodecVersion=1.21.0 graalVersion=25 hamcrestVersion=3.0 jackson2Version=2.21.3 -jacksonVersion=3.1.2 +jacksonVersion=3.1.3 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 kotlinVersion=2.3.20 From 647a9dc768eb9cec3582f3c6803c83fb0a89a84e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:18:00 +0100 Subject: [PATCH 564/930] Upgrade to Jakarta XML Bind 4.0.5 Closes gh-50345 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e98f1024dff0..12ef9cd26195 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -972,7 +972,7 @@ bom { .formatted(version.major(), version.minor()), "jakarta.ws.rs") } } - library("Jakarta XML Bind", "4.0.4") { + library("Jakarta XML Bind", "4.0.5") { alignWith { version { from "org.glassfish.jaxb:jaxb-core" From 04c8f5bfae7eee04da3ed7fcc9be3d3341294b48 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:18:05 +0100 Subject: [PATCH 565/930] Upgrade to jOOQ 3.21.3 Closes gh-50346 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 12ef9cd26195..818991b13cb1 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1143,7 +1143,7 @@ bom { ] } } - library("jOOQ", "3.21.2") { + library("jOOQ", "3.21.3") { group("org.jooq") { bom("jooq-bom") plugins = [ From 6258ba37518a096e9c157d52aa7869628803012c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:18:09 +0100 Subject: [PATCH 566/930] Upgrade to Kotlin 2.3.21 Closes gh-50347 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7774630dc2a2..451c079e0a05 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ jackson2Version=2.21.3 jacksonVersion=3.1.3 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 -kotlinVersion=2.3.20 +kotlinVersion=2.3.21 mavenVersion=3.9.13 mockitoVersion=5.23.0 nativeBuildToolsVersion=1.1.0 From 319cc21bf4c3b151567190fce5dc398775498580 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:18:14 +0100 Subject: [PATCH 567/930] Upgrade to Netty 4.2.13.Final Closes gh-50348 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 818991b13cb1..ce89c9f469c0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1752,7 +1752,7 @@ bom { releaseNotes("https://github.com/neo4j/neo4j-java-driver/releases/tag/{version}") } } - library("Netty", "4.2.12.Final") { + library("Netty", "4.2.13.Final") { prohibit { contains ".Alpha" contains ".Beta" From 32698af56a6398207ceb630167c71266f0e586e2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:18:19 +0100 Subject: [PATCH 568/930] Upgrade to Postgresql 42.7.11 Closes gh-50349 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ce89c9f469c0..3601ea5a64d5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1862,7 +1862,7 @@ bom { javadoc("https://javadoc.io/doc/org.messaginghub/pooled-jms/{version}", "org.messaginghub.pooled.jms") } } - library("Postgresql", "42.7.10") { + library("Postgresql", "42.7.11") { group("org.postgresql") { modules = [ "postgresql" From fe5e196e65bc020c022011d80a9d4ca1b9539962 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:18:24 +0100 Subject: [PATCH 569/930] Upgrade to Pulsar 4.2.1 Closes gh-50350 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3601ea5a64d5..1f5d1d4c9651 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1894,7 +1894,7 @@ bom { releaseNotes("https://github.com/prometheus/client_java/releases/tag/parent-{version}") } } - library("Pulsar", "4.2.0") { + library("Pulsar", "4.2.1") { group("org.apache.pulsar") { bom("pulsar-bom") } From 38f76cc357a3410e01a5bd15a9788c0e3604f7d2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:18:28 +0100 Subject: [PATCH 570/930] Upgrade to R2DBC MySQL 1.4.2 Closes gh-50351 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1f5d1d4c9651..440110690943 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1993,7 +1993,7 @@ bom { releaseNotes("https://github.com/r2dbc/r2dbc-mssql/releases/tag/v{version}") } } - library("R2DBC MySQL", "1.4.1") { + library("R2DBC MySQL", "1.4.2") { group("io.asyncer") { modules = [ "r2dbc-mysql" From 85db5e41b3c4645dd5562ccbd847e7a67b5ea4bd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:18:33 +0100 Subject: [PATCH 571/930] Upgrade to SAAJ Impl 3.0.5 Closes gh-50352 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 440110690943..535ad53e1e9b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2440,7 +2440,7 @@ bom { .formatted(version.major(), version.minor())) } } - library("SAAJ Impl", "3.0.4") { + library("SAAJ Impl", "3.0.5") { group("com.sun.xml.messaging.saaj") { modules = [ "saaj-impl" From 94f0279d62d47c585aa8e4d267b3e805f9faeaa6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:18:38 +0100 Subject: [PATCH 572/930] Upgrade to SQLite JDBC 3.53.1.0 Closes gh-50353 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 535ad53e1e9b..140d44c99300 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2759,7 +2759,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ws/releases/tag/v{version}") } } - library("SQLite JDBC", "3.53.0.0") { + library("SQLite JDBC", "3.53.1.0") { group("org.xerial") { modules = [ "sqlite-jdbc" From 87182735830f84b3adccd346d1b4c1eee052b2d4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 May 2026 12:18:42 +0100 Subject: [PATCH 573/930] Upgrade to Tomcat 11.0.22 Closes gh-50354 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 451c079e0a05..3d2d78d6aa5c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,7 +23,7 @@ nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.6 springFrameworkVersion=7.0.7 springFramework60xVersion=6.0.23 -tomcatVersion=11.0.21 +tomcatVersion=11.0.22 protobufGradlePluginVersion=0.9.6 kotlin.stdlib.default.dependency=false From b23ad4804722b5fc4502293c82d8baa6a36a53a5 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 12 May 2026 08:55:38 -0700 Subject: [PATCH 574/930] Simplify ProtobufPluginAction `versionFromRuntimeClasspath` Simplify the stream logic to make use of `getAllComponents()` rather than using `getAllDependencies()` and `DependencyResult::getFrom`. See gh-50405 --- .../boot/gradle/plugin/ProtobufPluginAction.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java index 1ce24affa7f9..91d33bd8da9e 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java @@ -33,7 +33,6 @@ import org.gradle.api.artifacts.DependencyResolveDetails; import org.gradle.api.artifacts.ModuleVersionSelector; import org.gradle.api.artifacts.component.ModuleComponentIdentifier; -import org.gradle.api.artifacts.result.DependencyResult; import org.gradle.api.artifacts.result.ResolvedComponentResult; import org.jspecify.annotations.Nullable; @@ -141,9 +140,8 @@ private Optional versionFromRuntimeClasspath(Project project, Dependency .getByName("runtimeClasspath") .getIncoming() .getResolutionResult() - .getAllDependencies() + .getAllComponents() .stream() - .map(DependencyResult::getFrom) .map(ResolvedComponentResult::getId) .filter(ModuleComponentIdentifier.class::isInstance) .map(ModuleComponentIdentifier.class::cast) From 6d307ed2508bf695323d47db31fd505e54bb9fea Mon Sep 17 00:00:00 2001 From: Dongliang Xie Date: Tue, 12 May 2026 20:50:29 +0800 Subject: [PATCH 575/930] Align protoc version with protobuf-java Update `ProtobufPluginAction` to align the protoc version with `protobuf-java` rather than `protobuf-java-util`. The util jar is only required for server gRPC code. See gh-50405 Signed-off-by: Dongliang Xie --- .../gradle/plugin/ProtobufPluginAction.java | 3 ++- .../ProtobufPluginActionIntegrationTests.java | 19 +++++++++++++++++++ ...rotobufPluginActionIntegrationTests.gradle | 3 +-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java index 91d33bd8da9e..897c4eadacb1 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java @@ -41,6 +41,7 @@ * applied. * * @author Andy Wilkinson + * @author Dongliang Xie */ final class ProtobufPluginAction implements PluginApplicationAction { @@ -49,7 +50,7 @@ final class ProtobufPluginAction implements PluginApplicationAction { private static final Dependency grpcDependency = new Dependency("io.grpc", "protoc-gen-grpc-java"); private static final List versionAlignment = List.of( - protocDependency.alignVersionWith("com.google.protobuf", "protobuf-java-util"), + protocDependency.alignVersionWith("com.google.protobuf", "protobuf-java"), grpcDependency.alignVersionWith("io.grpc", "grpc-util")); @Override diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.java index 1bec7cf1c885..aef986a3f756 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.java @@ -16,6 +16,10 @@ package org.springframework.boot.gradle.plugin; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + import org.junit.jupiter.api.TestTemplate; import org.springframework.boot.gradle.junit.GradleCompatibility; @@ -27,6 +31,7 @@ * Integration tests for {@link ProtobufPluginAction}. * * @author Andy Wilkinson + * @author Dongliang Xie */ @GradleCompatibility class ProtobufPluginActionIntegrationTests { @@ -59,6 +64,20 @@ void alignsVersionOfProtocDependency() { .contains("com.google.protobuf:protoc:null -> 4.34.0"); } + @TestTemplate + void generatesProtoSourcesWhenProtobufJavaProvidesProtocVersion() throws IOException { + File proto = new File(this.gradleBuild.getProjectDir(), "src/main/proto/example.proto"); + proto.getParentFile().mkdirs(); + Files.writeString(proto.toPath(), """ + syntax = "proto3"; + option java_package = "com.example"; + message Example { + string name = 1; + } + """); + this.gradleBuild.build("generateProto"); + } + @TestTemplate void alignsVersionOfGrpcDependency() { assertThat(this.gradleBuild.build("dependencies", "--configuration", "protobufToolsLocator_grpc").getOutput()) diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle index 103a6ed72d48..5f71e4136572 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle @@ -29,7 +29,7 @@ repositories { } dependencies { - implementation("com.google.protobuf:protobuf-java-util:4.34.0") + implementation("com.google.protobuf:protobuf-java:4.34.0") implementation("io.grpc:grpc-util:1.79.0") } @@ -60,4 +60,3 @@ tasks.register("generateProtoTasksGrpcPluginOptions") { tasks.withType(com.google.protobuf.gradle.GenerateProtoTask).each { println "${it.name}: ${it.pluginsForCaching.collect { it.options }}" } } } - From 949d5e0f3f48e524f64155b6627f8fca1145233b Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Fri, 8 May 2026 10:51:24 +0800 Subject: [PATCH 576/930] Polish AppendableByteArray See gh-50369 Signed-off-by: Yanming Zhou --- .../org/springframework/boot/json/AppendableByteArray.java | 6 ++++-- .../springframework/boot/json/AppendableByteArrayTests.java | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java b/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java index dfdf1da4105c..a5f271352d28 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java @@ -25,6 +25,8 @@ import java.nio.charset.CoderResult; import java.nio.charset.CodingErrorAction; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -36,7 +38,7 @@ */ class AppendableByteArray implements Appendable { - private static ThreadLocal> cache = new ThreadLocal<>(); + private static final ThreadLocal<@Nullable SoftReference> cache = new ThreadLocal<>(); private static final int DEFAULT_INITIAL_SIZE = 8192; @@ -66,7 +68,7 @@ class AppendableByteArray implements Appendable { } @Override - public AppendableByteArray append(CharSequence charSequence, int start, int end) throws IOException { + public AppendableByteArray append(@Nullable CharSequence charSequence, int start, int end) throws IOException { return append(((charSequence != null) ? charSequence : "null").subSequence(start, end)); } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java b/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java index fb901bfcacad..22cb350b450a 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java @@ -36,7 +36,7 @@ */ class AppendableByteArrayTests { - private static String string = """ + private static final String string = """ This is a long(ish) string. At least it's longer that the initial size and the overflow size. We can write it out and test if the bytes match. From 0608a35e0db7cc519773247ec98064b127f04ba0 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 7 May 2026 08:13:09 -0700 Subject: [PATCH 577/930] Remove compatibility code for EOL dependency versions Remove NoSuchMethodError catch blocks and reflective fallbacks for dependency versions that are no longer supported See gh-50383 Signed-off-by: Sebastien Tardif --- .../HazelcastCacheMeterBinderProvider.java | 54 +------------------ ...azelcastCacheMeterBinderProviderTests.java | 13 ----- .../FlywayAutoConfiguration.java | 2 +- .../FlywayMigrationInitializer.java | 2 +- .../boot/jetty/GracefulShutdown.java | 43 ++++----------- ...bleReferenceClearingContextCustomizer.java | 10 +--- .../TomcatWebServerFactoryCustomizer.java | 8 +-- 7 files changed, 15 insertions(+), 117 deletions(-) diff --git a/module/spring-boot-cache/src/main/java/org/springframework/boot/cache/metrics/HazelcastCacheMeterBinderProvider.java b/module/spring-boot-cache/src/main/java/org/springframework/boot/cache/metrics/HazelcastCacheMeterBinderProvider.java index ac0f4558c8d9..dfb665364484 100644 --- a/module/spring-boot-cache/src/main/java/org/springframework/boot/cache/metrics/HazelcastCacheMeterBinderProvider.java +++ b/module/spring-boot-cache/src/main/java/org/springframework/boot/cache/metrics/HazelcastCacheMeterBinderProvider.java @@ -16,22 +16,10 @@ package org.springframework.boot.cache.metrics; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; - import com.hazelcast.spring.cache.HazelcastCache; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.binder.MeterBinder; import io.micrometer.core.instrument.binder.cache.HazelcastCacheMetrics; -import org.jspecify.annotations.Nullable; - -import org.springframework.aot.hint.ExecutableMode; -import org.springframework.aot.hint.RuntimeHints; -import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.boot.cache.metrics.HazelcastCacheMeterBinderProvider.HazelcastCacheMeterBinderProviderRuntimeHints; -import org.springframework.context.annotation.ImportRuntimeHints; -import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; /** * {@link CacheMeterBinderProvider} implementation for Hazelcast. @@ -39,51 +27,11 @@ * @author Stephane Nicoll * @since 4.0.0 */ -@ImportRuntimeHints(HazelcastCacheMeterBinderProviderRuntimeHints.class) public class HazelcastCacheMeterBinderProvider implements CacheMeterBinderProvider { @Override public MeterBinder getMeterBinder(HazelcastCache cache, Iterable tags) { - try { - return new HazelcastCacheMetrics(cache.getNativeCache(), tags); - } - catch (NoSuchMethodError ex) { - // Hazelcast 4 - return createHazelcast4CacheMetrics(cache, tags); - } - } - - private MeterBinder createHazelcast4CacheMetrics(HazelcastCache cache, Iterable tags) { - try { - Method nativeCacheAccessor = ReflectionUtils.findMethod(HazelcastCache.class, "getNativeCache"); - Assert.state(nativeCacheAccessor != null, "'nativeCacheAccessor' must not be null"); - Object nativeCache = ReflectionUtils.invokeMethod(nativeCacheAccessor, cache); - return HazelcastCacheMetrics.class.getConstructor(Object.class, Iterable.class) - .newInstance(nativeCache, tags); - } - catch (Exception ex) { - throw new IllegalStateException("Failed to create MeterBinder for Hazelcast", ex); - } - } - - static class HazelcastCacheMeterBinderProviderRuntimeHints implements RuntimeHintsRegistrar { - - @Override - public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { - try { - Method getNativeCacheMethod = ReflectionUtils.findMethod(HazelcastCache.class, "getNativeCache"); - Assert.state(getNativeCacheMethod != null, "Unable to find 'getNativeCache' method"); - Constructor constructor = HazelcastCacheMetrics.class.getConstructor(Object.class, Iterable.class); - hints.reflection() - .registerMethod(getNativeCacheMethod, ExecutableMode.INVOKE) - .registerConstructor(constructor, ExecutableMode.INVOKE); - } - catch (NoSuchMethodException ex) { - throw new IllegalStateException(ex); - } - - } - + return new HazelcastCacheMetrics(cache.getNativeCache(), tags); } } diff --git a/module/spring-boot-cache/src/test/java/org/springframework/boot/cache/metrics/HazelcastCacheMeterBinderProviderTests.java b/module/spring-boot-cache/src/test/java/org/springframework/boot/cache/metrics/HazelcastCacheMeterBinderProviderTests.java index 6f2aa704c403..544de3868193 100644 --- a/module/spring-boot-cache/src/test/java/org/springframework/boot/cache/metrics/HazelcastCacheMeterBinderProviderTests.java +++ b/module/spring-boot-cache/src/test/java/org/springframework/boot/cache/metrics/HazelcastCacheMeterBinderProviderTests.java @@ -24,10 +24,6 @@ import io.micrometer.core.instrument.binder.cache.HazelcastCacheMetrics; import org.junit.jupiter.api.Test; -import org.springframework.aot.hint.RuntimeHints; -import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; -import org.springframework.boot.cache.metrics.HazelcastCacheMeterBinderProvider.HazelcastCacheMeterBinderProviderRuntimeHints; - import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -51,13 +47,4 @@ void hazelcastCacheProvider() { assertThat(meterBinder).isInstanceOf(HazelcastCacheMetrics.class); } - @Test - void shouldRegisterHints() { - RuntimeHints runtimeHints = new RuntimeHints(); - new HazelcastCacheMeterBinderProviderRuntimeHints().registerHints(runtimeHints, getClass().getClassLoader()); - assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(HazelcastCache.class, "getNativeCache")) - .accepts(runtimeHints); - assertThat(RuntimeHintsPredicates.reflection().onType(HazelcastCacheMetrics.class)).accepts(runtimeHints); - } - } diff --git a/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/FlywayAutoConfiguration.java b/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/FlywayAutoConfiguration.java index 745548bf2da8..6f6aac5d9c6b 100644 --- a/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/FlywayAutoConfiguration.java +++ b/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/FlywayAutoConfiguration.java @@ -319,7 +319,7 @@ private void configureExecuteInTransaction(FluentConfiguration configuration, Fl map.from(properties.isExecuteInTransaction()).to(configuration::executeInTransaction); } catch (NoSuchMethodError ex) { - // Flyway < 9.14 + // Flyway < 10.x/11.x compatibility } } diff --git a/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/FlywayMigrationInitializer.java b/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/FlywayMigrationInitializer.java index 6db15d3577b0..765360beab3f 100644 --- a/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/FlywayMigrationInitializer.java +++ b/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/FlywayMigrationInitializer.java @@ -67,7 +67,7 @@ public void afterPropertiesSet() throws Exception { this.flyway.migrate(); } catch (NoSuchMethodError ex) { - // Flyway < 7.0 + // Flyway < 10.x/11.x compatibility this.flyway.getClass().getMethod("migrate").invoke(this.flyway); } } diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/GracefulShutdown.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/GracefulShutdown.java index 1742a119356f..3157a2fdce72 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/GracefulShutdown.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/GracefulShutdown.java @@ -16,8 +16,6 @@ package org.springframework.boot.jetty; -import java.lang.reflect.Method; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.function.Supplier; @@ -30,7 +28,6 @@ import org.springframework.boot.web.server.GracefulShutdownCallback; import org.springframework.boot.web.server.GracefulShutdownResult; import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; /** * Handles Jetty graceful shutdown. @@ -56,44 +53,22 @@ final class GracefulShutdown { void shutDownGracefully(GracefulShutdownCallback callback) { logger.info("Commencing graceful shutdown. Waiting for active requests to complete"); new Thread(() -> awaitShutdown(callback), "jetty-shutdown").start(); - boolean jetty10 = isJetty10(); for (Connector connector : this.server.getConnectors()) { - shutdown(connector, !jetty10); + shutdown(connector); } - } - @SuppressWarnings("unchecked") - private void shutdown(Connector connector, boolean getResult) { - Future result; + private void shutdown(Connector connector) { try { - result = connector.shutdown(); - } - catch (NoSuchMethodError ex) { - Method shutdown = ReflectionUtils.findMethod(connector.getClass(), "shutdown"); - Assert.state(shutdown != null, "'shutdown' must not be null"); - result = (Future) ReflectionUtils.invokeMethod(shutdown, connector); + Future result = connector.shutdown(); + Assert.state(result != null, "'result' must not be null"); + result.get(); } - if (getResult) { - try { - Assert.state(result != null, "'result' must not be null"); - result.get(); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - catch (ExecutionException ex) { - // Continue - } - } - } - - private boolean isJetty10() { - try { - return CompletableFuture.class.equals(Connector.class.getMethod("shutdown").getReturnType()); + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); } - catch (Exception ex) { - return false; + catch (ExecutionException ex) { + // Continue } } diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/DisableReferenceClearingContextCustomizer.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/DisableReferenceClearingContextCustomizer.java index 5f77f3e78970..c7a24cc324ab 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/DisableReferenceClearingContextCustomizer.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/DisableReferenceClearingContextCustomizer.java @@ -33,14 +33,8 @@ public void customize(Context context) { if (!(context instanceof StandardContext standardContext)) { return; } - try { - standardContext.setClearReferencesRmiTargets(false); - standardContext.setClearReferencesThreadLocals(false); - } - catch (NoSuchMethodError ex) { - // Earlier version of Tomcat (probably without - // setClearReferencesThreadLocals). Continue. - } + standardContext.setClearReferencesRmiTargets(false); + standardContext.setClearReferencesThreadLocals(false); } } diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/TomcatWebServerFactoryCustomizer.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/TomcatWebServerFactoryCustomizer.java index e14b4b57c385..3ffe57f0331c 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/TomcatWebServerFactoryCustomizer.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/TomcatWebServerFactoryCustomizer.java @@ -274,13 +274,7 @@ private void customizeRemoteIpValve(ConfigurableTomcatWebServerFactory factory) valve.setTrustedProxies(remoteIpProperties.getTrustedProxies()); // The internal proxies default to a list of "safe" internal IP addresses valve.setInternalProxies(remoteIpProperties.getInternalProxies()); - try { - valve.setHostHeader(remoteIpProperties.getHostHeader()); - } - catch (NoSuchMethodError ex) { - // Avoid failure with war deployments to Tomcat 8.5 before 8.5.44 and - // Tomcat 9 before 9.0.23 - } + valve.setHostHeader(remoteIpProperties.getHostHeader()); valve.setPortHeader(remoteIpProperties.getPortHeader()); valve.setProtocolHeaderHttpsValue(remoteIpProperties.getProtocolHeaderHttpsValue()); // ... so it's safe to add this valve by default. From 2052af876d215b1477a7b35e0e6c5afce6b49e82 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 May 2026 17:43:48 +0100 Subject: [PATCH 578/930] Upgrade to Hibernate 7.3.4.Final Closes gh-50430 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 01a70049bc40..c3d69a74535a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -635,7 +635,7 @@ bom { releaseNotes("https://github.com/hazelcast/hazelcast/releases/tag/v{version}") } } - library("Hibernate", "7.2.13.Final") { + library("Hibernate", "7.3.4.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From a329d7009e374971622021a8f77c4fd5b5d39118 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 May 2026 20:25:33 +0100 Subject: [PATCH 579/930] Fix graceful shutdown with Jetty Thanks to the misleadingly named isJetty10 method, 6b9ccdca unintentionally introduced a call to Future.get() that was only required with Jetty 9. The method probably should have been called isAtLeastJetty10. See gh-50383 --- .../boot/jetty/GracefulShutdown.java | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/GracefulShutdown.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/GracefulShutdown.java index 3157a2fdce72..4727087d2146 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/GracefulShutdown.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/GracefulShutdown.java @@ -16,8 +16,6 @@ package org.springframework.boot.jetty; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; import java.util.function.Supplier; import org.apache.commons.logging.Log; @@ -27,7 +25,6 @@ import org.springframework.boot.web.server.GracefulShutdownCallback; import org.springframework.boot.web.server.GracefulShutdownResult; -import org.springframework.util.Assert; /** * Handles Jetty graceful shutdown. @@ -59,17 +56,7 @@ void shutDownGracefully(GracefulShutdownCallback callback) { } private void shutdown(Connector connector) { - try { - Future result = connector.shutdown(); - Assert.state(result != null, "'result' must not be null"); - result.get(); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - catch (ExecutionException ex) { - // Continue - } + connector.shutdown(); } private void awaitShutdown(GracefulShutdownCallback callback) { From f2b69df77e28e85e536aac132d0be42079373ffa Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:11:17 +0100 Subject: [PATCH 580/930] Start building against Micrometer 1.17.0 snapshots See gh-50559 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index cc522f4eba99..490c6061b7a1 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1613,7 +1613,7 @@ bom { releaseNotes("https://github.com/apache/maven-war-plugin/releases/tag/maven-war-plugin-{version}") } } - library("Micrometer", "1.17.0-RC1") { + library("Micrometer", "1.17.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ From 8dfe2970895b4085f133ec399dba4d67e92d8b88 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:11:21 +0100 Subject: [PATCH 581/930] Start building against Micrometer Tracing 1.7.0 snapshots See gh-50560 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 490c6061b7a1..ce1e2fac9845 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1636,7 +1636,7 @@ bom { releaseNotes("https://github.com/micrometer-metrics/micrometer/releases/tag/v{version}") } } - library("Micrometer Tracing", "1.7.0-RC1") { + library("Micrometer Tracing", "1.7.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { bom("micrometer-tracing-bom") From 77baefba8dfd68ba4ab789e53b9b04deed77fd02 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:11:26 +0100 Subject: [PATCH 582/930] Start building against Reactor Bom 2025.0.6 snapshots See gh-50561 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ce1e2fac9845..87c7a3e633a2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2081,7 +2081,7 @@ bom { ] } } - library("Reactor Bom", "2025.0.5") { + library("Reactor Bom", "2025.0.6-SNAPSHOT") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From 2fb8eefcafc4a5bd154ec0f9125e390ccae0eeaf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:11:30 +0100 Subject: [PATCH 583/930] Start building against Spring AMQP 4.1.0 snapshots See gh-50562 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 87c7a3e633a2..24860b117c41 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2512,7 +2512,7 @@ bom { ] } } - library("Spring AMQP", "4.1.0-RC1") { + library("Spring AMQP", "4.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.amqp") { bom("spring-amqp-bom") From 2e7679a9aeeded4eb48a2d63861fe105625f5dfc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:11:35 +0100 Subject: [PATCH 584/930] Start building against Spring Batch 6.0.4 snapshots See gh-50563 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 24860b117c41..74c76aca2235 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2527,7 +2527,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-amqp/releases/tag/v{version}") } } - library("Spring Batch", "6.0.3") { + library("Spring Batch", "6.0.4-SNAPSHOT") { considerSnapshots() group("org.springframework.batch") { bom("spring-batch-bom") From 6ccca4a5f13cbe120954167ef9150d93526d01d9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:11:40 +0100 Subject: [PATCH 585/930] Start building against Spring Data Bom 2026.0.0 snapshots See gh-50564 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 74c76aca2235..42885462e20d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2542,7 +2542,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-batch/releases/tag/v{version}") } } - library("Spring Data Bom", "2026.0.0-RC1") { + library("Spring Data Bom", "2026.0.0-SNAPSHOT") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From 92a904b70d8cc243cc3647a7f82ff8b90fd6286c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:11:44 +0100 Subject: [PATCH 586/930] Start building against Spring Framework 7.0.8 snapshots See gh-50565 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3d2d78d6aa5c..83b6b51438de 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ mockitoVersion=5.23.0 nativeBuildToolsVersion=1.1.0 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.6 -springFrameworkVersion=7.0.7 +springFrameworkVersion=7.0.8-SNAPSHOT springFramework60xVersion=6.0.23 tomcatVersion=11.0.22 protobufGradlePluginVersion=0.9.6 From b5d4540cceb3fd1777c2f172cdc1ebdf338e2d51 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:11:49 +0100 Subject: [PATCH 587/930] Start building against Spring gRPC 1.1.0 snapshots See gh-50566 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 42885462e20d..8139ee33d8f0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2590,7 +2590,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-graphql/releases/tag/v{version}") } } - library("Spring gRPC", "1.1.0-RC1") { + library("Spring gRPC", "1.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.grpc") { modules = [ From 1883d162cab165d3d593cd7d317406839feb8704 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:11:53 +0100 Subject: [PATCH 588/930] Start building against Spring HATEOAS 3.1.0 snapshots See gh-50567 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8139ee33d8f0..6eb221bd8168 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2607,7 +2607,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-grpc/releases/tag/v{version}") } } - library("Spring HATEOAS", "3.1.0-RC1") { + library("Spring HATEOAS", "3.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.hateoas") { modules = [ From 61e477aac54bd7ef6604b2a2ae929548a38efd80 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:11:57 +0100 Subject: [PATCH 589/930] Start building against Spring Integration 7.1.0 snapshots See gh-50568 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6eb221bd8168..0961038ba9d2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2624,7 +2624,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-hateoas/releases/tag/{version}") } } - library("Spring Integration", "7.1.0-RC1") { + library("Spring Integration", "7.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.integration") { bom("spring-integration-bom") From d50d40aff91f004690e6e8b5f902f8fa33bac4db Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:12:02 +0100 Subject: [PATCH 590/930] Start building against Spring Kafka 4.1.0 snapshots See gh-50569 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0961038ba9d2..b02dc01a5a79 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2639,7 +2639,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-integration/releases/tag/v{version}") } } - library("Spring Kafka", "4.1.0-RC1") { + library("Spring Kafka", "4.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.kafka") { modules = [ From 8262f4b7b8e7babca3972a038e73396cc973151a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:12:07 +0100 Subject: [PATCH 591/930] Start building against Spring LDAP 4.1.0 snapshots See gh-50570 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b02dc01a5a79..ff38bbe83a7a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2657,7 +2657,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-kafka/releases/tag/v{version}") } } - library("Spring LDAP", "4.1.0-RC1") { + library("Spring LDAP", "4.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.ldap") { modules = [ From 21c3cfbac4c93e5da2ef05015bbdddd00b59ff09 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:12:11 +0100 Subject: [PATCH 592/930] Start building against Spring Pulsar 2.0.6 snapshots See gh-50571 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ff38bbe83a7a..9dbb0c3c438c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2677,7 +2677,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ldap/releases/tag/{version}") } } - library("Spring Pulsar", "2.0.5") { + library("Spring Pulsar", "2.0.6-SNAPSHOT") { considerSnapshots() group("org.springframework.pulsar") { bom("spring-pulsar-bom") From 297c4826b54cd958451ebdc527b067725f431e25 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:12:16 +0100 Subject: [PATCH 593/930] Start building against Spring RESTDocs 4.0.1 snapshots See gh-50572 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9dbb0c3c438c..43de1af7189c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2692,7 +2692,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-pulsar/releases/tag/v{version}") } } - library("Spring RESTDocs", "4.0.0") { + library("Spring RESTDocs", "4.0.1-SNAPSHOT") { considerSnapshots() group("org.springframework.restdocs") { bom("spring-restdocs-bom") From ad74de496fc807c31af821e2c70d611052abc496 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:12:20 +0100 Subject: [PATCH 594/930] Start building against Spring Security 7.1.0 snapshots See gh-50573 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 43de1af7189c..158accac4b7f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2707,7 +2707,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-restdocs/releases/tag/v{version}") } } - library("Spring Security", "7.1.0-RC1") { + library("Spring Security", "7.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { bom("spring-security-bom") From 3ed2cccc92558eb850cd7f2afc325298762ec7bf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:12:25 +0100 Subject: [PATCH 595/930] Start building against Spring Session 4.1.0 snapshots See gh-50574 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 158accac4b7f..8f2a46c70690 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2722,7 +2722,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-security/releases/tag/{version}") } } - library("Spring Session", "4.1.0-RC1") { + library("Spring Session", "4.1.0-SNAPSHOT") { considerSnapshots() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) From f0afce6abb0d6a0749690bfa45d385097b6961a3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:12:30 +0100 Subject: [PATCH 596/930] Start building against Spring WS 5.0.2 snapshots See gh-50575 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8f2a46c70690..f7b3eba2acd6 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2745,7 +2745,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-session/releases/tag/{version}") } } - library("Spring WS", "5.0.1") { + library("Spring WS", "5.0.2-SNAPSHOT") { considerSnapshots() group("org.springframework.ws") { bom("spring-ws-bom") From e30ab205aa31f74a09435e1b5ca21e62dd0a29a8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:19:00 +0100 Subject: [PATCH 597/930] Upgrade to Couchbase Client 3.11.3 Closes gh-50576 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index f7b3eba2acd6..785903218a36 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -312,7 +312,7 @@ bom { site("https://commons.apache.org/proper/commons-pool") } } - library("Couchbase Client", "3.11.2") { + library("Couchbase Client", "3.11.3") { group("com.couchbase.client") { modules = [ "java-client" From fb6be88593f786343449eb19b14afc638392881f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:19:05 +0100 Subject: [PATCH 598/930] Upgrade to Elasticsearch Client 9.4.1 Closes gh-50577 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 785903218a36..b2b6258a8008 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -401,7 +401,7 @@ bom { releaseNotes("https://github.com/ehcache/ehcache3/releases/tag/v{version}") } } - library("Elasticsearch Client", "9.3.4") { + library("Elasticsearch Client", "9.4.1") { prohibit { contains "-alpha" contains "-beta" From 41dbadeb6b7ba403f5631248d0e54d5d8e242ee1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:19:12 +0100 Subject: [PATCH 599/930] Upgrade to Hibernate 7.4.0.Final Closes gh-50578 --- .../metrics/HibernateMetricsAutoConfiguration.java | 2 +- platform/spring-boot-dependencies/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/autoconfigure/metrics/HibernateMetricsAutoConfiguration.java b/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/autoconfigure/metrics/HibernateMetricsAutoConfiguration.java index 3951ad2bb0e6..a46d76f75000 100644 --- a/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/autoconfigure/metrics/HibernateMetricsAutoConfiguration.java +++ b/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/autoconfigure/metrics/HibernateMetricsAutoConfiguration.java @@ -23,7 +23,7 @@ import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.PersistenceException; import org.hibernate.SessionFactory; -import org.hibernate.stat.HibernateMetrics; +import org.hibernate.orm.micrometer.HibernateMetrics; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b2b6258a8008..b4499a4f32a6 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -635,7 +635,7 @@ bom { releaseNotes("https://github.com/hazelcast/hazelcast/releases/tag/v{version}") } } - library("Hibernate", "7.3.4.Final") { + library("Hibernate", "7.4.0.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From 03a9977cd936928d3f87de95fd540aaad4ecd8d6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:19:16 +0100 Subject: [PATCH 600/930] Upgrade to Jaxen 2.0.3 Closes gh-50579 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b4499a4f32a6..91500666b59a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1035,7 +1035,7 @@ bom { ] } } - library("Jaxen", "2.0.1") { + library("Jaxen", "2.0.3") { group("jaxen") { modules = [ "jaxen" From e57aebdfbbec151ee805718fe4fa301232ffb31e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:19:22 +0100 Subject: [PATCH 601/930] Upgrade to jOOQ 3.21.4 Closes gh-50580 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 91500666b59a..2cf0124a6451 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1143,7 +1143,7 @@ bom { ] } } - library("jOOQ", "3.21.3") { + library("jOOQ", "3.21.4") { group("org.jooq") { bom("jooq-bom") plugins = [ From 622bfaed69bcd8c20df695446c4dd409bbc776b4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:19:27 +0100 Subject: [PATCH 602/930] Upgrade to Lettuce 7.5.2.RELEASE Closes gh-50581 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 2cf0124a6451..840b52f020cd 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1316,7 +1316,7 @@ bom { releaseNotes("https://github.com/Kotlin/kotlinx.serialization/releases/tag/v{version}") } } - library("Lettuce", "7.5.1.RELEASE") { + library("Lettuce", "7.5.2.RELEASE") { prohibit { contains ".BETA" because "we don't want betas" From 99fc624a4b47753d8cc98efa447b53fe3436959b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:19:31 +0100 Subject: [PATCH 603/930] Upgrade to Liquibase 5.0.3 Closes gh-50582 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 840b52f020cd..467ef70f970c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1340,7 +1340,7 @@ bom { releaseNotes("https://github.com/redis/lettuce/releases/tag/{version}") } } - library("Liquibase", "5.0.2") { + library("Liquibase", "5.0.3") { group("org.liquibase") { modules = [ "liquibase-cdi", From 7a9ab3e782d8778c1dba2dc6a1f2eaacee72dc8f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:19:36 +0100 Subject: [PATCH 604/930] Upgrade to Maven Enforcer Plugin 3.6.3 Closes gh-50583 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 467ef70f970c..e105096b0b0d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1480,7 +1480,7 @@ bom { releaseNotes("https://github.com/apache/maven-deploy-plugin/releases/tag/maven-deploy-plugin-{version}") } } - library("Maven Enforcer Plugin", "3.6.2") { + library("Maven Enforcer Plugin", "3.6.3") { group("org.apache.maven.plugins") { plugins = [ "maven-enforcer-plugin" From dcb928eea04005c1d07b9afdef730b1acf42f87c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:19:40 +0100 Subject: [PATCH 605/930] Upgrade to MongoDB 5.7.0 Closes gh-50584 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e105096b0b0d..9f7888b53646 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1658,7 +1658,7 @@ bom { releaseNotes("https://github.com/mockito/mockito/releases/tag/v{version}") } } - library("MongoDB", "5.7.0-beta1") { + library("MongoDB", "5.7.0") { alignWith { version { of "org.mongodb:mongodb-driver-core" From ded06ef964b2ac2ac66be376bebb2f029e7406fd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:19:44 +0100 Subject: [PATCH 606/930] Upgrade to Native Build Tools Plugin 1.1.1 Closes gh-50585 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 83b6b51438de..30aa3f0a755a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ junitJupiterVersion=6.0.3 kotlinVersion=2.3.21 mavenVersion=3.9.13 mockitoVersion=5.23.0 -nativeBuildToolsVersion=1.1.0 +nativeBuildToolsVersion=1.1.1 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.6 springFrameworkVersion=7.0.8-SNAPSHOT From b100dbd5a2b06ecd9033fbfaa02ebf0778382e82 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:19:49 +0100 Subject: [PATCH 607/930] Upgrade to Neo4j Java Driver 6.1.0 Closes gh-50586 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9f7888b53646..3de0adaf04cf 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1729,7 +1729,7 @@ bom { ] } } - library("Neo4j Java Driver", "6.0.5") { + library("Neo4j Java Driver", "6.1.0") { alignWith { version { of "org.neo4j.driver:neo4j-java-driver" From 300e3026f094d2f14a22a10cc184c82f3076bb60 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:19:55 +0100 Subject: [PATCH 608/930] Upgrade to Netty 4.2.14.Final Closes gh-50587 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3de0adaf04cf..debbc415e43c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1752,7 +1752,7 @@ bom { releaseNotes("https://github.com/neo4j/neo4j-java-driver/releases/tag/{version}") } } - library("Netty", "4.2.13.Final") { + library("Netty", "4.2.14.Final") { prohibit { contains ".Alpha" contains ".Beta" From 169442fd2f7eed856a0534d523f2c02f625acf2c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:20:00 +0100 Subject: [PATCH 609/930] Upgrade to OpenTelemetry 1.62.0 Closes gh-50588 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index debbc415e43c..7ff8076ac90e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1768,7 +1768,7 @@ bom { releaseNotes("https://github.com/netty/netty/releases/tag/netty-{version}") } } - library("OpenTelemetry", "1.60.1") { + library("OpenTelemetry", "1.62.0") { group("io.opentelemetry") { bom("opentelemetry-bom") } From f6ff1f43a2ce07876831d3c8a2e22ee01ff33c2c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:20:05 +0100 Subject: [PATCH 610/930] Upgrade to Protobuf Maven Plugin 5.1.4 Closes gh-50589 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7ff8076ac90e..a96784308926 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1842,7 +1842,7 @@ bom { releaseNotes("https://github.com/oracle/oracle-r2dbc/releases/tag/{version}") } } - library("Protobuf Maven Plugin", "5.1.3") { + library("Protobuf Maven Plugin", "5.1.4") { group("io.github.ascopes") { plugins = [ "protobuf-maven-plugin" From 5a9bf446f2eb8bc33cdbaaed3cc4be0a7730c3d2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:20:09 +0100 Subject: [PATCH 611/930] Upgrade to Protobuf Java 4.34.2 Closes gh-50590 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a96784308926..3fdf896331df 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1930,7 +1930,7 @@ bom { releaseNotes("https://github.com/googleapis/sdk-platform-java/releases/tag/v-{version}") } } - library("Protobuf Java", "4.34.1") { + library("Protobuf Java", "4.34.2") { group("com.google.protobuf") { bom("protobuf-bom") } From a23febf3faf81096892bc4bdb6a10a32bd3bd07d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:20:14 +0100 Subject: [PATCH 612/930] Upgrade to SLF4J 2.0.18 Closes gh-50591 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3fdf896331df..e17fb68e0d78 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2484,7 +2484,7 @@ bom { releaseNotes("https://github.com/sendgrid/sendgrid-java/releases/tag/{version}") } } - library("SLF4J", "2.0.17") { + library("SLF4J", "2.0.18") { prohibit { contains "-alpha" because "we don't want alphas" From dcf7f15e1d88c7d7dcd451e6ac2e61370f35e53f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 12:20:54 +0100 Subject: [PATCH 613/930] Change upgrade policy to same-minor-version --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e17fb68e0d78..6024148fe40b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -23,7 +23,7 @@ description = "Spring Boot Dependencies" bom { upgrade { - policy = "same-major-version" + policy = "same-minor-version" gitHub { issueLabels = [ "type: dependency-upgrade", From a573e121fb364cefda1ab401f343c4bbc128a422 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 14:10:20 +0100 Subject: [PATCH 614/930] fixup! Upgrade to Hibernate 7.4.0.Final --- .../org/springframework/boot/hibernate/SpringJtaPlatform.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/SpringJtaPlatform.java b/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/SpringJtaPlatform.java index 6342a1073184..ef824e3a6684 100644 --- a/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/SpringJtaPlatform.java +++ b/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/SpringJtaPlatform.java @@ -45,11 +45,13 @@ public SpringJtaPlatform(JtaTransactionManager transactionManager) { } @Override + @SuppressWarnings("NullAway") protected @Nullable TransactionManager locateTransactionManager() { return this.transactionManager.getTransactionManager(); } @Override + @SuppressWarnings("NullAway") protected @Nullable UserTransaction locateUserTransaction() { return this.transactionManager.getUserTransaction(); } From d6a23b72b887afd68e9a7097db1c004a65a06e90 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 May 2026 14:10:27 +0100 Subject: [PATCH 615/930] Deprecate SpringJtaPlatform This deprecation should have happened as part of c54d05c for #48969. Closes gh-50592 --- .../springframework/boot/hibernate/SpringJtaPlatform.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/SpringJtaPlatform.java b/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/SpringJtaPlatform.java index ef824e3a6684..36ed1fe2df60 100644 --- a/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/SpringJtaPlatform.java +++ b/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/SpringJtaPlatform.java @@ -21,6 +21,7 @@ import org.hibernate.engine.transaction.jta.platform.internal.AbstractJtaPlatform; import org.jspecify.annotations.Nullable; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.jta.JtaTransactionManager; import org.springframework.util.Assert; @@ -32,7 +33,11 @@ * @author Josh Long * @author Phillip Webb * @since 4.0.0 + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of @link + * {@link HibernateJpaVendorAdapter#setJtaTransactionManager(Object)} and + * {@link org.springframework.orm.jpa.hibernate.ConfigurableJtaPlatform} */ +@Deprecated(since = "4.1.0", forRemoval = true) public class SpringJtaPlatform extends AbstractJtaPlatform { private static final long serialVersionUID = 1L; From 7cf7d23f9db732165d83d98e857156ffb129bd2b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 29 May 2026 09:55:37 +0100 Subject: [PATCH 616/930] Upgrade to MongoDB 5.8.0 Closes gh-50608 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6024148fe40b..c77954e2dcc4 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1658,7 +1658,7 @@ bom { releaseNotes("https://github.com/mockito/mockito/releases/tag/v{version}") } } - library("MongoDB", "5.7.0") { + library("MongoDB", "5.8.0") { alignWith { version { of "org.mongodb:mongodb-driver-core" From 0a10070ce5cca63e18845e6193dcf158cb4f81a8 Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:37:51 -0700 Subject: [PATCH 617/930] Upgrade to ActiveMQ 6.2.6 Closes gh-50652 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c77954e2dcc4..50c9d970ed26 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -31,7 +31,7 @@ bom { ] } } - library("ActiveMQ", "6.2.5") { + library("ActiveMQ", "6.2.6") { group("org.apache.activemq") { modules = [ "activemq-console", From 672f36d053c56eafb0ff186bab128edc8aef4206 Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:37:55 -0700 Subject: [PATCH 618/930] Upgrade to Byte Buddy 1.18.9 Closes gh-50653 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 50c9d970ed26..64f45ca72d3c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -186,7 +186,7 @@ bom { releaseNotes("https://github.com/mojohaus/build-helper-maven-plugin/releases/tag/{version}") } } - library("Byte Buddy", "1.18.8") { + library("Byte Buddy", "1.18.9") { group("net.bytebuddy") { modules = [ "byte-buddy", From 9d60a76135e7e0cd4d1f63bf34df06a0d07a960d Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:38:00 -0700 Subject: [PATCH 619/930] Upgrade to Cassandra Driver 4.19.3 Closes gh-50654 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 64f45ca72d3c..78037cb6b2b5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -231,7 +231,7 @@ bom { releaseNotes("https://github.com/ben-manes/caffeine/releases/tag/v{version}") } } - library("Cassandra Driver", "4.19.2") { + library("Cassandra Driver", "4.19.3") { group("org.apache.cassandra") { bom("java-driver-bom") { permit("com.datastax.oss:native-protocol") From 6b98cf3071129ef1d57f7ec6b2d859b2b306b56f Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:38:06 -0700 Subject: [PATCH 620/930] Upgrade to Elasticsearch Client 9.4.2 Closes gh-50655 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 78037cb6b2b5..7368b0cffd4e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -401,7 +401,7 @@ bom { releaseNotes("https://github.com/ehcache/ehcache3/releases/tag/v{version}") } } - library("Elasticsearch Client", "9.4.1") { + library("Elasticsearch Client", "9.4.2") { prohibit { contains "-alpha" contains "-beta" From f7a89a77b53219a6c4fb77b606704c5fdd7a8d6a Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:38:12 -0700 Subject: [PATCH 621/930] Upgrade to Glassfish JAXB 4.0.9 Closes gh-50656 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7368b0cffd4e..166fe9174f41 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -484,7 +484,7 @@ bom { releaseNotes("https://github.com/git-commit-id/git-commit-id-maven-plugin/releases/tag/v{version}") } } - library("Glassfish JAXB", "4.0.8") { + library("Glassfish JAXB", "4.0.9") { alignWith { dependencyManagementDeclaredIn("org.glassfish.jaxb:jaxb-bom") { excluding { candidate -> candidate.classifier().equals("sources") } From 130acc0389ac658f1cce301de15770bde35affa4 Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:38:19 -0700 Subject: [PATCH 622/930] Upgrade to Jackson 2 Bom 2.21.4 Closes gh-50657 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 30aa3f0a755a..4749e85b085d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ checkstyleToolVersion=10.12.4 commonsCodecVersion=1.21.0 graalVersion=25 hamcrestVersion=3.0 -jackson2Version=2.21.3 +jackson2Version=2.21.4 jacksonVersion=3.1.3 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 From 24983e7f98a3808167fd5b8d2c89af3b5236b804 Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:38:25 -0700 Subject: [PATCH 623/930] Upgrade to Jackson Bom 3.1.4 Closes gh-50658 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4749e85b085d..4119558fc171 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ commonsCodecVersion=1.21.0 graalVersion=25 hamcrestVersion=3.0 jackson2Version=2.21.4 -jacksonVersion=3.1.3 +jacksonVersion=3.1.4 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 kotlinVersion=2.3.21 From 3a60ac71c2b7757243a615bac0fd7778a2bb3a62 Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:38:29 -0700 Subject: [PATCH 624/930] Upgrade to Jakarta Json Bind 3.0.2 Closes gh-50659 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 166fe9174f41..59f96dcaa1fd 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -854,7 +854,7 @@ bom { releaseNotes("https://github.com/jakartaee/jsonp-api/releases/tag/{version}-RELEASE") } } - library("Jakarta Json Bind", "3.0.1") { + library("Jakarta Json Bind", "3.0.2") { group("jakarta.json.bind") { modules = [ "jakarta.json.bind-api" From b5e32570ce376c34a37b2ef2d506cc46791eaefb Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:38:35 -0700 Subject: [PATCH 625/930] Upgrade to Jaxen 2.0.5 Closes gh-50660 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 59f96dcaa1fd..0039494fbe34 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1035,7 +1035,7 @@ bom { ] } } - library("Jaxen", "2.0.3") { + library("Jaxen", "2.0.5") { group("jaxen") { modules = [ "jaxen" From 4761d86dbbb1dbd77b08e31e84529f6f4eb13576 Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:38:40 -0700 Subject: [PATCH 626/930] Upgrade to Jetty 12.1.10 Closes gh-50661 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0039494fbe34..0e5348d39aef 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1115,7 +1115,7 @@ bom { ] } } - library("Jetty", "12.1.8") { + library("Jetty", "12.1.10") { prohibit { contains ".alpha" because "we don't want alpha dependencies" From ebdfa4189bce58715f06e16c31ea73db683f77e0 Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:38:44 -0700 Subject: [PATCH 627/930] Upgrade to Kafka 4.2.1 Closes gh-50662 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0e5348d39aef..ee81cab1b5e6 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1241,7 +1241,7 @@ bom { releaseNotes("https://junit.org/junit5/docs/{version}/release-notes") } } - library("Kafka", "4.2.0") { + library("Kafka", "4.2.1") { group("org.apache.kafka") { modules = [ "connect", From b858b772f4e49e3334e46db089bfe451a29623b3 Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:38:49 -0700 Subject: [PATCH 628/930] Upgrade to Logback 1.5.34 Closes gh-50663 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ee81cab1b5e6..6eea1b4131e2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1373,7 +1373,7 @@ bom { releaseNotes("https://github.com/apache/logging-log4j2/releases/tag/rel%2F{version}") } } - library("Logback", "1.5.32") { + library("Logback", "1.5.34") { group("ch.qos.logback") { modules = [ "logback-classic", From e6bd2cce0449e2e2fe64d8080c9adc13b066cdd1 Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:38:53 -0700 Subject: [PATCH 629/930] Upgrade to Maven Failsafe Plugin 3.5.6 Closes gh-50664 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6eea1b4131e2..73bd51ebc673 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1490,7 +1490,7 @@ bom { releaseNotes("https://github.com/apache/maven-enforcer/releases/tag/enforcer-{version}") } } - library("Maven Failsafe Plugin", "3.5.5") { + library("Maven Failsafe Plugin", "3.5.6") { group("org.apache.maven.plugins") { plugins = [ "maven-failsafe-plugin" From 6ef004ec156947c4148cc591b19f35605c95e36f Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:38:58 -0700 Subject: [PATCH 630/930] Upgrade to Maven Surefire Plugin 3.5.6 Closes gh-50665 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 73bd51ebc673..4a71a92598ca 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1593,7 +1593,7 @@ bom { releaseNotes("https://github.com/apache/maven-source-plugin/releases/tag/maven-source-plugin-{version}") } } - library("Maven Surefire Plugin", "3.5.5") { + library("Maven Surefire Plugin", "3.5.6") { group("org.apache.maven.plugins") { plugins = [ "maven-surefire-plugin" From b973777703a21c78ced4cc28154d35b2a63f180a Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:39:04 -0700 Subject: [PATCH 631/930] Upgrade to Netty 4.2.15.Final Closes gh-50666 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4a71a92598ca..fd7930d79e42 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1752,7 +1752,7 @@ bom { releaseNotes("https://github.com/neo4j/neo4j-java-driver/releases/tag/{version}") } } - library("Netty", "4.2.14.Final") { + library("Netty", "4.2.15.Final") { prohibit { contains ".Alpha" contains ".Beta" From f98f06388f21381d01b9e02039f0b89672eab130 Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:39:08 -0700 Subject: [PATCH 632/930] Upgrade to Oracle Database 23.26.2.0.0 Closes gh-50667 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index fd7930d79e42..ac109d51a86b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1791,7 +1791,7 @@ bom { releaseNotes("https://github.com/open-telemetry/opentelemetry-java/releases/tag/v{version}") } } - library("Oracle Database", "23.26.1.0.0") { + library("Oracle Database", "23.26.2.0.0") { alignWith { dependencyManagementDeclaredIn("com.oracle.database.jdbc:ojdbc-bom") } From 739daa7ba83b5f68da198d9049bccec96cbfb657 Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 09:52:02 -0700 Subject: [PATCH 633/930] Filter special purpose IPs (RFC 6890) in `externalAddresses()` Update `InetAddressFilter.externalAddresses()` to filter special purpose IP addresses as defined by RFC 6890. As a byproduct of this commit to additional factory methods (`specialPurpose()` and `multicast()`) have also been introduced. Closes gh-50668 --- .../boot/http/client/InetAddressFilter.java | 29 ++- .../http/client/InetAddressFilterTests.java | 216 ++++++++++++++++++ 2 files changed, 243 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/InetAddressFilter.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/InetAddressFilter.java index 2dfd54be6936..94ebc86ebe08 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/InetAddressFilter.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/InetAddressFilter.java @@ -185,12 +185,16 @@ default InetAddressFilter negate() { /** * Return a filter that will match external (non-private) IP addresses. External - * addresses are all non-{@link #internalAddresses() internal addresses} + * addresses are all {@link #routable() routable} addresses that are not: + *
      + *
    • {@link #multicast() multicast}
    • + *
    • {@link #specialPurpose() special purpose}
    • + *
    * @return a filter for external IP addresses * @see #internalAddresses() */ static InetAddressFilter externalAddresses() { - return routable().andNot(InternalInetAddressFilter.instance); + return routable().andNot(multicast(), specialPurpose()); } /** @@ -224,6 +228,27 @@ static InetAddressFilter routable() { }; } + /** + * Returns a filter that will match all multicast addresses. + * @return a filter for multicast IP addresses + */ + static InetAddressFilter multicast() { + return InetAddress::isMulticastAddress; + } + + /** + * Returns a filter that will match special purpose IP addresses as defined by + * RFC 6890. + * @return a filter for this network + */ + static InetAddressFilter specialPurpose() { + return of("0.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "192.0.0.0/24", "192.0.0.0/29", + "192.0.2.0/24", "192.88.99.0/24", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "240.0.0.0/4", + "255.255.255.255/32", "::1/128", "::/128", "64:ff9b::/96", "100::/64", "2001::/23", "2001::/32", + "2001:2::/48", "2001:db8::/32", "2001:10::/28", "2002::/16", "fc00::/7", "fe80::/10") + .or(InternalInetAddressFilter.instance); + } + /** * Return a filter that is the negation of all the given addresses. * @param addresses the addresses to negate in any form supported by diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/InetAddressFilterTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/InetAddressFilterTests.java index 3eb8d4089138..31106bacb945 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/InetAddressFilterTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/InetAddressFilterTests.java @@ -327,6 +327,65 @@ void ipv6NonRoutable() { assertThat(filter).doesNotMatch("::"); } + @Test + void ipv4CurrentLocalThisNetwork() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("0.0.0.4"); + } + + @Test + void ipv4Subnet() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("169.254.0.1"); + assertThat(filter).doesNotMatch("255.255.255.255"); + } + + @Test + void ipv4Documentation() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("192.0.2.1"); + assertThat(filter).doesNotMatch("203.0.113.1"); + } + + @Test + void ipv4Multicast() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("224.0.0.1"); + } + + @Test + void ipv4ReservedForFutureUse() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("240.0.0.1"); + } + + @Test + void ipv4SharedAddressSpace() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("100.64.0.1"); + assertThat(filter).doesNotMatch("100.127.255.254"); + } + + @Test + void ipv4PerformanceBenchmarking() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("198.18.0.1"); + assertThat(filter).doesNotMatch("198.19.255.255"); + } + + @Test + void ipv4AllHostsMulticastGroup() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("224.0.0.1"); + assertThat(filter).doesNotMatch("239.255.255.250"); + } + + @Test + void ipv6NodeLinkMulticastAddress() { + InetAddressFilter filter = InetAddressFilter.externalAddresses(); + assertThat(filter).doesNotMatch("ff02::1"); + } + } @Nested @@ -521,6 +580,163 @@ void routable() { } + @Nested + class Multicast { + + @Test + void multicast() { + InetAddressFilter filter = InetAddressFilter.multicast(); + assertThat(filter).matches("239.255.255.255"); + assertThat(filter).matches("FF00:0000:0000:0000:0000:0000:0000:0001"); + } + + } + + @Nested + class SpecialPurpose { + + @Test + void publicAddress() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).doesNotMatch("8.8.8.8"); + } + + @Test + void thisHostNetwork() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("0.0.0.1"); + } + + @Test + void privateUseNetwork() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("10.0.0.1"); + assertThat(filter).matches("172.16.0.1"); + assertThat(filter).matches("192.168.0.1"); + } + + @Test + void sharedAddressSpace() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("100.64.0.1"); + } + + @Test + void loopback() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("127.0.0.0"); + assertThat(filter).matches("::1"); + } + + @Test + void linkLocal() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("169.254.0.1"); + } + + @Test + void protocolAssignments() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("192.0.0.1"); + assertThat(filter).matches("2001:0000:0000:0000:0000:0000:0000:0001"); + } + + @Test + void dsLite() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("192.0.0.1"); + } + + @Test + void documentation() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("192.0.2.1"); + assertThat(filter).matches("198.51.100.1"); + assertThat(filter).matches("203.0.113.1"); + assertThat(filter).matches("2001:0db8:0000:0000:0000:0000:0000:0001"); + } + + @Test + void SixToFourRelay() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("192.88.99.1"); + } + + @Test + void deviceBenchmarking() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("198.18.0.1"); + assertThat(filter).matches("2001:0002:0000:0000:0000:0000:0000:0001"); + } + + @Test + void futureUse() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("240.0.0.1"); + } + + @Test + void limitedBroadcast() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("255.255.255.255"); + } + + @Test + void unspecifiedAddress() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("0000:0000:0000:0000:0000:0000:0000:0001"); + } + + @Test + void ipv4ToIpv6AddressTranslation() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("0064:ff9b:0000:0000:0000:0000:0000:0001"); + } + + @Test + void ipv4MappedAddress() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("0000:0000:0000:0000:0000:ffff:0000:0001"); + } + + @Test + void discardOnly() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("0100:0000:0000:0000:0000:0000:0000:0001"); + } + + @Test + void teredo() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("2001:0000:0000:0000:0000:0000:0000:0001"); + } + + @Test + void orchid() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("2001:10:0000:0000:0000:0000:0000:0001"); + } + + @Test + void sixToFour() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("2002:0000:0000:0000:0000:0000:0000:0001"); + } + + @Test + void uniqueLocal() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("fc00:0000:0000:0000:0000:0000:0000:0001"); + } + + @Test + void linkedScopedUnicast() { + InetAddressFilter filter = InetAddressFilter.specialPurpose(); + assertThat(filter).matches("fe80:0000:0000:0000:0000:0000:0000:0001"); + } + + } + @Nested class NotTests { From 3c9af65613e2b2d1d0e7b1e883ff14e01d38000f Mon Sep 17 00:00:00 2001 From: Dependabot Standalone Date: Tue, 2 Jun 2026 10:42:38 -0700 Subject: [PATCH 634/930] Revert "Upgrade to Oracle Database 23.26.2.0.0" This reverts commit f98f06388f21381d01b9e02039f0b89672eab130. See gh-50667 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ac109d51a86b..fd7930d79e42 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1791,7 +1791,7 @@ bom { releaseNotes("https://github.com/open-telemetry/opentelemetry-java/releases/tag/v{version}") } } - library("Oracle Database", "23.26.2.0.0") { + library("Oracle Database", "23.26.1.0.0") { alignWith { dependencyManagementDeclaredIn("com.oracle.database.jdbc:ojdbc-bom") } From 5c88b4453d0fba3f69353c6d1fa1b9042b35532b Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 2 Jun 2026 09:39:08 -0700 Subject: [PATCH 635/930] Upgrade to Oracle Database 23.26.2.0.0 Closes gh-50667 --- .../boot/jdbc/DataSourceBuilderTests.java | 20 ++++++++++++++----- ...OracleUcpDataSourceConfigurationTests.java | 14 ++++++++++--- ...nnectionDetailsBeanPostProcessorTests.java | 15 +++++++++++--- .../spring-boot-dependencies/build.gradle | 2 +- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java index 82891618719e..ff8dc53d8775 100644 --- a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java +++ b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java @@ -34,9 +34,11 @@ import com.zaxxer.hikari.HikariDataSource; import oracle.jdbc.internal.OpaqueString; import oracle.jdbc.pool.OracleDataSource; +import oracle.ucp.config.PoolPropertiesException; import oracle.ucp.jdbc.PoolDataSource; import oracle.ucp.jdbc.PoolDataSourceImpl; import org.apache.commons.dbcp2.BasicDataSource; +import org.assertj.core.util.introspection.FieldSupport; import org.h2.Driver; import org.h2.jdbcx.JdbcDataSource; import org.jspecify.annotations.Nullable; @@ -383,8 +385,9 @@ void buildWhenDerivedFromOracleDataSourceWithPasswordSetReturnsDataSource() thro .password("secret2") .build(); assertThat(built.getUser()).isEqualTo("test2"); - assertThat(built).extracting("password") - .extracting((opaque) -> ((OpaqueString) opaque).get()) + assertThat(built) + .extracting((target) -> FieldSupport.extraction().fieldValue("password", OpaqueString.class, target)) + .extracting(OpaqueString::get) .isEqualTo("secret2"); assertThat(built.getURL()).isEqualTo("example.com"); } @@ -398,12 +401,19 @@ void buildWhenDerivedFromOracleUcpWithPasswordSetReturnsDataSource() throws SQLE DataSourceBuilder builder = DataSourceBuilder.derivedFrom(dataSource); PoolDataSource built = (PoolDataSource) builder.username("test2").password("secret2").build(); assertThat(built.getUser()).isEqualTo("test2"); - assertThat(built).extracting("password") - .extracting((opaque) -> ((oracle.ucp.util.OpaqueString) opaque).get()) - .isEqualTo("secret2"); + assertThat(built).extracting(this::getPassword).isEqualTo("secret2"); assertThat(built.getURL()).isEqualTo("example.com"); } + private String getPassword(PoolDataSource target) { + try { + return ((oracle.ucp.util.OpaqueString) target.getPoolProperties().getPoolProperty("password")).get(); + } + catch (PoolPropertiesException ex) { + throw new IllegalStateException(ex); + } + } + @Test void buildWhenDerivedFromEmbeddedDatabase() { EmbeddedDatabase database = new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).build(); diff --git a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/autoconfigure/OracleUcpDataSourceConfigurationTests.java b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/autoconfigure/OracleUcpDataSourceConfigurationTests.java index 384c9dcac082..65e6e1dad5b5 100644 --- a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/autoconfigure/OracleUcpDataSourceConfigurationTests.java +++ b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/autoconfigure/OracleUcpDataSourceConfigurationTests.java @@ -21,6 +21,7 @@ import javax.sql.DataSource; +import oracle.ucp.config.PoolPropertiesException; import oracle.ucp.jdbc.PoolDataSource; import oracle.ucp.jdbc.PoolDataSourceImpl; import oracle.ucp.util.OpaqueString; @@ -123,13 +124,20 @@ void usesCustomJdbcConnectionDetailsWhenDefined() { assertThat(dataSource).isInstanceOf(PoolDataSourceImpl.class); PoolDataSourceImpl oracleUcp = (PoolDataSourceImpl) dataSource; assertThat(oracleUcp.getUser()).isEqualTo("user-1"); - assertThat(oracleUcp).extracting("password") - .extracting((o) -> ((OpaqueString) o).get()) - .isEqualTo("password-1"); + assertThat(oracleUcp).extracting(this::getPassword).isEqualTo("password-1"); assertThat(oracleUcp.getConnectionFactoryClassName()) .isEqualTo(DatabaseDriver.POSTGRESQL.getDriverClassName()); assertThat(oracleUcp.getURL()).isEqualTo("jdbc:customdb://customdb.example.com:12345/database-1"); }); } + private String getPassword(PoolDataSource target) { + try { + return ((OpaqueString) target.getPoolProperties().getPoolProperty("password")).get(); + } + catch (PoolPropertiesException ex) { + throw new IllegalStateException(ex); + } + } + } diff --git a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/autoconfigure/OracleUcpJdbcConnectionDetailsBeanPostProcessorTests.java b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/autoconfigure/OracleUcpJdbcConnectionDetailsBeanPostProcessorTests.java index a3b19c2c9af6..216938ec7b7a 100644 --- a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/autoconfigure/OracleUcpJdbcConnectionDetailsBeanPostProcessorTests.java +++ b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/autoconfigure/OracleUcpJdbcConnectionDetailsBeanPostProcessorTests.java @@ -18,6 +18,8 @@ import java.sql.SQLException; +import oracle.ucp.config.PoolPropertiesException; +import oracle.ucp.jdbc.PoolDataSource; import oracle.ucp.jdbc.PoolDataSourceImpl; import oracle.ucp.util.OpaqueString; import org.junit.jupiter.api.Test; @@ -49,11 +51,18 @@ void setUsernamePasswordUrlAndDriverClassName() throws SQLException { new TestJdbcConnectionDetails()); assertThat(dataSource.getURL()).isEqualTo("jdbc:customdb://customdb.example.com:12345/database-1"); assertThat(dataSource.getUser()).isEqualTo("user-1"); - assertThat(dataSource).extracting("password") - .extracting((password) -> ((OpaqueString) password).get()) - .isEqualTo("password-1"); + assertThat(dataSource).extracting(this::getPassword).isEqualTo("password-1"); assertThat(dataSource.getConnectionFactoryClassName()) .isEqualTo(DatabaseDriver.POSTGRESQL.getDriverClassName()); } + private String getPassword(PoolDataSource target) { + try { + return ((OpaqueString) target.getPoolProperties().getPoolProperty("password")).get(); + } + catch (PoolPropertiesException ex) { + throw new IllegalStateException(ex); + } + } + } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index fd7930d79e42..ac109d51a86b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1791,7 +1791,7 @@ bom { releaseNotes("https://github.com/open-telemetry/opentelemetry-java/releases/tag/v{version}") } } - library("Oracle Database", "23.26.1.0.0") { + library("Oracle Database", "23.26.2.0.0") { alignWith { dependencyManagementDeclaredIn("com.oracle.database.jdbc:ojdbc-bom") } From 1d5dc3e3c81216d2bf011d76ae10a5887728c116 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 2 Jun 2026 16:14:22 -0700 Subject: [PATCH 636/930] Remove Jetty upgrade restriction --- platform/spring-boot-dependencies/build.gradle | 4 ---- 1 file changed, 4 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ac109d51a86b..003b41d17a5c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1120,10 +1120,6 @@ bom { contains ".alpha" because "we don't want alpha dependencies" } - prohibit { - versionRange "[12.1.9]" - because "it contains a regression (https://github.com/jetty/jetty.project/issues/15021)" - } group("org.eclipse.jetty.ee11") { bom("jetty-ee11-bom") } From cd65e26773592102e2a7d9989138cbde85a1e749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 3 Jun 2026 17:00:09 +0200 Subject: [PATCH 637/930] Upgrade to Byte Buddy 1.18.10 Closes gh-50693 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 003b41d17a5c..a25b30214bed 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -186,7 +186,7 @@ bom { releaseNotes("https://github.com/mojohaus/build-helper-maven-plugin/releases/tag/{version}") } } - library("Byte Buddy", "1.18.9") { + library("Byte Buddy", "1.18.10") { group("net.bytebuddy") { modules = [ "byte-buddy", From a4af305983414efbb1b771775636fcb1869105fb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 3 Jun 2026 21:46:28 +0100 Subject: [PATCH 638/930] Revert "Upgrade to Netty 4.2.15.Final" This reverts commit b973777703a21c78ced4cc28154d35b2a63f180a. See gh-50666 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a25b30214bed..cadc81651fee 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1748,7 +1748,7 @@ bom { releaseNotes("https://github.com/neo4j/neo4j-java-driver/releases/tag/{version}") } } - library("Netty", "4.2.15.Final") { + library("Netty", "4.2.14.Final") { prohibit { contains ".Alpha" contains ".Beta" From 4beb168979965d4a6527060f5203fb66bebff755 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 12:20:23 +0100 Subject: [PATCH 639/930] Upgrade to Spring HATEOAS 3.1.1 Closes gh-50567 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a25b30214bed..07a4011362f7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2603,7 +2603,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-grpc/releases/tag/v{version}") } } - library("Spring HATEOAS", "3.1.0-SNAPSHOT") { + library("Spring HATEOAS", "3.1.1") { considerSnapshots() group("org.springframework.hateoas") { modules = [ From 0065fabf3f37d7eff0559f6b638afe8f1723883e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 12:24:12 +0100 Subject: [PATCH 640/930] Upgrade to Jaxen 2.0.6 Closes gh-50710 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 07a4011362f7..9f2d9faaf041 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1035,7 +1035,7 @@ bom { ] } } - library("Jaxen", "2.0.5") { + library("Jaxen", "2.0.6") { group("jaxen") { modules = [ "jaxen" From b6c4978be76ecdbeec1997904c204a6bb6d3343e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 12:24:17 +0100 Subject: [PATCH 641/930] Upgrade to Jetty Reactive HTTPClient 4.1.5 Closes gh-50711 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9f2d9faaf041..026d651b5200 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1108,7 +1108,7 @@ bom { releaseNotes("https://github.com/eclipse-ee4j/jersey/releases/tag/{version}") } } - library("Jetty Reactive HTTPClient", "4.1.4") { + library("Jetty Reactive HTTPClient", "4.1.5") { group("org.eclipse.jetty") { modules = [ "jetty-reactive-httpclient" From b556cab63215a4d9e27fc3152cb1b29192bc4afe Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 12:24:23 +0100 Subject: [PATCH 642/930] Upgrade to jOOQ 3.21.5 Closes gh-50712 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 026d651b5200..a1665a17cdfa 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1139,7 +1139,7 @@ bom { ] } } - library("jOOQ", "3.21.4") { + library("jOOQ", "3.21.5") { group("org.jooq") { bom("jooq-bom") plugins = [ From 09b9b80c168ac3dd72d3c9af1959e78eb6ba90da Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 12:24:23 +0100 Subject: [PATCH 643/930] Upgrade to Micrometer 1.17.0 Closes gh-50559 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a1665a17cdfa..8e8f70eada65 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1609,7 +1609,7 @@ bom { releaseNotes("https://github.com/apache/maven-war-plugin/releases/tag/maven-war-plugin-{version}") } } - library("Micrometer", "1.17.0-SNAPSHOT") { + library("Micrometer", "1.17.0") { considerSnapshots() group("io.micrometer") { modules = [ From 1551d7d6e4e253c8c830538b742ca6bf6393e188 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 12:24:24 +0100 Subject: [PATCH 644/930] Upgrade to Micrometer Tracing 1.7.0 Closes gh-50560 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8e8f70eada65..54bbae558379 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1632,7 +1632,7 @@ bom { releaseNotes("https://github.com/micrometer-metrics/micrometer/releases/tag/v{version}") } } - library("Micrometer Tracing", "1.7.0-SNAPSHOT") { + library("Micrometer Tracing", "1.7.0") { considerSnapshots() group("io.micrometer") { bom("micrometer-tracing-bom") From 94f4385681d91c269336f14c5e5f4a2a26ce2880 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 12:24:29 +0100 Subject: [PATCH 645/930] Upgrade to Pulsar 4.2.2 Closes gh-50713 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 54bbae558379..6a2714b3b442 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1891,7 +1891,7 @@ bom { releaseNotes("https://github.com/prometheus/client_java/releases/tag/parent-{version}") } } - library("Pulsar", "4.2.1") { + library("Pulsar", "4.2.2") { group("org.apache.pulsar") { bom("pulsar-bom") } From 6d3d0a85bc65f7a1c1bd5d990dceb3e1220c90ac Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 12:24:30 +0100 Subject: [PATCH 646/930] Upgrade to Reactor Bom 2025.0.6 Closes gh-50561 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6a2714b3b442..49c892cc1b5c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2077,7 +2077,7 @@ bom { ] } } - library("Reactor Bom", "2025.0.6-SNAPSHOT") { + library("Reactor Bom", "2025.0.6") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From f78c4720ee34b4dc711e848d64577d640eef6a26 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 12:24:36 +0100 Subject: [PATCH 647/930] Upgrade to SAAJ Impl 3.0.6 Closes gh-50714 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 49c892cc1b5c..0ddb6de9f131 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2437,7 +2437,7 @@ bom { .formatted(version.major(), version.minor())) } } - library("SAAJ Impl", "3.0.5") { + library("SAAJ Impl", "3.0.6") { group("com.sun.xml.messaging.saaj") { modules = [ "saaj-impl" From 70943099afa1ebb53f9027357812e03bc2342275 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 12:24:36 +0100 Subject: [PATCH 648/930] Upgrade to Spring Data Bom 2026.0.0 Closes gh-50564 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0ddb6de9f131..d12ae7c6a867 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2538,7 +2538,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-batch/releases/tag/v{version}") } } - library("Spring Data Bom", "2026.0.0-SNAPSHOT") { + library("Spring Data Bom", "2026.0.0") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From fd586df8c346930407145de78ac51bccbd4a9e6f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 12:24:37 +0100 Subject: [PATCH 649/930] Upgrade to Spring Framework 7.0.8 Closes gh-50565 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4119558fc171..33b55960aaac 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ mockitoVersion=5.23.0 nativeBuildToolsVersion=1.1.1 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.6 -springFrameworkVersion=7.0.8-SNAPSHOT +springFrameworkVersion=7.0.8 springFramework60xVersion=6.0.23 tomcatVersion=11.0.22 protobufGradlePluginVersion=0.9.6 From 424c72179c6b9af01d59b9ce49686723db3e49fc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 12:24:37 +0100 Subject: [PATCH 650/930] Upgrade to Spring LDAP 4.1.0 Closes gh-50570 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d12ae7c6a867..10d3dc2933df 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2653,7 +2653,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-kafka/releases/tag/v{version}") } } - library("Spring LDAP", "4.1.0-SNAPSHOT") { + library("Spring LDAP", "4.1.0") { considerSnapshots() group("org.springframework.ldap") { modules = [ From bac13dcb59a9e253f7154a2a0134863fc8acce44 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 12:24:38 +0100 Subject: [PATCH 651/930] Upgrade to Spring RESTDocs 4.0.1 Closes gh-50572 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 10d3dc2933df..6853f0228491 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2688,7 +2688,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-pulsar/releases/tag/v{version}") } } - library("Spring RESTDocs", "4.0.1-SNAPSHOT") { + library("Spring RESTDocs", "4.0.1") { considerSnapshots() group("org.springframework.restdocs") { bom("spring-restdocs-bom") From 58764001d275834c8bd5bfa4f1dcce57b0183773 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 12:24:43 +0100 Subject: [PATCH 652/930] Upgrade to SQLite JDBC 3.53.2.0 Closes gh-50715 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6853f0228491..c69e504814c6 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2756,7 +2756,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ws/releases/tag/v{version}") } } - library("SQLite JDBC", "3.53.1.0") { + library("SQLite JDBC", "3.53.2.0") { group("org.xerial") { modules = [ "sqlite-jdbc" From af2f956c7d307da8ab5613680115bde080ca76c7 Mon Sep 17 00:00:00 2001 From: leestana01 Date: Sat, 6 Jun 2026 01:18:46 +0900 Subject: [PATCH 653/930] Embedded LDAP SSL should not be enabled when its bundle is empty See gh-50700 Signed-off-by: leestana01 --- .../autoconfigure/embedded/EmbeddedLdapProperties.java | 3 ++- .../embedded/EmbeddedLdapAutoConfigurationTests.java | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java index f3a7303ee08a..c361ed7592d0 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java @@ -24,6 +24,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.convert.Delimiter; import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; /** * Configuration properties for Embedded LDAP. @@ -150,7 +151,7 @@ public static class Ssl { private @Nullable String bundle; public boolean isEnabled() { - return (this.enabled != null) ? this.enabled : this.bundle != null; + return (this.enabled != null) ? this.enabled : StringUtils.hasText(this.bundle); } public void setEnabled(boolean enabled) { diff --git a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java index 9830effb7abc..f25a153d33b9 100644 --- a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java +++ b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java @@ -411,6 +411,13 @@ void whenInvalidSslBundleIsConfiguredThenStartFails() { }); } + @Test + void sslIsNotEnabledWhenBundleIsEmpty() { + EmbeddedLdapProperties properties = new EmbeddedLdapProperties(); + properties.getSsl().setBundle(""); + assertThat(properties.getSsl().isEnabled()).isFalse(); + } + @Configuration(proxyBeanMethods = false) static class LdapClientConfiguration { From 971cdd364bfbce69047e639d697e2191b6e45c78 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 18:06:54 +0100 Subject: [PATCH 654/930] Upgrade to Hibernate 7.4.1.Final Closes gh-50732 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c69e504814c6..057716eec34c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -635,7 +635,7 @@ bom { releaseNotes("https://github.com/hazelcast/hazelcast/releases/tag/v{version}") } } - library("Hibernate", "7.4.0.Final") { + library("Hibernate", "7.4.1.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From 09dd4f8a2b6c03d7c97ff601bab3ff8cc2a5a39e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 18:06:54 +0100 Subject: [PATCH 655/930] Upgrade to Spring AMQP 4.1.0 Closes gh-50562 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 057716eec34c..307d77afcdf2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2508,7 +2508,7 @@ bom { ] } } - library("Spring AMQP", "4.1.0-SNAPSHOT") { + library("Spring AMQP", "4.1.0") { considerSnapshots() group("org.springframework.amqp") { bom("spring-amqp-bom") From c99f475a7ae94acb48d1bd074f585de1c48a88d0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 18:06:55 +0100 Subject: [PATCH 656/930] Upgrade to Spring gRPC 1.1.0 Closes gh-50566 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 307d77afcdf2..fe87daf57b18 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2586,7 +2586,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-graphql/releases/tag/v{version}") } } - library("Spring gRPC", "1.1.0-SNAPSHOT") { + library("Spring gRPC", "1.1.0") { considerSnapshots() group("org.springframework.grpc") { modules = [ From 3c3ed2292ca66667540ae66a545f489bc137a94c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 18:06:55 +0100 Subject: [PATCH 657/930] Upgrade to Spring Pulsar 2.0.6 Closes gh-50571 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index fe87daf57b18..dfb48582a270 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2673,7 +2673,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ldap/releases/tag/{version}") } } - library("Spring Pulsar", "2.0.6-SNAPSHOT") { + library("Spring Pulsar", "2.0.6") { considerSnapshots() group("org.springframework.pulsar") { bom("spring-pulsar-bom") From 7afa2de484593e20b6aa276d11e60c2f997c83b5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 18:06:56 +0100 Subject: [PATCH 658/930] Upgrade to Spring Security 7.1.0 Closes gh-50573 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index dfb48582a270..1675639cbf0a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2703,7 +2703,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-restdocs/releases/tag/v{version}") } } - library("Spring Security", "7.1.0-SNAPSHOT") { + library("Spring Security", "7.1.0") { considerSnapshots() group("org.springframework.security") { bom("spring-security-bom") From 20371050ba48a1e1f7c9d73802aca5128676fdf4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jun 2026 20:48:35 +0100 Subject: [PATCH 659/930] Upgrade to Spring Kafka 4.1.0 Closes gh-50569 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1675639cbf0a..a83feaa59d34 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2635,7 +2635,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-integration/releases/tag/v{version}") } } - library("Spring Kafka", "4.1.0-SNAPSHOT") { + library("Spring Kafka", "4.1.0") { considerSnapshots() group("org.springframework.kafka") { modules = [ From 53ddba743a03505b6a43f041bc8740c7001cd65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 10 Jun 2026 07:57:04 +0200 Subject: [PATCH 660/930] Upgrade to Spring Session 4.1.0 Closes gh-50574 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a83feaa59d34..1b0ea3a980d9 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2718,7 +2718,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-security/releases/tag/{version}") } } - library("Spring Session", "4.1.0-SNAPSHOT") { + library("Spring Session", "4.1.0") { considerSnapshots() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) From 268c5ad1737fcb43b752dd820f2271994bf692e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 10 Jun 2026 07:57:04 +0200 Subject: [PATCH 661/930] Upgrade to Spring WS 5.0.2 Closes gh-50575 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1b0ea3a980d9..4cacc16ef3b8 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2741,7 +2741,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-session/releases/tag/{version}") } } - library("Spring WS", "5.0.2-SNAPSHOT") { + library("Spring WS", "5.0.2") { considerSnapshots() group("org.springframework.ws") { bom("spring-ws-bom") From e1f6b96b30cfef8619209c587c0974c9194a6718 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 10 Jun 2026 12:46:22 +0100 Subject: [PATCH 662/930] Upgrade to Spring GraphQL 2.0.4 Closes gh-50741 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4cacc16ef3b8..1a3ac4eeafd5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2568,7 +2568,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-framework/releases/tag/v{version}") } } - library("Spring GraphQL", "2.0.4-SNAPSHOT") { + library("Spring GraphQL", "2.0.4") { considerSnapshots() group("org.springframework.graphql") { modules = [ From 6a6dedc4354cbafadaf8aab2803fc3acd7e6b8de Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 10 Jun 2026 15:57:08 +0100 Subject: [PATCH 663/930] Upgrade to Spring Integration 7.1.0 Closes gh-50568 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1a3ac4eeafd5..85f8543b5e6f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2620,7 +2620,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-hateoas/releases/tag/{version}") } } - library("Spring Integration", "7.1.0-SNAPSHOT") { + library("Spring Integration", "7.1.0") { considerSnapshots() group("org.springframework.integration") { bom("spring-integration-bom") From 1a5815cf3183a0c9493cece8ba550a6e8b125d67 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 10 Jun 2026 16:21:24 +0100 Subject: [PATCH 664/930] Upgrade to Spring Batch 6.0.4 Closes gh-50563 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 85f8543b5e6f..4dffef741575 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2523,7 +2523,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-amqp/releases/tag/v{version}") } } - library("Spring Batch", "6.0.4-SNAPSHOT") { + library("Spring Batch", "6.0.4") { considerSnapshots() group("org.springframework.batch") { bom("spring-batch-bom") From 8ecd35690bf458528556a9feeecd00e4103543f0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 10 Jun 2026 22:05:45 +0100 Subject: [PATCH 665/930] Next development version (v4.1.1-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 33b55960aaac..fe6d51f268f6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=4.1.0-SNAPSHOT +version=4.1.1-SNAPSHOT latestVersion=true spring.build-type=oss From a7f5c796c96515ca120d2dd09f362b55b848217c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sat, 13 Jun 2026 21:26:02 +0100 Subject: [PATCH 666/930] Stop auto-config of SpringReactiveOpaqueTokenIntrospector w/o WebFlux SpringReactiveOpaqueTokenIntrospector uses WebClient in its implementation so the auto-configuration of it must back off when spring-webflux is not on the classpath. Failing to do so results in a ClassNotFoundException for WebFlux's BodyInserters if the auto-configuration's other conditions are met. Fixes gh-50764 --- ...OpaqueTokenIntrospectionClientConfiguration.java | 3 +++ ...eOAuth2ResourceServerAutoConfigurationTests.java | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java index 0cbf30ee2d49..1f1b4e86a3ba 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOpaqueTokenIntrospectionClientConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive; 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.ConditionalOnProperty; import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties; @@ -25,6 +26,7 @@ import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector; import org.springframework.util.Assert; +import org.springframework.web.reactive.function.client.WebClient; /** * Configures an {@link ReactiveOpaqueTokenIntrospector} when a token introspection @@ -34,6 +36,7 @@ */ @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(ReactiveOpaqueTokenIntrospector.class) +@ConditionalOnClass(WebClient.class) class ReactiveOpaqueTokenIntrospectionClientConfiguration { @Bean diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index a4206c8da860..a0d5afaaebda 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -53,6 +53,7 @@ import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.boot.testsupport.classpath.resources.WithResource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -431,6 +432,18 @@ void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionC }); } + @Test + @ClassPathExclusions("spring-webflux-*.jar") + void withoutWebFluxWhenIntrospectionUriIsAvailableNoTokenIntrospectorIsConfigured() { + new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactiveOAuth2ResourceServerAutoConfiguration.class)) + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id", + "spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret") + .run((context) -> assertThat(context).doesNotHaveBean(ReactiveOpaqueTokenIntrospector.class)); + } + private void assertSpringReactiveOpaqueTokenIntrospectorBuilderCustomization( AssertableReactiveWebApplicationContext context) { SpringReactiveOpaqueTokenIntrospectorBuilderCustomizer customizer = context.getBean( From d607b4c226ebd452dee73d1221fdf266ef38fce2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Jun 2026 14:15:36 +0100 Subject: [PATCH 667/930] Remove dependency management for Protobuf Common Protos Boot doesn't interact with the dependency directly so we should not manage its version. Instead, we should allow the version that comes in transitively through gRPC Java to be used instead. Closes gh-50772 --- platform/spring-boot-dependencies/build.gradle | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4dffef741575..96a9d855c55d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1915,17 +1915,6 @@ bom { releaseNotes("https://qpid.apache.org/releases/qpid-protonj2-{version}/release-notes.html") } } - library("Protobuf Common Protos", "2.70.0") { - group("com.google.api.grpc") { - modules = [ - "proto-google-common-protos" - ] - } - links { - github("https://github.com/googleapis/sdk-platform-java") - releaseNotes("https://github.com/googleapis/sdk-platform-java/releases/tag/v-{version}") - } - } library("Protobuf Java", "4.34.2") { group("com.google.protobuf") { bom("protobuf-bom") From 4065e767bf45daeeadf8f77e40e28df27e83491f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Jun 2026 14:16:11 +0100 Subject: [PATCH 668/930] Configure Bomr to align gRPC dependency versions with Spring gRPC Closes gh-50769 --- .../springframework/boot/build/bom/Library.java | 3 +++ gradle.properties | 1 + platform/spring-boot-dependencies/build.gradle | 14 +++++++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java index 0e1b335772da..6140b835ee30 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java @@ -664,6 +664,9 @@ private Library findManagingLibrary() { } private List getBomDependencies(Library manager) { + if (manager == null) { + return Collections.emptyList(); + } return manager.getGroups() .stream() .flatMap((group) -> group.getBoms() diff --git a/gradle.properties b/gradle.properties index fe6d51f268f6..add296dbf695 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,6 +21,7 @@ mockitoVersion=5.23.0 nativeBuildToolsVersion=1.1.1 nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.6 +springGrpcVersion=1.1.0 springFrameworkVersion=7.0.8 springFramework60xVersion=6.0.23 tomcatVersion=11.0.22 diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 96a9d855c55d..1c7338f3645e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -562,6 +562,12 @@ bom { group("io.grpc") { bom("grpc-bom") } + alignWith { + version { + of "io.grpc:grpc-core" + from "org.springframework.grpc:spring-grpc-core" + } + } links { github("https://github.com/grpc/grpc-java") docs("https://grpc.io/docs/languages/java/") @@ -1919,6 +1925,12 @@ bom { group("com.google.protobuf") { bom("protobuf-bom") } + alignWith { + property { + name "protobuf-java.version" + of "org.springframework.grpc:spring-grpc-dependencies:$springGrpcVersion" + } + } links { site("https://protobuf.dev") github("https://github.com/protocolbuffers/protobuf") @@ -2575,7 +2587,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-graphql/releases/tag/v{version}") } } - library("Spring gRPC", "1.1.0") { + library("Spring gRPC", "${springGrpcVersion}") { considerSnapshots() group("org.springframework.grpc") { modules = [ From 2cbb5bfb17992d0d17a131cec2dd369b784aee76 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Jun 2026 14:19:30 +0100 Subject: [PATCH 669/930] Upgrade to Protobuf Java 4.35.0 Closes gh-50770 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1c7338f3645e..cc7d15479c6a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1921,7 +1921,7 @@ bom { releaseNotes("https://qpid.apache.org/releases/qpid-protonj2-{version}/release-notes.html") } } - library("Protobuf Java", "4.34.2") { + library("Protobuf Java", "4.35.0") { group("com.google.protobuf") { bom("protobuf-bom") } From 699d9c34c3e2ead02b704dc7c1c1d1747684b99b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Jun 2026 14:19:53 +0100 Subject: [PATCH 670/930] Upgrade to Grpc Java 1.81.0 Closes gh-50771 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index cc7d15479c6a..1c069809d033 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -558,7 +558,7 @@ bom { site("https://groovy-lang.org") } } - library("Grpc Java", "1.80.0") { + library("Grpc Java", "1.81.0") { group("io.grpc") { bom("grpc-bom") } From 2edca39504f8e9c2943262d3e60ba34b0e7751ac Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jun 2026 06:44:48 +0100 Subject: [PATCH 671/930] Align Prometheus Client dependency with Micrometer Closes gh-50780 --- platform/spring-boot-dependencies/build.gradle | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1c069809d033..8818eff7c371 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1877,10 +1877,17 @@ bom { releaseNotes("https://github.com/pgjdbc/pgjdbc/releases/tag/REL{version}") } } - library("Prometheus Client", "1.5.1") { + library("Prometheus Client", "1.7.0") { group("io.prometheus") { bom("prometheus-metrics-bom") } + alignWith { + version { + of "io.prometheus:prometheus-metrics-core" + from "io.micrometer:micrometer-registry-prometheus" + managedBy "Micrometer" + } + } links { site("https://github.com/prometheus/client_java") javadoc("prometheus-metrics-tracer-common", (version) -> "https://javadoc.io/doc/io.prometheus/prometheus-metrics-tracer-common/%s".formatted(version), "io.prometheus.metrics.tracer.common") From 485bd7c6367147f9387545f30d3346bdef93fca9 Mon Sep 17 00:00:00 2001 From: Ahmed El amraouiyine Date: Mon, 15 Jun 2026 22:25:31 +0100 Subject: [PATCH 672/930] Bind empty strings to empty maps Align `MapBinder` with `IndexedElementsBinder` to ensure that empty strings are bound as empty `Map` instances rather than throwing a `ConverterNotFoundException`. See gh-50773 Signed-off-by: Ahmed El amraouiyine --- .../context/properties/bind/MapBinder.java | 4 ++++ .../properties/bind/MapBinderTests.java | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java index f1e57fa32eee..a8aa438edf6a 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java @@ -33,6 +33,7 @@ import org.springframework.boot.context.properties.source.IterableConfigurationPropertySource; import org.springframework.core.CollectionFactory; import org.springframework.core.ResolvableType; +import org.springframework.util.ObjectUtils; /** * {@link AggregateBinder} for Maps. @@ -64,6 +65,9 @@ protected boolean isAllowRecursiveBinding(@Nullable ConfigurationPropertySource if (property != null) { getContext().setConfigurationProperty(property); Object result = getContext().getPlaceholdersResolver().resolvePlaceholders(property.getValue()); + if (ObjectUtils.isEmpty(result)) { + return createMap(target); + } return getContext().getConverter().convert(result, target); } } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java index 7b591d785a14..1cd946356043 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java @@ -274,6 +274,15 @@ void bindToMapWhenNoValueShouldReturnUnbound() { assertThat(result.isBound()).isFalse(); } + @Test + void bindToMapWhenEmptyStringShouldReturnEmptyMap() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo", ""); + this.sources.add(source); + Map result = this.binder.bind("foo", STRING_STRING_MAP).get(); + assertThat(result).isEmpty(); + } + @Test void bindToMapShouldConvertKey() { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); @@ -510,6 +519,17 @@ void nestedMapsShouldNotBindToNull() { assertThat(foo2.getValue()).isEqualTo("three"); } + @Test + void nestedMapsWhenEmptyStringShouldReturnEmptyMap() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value", "one"); + source.put("foo.foos", ""); + this.sources.add(source); + BindResult foo = this.binder.bind("foo", NestableFoo.class); + assertThat(foo.get().getValue()).isEqualTo("one"); + assertThat(foo.get().getFoos()).isEmpty(); + } + @Test void bindToMapWithCustomConverter() { DefaultConversionService conversionService = new DefaultConversionService(); From 12eb46696aa65e0e8dc11ed305bd4685110c127b Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sat, 20 Jun 2026 19:43:51 -0700 Subject: [PATCH 673/930] Polish 'Bind empty strings to empty maps' See gh-50773 --- .../boot/context/properties/bind/MapBinder.java | 4 ++-- .../boot/context/properties/bind/MapBinderTests.java | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java index a8aa438edf6a..db40be47d8c0 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java @@ -33,13 +33,13 @@ import org.springframework.boot.context.properties.source.IterableConfigurationPropertySource; import org.springframework.core.CollectionFactory; import org.springframework.core.ResolvableType; -import org.springframework.util.ObjectUtils; /** * {@link AggregateBinder} for Maps. * * @author Phillip Webb * @author Madhura Bhave + * @author Ahmed El Amraouiyine */ class MapBinder extends AggregateBinder> { @@ -65,7 +65,7 @@ protected boolean isAllowRecursiveBinding(@Nullable ConfigurationPropertySource if (property != null) { getContext().setConfigurationProperty(property); Object result = getContext().getPlaceholdersResolver().resolvePlaceholders(property.getValue()); - if (ObjectUtils.isEmpty(result)) { + if (result instanceof CharSequence charSequence && charSequence.isEmpty()) { return createMap(target); } return getContext().getConverter().convert(result, target); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java index 1cd946356043..3e6165472279 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java @@ -65,6 +65,7 @@ * * @author Phillip Webb * @author Madhura Bhave + * @author Ahmed El Amraouiyine */ class MapBinderTests { From 7483278e7d3e9b946f950b4cfb27204efe3bc24a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 24 Jun 2026 08:58:17 +0100 Subject: [PATCH 674/930] Only apply grpc defaults when grpc plugin is configured Previously, Spring Boot's Gradle plugin reacted to the Protobuf plugin by configuring Protobuf's grpc plugin. This worked well for projects using gRPC but went too far for those using Protobuf without gRPC. This commit dials the support back a little bit. Now, our recommended gRPC defaults are only applied when Protobuf is configured with the grpc plugin. Closes gh-50822 --- .../antora/modules/gradle-plugin/pages/reacting.adoc | 5 ++++- .../boot/gradle/plugin/ProtobufPluginAction.java | 10 ++++++---- .../plugin/ProtobufPluginActionIntegrationTests.gradle | 6 ++++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc index 5ccd7f80e3f2..33558dd5a706 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc @@ -113,5 +113,8 @@ When the {url-cyclonedx-docs-gradle-plugin}[CycloneDX plugin] is applied to a pr When the {url-protobuf-docs-gradle-plugin}[Protobuf plugin] is applied to a project, the Spring Boot plugin: . Configures `protoc` to use the artifact `com.google.protobuf:protoc`, aligning its version with that of Protobuf dependencies on the runtime classpath. -. Configures the `grpc` plugin to use the artifact `io.grpc:protoc-gen-grpc-java`, aligning its version with that of gRPC dependencies on the runtime classpath. + +Additionally, when the Protobuf `grpc` plugin is configured, the Spring Boot plugin: + +. Configures it to use the artifact `io.grpc:protoc-gen-grpc-java`, aligning its version with that of gRPC dependencies on the runtime classpath. . Configures the `grpc` plugin of all generate proto tasks with the option `@generated=omit`. diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java index 897c4eadacb1..338bd21b451d 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java @@ -62,8 +62,7 @@ public Class> getPluginClass() { public void execute(Project project) { ProtobufExtension protobuf = project.getExtensions().getByType(ProtobufExtension.class); protobuf.protoc(this::configureProtoc); - protobuf.plugins(this::configurePlugins); - protobuf.generateProtoTasks(this::configureGenerateProtoTasks); + protobuf.plugins((plugins) -> configurePlugins(plugins, protobuf)); project.getConfigurations() .named(this::isProtobufToolsLocator) .configureEach((configuration) -> configureProtobufToolsLocator(project, configuration)); @@ -73,8 +72,11 @@ private void configureProtoc(ExecutableLocator protoc) { protoc.setArtifact(protocDependency.asDependencySpec()); } - private ExecutableLocator configurePlugins(NamedDomainObjectContainer plugins) { - return plugins.create("grpc", (grpc) -> grpc.setArtifact(grpcDependency.asDependencySpec())); + private void configurePlugins(NamedDomainObjectContainer plugins, ProtobufExtension protobuf) { + plugins.matching((plugin) -> "grpc".equals(plugin.getName())).configureEach((grpc) -> { + grpc.setArtifact(grpcDependency.asDependencySpec()); + protobuf.generateProtoTasks(this::configureGenerateProtoTasks); + }); } private void configureGenerateProtoTasks(GenerateProtoTaskCollection tasks) { diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle index 9008568a1f57..4eb508578b99 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle @@ -33,6 +33,12 @@ dependencies { implementation("io.grpc:grpc-util:1.79.0") } +protobuf { + plugins { + grpc {} + } +} + tasks.register("protocArtifact") { doFirst { protobuf { From f7c2c0b2a691f5828f2cc07b28e723408737b507 Mon Sep 17 00:00:00 2001 From: Doehyun Baek Date: Sat, 20 Jun 2026 08:09:46 +0000 Subject: [PATCH 675/930] Align documentation with changes made during 4.1's development Signed-off-by: Doehyun Baek See gh-50800 --- .../src/docs/antora/modules/maven-plugin/pages/run.adoc | 2 -- .../src/docs/antora/modules/how-to/pages/hotswapping.adoc | 4 +--- .../antora/modules/reference/pages/features/dev-services.adoc | 2 +- .../docs/antora/modules/reference/pages/features/logging.adoc | 2 +- .../docs/antora/modules/reference/pages/security/oauth2.adoc | 4 ++-- .../reference/pages/testing/spring-boot-applications.adoc | 4 ++-- .../modules/reference/pages/testing/testcontainers.adoc | 2 +- .../oauth2/client/autoconfigure/OAuth2ClientProperties.java | 2 +- 8 files changed, 9 insertions(+), 13 deletions(-) diff --git a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/run.adoc b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/run.adoc index b06bf52fd78d..aa2eeb4b6a8c 100644 --- a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/run.adoc +++ b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/run.adoc @@ -26,8 +26,6 @@ include::example$running/devtools-pom.xml[tags=devtools] When `devtools` is running, it detects changes when you recompile your application and automatically refreshes it. This works for not only resources but code as well. -It also provides a LiveReload server so that it can automatically trigger a browser refresh whenever things change. - Devtools can also be configured to only refresh the browser whenever a static resource has changed (and ignore any change in the code). Just include the following property in your project: diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/hotswapping.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/hotswapping.adoc index 906e32e2ccb0..ce2e7437e39f 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/hotswapping.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/hotswapping.adoc @@ -10,14 +10,12 @@ This section answers questions about how it works. == Reload Static Content There are several options for hot reloading. -The recommended approach is to use xref:reference:using/devtools.adoc[`spring-boot-devtools`], as it provides additional development-time features, such as support for fast application restarts and LiveReload as well as sensible development-time configuration (such as template caching). +The recommended approach is to use xref:reference:using/devtools.adoc[`spring-boot-devtools`], as it provides additional development-time features, such as support for fast application restarts as well as sensible development-time configuration (such as template caching). Devtools works by monitoring the classpath for changes. This means that static resource changes must be "built" for the change to take effect. By default, this happens automatically in Eclipse when you save your changes. In IntelliJ IDEA, the Make Project command triggers the necessary build. Due to the xref:reference:using/devtools.adoc#using.devtools.restart.excluding-resources[default restart exclusions], changes to static resources do not trigger a restart of your application. -They do, however, trigger a live reload. - Alternatively, running in an IDE (especially with debugging on) is a good way to do development (all modern IDEs allow reloading of static resources and usually also allow hot-swapping of Java class changes). Finally, the xref:build-tool-plugin:index.adoc[Maven and Gradle plugins] can be configured (see the `addResources` property) to support running from the command line with reloading of static files directly from source. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc index f18d9d9f9dec..f06dee89a7a4 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc @@ -129,7 +129,7 @@ The following service connections are currently supported: | Containers named "rabbitmq" with container port 5672 mapped | javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] -| Containers named "rabbitmq" with container port 5552 mapped +| Containers named "rabbitmq" with container port 5552 mapped, or port 5551 mapped when SSL is configured | javadoc:org.springframework.boot.data.redis.autoconfigure.DataRedisConnectionDetails[] | Containers named "redis", "redis/redis-stack" or "redis/redis-stack-server" diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc index 53760e83f542..64676c943345 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc @@ -215,7 +215,7 @@ As a result, specific configuration keys (such as `logback.configurationFile` fo [[features.logging.file-rotation]] == File Rotation -If you are using Logback of Log4J2, it is possible to fine-tune log rotation settings using your `application.properties` or `application.yaml` file. +If you are using Logback or Log4j2, it is possible to fine-tune log rotation settings using your `application.properties` or `application.yaml` file. For all other logging system, you will need to configure rotation settings directly yourself. The following rotation policy properties are supported for Logback: diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/oauth2.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/oauth2.adoc index 143238d79443..524af65add5e 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/oauth2.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/security/oauth2.adoc @@ -124,8 +124,8 @@ For production environments, consider using a javadoc:org.springframework.securi [[security.oauth2.client.common-providers]] === OAuth2 Client Registration for Common Providers -For common OAuth2 and OpenID providers (Google, Github, Facebook, and Okta), we provide a set of provider defaults. -The IDs of these common providers are `google`, `github`, `facebook`, and `okta`, respectively. +For common OAuth2 and OpenID providers (Google, Github, Facebook, X, and Okta), we provide a set of provider defaults. +The IDs of these common providers are `google`, `github`, `facebook`, `x`, and `okta`, respectively. If you do not need to customize these providers, set the registration's `provider` property to the ID of one of the common providers. Alternatively, you can xref:security/oauth2.adoc#security.oauth2.client[use a registration ID that matches the ID of the provider]. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc index 361e9eec4d4d..f5307e6757f6 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc @@ -740,11 +740,11 @@ TIP: A list of the auto-configuration settings that are enabled by javadoc:org.s The specific beans that you want to test should be specified by using the `value` or `components` attribute of javadoc:org.springframework.boot.restclient.test.autoconfigure.RestClientTest[format=annotation]. -When using a javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] in the beans under test and `RestTemplateBuilder.rootUri(String rootUri)` has been called when building the javadoc:org.springframework.web.client.RestTemplate[], then the root URI should be omitted from the javadoc:org.springframework.test.web.client.MockRestServiceServer[] expectations as shown in the following example: +When using a javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] in the beans under test and `RestTemplateBuilder.baseUri(String baseUri)` has been called when building the javadoc:org.springframework.web.client.RestTemplate[], then the base URI should be omitted from the javadoc:org.springframework.test.web.client.MockRestServiceServer[] expectations as shown in the following example: include-code::MyRestTemplateServiceTests[] -When using a javadoc:org.springframework.web.client.RestClient$Builder[] in the beans under test, or when using a javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] without calling `rootUri(String rootURI)`, the full URI must be used in the javadoc:org.springframework.test.web.client.MockRestServiceServer[] expectations as shown in the following example: +When using a javadoc:org.springframework.web.client.RestClient$Builder[] in the beans under test, or when using a javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] without calling `baseUri(String baseUri)`, the full URI must be used in the javadoc:org.springframework.test.web.client.MockRestServiceServer[] expectations as shown in the following example: include-code::MyRestClientServiceTests[] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc index cd517fe18e3c..92bde635fa73 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc @@ -200,7 +200,7 @@ For example, a javadoc:{url-testcontainers-postgresql-javadoc}/org.testcontainer If you want to create only a subset of the applicable types, you can use the `type` attribute of javadoc:org.springframework.boot.testcontainers.service.connection.ServiceConnection[format=annotation]. To create a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] bean from a javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[], you must opt in using the `type` attribute of javadoc:org.springframework.boot.testcontainers.service.connection.ServiceConnection[format=annotation]. -The container must also expose port 5552, the RabbitMQ streams port. +The container must also expose port 5552, or port 5551 when SSL is configured, for RabbitMQ Streams. ==== By default `Container.getDockerImageName().getRepository()` is used to obtain the name used to find connection details. diff --git a/module/spring-boot-security-oauth2-client/src/main/java/org/springframework/boot/security/oauth2/client/autoconfigure/OAuth2ClientProperties.java b/module/spring-boot-security-oauth2-client/src/main/java/org/springframework/boot/security/oauth2/client/autoconfigure/OAuth2ClientProperties.java index c87a49c033d6..f57b8a81f74c 100644 --- a/module/spring-boot-security-oauth2-client/src/main/java/org/springframework/boot/security/oauth2/client/autoconfigure/OAuth2ClientProperties.java +++ b/module/spring-boot-security-oauth2-client/src/main/java/org/springframework/boot/security/oauth2/client/autoconfigure/OAuth2ClientProperties.java @@ -79,7 +79,7 @@ public static class Registration { /** * Reference to the OAuth 2.0 provider to use. May reference one of the common - * providers (google, github, facebook, okta) or the ID of a custom provider + * providers (google, github, facebook, x, okta) or the ID of a custom provider * configured using 'spring.security.oauth2.client.provider.<id>.*' * properties. When not set, the ID of this registration is used to identify the * provider. From 248f6eee0160958ce2d32e145d4aabe45093dca3 Mon Sep 17 00:00:00 2001 From: Doehyun Baek Date: Fri, 12 Jun 2026 05:18:37 +0000 Subject: [PATCH 676/930] Align docs with recent changes Signed-off-by: Doehyun Baek See gh-50759 --- .../src/docs/antora/modules/reference/pages/data/sql.adoc | 2 +- .../modules/reference/pages/features/external-config.adoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc index fa8a38f48cf2..4b87a7e1ea40 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc @@ -274,7 +274,7 @@ include-code::CityRepository[] Spring Data JPA repositories support three different modes of bootstrapping: default, deferred, and lazy. To enable deferred or lazy bootstrapping, set the configprop:spring.data.jpa.repositories.bootstrap-mode[] property to `deferred` or `lazy` respectively. -When using deferred or lazy bootstrapping, the auto-configured javadoc:org.springframework.boot.jpa.EntityManagerFactoryBuilder[] will use the context's javadoc:org.springframework.core.task.AsyncTaskExecutor[], if any, as the bootstrap executor. +When using deferred bootstrapping, the auto-configured javadoc:org.springframework.boot.jpa.EntityManagerFactoryBuilder[] requires a bootstrap executor and will use the context's javadoc:org.springframework.core.task.AsyncTaskExecutor[], if one can be determined. If more than one exists, the one named `applicationTaskExecutor` will be used. [NOTE] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc index 38d348e788c7..9ce3d49f2430 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc @@ -874,7 +874,7 @@ This will happen automatically if you use Spring Boot's Gradle plugin or if you NOTE: The use of javadoc:java.util.Optional[] with javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation] is not recommended as it is primarily intended for use as a return type. As such, it is not well-suited to configuration property injection. -For consistency with properties of other types, if you do declare an javadoc:java.util.Optional[] property and it has no value, `null` rather than an empty javadoc:java.util.Optional[] will be bound. +If you do declare an javadoc:java.util.Optional[] constructor-bound property and it has no value, an empty javadoc:java.util.Optional[] will be bound. TIP: To use a reserved keyword in the name of a property, such as `my.service.import`, use the javadoc:org.springframework.boot.context.properties.bind.Name[format=annotation] annotation on the constructor parameter. From a26286801cbf46b283890ff8d2663aba17a4d9cd Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Thu, 18 Jun 2026 21:04:19 +0700 Subject: [PATCH 677/930] Add Spring gRPC properties See gh-50789 Signed-off-by: Tran Ngoc Nhan --- .../modules/appendix/pages/application-properties/index.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/appendix/pages/application-properties/index.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/appendix/pages/application-properties/index.adoc index 21cc3c5acb76..0d24c72a759d 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/appendix/pages/application-properties/index.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/appendix/pages/application-properties/index.adoc @@ -25,6 +25,8 @@ include::partial$configuration-properties/devtools.adoc[] include::partial$configuration-properties/docker-compose.adoc[] +include::partial$configuration-properties/grpc.adoc[] + include::partial$configuration-properties/integration.adoc[] include::partial$configuration-properties/json.adoc[] From 16445de3abb726b9c7e54c5e2ef8c0c096aa36e4 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Mon, 1 Jun 2026 22:16:13 +0200 Subject: [PATCH 678/930] Align W3CHeaderParser decoding with RFC 3986 Replace `URLDecoder.decode` with `StringUtils.uriDecode` to ensure RFC 3986-compliant decoding. Unlike `URLDecoder`, URI decoding does not treat `+` as a space and only decodes percent-encoded sequences. See gh-50650 Signed-off-by: Dmytro Nosan --- .../boot/opentelemetry/autoconfigure/W3CHeaderParser.java | 3 +-- .../opentelemetry/autoconfigure/W3CHeaderParserTests.java | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParser.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParser.java index faf4963cfa2a..268509f8752c 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParser.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParser.java @@ -16,7 +16,6 @@ package org.springframework.boot.opentelemetry.autoconfigure; -import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.LinkedHashMap; @@ -71,7 +70,7 @@ private static String percentDecode(String value) { if (value.indexOf('%') == -1) { return value; } - return URLDecoder.decode(value, StandardCharsets.UTF_8); + return StringUtils.uriDecode(value, StandardCharsets.UTF_8); } } diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParserTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParserTests.java index a1a1945a4034..411bde52a164 100644 --- a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParserTests.java +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/W3CHeaderParserTests.java @@ -55,8 +55,10 @@ void shouldHandleWhitespaceAroundDelimiters() { @Test void shouldPercentDecodeValues() { - Map result = W3CHeaderParser.parse("serverNode=DF%2028,userId=Am%C3%A9lie"); - assertThat(result).containsExactly(Map.entry("serverNode", "DF 28"), Map.entry("userId", "Amélie")); + Map result = W3CHeaderParser + .parse("serverNode=DF%2028,userId=Am%C3%A9lie,application=%20spring+boot%20"); + assertThat(result).containsExactly(Map.entry("serverNode", "DF 28"), Map.entry("userId", "Amélie"), + Map.entry("application", " spring+boot ")); } @Test From cd7e431e927e36c94420a2aa8e0f12433a1e12df Mon Sep 17 00:00:00 2001 From: Simon Verhoeven Date: Thu, 25 Jun 2026 15:35:10 +0200 Subject: [PATCH 679/930] Fix typos and grammatical errors in gRPC documentation See gh-50863 Signed-off-by: Simon Verhoeven --- .../modules/reference/pages/io/grpc.adoc | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc index 6cc9cc9f0f44..6b66d76b40d7 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc @@ -2,16 +2,16 @@ = gRPC Google Remote Procedure Call (gRPC) is a high-performance RPC framework that enables client-server communication using binary messages. -Spring Boot include support for developing and testing both client and server gRPC applications. +Spring Boot includes support for developing and testing both client and server gRPC applications. -The underling message format used by gRPC is Protocol Buffers which allow messages to be created and consumed by a wide variety of programming languages. +The underlying message format used by gRPC is Protocol Buffers, which allow messages to be created and consumed by a wide variety of programming languages. [[io.grpc.servicedefinitions]] == Service Definitions -To develop a gRPC application you first need a Protocol Buffers service definition file. +To develop a gRPC application, you first need a Protocol Buffers service definition file. A `.proto` file defines the services and messages that your application can consume or provide. Here's an example of a typical `.proto` file that uses https://protobuf.dev/programming-guides/proto3/[the `proto3` revision] of the protocol buffers language: @@ -36,11 +36,11 @@ message HelloReply { } ---- -This file defines a `HelloWorld` service with a single method that accepts a `HelloReqest` message and return a `HelloReply` message. -The `HelloReqest` message contains a `name` string field. +This file defines a `HelloWorld` service with a single method that accepts a `HelloRequest` message and returns a `HelloReply` message. +The `HelloRequest` message contains a `name` string field. The `HelloReply` message contains a `message` string field. -With the exception of a the `java_package` and `java_multiple_files` options, there is nothing in the `.proto` file that is specific to the Java programming langage. +With the exception of the `java_package` and `java_multiple_files` options, there is nothing in the `.proto` file that is specific to the Java programming language. @@ -48,17 +48,17 @@ With the exception of a the `java_package` and `java_multiple_files` options, th === Generating Java Code Since `.proto` files are language agnostic, we need a process to convert them into usable Java code. -We can then use the generated code to either make a remote procedure call to running service, or implement the service ourselves so that others may call it. +We can then use the generated code to either make a remote procedure call to a running service or implement the service ourselves so that others may call it. The exact process you use to generate code will depend on your build system. -Spring Boot supports for both Maven and Gradle protobuf plugins, but you are free to use whatever solution works best for you. +Spring Boot supports both Maven and Gradle protobuf plugins, but you are free to use whatever solution works best for you. [[io.grpc.servicedefinitions.generatingjavacode.maven]] ==== Using the Maven Plugin -Spring Boot include dependency management for the `io.github.ascopes:protobuf-maven-plugin` Maven plugin. +Spring Boot includes dependency management for the `io.github.ascopes:protobuf-maven-plugin` Maven plugin. If you are using the `spring-boot-starter-parent` POM, you'll also get sensible out-of-the-box configuration. The following shows a typical Maven POM file that uses the plugin: @@ -131,7 +131,7 @@ If you use Spring Boot's dependency management the `${protobuf-java.version}` an [[io.grpc.servicedefinitions.generatingjavacode.gradle]] ==== Using the Gradle Plugin -Spring Boot include dependency management for the `com.google.protobuf:protobuf-gradle-plugin` Gradle plugin. +Spring Boot includes dependency management for the `com.google.protobuf:protobuf-gradle-plugin` Gradle plugin. In addition, the `spring-boot-gradle-plugin` will react to the presence of the protobuf plugin and configure it appropriately. The following shows a typical Gradle file that uses the plugin: @@ -163,7 +163,7 @@ endif::[] } ---- -Since this gradle file uses both the `org.springframework.boot` and `com.google.protobuf` plugins, you'll get the following: +Since this Gradle file uses both the `org.springframework.boot` and `com.google.protobuf` plugins, you'll get the following: * Configuration of the `protoc` version. * Configuration of the `protoc-gen-grpc-java` version. @@ -179,11 +179,11 @@ TIP: If you don't use `org.springframework.boot` plugin, or you want to configur Spring Boot provides a `spring-boot-grpc-server` module and a `spring-boot-starter-grpc-server` starter POM that you can use for server applications. -In order to write the actual server code, you'll need to extended one or more of base classes generated from your `.proto` file and expose them as Spring beans. +In order to write the actual server code, you'll need to extend one or more of the base classes generated from your `.proto` file and expose them as Spring beans. Spring gRPC will automatically expose any bean that implements javadoc:io.grpc.BindableService[] as a gRPC server. Since all `.proto` generated classes implement javadoc:io.grpc.BindableService[], adding them as beans is enough to expose them over gRPC. -TIP: For more details see {url-spring-grpc-docs}/server.html#_create_a_grpc_service[the Spring gRPC documentation]. +TIP: For more details, see {url-spring-grpc-docs}/server.html#_create_a_grpc_service[the Spring gRPC documentation]. The following example shows how the `HelloWorld` service from the `.proto` file above could be implemented. In this example, we're using the javadoc:org.springframework.grpc.server.service.GrpcService[format=annotation] annotation and assuming that the code is in a package that will be picked up by component scanning: @@ -213,7 +213,7 @@ $ grpcurl -d '{"name":"Spring"}' -plaintext localhost:9090 HelloWorld.SayHello If you find that the version of Netty provided by the `spring-boot-starter-grpc-server` starter POM isn't compatible with other libraries you use, you can switch to a "`shaded`" version. -To switch, you can excluded `io.grpc:grpc-netty` and include `io.grpc:grpc-netty-shaded`. +To switch, you can exclude `io.grpc:grpc-netty` and include `io.grpc:grpc-netty-shaded`. For example: @@ -317,10 +317,10 @@ dependencies { + ====== -TIP: Remember to include a Servlet Container dependency, for example using `spring-boot-starter-tomcat`, and to set `server.http2.enabled` to `true`. +TIP: Remember to include a Servlet Container dependency, for example, using `spring-boot-starter-tomcat`, and to set `server.http2.enabled` to `true`. NOTE: When using a servlet container, certain gRPC server configuration properties are not relevant and will be ignored. -For example, configprop:spring.grpc.server.port[] is ignored since configprop:server.port[] used used to set a web server port. +For example, configprop:spring.grpc.server.port[] is ignored since configprop:server.port[] is used to set a web server port. @@ -343,7 +343,7 @@ spring: Client authentication can also be configured by setting configprop:spring.grpc.server.ssl.client-auth[] to `optional` or `require`. -TIP: To temporarily disable server SSL support, for example to aid with testing, you can set configprop:spring.grpc.server.ssl.enabled[] to `false`. +TIP: To temporarily disable server SSL support, for example, to aid with testing, you can set configprop:spring.grpc.server.ssl.enabled[] to `false`. @@ -604,7 +604,7 @@ You can configure gRPC connections to use standard one-way-TLS, or mutual TLS ==== Standard one-way TLS To use standard one-way TLS, you can set the `ssl.enabled` property to `true` in your channel properties. -For example, the following will enabled an SSL/TLS connection for the `myservice` channel: +For example, the following will enable an SSL/TLS connection for the `myservice` channel: [configprops,yaml] ---- @@ -643,7 +643,7 @@ spring: [TIP] ==== -To temporarily disable client SSL support, for example to aid with testing, you can set `bypass-certificate-validation` to `true` on your channel config: +To temporarily disable client SSL support, for example, to aid with testing, you can set `bypass-certificate-validation` to `true` on your channel config: [configprops,yaml] ---- @@ -667,7 +667,7 @@ In this mode, the in-process channel factory is auto-configured in addition to t To prevent users from having to deal with multiple channel factories, a composite channel factory is configured as the primary channel factory bean. The composite consults its composed factories to find the first one that supports the channel target. -To use the in-process server the channel target must be set to `in-process:` +To use the in-process server, the channel target must be set to `in-process:` TIP: To disable the in-process channel factory, you can set the configprop:spring.grpc.client.inprocess.enabled[] property to `false`. @@ -700,14 +700,14 @@ include-code::MyGrpcConfiguration[] [[io.grpc.testing]] == Testing gRPC Applications -To help test your gRPC client and server applications you can use the `spring-boot-grpc-test` module or the `spring-boot-starter-grpc-client-test` / `spring-boot-starter-grpc-server-test` starter POMs. +To help test your gRPC client and server applications, you can use the `spring-boot-grpc-test` module or the `spring-boot-starter-grpc-client-test` / `spring-boot-starter-grpc-server-test` starter POMs. [[io.grpc.testing.test-transport]] === Using In-Process Test Transport The javadoc:org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport[format=annotation] annotation allows you to quickly replace gRPC communication channels with in-process channels specifically designed for testing. -Unlike regular in-process channels, these test channels to not require any configuration. +Unlike regular in-process channels, these test channels do not require any configuration. Using test gRPC transport means that you don't need to actually listen on a network port to start your application. This allows your tests to run quickly, whilst still ensuring that your application works as expected. @@ -740,4 +740,4 @@ include-code::MyGrpcIntegrationTests[] -// FIXME https://github.com/spring-projects/spring-grpc/issues/397 \ No newline at end of file +// FIXME https://github.com/spring-projects/spring-grpc/issues/397 From 64940c8dfdada8d71ef9111ab77a0124d706465a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sat, 27 Jun 2026 13:15:51 +0100 Subject: [PATCH 680/930] Polish "Fix typos and grammatical errors in gRPC documentation" See gh-50863 Signed-off-by: Andy Wilkinson --- .../src/docs/antora/modules/reference/pages/io/grpc.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc index 6b66d76b40d7..466933a3a9ac 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc @@ -317,7 +317,7 @@ dependencies { + ====== -TIP: Remember to include a Servlet Container dependency, for example, using `spring-boot-starter-tomcat`, and to set `server.http2.enabled` to `true`. +TIP: Remember to include a Servlet Container dependency – `spring-boot-starter-tomcat`, for example – and to set configprop:server.http2.enabled[] to `true`. NOTE: When using a servlet container, certain gRPC server configuration properties are not relevant and will be ignored. For example, configprop:spring.grpc.server.port[] is ignored since configprop:server.port[] is used to set a web server port. @@ -343,7 +343,7 @@ spring: Client authentication can also be configured by setting configprop:spring.grpc.server.ssl.client-auth[] to `optional` or `require`. -TIP: To temporarily disable server SSL support, for example, to aid with testing, you can set configprop:spring.grpc.server.ssl.enabled[] to `false`. +TIP: To temporarily disable server SSL support – to aid with testing, for example – set configprop:spring.grpc.server.ssl.enabled[] to `false`. @@ -643,7 +643,7 @@ spring: [TIP] ==== -To temporarily disable client SSL support, for example, to aid with testing, you can set `bypass-certificate-validation` to `true` on your channel config: +To temporarily disable client SSL support – to aid with testing, for example – set `bypass-certificate-validation` to `true` on your channel config: [configprops,yaml] ---- From 3cd1422f8e6e8efa33588d30924bf0570234a3f0 Mon Sep 17 00:00:00 2001 From: Doehyun Baek Date: Wed, 17 Jun 2026 06:51:43 +0000 Subject: [PATCH 681/930] Document OTLP SSL service connections See gh-50782 Signed-off-by: Doehyun Baek --- .../antora/modules/reference/pages/features/dev-services.adoc | 1 + .../antora/modules/reference/pages/testing/testcontainers.adoc | 1 + 2 files changed, 2 insertions(+) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc index f06dee89a7a4..592ef98102cd 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc @@ -152,6 +152,7 @@ SSL is supported for the following service connections: * Cassandra * Elasticsearch * MongoDB +* OpenTelemetry (OTLP logging, metrics, and tracing) * RabbitMQ * RabbitMQ Streams * Redis diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc index 92bde635fa73..4142d8eead44 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc @@ -241,6 +241,7 @@ The SSL annotations are supported for the following service connections: * Elasticsearch * Kafka * MongoDB +* OpenTelemetry (OTLP logging, metrics, and tracing) * RabbitMQ * RabbitMQ Streams * Redis From b140c8d0fa4765e0be83d478c58614e1ad94dd80 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sun, 28 Jun 2026 18:18:39 +0100 Subject: [PATCH 682/930] Align docs with changes to Gradle gRPC configuration Closes gh-50822 --- .../docs/antora/modules/reference/pages/io/grpc.adoc | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc index 466933a3a9ac..e65b15df1974 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/grpc.adoc @@ -161,12 +161,16 @@ ifeval::["{build-and-artifact-release-type}" == "opensource-snapshot"] maven { url 'https://repo.spring.io/snapshot' } endif::[] } ----- -Since this Gradle file uses both the `org.springframework.boot` and `com.google.protobuf` plugins, you'll get the following: +protobuf { + plugins { + grpc {} + } +} +---- -* Configuration of the `protoc` version. -* Configuration of the `protoc-gen-grpc-java` version. +The `org.springframework.boot` reacts to the `com.google.protobuf` plugin and configures `protoc` and its version. +Additionally, because the `protobuf` extension has been configured to use the `grpc` plugin, the `org.springframework.boot` plugin also configures `protoc-gen-grpc-java` and its version. The `.proto` files should be added to `src/main/proto`. From c7dd44815a5f0bb2ba22f682435212c3f807ed3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 14 Jul 2026 16:04:41 +0200 Subject: [PATCH 683/930] Upgrade to Native Build Tools Plugin 1.1.4 Closes gh-50947 --- .../boot/gradle/docs/AotDocumentationTests.java | 8 +++++--- gradle.properties | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/AotDocumentationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/AotDocumentationTests.java index 54beb9af816f..8ebdf513712e 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/AotDocumentationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/AotDocumentationTests.java @@ -37,9 +37,11 @@ class AotDocumentationTests { @TestTemplate void applyNativeImagePlugin() { - assertThat(this.gradleBuild.script(Examples.DIR + "aot/apply-native-image-plugin").build("tasks").getOutput()) - .contains("nativeCompile") - .contains("aotClasses"); + assertThat(this.gradleBuild.script(Examples.DIR + "aot/apply-native-image-plugin") + .expectDeprecationMessages("nativeBuild - Deprecated alias for nativeCompile.", + "nativeTestBuild - Deprecated alias for nativeTestCompile.") + .build("tasks") + .getOutput()).contains("nativeCompile").contains("aotClasses"); } @TestTemplate diff --git a/gradle.properties b/gradle.properties index d3bf60e1a97b..0d865fb057e4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ junitJupiterVersion=6.0.3 kotlinVersion=2.3.21 mavenVersion=3.9.13 mockitoVersion=5.23.0 -nativeBuildToolsVersion=1.1.1 +nativeBuildToolsVersion=1.1.4 nullabilityPluginVersion=0.0.14 snakeYamlVersion=2.6 springGrpcVersion=1.1.0 From 8c519369ec0f3028c95648851497b10def2f8d31 Mon Sep 17 00:00:00 2001 From: Ns <397827222@qq.com> Date: Sat, 20 Jun 2026 22:56:25 +0800 Subject: [PATCH 684/930] Lazily resolve JPA fallback bootstrap executor Previously, the `entityManagerFactoryBuilder` bean method injected a `Map` to determine the fallback executor used for background JPA bootstrapping. A `Map` parameter is resolved eagerly, so this forced early initialization of every `AsyncTaskExecutor` bean whenever the builder was created, even when background bootstrapping was not in use. When an `AsyncTaskExecutor` directly or transitively depended on the `EntityManagerFactory`, this resulted in a `BeanCurrentlyInCreationException`. The fallback executor is now resolved lazily. `EntityManagerFactoryBuilder` holds a `Supplier` that is only invoked when background bootstrapping is actually required, and the executor is then looked up from the `BeanFactory` rather than eagerly injected. A new `Supplier`-based constructor is added for this purpose; the existing constructor that accepts an `AsyncTaskExecutor` is retained and delegates to it. The same eager `Map` injection, along with an unused private method, is also removed from `DataJpaRepositoriesAutoConfiguration`. See gh-50801 Signed-off-by: Ns <397827222@qq.com> --- .../DataJpaRepositoriesAutoConfiguration.java | 15 +-------- .../HibernateJpaAutoConfigurationTests.java | 22 +++++++++++++ .../boot/jpa/EntityManagerFactoryBuilder.java | 32 ++++++++++++++++--- .../autoconfigure/JpaBaseConfiguration.java | 11 ++++--- .../jpa/EntityManagerFactoryBuilderTests.java | 29 +++++++++++++++++ 5 files changed, 86 insertions(+), 23 deletions(-) diff --git a/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java b/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java index 5bcaed3c0fc5..c4886199a283 100644 --- a/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java +++ b/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java @@ -16,12 +16,8 @@ package org.springframework.boot.data.jpa.autoconfigure; -import java.util.Map; - import javax.sql.DataSource; -import org.jspecify.annotations.Nullable; - import org.springframework.boot.LazyInitializationExcludeFilter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -38,7 +34,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; -import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.envers.repository.config.EnableEnversRepositories; import org.springframework.data.envers.repository.support.EnversRevisionRepositoryFactoryBean; @@ -83,8 +78,7 @@ public final class DataJpaRepositoriesAutoConfiguration { @Bean @ConditionalOnProperty(name = "spring.data.jpa.repositories.bootstrap-mode", havingValue = "deferred") - EntityManagerFactoryBuilderCustomizer entityManagerFactoryBootstrapExecutorCustomizer( - Map taskExecutors) { + EntityManagerFactoryBuilderCustomizer entityManagerFactoryBootstrapExecutorCustomizer() { return (builder) -> builder.requireBootstrapExecutor(() -> BootstrapExecutorRequiredException .ofProperty("spring.data.jpa.repositories.bootstrap-mode", "deferred")); } @@ -94,13 +88,6 @@ static LazyInitializationExcludeFilter eagerJpaMetamodelCacheCleanup() { return (name, definition, type) -> "org.springframework.data.jpa.util.JpaMetamodelCacheCleanup".equals(name); } - private @Nullable AsyncTaskExecutor determineBootstrapExecutor(Map taskExecutors) { - if (taskExecutors.size() == 1) { - return taskExecutors.values().iterator().next(); - } - return taskExecutors.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME); - } - static class JpaRepositoriesImportSelector implements ImportSelector { private static final boolean ENVERS_AVAILABLE = ClassUtils.isPresent( diff --git a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java index 5bbb2992cf15..5a6329ac679d 100644 --- a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java +++ b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java @@ -303,6 +303,15 @@ private EntityManagerFactoryBuilderCustomizer bootstrapExecutorCustomizer() { return (builder) -> builder.setBootstrapExecutor(new SimpleAsyncTaskExecutor()); } + @Test + void whenAsyncTaskExecutorIsDefinedInJpaDependentConfigurationDoesNotTriggerABeanCurrentlyInCreationException() { + this.contextRunner.withUserConfiguration(AsyncTaskExecutorDependingOnEntityManagerFactoryConfiguration.class) + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(EntityManagerFactory.class); + }); + } + @Test void customJpaProperties() { this.contextRunner @@ -1474,4 +1483,17 @@ SimpleAsyncTaskExecutor applicationTaskExecutor() { } + @Configuration(proxyBeanMethods = false) + static class AsyncTaskExecutorDependingOnEntityManagerFactoryConfiguration { + + AsyncTaskExecutorDependingOnEntityManagerFactoryConfiguration(EntityManagerFactory entityManagerFactory) { + } + + @Bean + SimpleAsyncTaskExecutor exampleTaskExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + } + } diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java index 65a56b7e13fa..46b8f87e00b4 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java @@ -68,7 +68,7 @@ public class EntityManagerFactoryBuilder { private final @Nullable URL persistenceUnitRootLocation; - private final @Nullable AsyncTaskExecutor fallbackBootstrapExecutor; + private final Supplier fallbackBootstrapExecutor; private @Nullable AsyncTaskExecutor bootstrapExecutor; @@ -105,7 +105,7 @@ public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, Function> jpaPropertiesFactory, @Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation) { - this(jpaVendorAdapter, jpaPropertiesFactory, persistenceUnitManager, persistenceUnitRootLocation, null); + this(jpaVendorAdapter, jpaPropertiesFactory, persistenceUnitManager, persistenceUnitRootLocation, () -> null); } /** @@ -126,6 +126,29 @@ public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, Function> jpaPropertiesFactory, @Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation, @Nullable AsyncTaskExecutor fallbackBootstrapExecutor) { + this(jpaVendorAdapter, jpaPropertiesFactory, persistenceUnitManager, persistenceUnitRootLocation, + () -> fallbackBootstrapExecutor); + } + + /** + * Create a new instance passing in the common pieces that will be shared if multiple + * EntityManagerFactory instances are created. + * @param jpaVendorAdapter a vendor adapter + * @param jpaPropertiesFactory the JPA properties to be passed to the persistence + * provider, based on the {@linkplain #dataSource(DataSource) configured data source} + * @param persistenceUnitManager optional source of persistence unit information (can + * be null) + * @param persistenceUnitRootLocation the persistence unit root location to use as a + * fallback or {@code null} + * @param fallbackBootstrapExecutor a supplier of the fallback executor to use when + * background bootstrapping is required but no explicit executor has been set. The + * supplier is only invoked when background bootstrapping is actually required. + * @since 4.1.1 + */ + public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, + Function> jpaPropertiesFactory, + @Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation, + Supplier fallbackBootstrapExecutor) { this.jpaVendorAdapter = jpaVendorAdapter; this.persistenceUnitManager = persistenceUnitManager; this.jpaPropertiesFactory = jpaPropertiesFactory; @@ -343,8 +366,9 @@ private Map jpaPropertyMap() { return EntityManagerFactoryBuilder.this.bootstrapExecutor; } if (EntityManagerFactoryBuilder.this.requireBootstrapExecutorExceptionSupplier != null) { - if (EntityManagerFactoryBuilder.this.fallbackBootstrapExecutor != null) { - return EntityManagerFactoryBuilder.this.fallbackBootstrapExecutor; + @Nullable AsyncTaskExecutor fallback = EntityManagerFactoryBuilder.this.fallbackBootstrapExecutor.get(); + if (fallback != null) { + return fallback; } RuntimeException ex = EntityManagerFactoryBuilder.this.requireBootstrapExecutorExceptionSupplier.get(); throw (ex != null) ? ex : new IllegalStateException("A bootstrap executor is required"); diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java index 6ea282cd6c42..81bd01579362 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java @@ -28,6 +28,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigurationPackages; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -123,11 +124,10 @@ public JpaVendorAdapter jpaVendorAdapter() { @ConditionalOnMissingBean public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, ObjectProvider persistenceUnitManager, - ObjectProvider customizers, - Map taskExecutors) { - AsyncTaskExecutor bootstrapExecutor = determineBootstrapExecutor(taskExecutors); + ObjectProvider customizers, ListableBeanFactory beanFactory) { EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(jpaVendorAdapter, - this::buildJpaProperties, persistenceUnitManager.getIfAvailable(), null, bootstrapExecutor); + this::buildJpaProperties, persistenceUnitManager.getIfAvailable(), null, + () -> determineBootstrapExecutor(beanFactory)); if (this.properties.getBootstrap() == Bootstrap.ASYNC) { builder.requireBootstrapExecutor( () -> BootstrapExecutorRequiredException.ofProperty("spring.jpa.bootstrap", "async")); @@ -136,7 +136,8 @@ public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter return builder; } - private @Nullable AsyncTaskExecutor determineBootstrapExecutor(Map taskExecutors) { + private @Nullable AsyncTaskExecutor determineBootstrapExecutor(ListableBeanFactory beanFactory) { + Map taskExecutors = beanFactory.getBeansOfType(AsyncTaskExecutor.class); return (taskExecutors.size() == 1) ? taskExecutors.values().iterator().next() : taskExecutors.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME); } diff --git a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java index 58b584f27951..0ac3a1616afa 100644 --- a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java +++ b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java @@ -18,7 +18,9 @@ import java.util.Collections; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; +import java.util.function.Supplier; import javax.sql.DataSource; @@ -125,6 +127,26 @@ void requireBootstrapExecutorWhenSupplierReturnsNullExecutorAndNoFallbackExecuto .withMessage("A bootstrap executor is required"); } + @Test + void requireBootstrapExecutorWhenFallbackExecutorSupplierProvidesExecutorDoesNotThrow() { + EntityManagerFactoryBuilder builder = createEmptyBuilderWithFallbackSupplier(SimpleAsyncTaskExecutor::new); + builder.requireBootstrapExecutor(() -> new IllegalStateException("BAD")); + DataSource dataSource = mock(); + assertThatNoException().isThrownBy(builder.dataSource(dataSource)::build); + } + + @Test + void fallbackExecutorSupplierIsNotInvokedWhenBootstrapExecutorNotRequired() { + AtomicBoolean invoked = new AtomicBoolean(); + EntityManagerFactoryBuilder builder = createEmptyBuilderWithFallbackSupplier(() -> { + invoked.set(true); + return new SimpleAsyncTaskExecutor(); + }); + DataSource dataSource = mock(); + builder.dataSource(dataSource).build(); + assertThat(invoked).isFalse(); + } + private EntityManagerFactoryBuilder createEmptyBuilder() { return createEmptyBuilder(null); } @@ -135,6 +157,13 @@ private EntityManagerFactoryBuilder createEmptyBuilder(@Nullable AsyncTaskExecut fallbackBootstrapExecutor); } + private EntityManagerFactoryBuilder createEmptyBuilderWithFallbackSupplier( + Supplier fallbackBootstrapExecutorSupplier) { + Function> jpaPropertiesFactory = (dataSource) -> Collections.emptyMap(); + return new EntityManagerFactoryBuilder(new TestJpaVendorAdapter(), jpaPropertiesFactory, null, null, + fallbackBootstrapExecutorSupplier); + } + static class TestJpaVendorAdapter extends AbstractJpaVendorAdapter { @Override From d4d20787c17d5f4c2f390a793d01a58d381f57e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 14 Jul 2026 17:45:54 +0200 Subject: [PATCH 685/930] Polish "Lazily resolve JPA fallback bootstrap executor" See gh-50801 --- .../HibernateJpaAutoConfigurationTests.java | 11 ++++++----- .../boot/jpa/EntityManagerFactoryBuilder.java | 6 ++++-- .../jpa/EntityManagerFactoryBuilderTests.java | 16 +++++----------- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java index 5a6329ac679d..3d8eec846c00 100644 --- a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java +++ b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java @@ -104,6 +104,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.SQLExceptionTranslator; @@ -304,11 +305,11 @@ private EntityManagerFactoryBuilderCustomizer bootstrapExecutorCustomizer() { } @Test - void whenAsyncTaskExecutorIsDefinedInJpaDependentConfigurationDoesNotTriggerABeanCurrentlyInCreationException() { - this.contextRunner.withUserConfiguration(AsyncTaskExecutorDependingOnEntityManagerFactoryConfiguration.class) + void whenAsyncTaskExecutorIsDefinedInJpaDependentConfigurationDoesNotFail() { + this.contextRunner.withUserConfiguration(TaskExecutorDependingOnEntityManagerFactoryConfiguration.class) .run((context) -> { assertThat(context).hasNotFailed(); - assertThat(context).hasSingleBean(EntityManagerFactory.class); + assertThat(context).hasSingleBean(EntityManagerFactory.class).hasSingleBean(AsyncTaskExecutor.class); }); } @@ -1484,9 +1485,9 @@ SimpleAsyncTaskExecutor applicationTaskExecutor() { } @Configuration(proxyBeanMethods = false) - static class AsyncTaskExecutorDependingOnEntityManagerFactoryConfiguration { + static class TaskExecutorDependingOnEntityManagerFactoryConfiguration { - AsyncTaskExecutorDependingOnEntityManagerFactoryConfiguration(EntityManagerFactory entityManagerFactory) { + TaskExecutorDependingOnEntityManagerFactoryConfiguration(EntityManagerFactory entityManagerFactory) { } @Bean diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java index 46b8f87e00b4..4d241563f623 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java @@ -121,7 +121,10 @@ public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, * @param fallbackBootstrapExecutor the fallback executor to use when background * bootstrapping is required but no explicit executor has been set * @since 4.1.0 + * @deprecated since 4.1.1 for removal in 4.3.0 in favor of + * {@link #EntityManagerFactoryBuilder(JpaVendorAdapter, Function, PersistenceUnitManager, URL, Supplier)} */ + @Deprecated(since = "4.1.1", forRemoval = true) public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, Function> jpaPropertiesFactory, @Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation, @@ -141,8 +144,7 @@ public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, * @param persistenceUnitRootLocation the persistence unit root location to use as a * fallback or {@code null} * @param fallbackBootstrapExecutor a supplier of the fallback executor to use when - * background bootstrapping is required but no explicit executor has been set. The - * supplier is only invoked when background bootstrapping is actually required. + * background bootstrapping is required, but no explicit executor has been set. * @since 4.1.1 */ public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, diff --git a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java index 0ac3a1616afa..33bcd614190b 100644 --- a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java +++ b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java @@ -104,7 +104,7 @@ void requireBootstrapExecutorWhenExecutorProvidedDoesNotThrow() { @Test void requireBootstrapExecutorWhenFallbackExecutorProvidesExecutorDoesNotThrow() { - EntityManagerFactoryBuilder builder = createEmptyBuilder(new SimpleAsyncTaskExecutor()); + EntityManagerFactoryBuilder builder = createEmptyBuilder(SimpleAsyncTaskExecutor::new); builder.requireBootstrapExecutor(() -> new IllegalStateException("BAD")); DataSource dataSource = mock(); assertThatNoException().isThrownBy(builder.dataSource(dataSource)::build); @@ -129,7 +129,7 @@ void requireBootstrapExecutorWhenSupplierReturnsNullExecutorAndNoFallbackExecuto @Test void requireBootstrapExecutorWhenFallbackExecutorSupplierProvidesExecutorDoesNotThrow() { - EntityManagerFactoryBuilder builder = createEmptyBuilderWithFallbackSupplier(SimpleAsyncTaskExecutor::new); + EntityManagerFactoryBuilder builder = createEmptyBuilder(SimpleAsyncTaskExecutor::new); builder.requireBootstrapExecutor(() -> new IllegalStateException("BAD")); DataSource dataSource = mock(); assertThatNoException().isThrownBy(builder.dataSource(dataSource)::build); @@ -138,7 +138,7 @@ void requireBootstrapExecutorWhenFallbackExecutorSupplierProvidesExecutorDoesNot @Test void fallbackExecutorSupplierIsNotInvokedWhenBootstrapExecutorNotRequired() { AtomicBoolean invoked = new AtomicBoolean(); - EntityManagerFactoryBuilder builder = createEmptyBuilderWithFallbackSupplier(() -> { + EntityManagerFactoryBuilder builder = createEmptyBuilder(() -> { invoked.set(true); return new SimpleAsyncTaskExecutor(); }); @@ -148,16 +148,10 @@ void fallbackExecutorSupplierIsNotInvokedWhenBootstrapExecutorNotRequired() { } private EntityManagerFactoryBuilder createEmptyBuilder() { - return createEmptyBuilder(null); + return createEmptyBuilder(() -> null); } - private EntityManagerFactoryBuilder createEmptyBuilder(@Nullable AsyncTaskExecutor fallbackBootstrapExecutor) { - Function> jpaPropertiesFactory = (dataSource) -> Collections.emptyMap(); - return new EntityManagerFactoryBuilder(new TestJpaVendorAdapter(), jpaPropertiesFactory, null, null, - fallbackBootstrapExecutor); - } - - private EntityManagerFactoryBuilder createEmptyBuilderWithFallbackSupplier( + private EntityManagerFactoryBuilder createEmptyBuilder( Supplier fallbackBootstrapExecutorSupplier) { Function> jpaPropertiesFactory = (dataSource) -> Collections.emptyMap(); return new EntityManagerFactoryBuilder(new TestJpaVendorAdapter(), jpaPropertiesFactory, null, null, From 9eb03d13fb032ed2d530fda82e52d90300e3436b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 15 Jul 2026 09:45:44 +0200 Subject: [PATCH 686/930] Configure virtual threads on RedisMessageListenerContainer Closes gh-50884 --- ...ataRedisAnnotationDrivenConfiguration.java | 21 ++++++++++++ ...disAnnotationDrivenConfigurationTests.java | 34 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfiguration.java index bfff28513ab6..471a1a32f872 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfiguration.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfiguration.java @@ -19,8 +19,11 @@ 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.condition.ConditionalOnThreading; +import org.springframework.boot.thread.Threading; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.VirtualThreadTaskExecutor; import org.springframework.data.redis.annotation.EnableRedisListeners; import org.springframework.data.redis.config.RedisListenerConfigUtils; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -53,8 +56,26 @@ RedisMessageListenerContainerConfigurer redisMessageListenerContainerConfigurer( @Bean(name = DEFAULT_MESSAGE_LISTENER_BEAN_NAME) @ConditionalOnSingleCandidate(RedisConnectionFactory.class) @ConditionalOnMissingBean(name = DEFAULT_MESSAGE_LISTENER_BEAN_NAME) + @ConditionalOnThreading(Threading.PLATFORM) RedisMessageListenerContainer redisMessageListenerContainer(RedisMessageListenerContainerConfigurer configurer, RedisConnectionFactory redisConnectionFactory) { + return defaultMessageListenerContainer(configurer, redisConnectionFactory); + } + + @Bean(name = DEFAULT_MESSAGE_LISTENER_BEAN_NAME) + @ConditionalOnSingleCandidate(RedisConnectionFactory.class) + @ConditionalOnMissingBean(name = DEFAULT_MESSAGE_LISTENER_BEAN_NAME) + @ConditionalOnThreading(Threading.VIRTUAL) + RedisMessageListenerContainer redisMessageListenerContainerVirtualThreads( + RedisMessageListenerContainerConfigurer configurer, RedisConnectionFactory redisConnectionFactory) { + RedisMessageListenerContainer container = defaultMessageListenerContainer(configurer, redisConnectionFactory); + container + .setTaskExecutor(new VirtualThreadTaskExecutor(RedisMessageListenerContainer.DEFAULT_THREAD_NAME_PREFIX)); + return container; + } + + private static RedisMessageListenerContainer defaultMessageListenerContainer( + RedisMessageListenerContainerConfigurer configurer, RedisConnectionFactory redisConnectionFactory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); configurer.configure(container, redisConnectionFactory); return container; diff --git a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfigurationTests.java b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfigurationTests.java index 91fbb0748556..ee70c152672a 100644 --- a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfigurationTests.java +++ b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAnnotationDrivenConfigurationTests.java @@ -20,6 +20,8 @@ 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.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -27,6 +29,8 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.VirtualThreadTaskExecutor; import org.springframework.data.redis.config.RedisListenerConfigUtils; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.listener.RedisMessageListenerContainer; @@ -108,6 +112,36 @@ void containerCanBeConfigured() { }); } + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void whenVirtualThreadsAreDisableContainerDoesNotUseVirtualThreads() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(RedisMessageListenerContainer.class); + assertThat(context.getBean(RedisMessageListenerContainer.class)).extracting("taskExecutor") + .isInstanceOf(SimpleAsyncTaskExecutor.class); + }); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void whenVirtualThreadsAreEnabledOnJava21AndLaterContainerUsesVirtualThreads() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + assertThat(context).hasSingleBean(RedisMessageListenerContainer.class); + assertThat(context.getBean(RedisMessageListenerContainer.class)).extracting("taskExecutor") + .isInstanceOf(VirtualThreadTaskExecutor.class); + }); + } + + @Test + @EnabledForJreRange(max = JRE.JAVA_20) + void whenVirtualThreadsAreEnabledOnJava20AndEarlierContainerDoesNotUseVirtualThreads() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + assertThat(context).hasSingleBean(RedisMessageListenerContainer.class); + assertThat(context.getBean(RedisMessageListenerContainer.class)).extracting("taskExecutor") + .isInstanceOf(SimpleAsyncTaskExecutor.class); + }); + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(DataRedisProperties.class) static class TestConfiguration { From ad641b6bd6ba29dfb28582477dc787b38b3fa473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 15 Jul 2026 10:25:21 +0200 Subject: [PATCH 687/930] Prepare 4.1.x --- .github/workflows/build-and-deploy-snapshot.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/run-system-tests.yml | 2 +- .github/workflows/trigger-docs-build.yml | 2 +- README.adoc | 2 +- gradle.properties | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index 002f6eb34ac0..cbfa30f86a1a 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -3,7 +3,7 @@ on: workflow_dispatch: push: branches: - - 'main' + - '4.1.x' permissions: contents: read concurrency: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 893d69a9fdc2..9dd0c37adf6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: branches: - - 'main' + - '4.1.x' permissions: contents: read jobs: diff --git a/.github/workflows/run-system-tests.yml b/.github/workflows/run-system-tests.yml index f43c485a7192..099f00135a2a 100644 --- a/.github/workflows/run-system-tests.yml +++ b/.github/workflows/run-system-tests.yml @@ -2,7 +2,7 @@ name: Run System Tests on: push: branches: - - "main" + - '4.1.x' permissions: contents: read jobs: diff --git a/.github/workflows/trigger-docs-build.yml b/.github/workflows/trigger-docs-build.yml index ce40249669d7..9411e9d09a8b 100644 --- a/.github/workflows/trigger-docs-build.yml +++ b/.github/workflows/trigger-docs-build.yml @@ -1,7 +1,7 @@ name: Trigger Docs Build on: push: - branches: 'main' + branches: '4.1.x' paths: [ 'antora/*' ] workflow_dispatch: inputs: diff --git a/README.adoc b/README.adoc index 7eb4dda13491..149966ccad83 100755 --- a/README.adoc +++ b/README.adoc @@ -1,4 +1,4 @@ -= Spring Boot image:https://github.com/spring-projects/spring-boot/actions/workflows/build-and-deploy-snapshot.yml/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects/spring-boot/actions/workflows/build-and-deploy-snapshot.yml?query=branch%3Amain"] image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?&search.rootProjectNames=Spring%20Boot%20Build&search.rootProjectNames=spring-boot-build"] += Spring Boot image:https://github.com/spring-projects/spring-boot/actions/workflows/build-and-deploy-snapshot.yml/badge.svg?branch=4.1.x["Build Status", link="https://github.com/spring-projects/spring-boot/actions/workflows/build-and-deploy-snapshot.yml?query=branch%3A4.1.x"] image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?&search.rootProjectNames=Spring%20Boot%20Build&search.rootProjectNames=spring-boot-build"] :docs: https://docs.spring.io/spring-boot :github: https://github.com/spring-projects/spring-boot diff --git a/gradle.properties b/gradle.properties index 0d865fb057e4..50aa13ad7428 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ version=4.1.1-SNAPSHOT -latestVersion=true +latestVersion=false spring.build-type=oss org.gradle.caching=true From 6d28fc10f9386a039d6dbfe14ba8a240456abd93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 15 Jul 2026 10:30:43 +0200 Subject: [PATCH 688/930] Start development of Spring Boot 4.2.x --- .../workflows/build-and-deploy-snapshot.yml | 2 +- .github/workflows/release-milestone.yml | 4 +- .github/workflows/release.yml | 2 +- .../META-INF/spring/layers/custom.xml | 2 +- .../jar-layered-custom/jar/src/layers.xml | 2 +- .../war-layered-custom/war/src/layers.xml | 2 +- .../META-INF/spring/layers/default.xml | 2 +- .../src/main/xsd/layers-4.2.xsd | 100 ++++++++++++++++++ .../dependencies-layer-no-filter.xml | 2 +- .../src/test/resources/layers.xml | 2 +- .../resources/resource-layer-no-filter.xml | 2 +- eclipse/spring-boot-project.setup | 4 +- git/hooks/prepare-forward-merge | 2 +- gradle.properties | 2 +- .../spring-boot-dependencies/build.gradle | 2 +- 15 files changed, 116 insertions(+), 16 deletions(-) create mode 100644 build-plugin/spring-boot-maven-plugin/src/main/xsd/layers-4.2.xsd diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index 002f6eb34ac0..23a10e5d0492 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -30,7 +30,7 @@ jobs: - name: Deploy uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4 with: - build-name: ${{ vars.COMMERCIAL && format('spring-boot-commercial-{0}', '4.1.x') || format('spring-boot-{0}', '4.1.x') }} + build-name: ${{ vars.COMMERCIAL && format('spring-boot-commercial-{0}', '4.2.x') || format('spring-boot-{0}', '4.2.x') }} folder: 'deployment-repository' password: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_ARTIFACTORY_PASSWORD || secrets.ARTIFACTORY_PASSWORD }} project: ${{ vars.COMMERCIAL && 'spring' }} diff --git a/.github/workflows/release-milestone.yml b/.github/workflows/release-milestone.yml index c61954bd3980..4c33a2566a75 100644 --- a/.github/workflows/release-milestone.yml +++ b/.github/workflows/release-milestone.yml @@ -2,8 +2,8 @@ name: Release Milestone on: push: tags: - - v4.1.0-M[0-9] - - v4.1.0-RC[0-9] + - v4.2.0-M[0-9] + - v4.2.0-RC[0-9] permissions: contents: read concurrency: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32d2629ae994..13612d1d5acf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Release on: push: tags: - - v4.1.[0-9]+ + - v4.2.[0-9]+ permissions: contents: read concurrency: diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/src/main/resources/META-INF/spring/layers/custom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/src/main/resources/META-INF/spring/layers/custom.xml index 434cd9fbb7fd..a3e5e930dfaa 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/src/main/resources/META-INF/spring/layers/custom.xml +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/src/main/resources/META-INF/spring/layers/custom.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-4.2.xsd"> **/application*.* diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml index 77bfa58e8d47..13e533616163 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/layers/layers-4.2.xsd"> **/application*.* diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml index 77bfa58e8d47..13e533616163 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml +++ b/build-plugin/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-4.2.xsd"> **/application*.* diff --git a/build-plugin/spring-boot-maven-plugin/src/main/resources/META-INF/spring/layers/default.xml b/build-plugin/spring-boot-maven-plugin/src/main/resources/META-INF/spring/layers/default.xml index 0d2fedf8334d..c94067c8abee 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/resources/META-INF/spring/layers/default.xml +++ b/build-plugin/spring-boot-maven-plugin/src/main/resources/META-INF/spring/layers/default.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-4.2.xsd"> org/springframework/boot/loader/** diff --git a/build-plugin/spring-boot-maven-plugin/src/main/xsd/layers-4.2.xsd b/build-plugin/spring-boot-maven-plugin/src/main/xsd/layers-4.2.xsd new file mode 100644 index 000000000000..20219b9bd8b1 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/main/xsd/layers-4.2.xsd @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build-plugin/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml b/build-plugin/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml index abd81ae6adb8..563e476cf362 100644 --- a/build-plugin/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml +++ b/build-plugin/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-4.2.xsd"> diff --git a/build-plugin/spring-boot-maven-plugin/src/test/resources/layers.xml b/build-plugin/spring-boot-maven-plugin/src/test/resources/layers.xml index f4921f54f107..be1771b98d84 100644 --- a/build-plugin/spring-boot-maven-plugin/src/test/resources/layers.xml +++ b/build-plugin/spring-boot-maven-plugin/src/test/resources/layers.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-4.2.xsd"> META-INF/resources/** diff --git a/build-plugin/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml b/build-plugin/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml index e256884d6f07..5f71408c5613 100644 --- a/build-plugin/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml +++ b/build-plugin/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-4.2.xsd"> diff --git a/eclipse/spring-boot-project.setup b/eclipse/spring-boot-project.setup index 8486022ea6e4..8f93185b3941 100644 --- a/eclipse/spring-boot-project.setup +++ b/eclipse/spring-boot-project.setup @@ -10,8 +10,8 @@ xmlns:setup.p2="http://www.eclipse.org/oomph/setup/p2/1.0" xmlns:setup.workingsets="http://www.eclipse.org/oomph/setup/workingsets/1.0" xsi:schemaLocation="http://www.eclipse.org/oomph/setup/jdt/1.0 https://raw.githubusercontent.com/eclipse-oomph/oomph/master/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 https://raw.githubusercontent.com/eclipse-oomph/oomph/master/setups/models/Predicates.ecore http://www.eclipse.org/oomph/setup/workingsets/1.0 https://raw.githubusercontent.com/eclipse-oomph/oomph/master/setups/models/SetupWorkingSets.ecore" - name="spring.boot.4.1.x" - label="Spring Boot 4.1.x"> + name="spring.boot.4.2.x" + label="Spring Boot 4.2.x"> Date: Fri, 24 Apr 2026 01:00:50 +0200 Subject: [PATCH 689/930] Expose Path getters on ApplicationTemp See gh-50194 Signed-off-by: zxuhan7 --- .../boot/system/ApplicationHome.java | 21 +++++++++++++++++ .../boot/system/ApplicationTemp.java | 7 +++++- .../boot/system/ApplicationHomeTests.java | 23 +++++++++++++++++++ .../boot/system/ApplicationTempTests.java | 6 +++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationHome.java b/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationHome.java index a8b1ecc3d505..1c968e7149be 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationHome.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationHome.java @@ -23,6 +23,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; +import java.nio.file.Path; import java.security.CodeSource; import java.security.ProtectionDomain; import java.util.Enumeration; @@ -163,6 +164,17 @@ private File findDefaultHomeDir() { return this.source; } + /** + * Returns the path of the underlying source used to find the home directory. This is + * usually the jar file or a directory. Can return {@code null} if the source cannot + * be determined. + * @return the underlying source path or {@code null} + * @since 4.2.0 + */ + public @Nullable Path getSourcePath() { + return (this.source != null) ? this.source.toPath() : null; + } + /** * Returns the application home directory. * @return the home directory (never {@code null}) @@ -171,6 +183,15 @@ public File getDir() { return this.dir; } + /** + * Returns the path of the application home directory. + * @return the home path (never {@code null}) + * @since 4.2.0 + */ + public Path getPath() { + return this.dir.toPath(); + } + @Override public String toString() { return getDir().toString(); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java b/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java index c6395a554b61..42d59d6fba10 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java @@ -117,7 +117,12 @@ public File getDir(String subDir) { return createDirectory(getPath().resolve(subDir)).toFile(); } - private Path getPath() { + /** + * Return the path to be used for application specific temp files. + * @return the application temp path + * @since 4.2.0 + */ + public Path getPath() { if (this.path == null) { this.pathLock.lock(); try { diff --git a/core/spring-boot/src/test/java/org/springframework/boot/system/ApplicationHomeTests.java b/core/spring-boot/src/test/java/org/springframework/boot/system/ApplicationHomeTests.java index 75b5d10cff0f..816b75bf9866 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/system/ApplicationHomeTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/system/ApplicationHomeTests.java @@ -57,6 +57,29 @@ void whenSourceClassIsProvidedWithSpaceInItsPathThenApplicationHomeReflectsItsLo assertThat(applicationHome.getDir()).isEqualTo(app); } + @Test + void getPathReturnsSamePathAsGetDir() throws Exception { + File app = new File(this.tempDir, "app"); + ApplicationHome applicationHome = createApplicationHome(app); + assertThat(applicationHome.getPath()).isEqualTo(applicationHome.getDir().toPath()); + } + + @Test + void getSourcePathReturnsSamePathAsGetSource() throws Exception { + File app = new File(this.tempDir, "app"); + ApplicationHome applicationHome = createApplicationHome(app); + File source = applicationHome.getSource(); + assertThat(source).isNotNull(); + assertThat(applicationHome.getSourcePath()).isEqualTo(source.toPath()); + } + + @Test + void getSourcePathReturnsNullWhenSourceIsNull() { + ApplicationHome applicationHome = new ApplicationHome(); + assertThat(applicationHome.getSource()).isNull(); + assertThat(applicationHome.getSourcePath()).isNull(); + } + private ApplicationHome createApplicationHome(File location) throws Exception { File examplePackage = new File(location, "com/example"); examplePackage.mkdirs(); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/system/ApplicationTempTests.java b/core/spring-boot/src/test/java/org/springframework/boot/system/ApplicationTempTests.java index 3d74a0408b44..c41267b8baae 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/system/ApplicationTempTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/system/ApplicationTempTests.java @@ -79,6 +79,12 @@ void getSubDir() { assertThat(temp.getDir("abc")).isEqualTo(new File(temp.getDir(), "abc")); } + @Test + void getPathReturnsSamePathAsGetDir() { + ApplicationTemp temp = new ApplicationTemp(); + assertThat(temp.getPath()).isEqualTo(temp.getDir().toPath()); + } + @Test void posixPermissions() throws IOException { ApplicationTemp temp = new ApplicationTemp(); From 87c4755a866f3f2bf0ab146a9247a0272577d10e Mon Sep 17 00:00:00 2001 From: Venkata Naga Sai Srikanth Gollapudi <42247688+gollapudisrikanth@users.noreply.github.com> Date: Wed, 6 May 2026 23:25:55 -0500 Subject: [PATCH 690/930] Harmonize use of Collections and Maps in configuration properties See gh-50367 Signed-off-by: Venkata Naga Sai Srikanth Gollapudi <42247688+gollapudisrikanth@users.noreply.github.com> --- .../build/architecture/ArchitectureRules.java | 64 ++++++++++++++++++- .../architecture/ArchitectureCheckTests.java | 36 +++++++++++ .../ConfigurationPropertiesWithEnumMap.java | 48 ++++++++++++++ .../ConfigurationPropertiesWithHashMap.java | 42 ++++++++++++ .../ConfigurationPropertiesWithHashSet.java | 42 ++++++++++++ ...figurationPropertiesWithLinkedHashMap.java | 42 ++++++++++++ ...figurationPropertiesWithLinkedHashSet.java | 42 ++++++++++++ .../MyMapsProperties.java | 4 +- .../MyMapsProperties.kt | 2 +- ...ionPropertiesReportEndpointProperties.java | 4 +- .../env/EnvironmentEndpointProperties.java | 4 +- .../exchanges/HttpExchangesProperties.java | 4 +- .../boot/actuate/sbom/SbomProperties.java | 4 +- .../autoconfigure/ArtemisProperties.java | 4 +- .../autoconfigure/FlywayProperties.java | 6 +- .../autoconfigure/FreeMarkerProperties.java | 3 +- .../health/GrpcServerHealthProperties.java | 3 +- .../autoconfigure/HibernateProperties.java | 4 +- .../autoconfigure/JerseyProperties.java | 4 +- .../boot/jpa/autoconfigure/JpaProperties.java | 4 +- .../kafka/autoconfigure/KafkaProperties.java | 19 +++--- .../ldap/autoconfigure/LdapProperties.java | 4 +- .../mail/autoconfigure/MailProperties.java | 4 +- .../export/humio/HumioProperties.java | 4 +- .../prometheus/PrometheusProperties.java | 6 +- .../otlp/OtlpTracingProperties.java | 4 +- .../OpenTelemetryProperties.java | 4 +- .../logging/otlp/OtlpLoggingProperties.java | 4 +- .../QuartzEndpointProperties.java | 4 +- .../autoconfigure/QuartzProperties.java | 4 +- .../OAuth2AuthorizationServerProperties.java | 16 ++--- .../autoconfigure/OAuth2ClientProperties.java | 6 +- .../autoconfigure/SessionProperties.java | 4 +- .../autoconfigure/ServerProperties.java | 4 +- .../autoconfigure/WebServicesProperties.java | 4 +- 35 files changed, 385 insertions(+), 72 deletions(-) create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/enummap/ConfigurationPropertiesWithEnumMap.java create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/hashmap/ConfigurationPropertiesWithHashMap.java create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/hashset/ConfigurationPropertiesWithHashSet.java create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/linkedhashmap/ConfigurationPropertiesWithLinkedHashMap.java create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/linkedhashset/ConfigurationPropertiesWithLinkedHashSet.java diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java index bfb0460d68a8..2a4bb2d92c8d 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java @@ -37,7 +37,10 @@ import com.tngtech.archunit.core.domain.JavaClass.Predicates; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.domain.JavaConstructor; +import com.tngtech.archunit.core.domain.JavaConstructorCall; import com.tngtech.archunit.core.domain.JavaField; +import com.tngtech.archunit.core.domain.JavaFieldAccess; +import com.tngtech.archunit.core.domain.JavaFieldAccess.AccessType; import com.tngtech.archunit.core.domain.JavaMember; import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.domain.JavaModifier; @@ -77,6 +80,7 @@ * @author Ngoc Nhan * @author Moritz Halbritter * @author Stefano Cordio + * @author Venkata Naga Sai Srikanth Gollapudi */ final class ArchitectureRules { @@ -126,7 +130,11 @@ static List conditionalOnMissingBean(String annotationClass) { static List configurationProperties(String annotationClass) { return List.of(classLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass), - methodLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass)); + methodLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass), + initializedConfigurationPropertiesShouldUse("java.util.Set", "java.util.LinkedHashSet", + annotationClass), + initializedConfigurationPropertiesShouldUse("java.util.Map", + List.of("java.util.LinkedHashMap", "java.util.EnumMap"), annotationClass)); } static List configurationPropertiesBinding(String annotationClass) { @@ -428,6 +436,60 @@ private static ArchRule allDeprecatedConfigurationPropertiesShouldIncludeSince(S .allowEmptyShould(true); } + private static ArchRule initializedConfigurationPropertiesShouldUse(String propertyType, String implementationType, + String annotationClass) { + return initializedConfigurationPropertiesShouldUse(propertyType, List.of(implementationType), annotationClass); + } + + private static ArchRule initializedConfigurationPropertiesShouldUse(String propertyType, + List implementationTypes, String annotationClass) { + return ArchRuleDefinition.classes() + .that() + .areAnnotatedWith(annotationClass) + .or(areNestedInConfigurationPropertiesClasses(annotationClass)) + .should(useImplementationForInitializedProperties(propertyType, implementationTypes)) + .because("@ConfigurationProperties classes should preserve the property type's expected implementation") + .allowEmptyShould(true); + } + + private static ArchCondition useImplementationForInitializedProperties(String propertyType, + List implementationTypes) { + String description = implementationTypes.stream().collect(Collectors.joining(" or ")); + return check("use %s for initialized %s properties".formatted(description, propertyType), + (javaClass, events) -> javaClass.getFields() + .stream() + .filter((field) -> propertyType.equals(field.getRawType().getName())) + .forEach((field) -> checkPropertyInitializer(field, implementationTypes, events))); + } + + private static void checkPropertyInitializer(JavaField field, List implementationTypes, + ConditionEvents events) { + String description = implementationTypes.stream().collect(Collectors.joining(" or ")); + field.getAccessesToSelf() + .stream() + .filter((access) -> access.getAccessType() == AccessType.SET) + .flatMap((access) -> initializerConstructorCalls(access).stream()) + .filter((call) -> !implementationTypes.contains(call.getTargetOwner().getName())) + .forEach((call) -> addViolation(events, field, "%s should be initialized with %s instead of %s" + .formatted(field.getDescription(), description, call.getTargetOwner().getName()))); + } + + private static List initializerConstructorCalls(JavaFieldAccess fieldAccess) { + return fieldAccess.getOrigin() + .getConstructorCallsFromSelf() + .stream() + .filter((call) -> call.getLineNumber() == fieldAccess.getLineNumber()) + .toList(); + } + + private static DescribedPredicate areNestedInConfigurationPropertiesClasses(String annotationClass) { + return DescribedPredicate.describe("are nested in @ConfigurationProperties", + (javaClass) -> javaClass.getEnclosingClass() + .map((enclosing) -> enclosing.isAnnotatedWith(annotationClass) + || areNestedInConfigurationPropertiesClasses(annotationClass).test(enclosing)) + .orElse(false)); + } + private static ArchRule autoConfigurationClassesShouldBePublicAndFinal() { return ArchRuleDefinition.classes() .that(areRegularAutoConfiguration()) diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java index 0686e00a60fe..aaddc9feda3b 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java @@ -62,6 +62,7 @@ * @author Ivan Malutin * @author Dmytro Nosan * @author Stefano Cordio + * @author Venkata Naga Sai Srikanth Gollapudi */ class ArchitectureCheckTests { @@ -331,6 +332,41 @@ void whenMethodLevelConfigurationPropertiesContainsOnlyValueShouldSucceedAndWrit Task.CHECK_ARCHITECTURE_MAIN); } + @Test + void whenConfigurationPropertiesUsesHashMapShouldFailAndWriteReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/hashmap", "annotations"); + buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashMap"); + } + + @Test + void whenConfigurationPropertiesUsesHashSetShouldFailAndWriteReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/hashset", "annotations"); + buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashSet"); + } + + @Test + void whenConfigurationPropertiesUsesLinkedHashMapShouldSucceedAndWriteEmptyReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/linkedhashmap", "annotations"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN); + } + + @Test + void whenConfigurationPropertiesUsesEnumMapShouldSucceedAndWriteEmptyReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/enummap", "annotations"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN); + } + + @Test + void whenConfigurationPropertiesUsesLinkedHashSetShouldSucceedAndWriteEmptyReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/linkedhashset", "annotations"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN); + } + @Test void whenConfigurationPropertiesBindingBeanMethodIsNotStaticShouldFailAndWriteReport() throws IOException { prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/bindingnonstatic", "annotations"); diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/enummap/ConfigurationPropertiesWithEnumMap.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/enummap/ConfigurationPropertiesWithEnumMap.java new file mode 100644 index 000000000000..f3fa73b9cb30 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/enummap/ConfigurationPropertiesWithEnumMap.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.configurationproperties.enummap; + +import java.util.EnumMap; +import java.util.Map; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties; + +/** + * Test {@link TestConfigurationProperties} using {@link EnumMap}. + * + * @author Venkata Naga Sai Srikanth Gollapudi + */ +@TestConfigurationProperties("testing") +public class ConfigurationPropertiesWithEnumMap { + + private Map properties = new EnumMap<>(Example.class); + + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + enum Example { + + ONE + + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/hashmap/ConfigurationPropertiesWithHashMap.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/hashmap/ConfigurationPropertiesWithHashMap.java new file mode 100644 index 000000000000..1668ccc84af7 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/hashmap/ConfigurationPropertiesWithHashMap.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.configurationproperties.hashmap; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties; + +/** + * Test {@link TestConfigurationProperties} using {@link HashMap}. + * + * @author Venkata Naga Sai Srikanth Gollapudi + */ +@TestConfigurationProperties("testing") +public class ConfigurationPropertiesWithHashMap { + + private Map properties = new HashMap<>(); + + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/hashset/ConfigurationPropertiesWithHashSet.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/hashset/ConfigurationPropertiesWithHashSet.java new file mode 100644 index 000000000000..f8bfc8497d91 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/hashset/ConfigurationPropertiesWithHashSet.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.configurationproperties.hashset; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties; + +/** + * Test {@link TestConfigurationProperties} using {@link HashSet}. + * + * @author Venkata Naga Sai Srikanth Gollapudi + */ +@TestConfigurationProperties("testing") +public class ConfigurationPropertiesWithHashSet { + + private Set items = new HashSet<>(); + + public Set getItems() { + return this.items; + } + + public void setItems(Set items) { + this.items = items; + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/linkedhashmap/ConfigurationPropertiesWithLinkedHashMap.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/linkedhashmap/ConfigurationPropertiesWithLinkedHashMap.java new file mode 100644 index 000000000000..b8a8ed38a98f --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/linkedhashmap/ConfigurationPropertiesWithLinkedHashMap.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.configurationproperties.linkedhashmap; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties; + +/** + * Test {@link TestConfigurationProperties} using {@link LinkedHashMap}. + * + * @author Venkata Naga Sai Srikanth Gollapudi + */ +@TestConfigurationProperties("testing") +public class ConfigurationPropertiesWithLinkedHashMap { + + private Map properties = new LinkedHashMap<>(); + + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/linkedhashset/ConfigurationPropertiesWithLinkedHashSet.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/linkedhashset/ConfigurationPropertiesWithLinkedHashSet.java new file mode 100644 index 000000000000..d75c7db3f797 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/linkedhashset/ConfigurationPropertiesWithLinkedHashSet.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.configurationproperties.linkedhashset; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties; + +/** + * Test {@link TestConfigurationProperties} using {@link LinkedHashSet}. + * + * @author Venkata Naga Sai Srikanth Gollapudi + */ +@TestConfigurationProperties("testing") +public class ConfigurationPropertiesWithLinkedHashSet { + + private Set items = new LinkedHashSet<>(); + + public Set getItems() { + return this.items; + } + + public void setItems(Set items) { + this.items = items; + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/mapsfromenvironmentvariables/MyMapsProperties.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/mapsfromenvironmentvariables/MyMapsProperties.java index 71b7cb4c1b9d..441f57b726c8 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/mapsfromenvironmentvariables/MyMapsProperties.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/mapsfromenvironmentvariables/MyMapsProperties.java @@ -16,7 +16,7 @@ package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.relaxedbinding.mapsfromenvironmentvariables; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -24,7 +24,7 @@ @ConfigurationProperties("my.props") public class MyMapsProperties { - private final Map values = new HashMap<>(); + private final Map values = new LinkedHashMap<>(); public Map getValues() { return this.values; diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/mapsfromenvironmentvariables/MyMapsProperties.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/mapsfromenvironmentvariables/MyMapsProperties.kt index 057295216fd1..1e21315e2eb3 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/mapsfromenvironmentvariables/MyMapsProperties.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/mapsfromenvironmentvariables/MyMapsProperties.kt @@ -21,7 +21,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties("my.props") class MyMapsProperties { - val values: Map = HashMap() + val values: Map = LinkedHashMap() } diff --git a/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointProperties.java b/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointProperties.java index 8bc6c645f7f6..6f594812072c 100644 --- a/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointProperties.java +++ b/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointProperties.java @@ -16,7 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.context.properties; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint; @@ -42,7 +42,7 @@ public class ConfigurationPropertiesReportEndpointProperties { * Roles used to determine whether a user is authorized to be shown unsanitized * values. When empty, all authenticated users are authorized. */ - private final Set roles = new HashSet<>(); + private final Set roles = new LinkedHashSet<>(); public Show getShowValues() { return this.showValues; diff --git a/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointProperties.java b/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointProperties.java index 301276abb59a..d2de960491f9 100644 --- a/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointProperties.java +++ b/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointProperties.java @@ -16,7 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.env; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import org.springframework.boot.actuate.endpoint.Show; @@ -41,7 +41,7 @@ public class EnvironmentEndpointProperties { * Roles used to determine whether a user is authorized to be shown unsanitized * values. When empty, all authenticated users are authorized. */ - private final Set roles = new HashSet<>(); + private final Set roles = new LinkedHashSet<>(); public Show getShowValues() { return this.showValues; diff --git a/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesProperties.java b/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesProperties.java index 70883127d22a..fc049e7b0723 100644 --- a/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesProperties.java +++ b/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesProperties.java @@ -16,7 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.web.exchanges; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import org.springframework.boot.actuate.web.exchanges.Include; @@ -53,7 +53,7 @@ public static class Recording { * (excluding Authorization and Cookie), response headers (excluding Set-Cookie), * and time taken. */ - private Set include = new HashSet<>(Include.defaultIncludes()); + private Set include = new LinkedHashSet<>(Include.defaultIncludes()); public Set getInclude() { return this.include; diff --git a/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/sbom/SbomProperties.java b/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/sbom/SbomProperties.java index 94d6424eed98..3c05315caa6c 100644 --- a/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/sbom/SbomProperties.java +++ b/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/sbom/SbomProperties.java @@ -16,7 +16,7 @@ package org.springframework.boot.actuate.sbom; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.jspecify.annotations.Nullable; @@ -41,7 +41,7 @@ public class SbomProperties { /** * Additional SBOMs. */ - private Map additional = new HashMap<>(); + private Map additional = new LinkedHashMap<>(); public Sbom getApplication() { return this.application; diff --git a/module/spring-boot-artemis/src/main/java/org/springframework/boot/artemis/autoconfigure/ArtemisProperties.java b/module/spring-boot-artemis/src/main/java/org/springframework/boot/artemis/autoconfigure/ArtemisProperties.java index 94c681c3a0a1..3add6adba385 100644 --- a/module/spring-boot-artemis/src/main/java/org/springframework/boot/artemis/autoconfigure/ArtemisProperties.java +++ b/module/spring-boot-artemis/src/main/java/org/springframework/boot/artemis/autoconfigure/ArtemisProperties.java @@ -16,7 +16,7 @@ package org.springframework.boot.artemis.autoconfigure; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; @@ -216,7 +216,7 @@ public boolean isDefaultClusterPassword() { * @see TransportConstants#SERVER_ID_PROP_NAME */ public Map generateTransportParameters() { - Map parameters = new HashMap<>(); + Map parameters = new LinkedHashMap<>(); parameters.put(TransportConstants.SERVER_ID_PROP_NAME, getServerId()); return parameters; } diff --git a/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/FlywayProperties.java b/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/FlywayProperties.java index 9af24f3261a0..44ef3c2ac3ba 100644 --- a/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/FlywayProperties.java +++ b/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/FlywayProperties.java @@ -23,7 +23,7 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -137,7 +137,7 @@ public class FlywayProperties { /** * Placeholders and their replacements to apply to sql migration scripts. */ - private Map placeholders = new HashMap<>(); + private Map placeholders = new LinkedHashMap<>(); /** * Prefix of placeholders in migration scripts. @@ -310,7 +310,7 @@ public class FlywayProperties { /** * Properties to pass to the JDBC driver. */ - private Map jdbcProperties = new HashMap<>(); + private Map jdbcProperties = new LinkedHashMap<>(); /** * Path of the Kerberos config file. Requires Flyway Teams. diff --git a/module/spring-boot-freemarker/src/main/java/org/springframework/boot/freemarker/autoconfigure/FreeMarkerProperties.java b/module/spring-boot-freemarker/src/main/java/org/springframework/boot/freemarker/autoconfigure/FreeMarkerProperties.java index 89afd1946e84..ba0082af1f9a 100644 --- a/module/spring-boot-freemarker/src/main/java/org/springframework/boot/freemarker/autoconfigure/FreeMarkerProperties.java +++ b/module/spring-boot-freemarker/src/main/java/org/springframework/boot/freemarker/autoconfigure/FreeMarkerProperties.java @@ -18,7 +18,6 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @@ -123,7 +122,7 @@ public class FreeMarkerProperties { /** * Well-known FreeMarker keys which are passed to FreeMarker's Configuration. */ - private Map settings = new HashMap<>(); + private Map settings = new LinkedHashMap<>(); /** * List of template paths. diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java index 2912cad3669b..c2be3e85eed0 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthProperties.java @@ -19,7 +19,6 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -177,7 +176,7 @@ public static class Status { * Mapping of health statuses to gRPC service status. By default, registered * health statuses map to sensible defaults (for example, UP maps to SERVING). */ - private final Map mapping = new HashMap<>(); + private final Map mapping = new LinkedHashMap<>(); public List getOrder() { return this.order; diff --git a/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/autoconfigure/HibernateProperties.java b/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/autoconfigure/HibernateProperties.java index ae00735c58cf..e9299e4e720a 100644 --- a/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/autoconfigure/HibernateProperties.java +++ b/module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/autoconfigure/HibernateProperties.java @@ -17,7 +17,7 @@ package org.springframework.boot.hibernate.autoconfigure; import java.util.Collection; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Supplier; @@ -85,7 +85,7 @@ public Map determineHibernateProperties(Map jpaP } private Map getAdditionalProperties(Map existing, HibernateSettings settings) { - Map result = new HashMap<>(existing); + Map result = new LinkedHashMap<>(existing); applyScanner(result); getNaming().applyNamingStrategies(result); String ddlAuto = determineDdlAuto(existing, settings::getDdlAuto); diff --git a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/JerseyProperties.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/JerseyProperties.java index 2a858ad353aa..cc3906d936af 100644 --- a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/JerseyProperties.java +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/JerseyProperties.java @@ -16,7 +16,7 @@ package org.springframework.boot.jersey.autoconfigure; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.jspecify.annotations.Nullable; @@ -42,7 +42,7 @@ public class JerseyProperties { /** * Init parameters to pass to Jersey through the servlet or filter. */ - private Map init = new HashMap<>(); + private Map init = new LinkedHashMap<>(); private final Filter filter = new Filter(); diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaProperties.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaProperties.java index 5bcbc156318e..fcd8631c5008 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaProperties.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaProperties.java @@ -17,7 +17,7 @@ package org.springframework.boot.jpa.autoconfigure; import java.util.ArrayList; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -42,7 +42,7 @@ public class JpaProperties { /** * Additional native properties to set on the JPA provider. */ - private Map properties = new HashMap<>(); + private Map properties = new LinkedHashMap<>(); /** * Mapping resources (equivalent to "mapping-file" entries in persistence.xml). diff --git a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaProperties.java b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaProperties.java index 172ffd707808..70d4bc6bef3d 100644 --- a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaProperties.java +++ b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaProperties.java @@ -21,7 +21,7 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -59,6 +59,7 @@ * @author Andy Wilkinson * @author Scott Frederick * @author Yanming Zhou + * @author Venkata Naga Sai Srikanth Gollapudi * @since 4.0.0 */ @ConfigurationProperties("spring.kafka") @@ -79,7 +80,7 @@ public class KafkaProperties { * Additional properties, common to producers and consumers, used to configure the * client. */ - private final Map properties = new HashMap<>(); + private final Map properties = new LinkedHashMap<>(); private final Consumer consumer = new Consumer(); @@ -162,7 +163,7 @@ public Retry getRetry() { } private Map buildCommonProperties() { - Map properties = new HashMap<>(); + Map properties = new LinkedHashMap<>(); if (this.bootstrapServers != null) { properties.put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, this.bootstrapServers); } @@ -318,7 +319,7 @@ public static class Consumer { /** * Additional consumer-specific properties used to configure the client. */ - private final Map properties = new HashMap<>(); + private final Map properties = new LinkedHashMap<>(); public Ssl getSsl() { return this.ssl; @@ -540,7 +541,7 @@ public static class Producer { /** * Additional producer-specific properties used to configure the client. */ - private final Map properties = new HashMap<>(); + private final Map properties = new LinkedHashMap<>(); public Ssl getSsl() { return this.ssl; @@ -667,7 +668,7 @@ public static class Admin { /** * Additional admin-specific properties used to configure the client. */ - private final Map properties = new HashMap<>(); + private final Map properties = new LinkedHashMap<>(); /** * Close timeout. @@ -815,7 +816,7 @@ public static class Streams { /** * Additional Kafka properties used to configure the streams. */ - private final Map properties = new HashMap<>(); + private final Map properties = new LinkedHashMap<>(); public Ssl getSsl() { return this.ssl; @@ -1510,7 +1511,7 @@ public static class Jaas { /** * Additional JAAS options. */ - private final Map options = new HashMap<>(); + private final Map options = new LinkedHashMap<>(); public boolean isEnabled() { return this.enabled; @@ -1741,7 +1742,7 @@ public byte id() { } @SuppressWarnings("serial") - private static final class Properties extends HashMap { + private static final class Properties extends LinkedHashMap { java.util.function.Consumer in(String key) { return (value) -> put(key, value); diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/LdapProperties.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/LdapProperties.java index 636834cf6cf5..9bbb9bde845d 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/LdapProperties.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/LdapProperties.java @@ -16,7 +16,7 @@ package org.springframework.boot.ldap.autoconfigure; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.jspecify.annotations.Nullable; @@ -74,7 +74,7 @@ public class LdapProperties { /** * LDAP specification settings. */ - private final Map baseEnvironment = new HashMap<>(); + private final Map baseEnvironment = new LinkedHashMap<>(); private final Template template = new Template(); diff --git a/module/spring-boot-mail/src/main/java/org/springframework/boot/mail/autoconfigure/MailProperties.java b/module/spring-boot-mail/src/main/java/org/springframework/boot/mail/autoconfigure/MailProperties.java index de62fda00cf1..6f2fd0bc7240 100644 --- a/module/spring-boot-mail/src/main/java/org/springframework/boot/mail/autoconfigure/MailProperties.java +++ b/module/spring-boot-mail/src/main/java/org/springframework/boot/mail/autoconfigure/MailProperties.java @@ -18,7 +18,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.jspecify.annotations.Nullable; @@ -71,7 +71,7 @@ public class MailProperties { /** * Additional JavaMail Session properties. */ - private final Map properties = new HashMap<>(); + private final Map properties = new LinkedHashMap<>(); /** * Session JNDI name. When set, takes precedence over other Session settings. diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/humio/HumioProperties.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/humio/HumioProperties.java index befa265456f0..ede0cdb5cf78 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/humio/HumioProperties.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/humio/HumioProperties.java @@ -17,7 +17,7 @@ package org.springframework.boot.micrometer.metrics.autoconfigure.export.humio; import java.time.Duration; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.jspecify.annotations.Nullable; @@ -50,7 +50,7 @@ public class HumioProperties extends StepRegistryProperties { * are a distinct concept from Micrometer's tags. Micrometer's tags are used to divide * metrics along dimensional boundaries. */ - private Map tags = new HashMap<>(); + private Map tags = new LinkedHashMap<>(); /** * URI to ship metrics to. If you need to publish metrics to an internal proxy diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/prometheus/PrometheusProperties.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/prometheus/PrometheusProperties.java index d7ca355cf0c3..7d7de22eb3a9 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/prometheus/PrometheusProperties.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/prometheus/PrometheusProperties.java @@ -17,7 +17,7 @@ package org.springframework.boot.micrometer.metrics.autoconfigure.export.prometheus; import java.time.Duration; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.jspecify.annotations.Nullable; @@ -56,7 +56,7 @@ public class PrometheusProperties { /** * Additional properties to pass to the Prometheus client. */ - private final Map properties = new HashMap<>(); + private final Map properties = new LinkedHashMap<>(); /** * Step size (i.e. reporting frequency) to use. @@ -148,7 +148,7 @@ public static class Pushgateway { /** * Grouping key for the pushed metrics. */ - private Map groupingKey = new HashMap<>(); + private Map groupingKey = new LinkedHashMap<>(); /** * Operation that should be performed on shutdown. diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingProperties.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingProperties.java index edf073fa3494..b76c63cb6106 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingProperties.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingProperties.java @@ -17,7 +17,7 @@ package org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp; import java.time.Duration; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.jspecify.annotations.Nullable; @@ -64,7 +64,7 @@ public class OtlpTracingProperties { /** * Custom HTTP headers you want to pass to the collector, for example auth headers. */ - private Map headers = new HashMap<>(); + private Map headers = new LinkedHashMap<>(); private final Ssl ssl = new Ssl(); diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryProperties.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryProperties.java index a900b3e373ae..6e1f003677c6 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryProperties.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryProperties.java @@ -16,7 +16,7 @@ package org.springframework.boot.opentelemetry.autoconfigure; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -40,7 +40,7 @@ public class OpenTelemetryProperties { /** * Resource attributes. */ - private Map resourceAttributes = new HashMap<>(); + private Map resourceAttributes = new LinkedHashMap<>(); public boolean isEnabled() { return this.enabled; diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java index 569935763a66..ebb114ae24b6 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java @@ -17,7 +17,7 @@ package org.springframework.boot.opentelemetry.autoconfigure.logging.otlp; import java.time.Duration; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.jspecify.annotations.Nullable; @@ -64,7 +64,7 @@ public class OtlpLoggingProperties { /** * Custom HTTP headers you want to pass to the collector, for example auth headers. */ - private final Map headers = new HashMap<>(); + private final Map headers = new LinkedHashMap<>(); private final Ssl ssl = new Ssl(); diff --git a/module/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzEndpointProperties.java b/module/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzEndpointProperties.java index 176dd4b6e600..053b7ff7901d 100644 --- a/module/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzEndpointProperties.java +++ b/module/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzEndpointProperties.java @@ -16,7 +16,7 @@ package org.springframework.boot.quartz.autoconfigure; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import org.springframework.boot.actuate.endpoint.Show; @@ -41,7 +41,7 @@ public class QuartzEndpointProperties { * Roles used to determine whether a user is authorized to be shown unsanitized job or * trigger values. When empty, all authenticated users are authorized. */ - private final Set roles = new HashSet<>(); + private final Set roles = new LinkedHashSet<>(); public Show getShowValues() { return this.showValues; diff --git a/module/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzProperties.java b/module/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzProperties.java index 14c405cb9e66..73fbbc77ffc7 100644 --- a/module/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzProperties.java +++ b/module/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzProperties.java @@ -17,7 +17,7 @@ package org.springframework.boot.quartz.autoconfigure; import java.time.Duration; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.jspecify.annotations.Nullable; @@ -69,7 +69,7 @@ public class QuartzProperties { /** * Additional Quartz Scheduler properties. */ - private final Map properties = new HashMap<>(); + private final Map properties = new LinkedHashMap<>(); public JobStoreType getJobStoreType() { return this.jobStoreType; diff --git a/module/spring-boot-security-oauth2-authorization-server/src/main/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerProperties.java b/module/spring-boot-security-oauth2-authorization-server/src/main/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerProperties.java index ede513ef61a4..b44e870f8102 100644 --- a/module/spring-boot-security-oauth2-authorization-server/src/main/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerProperties.java +++ b/module/spring-boot-security-oauth2-authorization-server/src/main/java/org/springframework/boot/security/oauth2/server/authorization/autoconfigure/servlet/OAuth2AuthorizationServerProperties.java @@ -17,8 +17,8 @@ package org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet; import java.time.Duration; -import java.util.HashMap; -import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -55,7 +55,7 @@ public class OAuth2AuthorizationServerProperties implements InitializingBean { /** * Registered clients of the Authorization Server. */ - private final Map client = new HashMap<>(); + private final Map client = new LinkedHashMap<>(); /** * Authorization Server endpoints. @@ -380,27 +380,27 @@ public static class Registration { /** * Client authentication method(s) that the client may use. */ - private Set clientAuthenticationMethods = new HashSet<>(); + private Set clientAuthenticationMethods = new LinkedHashSet<>(); /** * Authorization grant type(s) that the client may use. */ - private Set authorizationGrantTypes = new HashSet<>(); + private Set authorizationGrantTypes = new LinkedHashSet<>(); /** * Redirect URI(s) that the client may use in redirect-based flows. */ - private Set redirectUris = new HashSet<>(); + private Set redirectUris = new LinkedHashSet<>(); /** * Redirect URI(s) that the client may use for logout. */ - private Set postLogoutRedirectUris = new HashSet<>(); + private Set postLogoutRedirectUris = new LinkedHashSet<>(); /** * Scope(s) that the client may use. */ - private Set scopes = new HashSet<>(); + private Set scopes = new LinkedHashSet<>(); public @Nullable String getClientId() { return this.clientId; diff --git a/module/spring-boot-security-oauth2-client/src/main/java/org/springframework/boot/security/oauth2/client/autoconfigure/OAuth2ClientProperties.java b/module/spring-boot-security-oauth2-client/src/main/java/org/springframework/boot/security/oauth2/client/autoconfigure/OAuth2ClientProperties.java index f57b8a81f74c..18d0fe7a0d7c 100644 --- a/module/spring-boot-security-oauth2-client/src/main/java/org/springframework/boot/security/oauth2/client/autoconfigure/OAuth2ClientProperties.java +++ b/module/spring-boot-security-oauth2-client/src/main/java/org/springframework/boot/security/oauth2/client/autoconfigure/OAuth2ClientProperties.java @@ -16,7 +16,7 @@ package org.springframework.boot.security.oauth2.client.autoconfigure; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -42,12 +42,12 @@ public class OAuth2ClientProperties implements InitializingBean { /** * OAuth provider details. */ - private final Map provider = new HashMap<>(); + private final Map provider = new LinkedHashMap<>(); /** * OAuth client registrations. */ - private final Map registration = new HashMap<>(); + private final Map registration = new LinkedHashMap<>(); public Map getProvider() { return this.provider; diff --git a/module/spring-boot-session/src/main/java/org/springframework/boot/session/autoconfigure/SessionProperties.java b/module/spring-boot-session/src/main/java/org/springframework/boot/session/autoconfigure/SessionProperties.java index 0f7c500b41b2..30216c894aba 100644 --- a/module/spring-boot-session/src/main/java/org/springframework/boot/session/autoconfigure/SessionProperties.java +++ b/module/spring-boot-session/src/main/java/org/springframework/boot/session/autoconfigure/SessionProperties.java @@ -19,7 +19,7 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Arrays; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import java.util.function.Supplier; @@ -90,7 +90,7 @@ public static class Servlet { /** * Session repository filter dispatcher types. */ - private Set filterDispatcherTypes = new HashSet<>( + private Set filterDispatcherTypes = new LinkedHashSet<>( Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST)); public int getFilterOrder() { diff --git a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/ServerProperties.java b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/ServerProperties.java index 32629886c8a2..415b7bc54a1b 100644 --- a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/ServerProperties.java +++ b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/ServerProperties.java @@ -20,7 +20,7 @@ import java.nio.charset.Charset; import java.time.Duration; import java.time.temporal.ChronoUnit; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; @@ -212,7 +212,7 @@ public static class Servlet { /** * Servlet context init parameters. */ - private final Map contextParameters = new HashMap<>(); + private final Map contextParameters = new LinkedHashMap<>(); /** * Context path of the application. diff --git a/module/spring-boot-webservices/src/main/java/org/springframework/boot/webservices/autoconfigure/WebServicesProperties.java b/module/spring-boot-webservices/src/main/java/org/springframework/boot/webservices/autoconfigure/WebServicesProperties.java index a93dd86a9d21..847c3de30d7e 100644 --- a/module/spring-boot-webservices/src/main/java/org/springframework/boot/webservices/autoconfigure/WebServicesProperties.java +++ b/module/spring-boot-webservices/src/main/java/org/springframework/boot/webservices/autoconfigure/WebServicesProperties.java @@ -16,7 +16,7 @@ package org.springframework.boot.webservices.autoconfigure; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -59,7 +59,7 @@ public static class Servlet { /** * Servlet init parameters to pass to Spring Web Services. */ - private Map init = new HashMap<>(); + private Map init = new LinkedHashMap<>(); /** * Load on startup priority of the Spring Web Services servlet. From 10fdd206063481fd466ebc1adf87edcc3da8f618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 1 Jul 2026 08:59:17 +0200 Subject: [PATCH 691/930] Polish contribution This commit applies the same rules to ConfigurationPropertiesSource. See gh-50367 --- .../build/architecture/ArchitectureCheck.java | 2 + .../ArchitectureCheckAnnotation.java | 7 +++ .../build/architecture/ArchitectureRules.java | 48 ++++++++----------- .../architecture/ArchitectureCheckTests.java | 42 ++++++++++++++++ .../TestConfigurationPropertiesSource.java | 28 +++++++++++ .../ConfigurationPropertiesWithEnumMap.java | 48 +++++++++++++++++++ .../ConfigurationPropertiesWithHashMap.java | 42 ++++++++++++++++ .../ConfigurationPropertiesWithHashSet.java | 42 ++++++++++++++++ ...figurationPropertiesWithLinkedHashMap.java | 42 ++++++++++++++++ ...figurationPropertiesWithLinkedHashSet.java | 42 ++++++++++++++++ .../kafka/autoconfigure/KafkaProperties.java | 1 - 11 files changed, 316 insertions(+), 28 deletions(-) create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/architecture/annotations/TestConfigurationPropertiesSource.java create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/enummap/ConfigurationPropertiesWithEnumMap.java create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/hashmap/ConfigurationPropertiesWithHashMap.java create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/hashset/ConfigurationPropertiesWithHashSet.java create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/linkedhashmap/ConfigurationPropertiesWithLinkedHashMap.java create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/linkedhashset/ConfigurationPropertiesWithLinkedHashSet.java diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java index 8fd11ee1286d..2995bdff9928 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java @@ -87,6 +87,8 @@ public ArchitectureCheck() { .classFor(getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONDITIONAL_ON_MISSING_BEAN)))); getRules().addAll(whenMainSources(() -> ArchitectureRules.configurationProperties(ArchitectureCheckAnnotation .classFor(getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES)))); + getRules().addAll(whenMainSources(() -> ArchitectureRules.configurationProperties(ArchitectureCheckAnnotation + .classFor(getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_SOURCE)))); getRules().addAll(whenMainSources( () -> ArchitectureRules.configurationPropertiesBinding(ArchitectureCheckAnnotation.classFor( getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_BINDING)))); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheckAnnotation.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheckAnnotation.java index 991bdaa7c27a..bd7ff6d6ea75 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheckAnnotation.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheckAnnotation.java @@ -42,6 +42,11 @@ public enum ArchitectureCheckAnnotation { */ CONFIGURATION_PROPERTIES, + /** + * Configuration properties source. + */ + CONFIGURATION_PROPERTIES_SOURCE, + /** * Deprecated configuration property. */ @@ -56,6 +61,8 @@ public enum ArchitectureCheckAnnotation { "org.springframework.boot.autoconfigure.condition.ConditionalOnClass", CONDITIONAL_ON_MISSING_BEAN.name(), "org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean", CONFIGURATION_PROPERTIES.name(), "org.springframework.boot.context.properties.ConfigurationProperties", + CONFIGURATION_PROPERTIES_SOURCE.name(), + "org.springframework.boot.context.properties.ConfigurationPropertiesSource", DEPRECATED_CONFIGURATION_PROPERTY.name(), "org.springframework.boot.context.properties.DeprecatedConfigurationProperty", CONFIGURATION_PROPERTIES_BINDING.name(), diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java index 2a4bb2d92c8d..b6c2440c4e97 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java @@ -131,10 +131,10 @@ static List conditionalOnMissingBean(String annotationClass) { static List configurationProperties(String annotationClass) { return List.of(classLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass), methodLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass), - initializedConfigurationPropertiesShouldUse("java.util.Set", "java.util.LinkedHashSet", - annotationClass), - initializedConfigurationPropertiesShouldUse("java.util.Map", - List.of("java.util.LinkedHashMap", "java.util.EnumMap"), annotationClass)); + configurationPropertyOfTypeShouldUseImplementation(annotationClass, "java.util.Set", + List.of("java.util.LinkedHashSet")), + configurationPropertyOfTypeShouldUseImplementation(annotationClass, "java.util.Map", + List.of("java.util.LinkedHashMap", "java.util.EnumMap"))); } static List configurationPropertiesBinding(String annotationClass) { @@ -436,42 +436,36 @@ private static ArchRule allDeprecatedConfigurationPropertiesShouldIncludeSince(S .allowEmptyShould(true); } - private static ArchRule initializedConfigurationPropertiesShouldUse(String propertyType, String implementationType, - String annotationClass) { - return initializedConfigurationPropertiesShouldUse(propertyType, List.of(implementationType), annotationClass); - } - - private static ArchRule initializedConfigurationPropertiesShouldUse(String propertyType, - List implementationTypes, String annotationClass) { + private static ArchRule configurationPropertyOfTypeShouldUseImplementation(String annotationClass, + String propertyType, List allowedImplementations) { return ArchRuleDefinition.classes() .that() .areAnnotatedWith(annotationClass) - .or(areNestedInConfigurationPropertiesClasses(annotationClass)) - .should(useImplementationForInitializedProperties(propertyType, implementationTypes)) - .because("@ConfigurationProperties classes should preserve the property type's expected implementation") + .or(areNestedInClassAnnotatedWith(annotationClass)) + .should(useAllowedImplementationForProperties(propertyType, allowedImplementations)) .allowEmptyShould(true); } - private static ArchCondition useImplementationForInitializedProperties(String propertyType, - List implementationTypes) { - String description = implementationTypes.stream().collect(Collectors.joining(" or ")); - return check("use %s for initialized %s properties".formatted(description, propertyType), + private static ArchCondition useAllowedImplementationForProperties(String propertyType, + List allowedImplementations) { + return check( + "use %s for properties of type %s".formatted(String.join(" or ", allowedImplementations), propertyType), (javaClass, events) -> javaClass.getFields() .stream() .filter((field) -> propertyType.equals(field.getRawType().getName())) - .forEach((field) -> checkPropertyInitializer(field, implementationTypes, events))); + .forEach((field) -> checkPropertyImplementation(field, allowedImplementations, events))); } - private static void checkPropertyInitializer(JavaField field, List implementationTypes, + private static void checkPropertyImplementation(JavaField field, List allowedImplementations, ConditionEvents events) { - String description = implementationTypes.stream().collect(Collectors.joining(" or ")); field.getAccessesToSelf() .stream() .filter((access) -> access.getAccessType() == AccessType.SET) .flatMap((access) -> initializerConstructorCalls(access).stream()) - .filter((call) -> !implementationTypes.contains(call.getTargetOwner().getName())) - .forEach((call) -> addViolation(events, field, "%s should be initialized with %s instead of %s" - .formatted(field.getDescription(), description, call.getTargetOwner().getName()))); + .filter((call) -> !allowedImplementations.contains(call.getTargetOwner().getName())) + .forEach((call) -> addViolation(events, field, + "%s should be initialized with %s instead of %s".formatted(field.getDescription(), + String.join(" or ", allowedImplementations), call.getTargetOwner().getName()))); } private static List initializerConstructorCalls(JavaFieldAccess fieldAccess) { @@ -482,11 +476,11 @@ private static List initializerConstructorCalls(JavaFieldAc .toList(); } - private static DescribedPredicate areNestedInConfigurationPropertiesClasses(String annotationClass) { - return DescribedPredicate.describe("are nested in @ConfigurationProperties", + private static DescribedPredicate areNestedInClassAnnotatedWith(String annotationClass) { + return DescribedPredicate.describe("one of its inner class", (javaClass) -> javaClass.getEnclosingClass() .map((enclosing) -> enclosing.isAnnotatedWith(annotationClass) - || areNestedInConfigurationPropertiesClasses(annotationClass).test(enclosing)) + || areNestedInClassAnnotatedWith(annotationClass).test(enclosing)) .orElse(false)); } diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java index aaddc9feda3b..62224bcefa95 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java @@ -47,6 +47,7 @@ import org.springframework.boot.build.architecture.annotations.TestConditionalOnMissingBean; import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties; import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesBinding; +import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource; import org.springframework.boot.build.architecture.annotations.TestDeprecatedConfigurationProperty; import org.springframework.util.ClassUtils; import org.springframework.util.FileSystemUtils; @@ -339,6 +340,13 @@ void whenConfigurationPropertiesUsesHashMapShouldFailAndWriteReport() throws IOE Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashMap"); } + @Test + void whenConfigurationPropertiesSourceUsesHashMapShouldFailAndWriteReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/hashmap", "annotations"); + buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashMap"); + } + @Test void whenConfigurationPropertiesUsesHashSetShouldFailAndWriteReport() throws IOException { prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/hashset", "annotations"); @@ -346,6 +354,13 @@ void whenConfigurationPropertiesUsesHashSetShouldFailAndWriteReport() throws IOE Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashSet"); } + @Test + void whenConfigurationPropertiesSourceUsesHashSetShouldFailAndWriteReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/hashset", "annotations"); + buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashSet"); + } + @Test void whenConfigurationPropertiesUsesLinkedHashMapShouldSucceedAndWriteEmptyReport() throws IOException { prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/linkedhashmap", "annotations"); @@ -353,6 +368,13 @@ void whenConfigurationPropertiesUsesLinkedHashMapShouldSucceedAndWriteEmptyRepor Task.CHECK_ARCHITECTURE_MAIN); } + @Test + void whenConfigurationPropertiesSourceUsesLinkedHashMapShouldSucceedAndWriteEmptyReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/linkedhashmap", "annotations"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN); + } + @Test void whenConfigurationPropertiesUsesEnumMapShouldSucceedAndWriteEmptyReport() throws IOException { prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/enummap", "annotations"); @@ -360,6 +382,13 @@ void whenConfigurationPropertiesUsesEnumMapShouldSucceedAndWriteEmptyReport() th Task.CHECK_ARCHITECTURE_MAIN); } + @Test + void whenConfigurationPropertiesSourceUsesEnumMapShouldSucceedAndWriteEmptyReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/enummap", "annotations"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN); + } + @Test void whenConfigurationPropertiesUsesLinkedHashSetShouldSucceedAndWriteEmptyReport() throws IOException { prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/linkedhashset", "annotations"); @@ -367,6 +396,13 @@ void whenConfigurationPropertiesUsesLinkedHashSetShouldSucceedAndWriteEmptyRepor Task.CHECK_ARCHITECTURE_MAIN); } + @Test + void whenConfigurationPropertiesSourceUsesLinkedHashSetShouldSucceedAndWriteEmptyReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/linkedhashset", "annotations"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN); + } + @Test void whenConfigurationPropertiesBindingBeanMethodIsNotStaticShouldFailAndWriteReport() throws IOException { prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/bindingnonstatic", "annotations"); @@ -604,6 +640,12 @@ GradleBuild withConfigurationPropertiesAnnotation() { return this; } + GradleBuild withConfigurationPropertiesSourceAnnotation() { + configureTasks(ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_SOURCE.name(), + TestConfigurationPropertiesSource.class.getName()); + return this; + } + GradleBuild withConfigurationPropertiesBindingAnnotation() { configureTasks(ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_BINDING.name(), TestConfigurationPropertiesBinding.class.getName()); diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/annotations/TestConfigurationPropertiesSource.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/annotations/TestConfigurationPropertiesSource.java new file mode 100644 index 000000000000..d4529eceef43 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/annotations/TestConfigurationPropertiesSource.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface TestConfigurationPropertiesSource { + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/enummap/ConfigurationPropertiesWithEnumMap.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/enummap/ConfigurationPropertiesWithEnumMap.java new file mode 100644 index 000000000000..1c5534177a25 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/enummap/ConfigurationPropertiesWithEnumMap.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.configurationpropertiessource.enummap; + +import java.util.EnumMap; +import java.util.Map; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource; + +/** + * Test {@link TestConfigurationPropertiesSource} using {@link EnumMap}. + * + * @author Stephane Nicoll + */ +@TestConfigurationPropertiesSource +public class ConfigurationPropertiesWithEnumMap { + + private Map properties = new EnumMap<>(Example.class); + + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + enum Example { + + ONE + + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/hashmap/ConfigurationPropertiesWithHashMap.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/hashmap/ConfigurationPropertiesWithHashMap.java new file mode 100644 index 000000000000..98d6b4e26b4b --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/hashmap/ConfigurationPropertiesWithHashMap.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.configurationpropertiessource.hashmap; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource; + +/** + * Test {@link TestConfigurationPropertiesSource} using {@link HashMap}. + * + * @author Stephane Nicoll + */ +@TestConfigurationPropertiesSource +public class ConfigurationPropertiesWithHashMap { + + private Map properties = new HashMap<>(); + + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/hashset/ConfigurationPropertiesWithHashSet.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/hashset/ConfigurationPropertiesWithHashSet.java new file mode 100644 index 000000000000..c7b4011f9d99 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/hashset/ConfigurationPropertiesWithHashSet.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.configurationpropertiessource.hashset; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource; + +/** + * Test {@link TestConfigurationPropertiesSource} using {@link HashSet}. + * + * @author Stephane Nicoll + */ +@TestConfigurationPropertiesSource +public class ConfigurationPropertiesWithHashSet { + + private Set items = new HashSet<>(); + + public Set getItems() { + return this.items; + } + + public void setItems(Set items) { + this.items = items; + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/linkedhashmap/ConfigurationPropertiesWithLinkedHashMap.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/linkedhashmap/ConfigurationPropertiesWithLinkedHashMap.java new file mode 100644 index 000000000000..d300433a0429 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/linkedhashmap/ConfigurationPropertiesWithLinkedHashMap.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.configurationpropertiessource.linkedhashmap; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource; + +/** + * Test {@link TestConfigurationPropertiesSource} using {@link LinkedHashMap}. + * + * @author Stephane Nicoll + */ +@TestConfigurationPropertiesSource +public class ConfigurationPropertiesWithLinkedHashMap { + + private Map properties = new LinkedHashMap<>(); + + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/linkedhashset/ConfigurationPropertiesWithLinkedHashSet.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/linkedhashset/ConfigurationPropertiesWithLinkedHashSet.java new file mode 100644 index 000000000000..085fea8b58df --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/linkedhashset/ConfigurationPropertiesWithLinkedHashSet.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.configurationpropertiessource.linkedhashset; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource; + +/** + * Test {@link TestConfigurationPropertiesSource} using {@link LinkedHashSet}. + * + * @author Stephane Nicoll + */ +@TestConfigurationPropertiesSource +public class ConfigurationPropertiesWithLinkedHashSet { + + private Set items = new LinkedHashSet<>(); + + public Set getItems() { + return this.items; + } + + public void setItems(Set items) { + this.items = items; + } + +} diff --git a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaProperties.java b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaProperties.java index 70d4bc6bef3d..d622810b5fee 100644 --- a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaProperties.java +++ b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaProperties.java @@ -59,7 +59,6 @@ * @author Andy Wilkinson * @author Scott Frederick * @author Yanming Zhou - * @author Venkata Naga Sai Srikanth Gollapudi * @since 4.0.0 */ @ConfigurationProperties("spring.kafka") From cb9b32bd2246fe5c4a1e8ed360def3cbe7f3f217 Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Sun, 17 May 2026 15:40:51 +0200 Subject: [PATCH 692/930] Add self-reference to ApplicationContextAssertProvider See gh-50500 Signed-off-by: Stefano Cordio --- .../ApplicationContextAssertProvider.java | 14 +++--- .../assertj/AssertableApplicationContext.java | 3 +- ...sertableReactiveWebApplicationContext.java | 4 +- .../AssertableWebApplicationContext.java | 3 +- .../AbstractApplicationContextRunner.java | 2 +- ...ApplicationContextAssertProviderTests.java | 49 ++++++++++++------- ...AbstractApplicationContextRunnerTests.java | 11 ++++- ...ndpointAdditionalPathIntegrationTests.java | 2 +- .../GrpcServerAutoConfigurationTests.java | 8 +-- ...rpcServerHealthAutoConfigurationTests.java | 2 +- ...2ResourceServerAutoConfigurationTests.java | 4 +- 11 files changed, 64 insertions(+), 38 deletions(-) diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProvider.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProvider.java index 1db7a364f8fd..bae56ad3034f 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProvider.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProvider.java @@ -44,16 +44,18 @@ * Any {@link ApplicationContext} method called on a context that has failed to start will * throw an {@link IllegalStateException}. * - * @param the application context type + * @param the self class reference + * @param the source application context type * @author Phillip Webb + * @author Stefano Cordio * @since 2.0.0 * @see AssertableApplicationContext * @see AssertableWebApplicationContext * @see AssertableReactiveWebApplicationContext * @see ApplicationContextAssert */ -public interface ApplicationContextAssertProvider - extends ApplicationContext, AssertProvider>, Closeable { +public interface ApplicationContextAssertProvider, C extends ApplicationContext> + extends ApplicationContext, AssertProvider>, Closeable { /** * Return an assert for AssertJ. @@ -63,7 +65,7 @@ public interface ApplicationContextAssertProvider */ @Deprecated(since = "2.0.0", forRemoval = false) @Override - ApplicationContextAssert assertThat(); + ApplicationContextAssert assertThat(); /** * Return the original source {@link ApplicationContext}. @@ -104,7 +106,7 @@ public interface ApplicationContextAssertProvider * {@link ApplicationContext} or throw an exception if the context fails to start. * @return a {@link ApplicationContextAssertProvider} instance */ - static , C extends ApplicationContext> T get(Class type, + static , C extends ApplicationContext> T get(Class type, Class contextType, Supplier contextSupplier) { return get(type, contextType, contextSupplier, new Class[0]); } @@ -125,7 +127,7 @@ static , C extends ApplicationCont * @since 3.4.0 */ @SuppressWarnings("unchecked") - static , C extends ApplicationContext> T get(Class type, + static , C extends ApplicationContext> T get(Class type, Class contextType, Supplier contextSupplier, Class... additionalContextInterfaces) { Assert.notNull(type, "'type' must not be null"); diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableApplicationContext.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableApplicationContext.java index db4cbc32f97a..4a79387d005d 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableApplicationContext.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableApplicationContext.java @@ -35,7 +35,8 @@ * @see ApplicationContext */ public interface AssertableApplicationContext - extends ApplicationContextAssertProvider, ConfigurableApplicationContext { + extends ApplicationContextAssertProvider, + ConfigurableApplicationContext { /** * Factory method to create a new {@link AssertableApplicationContext} instance. diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableReactiveWebApplicationContext.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableReactiveWebApplicationContext.java index 78a349d0ff86..a6fe90e87cd1 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableReactiveWebApplicationContext.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableReactiveWebApplicationContext.java @@ -33,8 +33,8 @@ * @see ReactiveWebApplicationContext * @see ReactiveWebApplicationContext */ -public interface AssertableReactiveWebApplicationContext - extends ApplicationContextAssertProvider, +public interface AssertableReactiveWebApplicationContext extends + ApplicationContextAssertProvider, ConfigurableReactiveWebApplicationContext { /** diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableWebApplicationContext.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableWebApplicationContext.java index ba61598ec6b7..1a6094d256d6 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableWebApplicationContext.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableWebApplicationContext.java @@ -35,7 +35,8 @@ * @see WebApplicationContext */ public interface AssertableWebApplicationContext - extends ApplicationContextAssertProvider, ConfigurableWebApplicationContext { + extends ApplicationContextAssertProvider, + ConfigurableWebApplicationContext { /** * Factory method to create a new {@link AssertableWebApplicationContext} instance. diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java index 7afc3a65ece9..9b9477078c98 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java @@ -110,7 +110,7 @@ * @see ReactiveWebApplicationContextRunner * @see ApplicationContextAssert */ -public abstract class AbstractApplicationContextRunner, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> { +public abstract class AbstractApplicationContextRunner, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> { private final RunnerConfiguration runnerConfiguration; diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java index e2269cbf6d38..7b9ebece7746 100644 --- a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java @@ -100,7 +100,8 @@ void getWhenSupplierIsNullShouldThrowException() { @Test void getWhenContextStartsShouldReturnProxyThatCallsRealMethods() { - ApplicationContextAssertProvider context = get(this.mockContextSupplier); + ApplicationContextAssertProvider context = get( + this.mockContextSupplier); assertThat((Object) context).isNotNull(); context.getBean("foo"); then(this.mockContext).should().getBean("foo"); @@ -108,7 +109,8 @@ void getWhenContextStartsShouldReturnProxyThatCallsRealMethods() { @Test void getWhenContextFailsShouldReturnProxyThatThrowsExceptions() { - ApplicationContextAssertProvider context = get(this.startupFailureSupplier); + ApplicationContextAssertProvider context = get( + this.startupFailureSupplier); assertThat((Object) context).isNotNull(); assertThatIllegalStateException().isThrownBy(() -> context.getBean("foo")) .withCause(this.startupFailure) @@ -117,13 +119,15 @@ void getWhenContextFailsShouldReturnProxyThatThrowsExceptions() { @Test void getSourceContextWhenContextStartsShouldReturnSourceContext() { - ApplicationContextAssertProvider context = get(this.mockContextSupplier); + ApplicationContextAssertProvider context = get( + this.mockContextSupplier); assertThat(context.getSourceApplicationContext()).isSameAs(this.mockContext); } @Test void getSourceContextWhenContextFailsShouldThrowException() { - ApplicationContextAssertProvider context = get(this.startupFailureSupplier); + ApplicationContextAssertProvider context = get( + this.startupFailureSupplier); assertThatIllegalStateException().isThrownBy(context::getSourceApplicationContext) .withCause(this.startupFailure) .withMessageContaining("failed to start"); @@ -131,13 +135,15 @@ void getSourceContextWhenContextFailsShouldThrowException() { @Test void getSourceContextOfTypeWhenContextStartsShouldReturnSourceContext() { - ApplicationContextAssertProvider context = get(this.mockContextSupplier); + ApplicationContextAssertProvider context = get( + this.mockContextSupplier); assertThat(context.getSourceApplicationContext(ApplicationContext.class)).isSameAs(this.mockContext); } @Test void getSourceContextOfTypeWhenContextFailsToStartShouldThrowException() { - ApplicationContextAssertProvider context = get(this.startupFailureSupplier); + ApplicationContextAssertProvider context = get( + this.startupFailureSupplier); assertThatIllegalStateException() .isThrownBy(() -> context.getSourceApplicationContext(ApplicationContext.class)) .withCause(this.startupFailure) @@ -146,59 +152,68 @@ void getSourceContextOfTypeWhenContextFailsToStartShouldThrowException() { @Test void getStartupFailureWhenContextStartsShouldReturnNull() { - ApplicationContextAssertProvider context = get(this.mockContextSupplier); + ApplicationContextAssertProvider context = get( + this.mockContextSupplier); assertThat(context.getStartupFailure()).isNull(); } @Test void getStartupFailureWhenContextFailsToStartShouldReturnException() { - ApplicationContextAssertProvider context = get(this.startupFailureSupplier); + ApplicationContextAssertProvider context = get( + this.startupFailureSupplier); assertThat(context.getStartupFailure()).isEqualTo(this.startupFailure); } @Test void assertThatWhenContextStartsShouldReturnAssertions() { - ApplicationContextAssertProvider context = get(this.mockContextSupplier); - ApplicationContextAssert contextAssert = assertThat(context); + ApplicationContextAssertProvider context = get( + this.mockContextSupplier); + ApplicationContextAssert contextAssert = assertThat(context); assertThat(contextAssert.getApplicationContext()).isSameAs(context); assertThat(contextAssert.getStartupFailure()).isNull(); } @Test void assertThatWhenContextFailsShouldReturnAssertions() { - ApplicationContextAssertProvider context = get(this.startupFailureSupplier); - ApplicationContextAssert contextAssert = assertThat(context); + ApplicationContextAssertProvider context = get( + this.startupFailureSupplier); + ApplicationContextAssert contextAssert = assertThat(context); assertThat(contextAssert.getApplicationContext()).isSameAs(context); assertThat(contextAssert.getStartupFailure()).isSameAs(this.startupFailure); } @Test void toStringWhenContextStartsShouldReturnSimpleString() { - ApplicationContextAssertProvider context = get(this.mockContextSupplier); + ApplicationContextAssertProvider context = get( + this.mockContextSupplier); assertThat(context.toString()).startsWith("Started application [ConfigurableApplicationContext.MockitoMock") .endsWith("id = [null], applicationName = [null], beanDefinitionCount = 0]"); } @Test void toStringWhenContextFailsToStartShouldReturnSimpleString() { - ApplicationContextAssertProvider context = get(this.startupFailureSupplier); + ApplicationContextAssertProvider context = get( + this.startupFailureSupplier); assertThat(context).hasToString("Unstarted application context " + "org.springframework.context.ApplicationContext[startupFailure=java.lang.RuntimeException]"); } @Test void closeShouldCloseContext() { - ApplicationContextAssertProvider context = get(this.mockContextSupplier); + ApplicationContextAssertProvider context = get( + this.mockContextSupplier); context.close(); then(this.mockContext).should().close(); } - private ApplicationContextAssertProvider get(Supplier contextSupplier) { + private ApplicationContextAssertProvider get( + Supplier contextSupplier) { return ApplicationContextAssertProvider.get(TestAssertProviderApplicationContext.class, ApplicationContext.class, contextSupplier); } - interface TestAssertProviderApplicationContext extends ApplicationContextAssertProvider { + interface TestAssertProviderApplicationContext + extends ApplicationContextAssertProvider { } diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java index 43fcdf7f74b3..755e43e29961 100644 --- a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java @@ -62,8 +62,15 @@ * @param the assertable context type * @author Stephane Nicoll * @author Phillip Webb + * @author Stefano Cordio */ -abstract class AbstractApplicationContextRunnerTests, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> { +abstract class AbstractApplicationContextRunnerTests, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> { + + @Test + void runContextShouldWorkWithSatisfiesAssertion() { + get().run((context) -> assertThat(context).satisfies((ctx) -> assertThat(ctx).hasNotFailed(), + (ctx) -> assertThat(ctx).doesNotHaveBean("foo"))); + } @Test void runWithInitializerShouldInitialize() { @@ -123,7 +130,7 @@ void runWithSystemPropertiesWhenValueIsNullShouldRemoveProperty() { } @Test - void runWithMultiplePropertyValuesShouldAllAllValues() { + void runWithMultiplePropertyValuesShouldHaveAllValues() { get().withPropertyValues("test.foo=1").withPropertyValues("test.bar=2").run((context) -> { Environment environment = context.getEnvironment(); assertThat(environment.getProperty("test.foo")).isEqualTo("1"); diff --git a/module/spring-boot-actuator-autoconfigure/src/testFixtures/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java b/module/spring-boot-actuator-autoconfigure/src/testFixtures/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java index c1efa3e75a19..9d67e2422a51 100644 --- a/module/spring-boot-actuator-autoconfigure/src/testFixtures/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java +++ b/module/spring-boot-actuator-autoconfigure/src/testFixtures/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java @@ -37,7 +37,7 @@ * @param the assertions * @author Madhura Bhave */ -public abstract class AbstractHealthEndpointAdditionalPathIntegrationTests, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> { +public abstract class AbstractHealthEndpointAdditionalPathIntegrationTests, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> { private final T runner; diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java index f0721b1e2fe3..305c564e4f74 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java @@ -86,7 +86,7 @@ class GrpcServerAutoConfigurationTests { private final ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(autoConfigurations) .with(this::noOpLifecycleBeans) .with(this::serviceBean); @@ -350,7 +350,7 @@ void nettyServerFactoryAutoConfiguredWithSsl() { "nettyGrpcServerLifecycle")); } - private ContextConsumer> assertThatServerIsConfigured( + private ContextConsumer> assertThatServerIsConfigured( Class expectedServerFactoryType, String expectedAddress, String expectedLifecycleBeanName) { return (context) -> { assertThat(context).getBean(GrpcServerFactory.class) @@ -365,12 +365,12 @@ private ContextConsumer> assertThatS }; } - private , C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> R serviceBean( + private , C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> R serviceBean( R contextRunner) { return contextRunner.withBean(BindableService.class, () -> this.service); } - private , C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> R noOpLifecycleBeans( + private , C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> R noOpLifecycleBeans( R contextRunner) { return contextRunner.withBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) .withBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java index 527c284f61ca..ebd3e8ff439c 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java @@ -348,7 +348,7 @@ private void assertNotConfigured(AssertableApplicationContext context) { assertThat(context).doesNotHaveBean(GrpcServerHealthAutoConfiguration.class); } - private , C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> R serviceBean( + private , C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> R serviceBean( R contextRunner) { return contextRunner.withBean(BindableService.class, () -> this.service); } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java index f20be2bf4ed7..202d66a2ae05 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java @@ -62,7 +62,7 @@ class GrpcServerOAuth2ResourceServerAutoConfigurationTests { private static final AutoConfigurations autoConfigurations = AutoConfigurations .of(OAuth2ResourceServerAutoConfiguration.class, GrpcServerOAuth2ResourceServerAutoConfiguration.class); - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(autoConfigurations) .withUserConfiguration(GrpcSecurityConfiguration.class) .with(this::serviceBean) @@ -115,7 +115,7 @@ void notConfiguredInWebApplicationWithNoBindableService() { .run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class)); } - private , C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> R serviceBean( + private , C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> R serviceBean( R contextRunner) { BindableService service = mock(); ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); From e3e01fe9d5615cfb4a30ef76ad70d0b1a06c79b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 15 Jul 2026 12:05:39 +0200 Subject: [PATCH 693/930] Polish "Add self-reference to ApplicationContextAssertProvider" See gh-50500 --- ...ApplicationContextAssertProviderTests.java | 46 +++++++------------ ...AbstractApplicationContextRunnerTests.java | 14 +++--- .../GrpcServerAutoConfigurationTests.java | 2 +- ...2ResourceServerAutoConfigurationTests.java | 2 +- 4 files changed, 25 insertions(+), 39 deletions(-) diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java index 7b9ebece7746..f54784483976 100644 --- a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java @@ -100,8 +100,7 @@ void getWhenSupplierIsNullShouldThrowException() { @Test void getWhenContextStartsShouldReturnProxyThatCallsRealMethods() { - ApplicationContextAssertProvider context = get( - this.mockContextSupplier); + ApplicationContextAssertProvider context = get(this.mockContextSupplier); assertThat((Object) context).isNotNull(); context.getBean("foo"); then(this.mockContext).should().getBean("foo"); @@ -109,8 +108,7 @@ void getWhenContextStartsShouldReturnProxyThatCallsRealMethods() { @Test void getWhenContextFailsShouldReturnProxyThatThrowsExceptions() { - ApplicationContextAssertProvider context = get( - this.startupFailureSupplier); + ApplicationContextAssertProvider context = get(this.startupFailureSupplier); assertThat((Object) context).isNotNull(); assertThatIllegalStateException().isThrownBy(() -> context.getBean("foo")) .withCause(this.startupFailure) @@ -119,15 +117,13 @@ void getWhenContextFailsShouldReturnProxyThatThrowsExceptions() { @Test void getSourceContextWhenContextStartsShouldReturnSourceContext() { - ApplicationContextAssertProvider context = get( - this.mockContextSupplier); + ApplicationContextAssertProvider context = get(this.mockContextSupplier); assertThat(context.getSourceApplicationContext()).isSameAs(this.mockContext); } @Test void getSourceContextWhenContextFailsShouldThrowException() { - ApplicationContextAssertProvider context = get( - this.startupFailureSupplier); + ApplicationContextAssertProvider context = get(this.startupFailureSupplier); assertThatIllegalStateException().isThrownBy(context::getSourceApplicationContext) .withCause(this.startupFailure) .withMessageContaining("failed to start"); @@ -135,15 +131,13 @@ void getSourceContextWhenContextFailsShouldThrowException() { @Test void getSourceContextOfTypeWhenContextStartsShouldReturnSourceContext() { - ApplicationContextAssertProvider context = get( - this.mockContextSupplier); + ApplicationContextAssertProvider context = get(this.mockContextSupplier); assertThat(context.getSourceApplicationContext(ApplicationContext.class)).isSameAs(this.mockContext); } @Test void getSourceContextOfTypeWhenContextFailsToStartShouldThrowException() { - ApplicationContextAssertProvider context = get( - this.startupFailureSupplier); + ApplicationContextAssertProvider context = get(this.startupFailureSupplier); assertThatIllegalStateException() .isThrownBy(() -> context.getSourceApplicationContext(ApplicationContext.class)) .withCause(this.startupFailure) @@ -152,62 +146,54 @@ void getSourceContextOfTypeWhenContextFailsToStartShouldThrowException() { @Test void getStartupFailureWhenContextStartsShouldReturnNull() { - ApplicationContextAssertProvider context = get( - this.mockContextSupplier); + ApplicationContextAssertProvider context = get(this.mockContextSupplier); assertThat(context.getStartupFailure()).isNull(); } @Test void getStartupFailureWhenContextFailsToStartShouldReturnException() { - ApplicationContextAssertProvider context = get( - this.startupFailureSupplier); + ApplicationContextAssertProvider context = get(this.startupFailureSupplier); assertThat(context.getStartupFailure()).isEqualTo(this.startupFailure); } @Test void assertThatWhenContextStartsShouldReturnAssertions() { - ApplicationContextAssertProvider context = get( - this.mockContextSupplier); - ApplicationContextAssert contextAssert = assertThat(context); + ApplicationContextAssertProvider context = get(this.mockContextSupplier); + ApplicationContextAssert contextAssert = assertThat(context); assertThat(contextAssert.getApplicationContext()).isSameAs(context); assertThat(contextAssert.getStartupFailure()).isNull(); } @Test void assertThatWhenContextFailsShouldReturnAssertions() { - ApplicationContextAssertProvider context = get( - this.startupFailureSupplier); - ApplicationContextAssert contextAssert = assertThat(context); + ApplicationContextAssertProvider context = get(this.startupFailureSupplier); + ApplicationContextAssert contextAssert = assertThat(context); assertThat(contextAssert.getApplicationContext()).isSameAs(context); assertThat(contextAssert.getStartupFailure()).isSameAs(this.startupFailure); } @Test void toStringWhenContextStartsShouldReturnSimpleString() { - ApplicationContextAssertProvider context = get( - this.mockContextSupplier); + ApplicationContextAssertProvider context = get(this.mockContextSupplier); assertThat(context.toString()).startsWith("Started application [ConfigurableApplicationContext.MockitoMock") .endsWith("id = [null], applicationName = [null], beanDefinitionCount = 0]"); } @Test void toStringWhenContextFailsToStartShouldReturnSimpleString() { - ApplicationContextAssertProvider context = get( - this.startupFailureSupplier); + ApplicationContextAssertProvider context = get(this.startupFailureSupplier); assertThat(context).hasToString("Unstarted application context " + "org.springframework.context.ApplicationContext[startupFailure=java.lang.RuntimeException]"); } @Test void closeShouldCloseContext() { - ApplicationContextAssertProvider context = get( - this.mockContextSupplier); + ApplicationContextAssertProvider context = get(this.mockContextSupplier); context.close(); then(this.mockContext).should().close(); } - private ApplicationContextAssertProvider get( - Supplier contextSupplier) { + private ApplicationContextAssertProvider get(Supplier contextSupplier) { return ApplicationContextAssertProvider.get(TestAssertProviderApplicationContext.class, ApplicationContext.class, contextSupplier); } diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java index 755e43e29961..d1b466da61f4 100644 --- a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java @@ -66,12 +66,6 @@ */ abstract class AbstractApplicationContextRunnerTests, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> { - @Test - void runContextShouldWorkWithSatisfiesAssertion() { - get().run((context) -> assertThat(context).satisfies((ctx) -> assertThat(ctx).hasNotFailed(), - (ctx) -> assertThat(ctx).doesNotHaveBean("foo"))); - } - @Test void runWithInitializerShouldInitialize() { AtomicBoolean called = new AtomicBoolean(); @@ -130,7 +124,7 @@ void runWithSystemPropertiesWhenValueIsNullShouldRemoveProperty() { } @Test - void runWithMultiplePropertyValuesShouldHaveAllValues() { + void runWithMultiplePropertyValuesShouldAllAllValues() { get().withPropertyValues("test.foo=1").withPropertyValues("test.bar=2").run((context) -> { Environment environment = context.getEnvironment(); assertThat(environment.getProperty("test.foo")).isEqualTo("1"); @@ -285,6 +279,12 @@ void runWithConfigurationsAndUserBeanShouldRegisterUserBeanLast() { }); } + @Test + void runShouldWorkWithSatisfiesAssertion() { + get().run((context) -> assertThat(context).satisfies((ctx) -> assertThat(ctx).hasNotFailed(), + (ctx) -> assertThat(ctx).doesNotHaveBean("foo"))); + } + @Test void changesMadeByInitializersShouldBeVisibleToRegisteredClasses() { get().withInitializer((context) -> context.getEnvironment().setActiveProfiles("test")) diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java index 305c564e4f74..69aab1d855bc 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java @@ -86,7 +86,7 @@ class GrpcServerAutoConfigurationTests { private final ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(autoConfigurations) .with(this::noOpLifecycleBeans) .with(this::serviceBean); diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java index 202d66a2ae05..fc6083a90e25 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java @@ -62,7 +62,7 @@ class GrpcServerOAuth2ResourceServerAutoConfigurationTests { private static final AutoConfigurations autoConfigurations = AutoConfigurations .of(OAuth2ResourceServerAutoConfiguration.class, GrpcServerOAuth2ResourceServerAutoConfiguration.class); - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(autoConfigurations) .withUserConfiguration(GrpcSecurityConfiguration.class) .with(this::serviceBean) From 728cbc67ff0f82e67807bc6f955c411562a0566a Mon Sep 17 00:00:00 2001 From: gichanGim Date: Sun, 28 Jun 2026 15:46:51 +0900 Subject: [PATCH 694/930] Make ResourceHandlerRegistrationCustomizer public See gh-50877 Signed-off-by: gichanGim --- ...ResourceHandlerRegistrationCustomizer.java | 36 +++++++++++++++++++ .../WebMvcAutoConfiguration.java | 7 ---- 2 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/ResourceHandlerRegistrationCustomizer.java diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/ResourceHandlerRegistrationCustomizer.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/ResourceHandlerRegistrationCustomizer.java new file mode 100644 index 000000000000..ec0c7bca6d94 --- /dev/null +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/ResourceHandlerRegistrationCustomizer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.webmvc.autoconfigure; + +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration; + +/** + * Callback interface that can be used to customize {@link ResourceHandlerRegistration}. + * + * @author Brian Clozel + * @since 4.2.0 + */ +@FunctionalInterface +public interface ResourceHandlerRegistrationCustomizer { + + /** + * Customize the given {@link ResourceHandlerRegistration}. + * @param registration the registration to customize + */ + void customize(ResourceHandlerRegistration registration); + +} diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java index 7d829e17d76d..281b124124a0 100644 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java @@ -676,13 +676,6 @@ T create(TemplateAvailabilityProviders templateAvailabilityProviders, Applicatio } - @FunctionalInterface - interface ResourceHandlerRegistrationCustomizer { - - void customize(ResourceHandlerRegistration registration); - - } - static class ResourceChainResourceHandlerRegistrationCustomizer implements ResourceHandlerRegistrationCustomizer { private final Resources resourceProperties; From f48cc3ac10cc58f8fd437b45beeb87f40ae671b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 20 Mar 2026 15:32:45 +0100 Subject: [PATCH 695/930] Move Spring AMQP 0.9 support to spring-boot-rabbitmq Now that Spring AMQP supports AMQP 1.0 as well, we need to straighten out our existing support. AMQP 0.9 is RabbitMQ specific, so we can no longer refer it to "amqp". This commit migrates the existing support to spring-boot-rabbitmq and renames the starters as well. For continuity, users should migrate from "spring-boot-starter-amqp" to "spring-boot-starter-rabbitmq". While the root package has been changed for consistency, most names are the same, including configuration properties still in the "spring.rabbitmq" namespace. Keys to configure the version of the RabbitMQ clients have migrated to 'rabbitmq.client' and 'rabbitmq.stream.client' respectively. Closes gh-49677 --- documentation/spring-boot-docs/build.gradle | 2 +- .../reference/pages/actuator/endpoints.adoc | 2 +- .../pages/features/dev-services.adoc | 4 +-- .../reference/pages/messaging/amqp.adoc | 18 +++++----- .../pages/testing/testcontainers.adoc | 10 +++--- .../custom/MyRabbitConfiguration.java | 2 +- .../receiving/custom/MyRabbitConfiguration.kt | 2 +- .../main/resources/META-INF/spring.factories | 8 ----- ...ot.autoconfigure.AutoConfiguration.imports | 3 -- .../build.gradle | 6 ++-- .../build.gradle | 2 +- ...ectionDetailsFactoryIntegrationTests.java} | 19 +++++----- ...ectionDetailsFactoryIntegrationTests.java} | 14 ++++---- ...ectionDetailsFactoryIntegrationTests.java} | 12 +++---- ...ectionDetailsFactoryIntegrationTests.java} | 17 ++++----- ...nectionDetailsFactoryIntegrationTests.java | 8 ++--- ...ectionDetailsFactoryIntegrationTests.java} | 10 +++--- ...ectionDetailsFactoryIntegrationTests.java} | 20 +++++------ ...ectionDetailsFactoryIntegrationTests.java} | 34 +++++++++--------- ...ectionDetailsFactoryIntegrationTests.java} | 18 +++++----- ...ectionDetailsFactoryIntegrationTests.java} | 20 +++++------ .../src/dockerTest/resources/logback-test.xml | 0 .../org/springframework/boot/rabbitmq}/ca.crt | 0 .../springframework/boot/rabbitmq}/client.crt | 0 .../springframework/boot/rabbitmq}/client.key | 0 ...tmq-stream-separate-services-compose.yaml} | 0 ...bbitmq-stream-single-service-compose.yaml} | 0 .../docker/compose/rabbitmq-compose.yaml} | 0 .../docker/compose/rabbitmq-ssl-compose.yaml} | 0 .../docker/compose/rabbitmq-ssl.conf | 0 .../compose/rabbitmq-stream-compose.yaml} | 0 .../compose/rabbitmq-stream-ssl-compose.yaml} | 0 .../docker/compose/rabbitmq-stream-ssl.conf | 0 .../springframework/boot/rabbitmq}/server.crt | 0 .../springframework/boot/rabbitmq}/server.key | 0 .../testcontainers/rabbitmq-stream-ssl.conf | 0 .../dockerTest/resources/spring.properties | 0 .../AbstractConnectionFactoryConfigurer.java | 4 +-- ...bitListenerContainerFactoryConfigurer.java | 10 +++--- .../CachingConnectionFactoryConfigurer.java | 4 +-- .../ConnectionFactoryCustomizer.java | 4 +-- ...bitListenerContainerFactoryConfigurer.java | 4 +-- .../EnvironmentBuilderCustomizer.java | 4 +-- .../PropertiesRabbitConnectionDetails.java | 4 +-- .../RabbitAnnotationDrivenConfiguration.java | 2 +- .../RabbitAutoConfiguration.java | 4 +-- .../RabbitConnectionDetails.java | 4 +-- ...RabbitConnectionFactoryBeanConfigurer.java | 6 ++-- ...RabbitListenerRetrySettingsCustomizer.java | 4 +-- .../autoconfigure/RabbitProperties.java | 4 +-- .../RabbitRetryTemplateSettings.java | 4 +-- .../RabbitStreamConfiguration.java | 8 ++--- .../RabbitStreamConnectionDetails.java | 4 +-- .../RabbitStreamTemplateConfigurer.java | 6 ++-- .../RabbitTemplateConfigurer.java | 6 ++-- .../RabbitTemplateCustomizer.java | 4 +-- ...RabbitTemplateRetrySettingsCustomizer.java | 4 +-- ...bitListenerContainerFactoryConfigurer.java | 4 +-- .../SslBundleRabbitConnectionFactoryBean.java | 2 +- ...bitHealthContributorAutoConfiguration.java | 8 ++--- .../autoconfigure/health/package-info.java | 2 +- ...ConnectionFactoryMetricsPostProcessor.java | 4 +-- .../RabbitMetricsAutoConfiguration.java | 6 ++-- .../autoconfigure/metrics/package-info.java | 2 +- .../rabbitmq}/autoconfigure/package-info.java | 2 +- ...ockerComposeConnectionDetailsFactory.java} | 14 ++++---- .../docker/compose/RabbitMqEnvironment.java} | 6 ++-- ...ockerComposeConnectionDetailsFactory.java} | 14 ++++---- .../docker/compose/package-info.java | 2 +- .../health/RabbitHealthIndicator.java | 4 +-- .../boot/rabbitmq}/health/package-info.java | 2 +- .../boot/rabbitmq}/metrics/RabbitMetrics.java | 4 +-- .../boot/rabbitmq}/metrics/package-info.java | 2 +- ...bbitContainerConnectionDetailsFactory.java | 6 ++-- ...tMqContainerConnectionDetailsFactory.java} | 6 ++-- ...eamContainerConnectionDetailsFactory.java} | 8 ++--- .../testcontainers/package-info.java | 2 +- ...itional-spring-configuration-metadata.json | 0 .../main/resources/META-INF/spring.factories | 8 +++++ ...ot.autoconfigure.AutoConfiguration.imports | 3 ++ ...ropertiesRabbitConnectionDetailsTests.java | 4 +-- .../RabbitAutoConfigurationTests.java | 18 +++++----- .../autoconfigure/RabbitPropertiesTests.java | 2 +- .../RabbitStreamConfigurationTests.java | 8 ++--- ...althContributorAutoConfigurationTests.java | 6 ++-- ...ationMeterBinderCycleIntegrationTests.java | 4 +-- .../RabbitMetricsAutoConfigurationTests.java | 4 +-- .../compose/RabbitMqEnvironmentTests.java} | 30 ++++++++-------- .../health/RabbitHealthIndicatorTests.java | 2 +- .../rabbitmq}/metrics/RabbitMetricsTests.java | 2 +- .../src/test/resources/logback-test.xml | 0 .../boot/rabbitmq}/autoconfigure/test.jks | Bin .../spring-boot-dependencies/build.gradle | 10 +++--- settings.gradle | 8 ++--- .../build.gradle | 6 ++-- .../SampleRabbitApplicationSslTests.java} | 4 +-- .../SampleRabbitApplicationTests.java} | 4 +-- .../rabbitmq}/SecureRabbitMqContainer.java | 2 +- .../rabbitmq/SampleRabbitApplication.java} | 8 ++--- .../main/java/smoketest/rabbitmq}/Sender.java | 2 +- .../smoketest/rabbitmq}/package-info.java | 2 +- .../src/test/resources/ssl/rabbitmq.conf | 0 .../src/test/resources/ssl/test-ca.crt | 0 .../src/test/resources/ssl/test-ca.key | 0 .../src/test/resources/ssl/test-client.crt | 0 .../src/test/resources/ssl/test-client.key | 0 .../src/test/resources/ssl/test-server.crt | 0 .../src/test/resources/ssl/test-server.key | 0 .../build.gradle | 4 +-- .../build.gradle | 4 +-- 110 files changed, 286 insertions(+), 284 deletions(-) delete mode 100644 module/spring-boot-amqp/src/main/resources/META-INF/spring.factories delete mode 100644 module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename module/{spring-boot-amqp => spring-boot-rabbitmq}/build.gradle (98%) rename module/{spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java => spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java} (74%) rename module/{spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java => spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java} (78%) rename module/{spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java => spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java} (78%) rename module/{spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java => spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java} (74%) rename module/{spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq}/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java (91%) rename module/{spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java => spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java} (89%) rename module/{spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java => spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java} (83%) rename module/{spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java => spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java} (80%) rename module/{spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java => spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java} (86%) rename module/{spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java => spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java} (85%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/logback-test.xml (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq}/ca.crt (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq}/client.crt (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq}/client.key (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-and-rabbitmq-stream-separate-services-compose.yaml} (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-and-rabbitmq-stream-single-service-compose.yaml} (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-compose.yaml} (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-ssl-compose.yaml} (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq}/docker/compose/rabbitmq-ssl.conf (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-compose.yaml} (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-ssl-compose.yaml} (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq}/docker/compose/rabbitmq-stream-ssl.conf (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq}/server.crt (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq}/server.key (100%) rename module/{spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp => spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq}/testcontainers/rabbitmq-stream-ssl.conf (100%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/dockerTest/resources/spring.properties (100%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/AbstractConnectionFactoryConfigurer.java (98%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java (96%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/CachingConnectionFactoryConfigurer.java (97%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/ConnectionFactoryCustomizer.java (93%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java (96%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/EnvironmentBuilderCustomizer.java (94%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/PropertiesRabbitConnectionDetails.java (94%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitAnnotationDrivenConfiguration.java (99%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitAutoConfiguration.java (99%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitConnectionDetails.java (97%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java (97%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitListenerRetrySettingsCustomizer.java (94%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitProperties.java (99%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitRetryTemplateSettings.java (95%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitStreamConfiguration.java (96%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitStreamConnectionDetails.java (96%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitStreamTemplateConfigurer.java (97%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitTemplateConfigurer.java (98%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitTemplateCustomizer.java (93%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java (94%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java (96%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/SslBundleRabbitConnectionFactoryBean.java (96%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java (91%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/health/package-info.java (91%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java (96%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java (92%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/metrics/package-info.java (91%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/autoconfigure/package-info.java (92%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java} (85%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironment.java} (89%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactory.java} (86%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/docker/compose/package-info.java (92%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/health/RabbitHealthIndicator.java (96%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/health/package-info.java (93%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/metrics/RabbitMetrics.java (96%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/metrics/package-info.java (93%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java (93%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactory.java} (93%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactory.java} (92%) rename module/{spring-boot-amqp/src/main/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq}/testcontainers/package-info.java (92%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/main/resources/META-INF/additional-spring-configuration-metadata.json (100%) create mode 100644 module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories create mode 100644 module/spring-boot-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename module/{spring-boot-amqp/src/test/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq}/autoconfigure/PropertiesRabbitConnectionDetailsTests.java (95%) rename module/{spring-boot-amqp/src/test/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitAutoConfigurationTests.java (99%) rename module/{spring-boot-amqp/src/test/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitPropertiesTests.java (99%) rename module/{spring-boot-amqp/src/test/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq}/autoconfigure/RabbitStreamConfigurationTests.java (98%) rename module/{spring-boot-amqp/src/test/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq}/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java (89%) rename module/{spring-boot-amqp/src/test/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq}/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java (94%) rename module/{spring-boot-amqp/src/test/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq}/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java (95%) rename module/{spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java => spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironmentTests.java} (57%) rename module/{spring-boot-amqp/src/test/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq}/health/RabbitHealthIndicatorTests.java (98%) rename module/{spring-boot-amqp/src/test/java/org/springframework/boot/amqp => spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq}/metrics/RabbitMetricsTests.java (97%) rename module/{spring-boot-amqp => spring-boot-rabbitmq}/src/test/resources/logback-test.xml (100%) rename module/{spring-boot-amqp/src/test/resources/org/springframework/boot/amqp => spring-boot-rabbitmq/src/test/resources/org/springframework/boot/rabbitmq}/autoconfigure/test.jks (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/build.gradle (90%) rename smoke-test/{spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java => spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationSslTests.java} (97%) rename smoke-test/{spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java => spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationTests.java} (96%) rename smoke-test/{spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp => spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq}/SecureRabbitMqContainer.java (98%) rename smoke-test/{spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java => spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/SampleRabbitApplication.java} (87%) rename smoke-test/{spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp => spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq}/Sender.java (97%) rename smoke-test/{spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp => spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq}/package-info.java (96%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/test/resources/ssl/rabbitmq.conf (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/test/resources/ssl/test-ca.crt (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/test/resources/ssl/test-ca.key (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/test/resources/ssl/test-client.crt (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/test/resources/ssl/test-client.key (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/test/resources/ssl/test-server.crt (100%) rename smoke-test/{spring-boot-smoke-test-amqp => spring-boot-smoke-test-rabbitmq}/src/test/resources/ssl/test-server.key (100%) rename starter/{spring-boot-starter-amqp-test => spring-boot-starter-rabbitmq-test}/build.gradle (85%) rename starter/{spring-boot-starter-amqp => spring-boot-starter-rabbitmq}/build.gradle (86%) diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index e3692e7a2d13..86ccc81a5673 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -94,7 +94,6 @@ dependencies { implementation(project(path: ":loader:spring-boot-loader-tools")) implementation(project(path: ":module:spring-boot-actuator")) implementation(project(path: ":module:spring-boot-actuator-autoconfigure")) - implementation(project(path: ":module:spring-boot-amqp")) implementation(project(path: ":module:spring-boot-cache")) implementation(project(path: ":module:spring-boot-cache-test")) implementation(project(path: ":module:spring-boot-data-cassandra")) @@ -130,6 +129,7 @@ dependencies { implementation(project(path: ":module:spring-boot-micrometer-metrics")) implementation(project(path: ":module:spring-boot-persistence")) implementation(project(path: ":module:spring-boot-r2dbc")) + implementation(project(path: ":module:spring-boot-rabbitmq")) implementation(project(path: ":module:spring-boot-restdocs")) implementation(project(path: ":module:spring-boot-reactor-netty")) implementation(project(path: ":module:spring-boot-restclient")) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc index 26091564a6d2..6943f9ac0a38 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc @@ -673,7 +673,7 @@ with the `key` listed in the following table: | Always responds with `UP`. | `rabbit` -| javadoc:org.springframework.boot.amqp.health.RabbitHealthIndicator[] +| javadoc:org.springframework.boot.rabbitmq.health.RabbitHealthIndicator[] | Checks that a Rabbit server is up. | `redis` diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc index 592ef98102cd..d73f66e962e7 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc @@ -125,10 +125,10 @@ The following service connections are currently supported: | javadoc:org.springframework.boot.r2dbc.autoconfigure.R2dbcConnectionDetails[] | Containers named "clickhouse/clickhouse-server", "gvenzl/oracle-free", "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres" -| javadoc:org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails[] +| javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails[] | Containers named "rabbitmq" with container port 5672 mapped -| javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] +| javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[] | Containers named "rabbitmq" with container port 5552 mapped, or port 5551 mapped when SSL is configured | javadoc:org.springframework.boot.data.redis.autoconfigure.DataRedisConnectionDetails[] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index aaa67268da6f..3864f7ebe9ea 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc @@ -3,7 +3,7 @@ The Advanced Message Queuing Protocol (AMQP) is a platform-neutral, wire-level protocol for message-oriented middleware. The Spring AMQP project applies core Spring concepts to the development of AMQP-based messaging solutions. -Spring Boot offers several conveniences for working with AMQP through RabbitMQ, including the `spring-boot-starter-amqp` starter. +Spring Boot offers several conveniences for working with AMQP through RabbitMQ, including the `spring-boot-starter-rabbitmq` starter. @@ -38,12 +38,12 @@ spring: NOTE: When specifying addresses that way, the `host` and `port` properties are ignored. If the address uses the `amqps` protocol, SSL support is enabled automatically. -See javadoc:org.springframework.boot.amqp.autoconfigure.RabbitProperties[] for more of the supported property-based configuration options. -To configure lower-level details of the RabbitMQ javadoc:com.rabbitmq.client.ConnectionFactory[] that is used by Spring AMQP, define a javadoc:org.springframework.boot.amqp.autoconfigure.ConnectionFactoryCustomizer[] bean. +See javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties[] for more of the supported property-based configuration options. +To configure lower-level details of the RabbitMQ javadoc:com.rabbitmq.client.ConnectionFactory[] that is used by Spring AMQP, define a javadoc:org.springframework.boot.rabbitmq.autoconfigure.ConnectionFactoryCustomizer[] bean. If a javadoc:org.springframework.amqp.rabbit.connection.ConnectionNameStrategy[] bean exists in the context, it will be automatically used to name connections created by the auto-configured javadoc:org.springframework.amqp.rabbit.connection.CachingConnectionFactory[]. -To make an application-wide, additive customization to the javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[], use a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitTemplateCustomizer[] bean. +To make an application-wide, additive customization to the javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[], use a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitTemplateCustomizer[] bean. TIP: See https://spring.io/blog/2010/06/14/understanding-amqp-the-protocol-used-by-rabbitmq/[Understanding AMQP, the protocol used by RabbitMQ] for more details. @@ -74,9 +74,9 @@ spring: ---- Retries are disabled by default. -You can also customize the javadoc:org.springframework.core.retry.RetryTemplate[] programmatically by declaring a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitTemplateRetrySettingsCustomizer[] bean. +You can also customize the javadoc:org.springframework.core.retry.RetryTemplate[] programmatically by declaring a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitTemplateRetrySettingsCustomizer[] bean. -If you need to create more javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] with the same settings as the factories used by the auto-configuration. +If you need to create more javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] with the same settings as the factories used by the auto-configuration. If there's a bean of type javadoc:org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservationConvention[] in the context, it will automatically be configured on the javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[]. @@ -97,7 +97,7 @@ spring: If a javadoc:org.springframework.amqp.support.converter.MessageConverter[], javadoc:org.springframework.rabbit.stream.support.converter.StreamMessageConverter[], javadoc:org.springframework.rabbit.stream.producer.ProducerCustomizer[] or javadoc:org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservationConvention[] bean is defined, it is associated automatically to the auto-configured javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[]. -If you need to create more javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[] with the same settings as the factories used by the auto-configuration. +If you need to create more javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.rabbit.stream.producer.RabbitStreamTemplate[] with the same settings as the factories used by the auto-configuration. @@ -120,7 +120,7 @@ include-code::MyBean[] TIP: See javadoc:org.springframework.amqp.rabbit.annotation.EnableRabbit[format=annotation] for more details. -If you need to create more javadoc:org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.amqp.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer[] and a javadoc:org.springframework.boot.amqp.autoconfigure.DirectRabbitListenerContainerFactoryConfigurer[] that you can use to initialize a javadoc:org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory[] and a javadoc:org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory[] with the same settings as the factories used by the auto-configuration. +If you need to create more javadoc:org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.rabbitmq.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer[] and a javadoc:org.springframework.boot.rabbitmq.autoconfigure.DirectRabbitListenerContainerFactoryConfigurer[] that you can use to initialize a javadoc:org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory[] and a javadoc:org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory[] with the same settings as the factories used by the auto-configuration. TIP: It does not matter which container type you chose. Those two beans are exposed by the auto-configuration. @@ -137,7 +137,7 @@ You can enable retries to handle situations where your listener throws an except By default, javadoc:org.springframework.amqp.rabbit.retry.RejectAndDontRequeueRecoverer[] is used, but you can define a javadoc:org.springframework.amqp.rabbit.retry.MessageRecoverer[] of your own. When retries are exhausted, the message is rejected and either dropped or routed to a dead-letter exchange if the broker is configured to do so. By default, retries are disabled. -You can also customize the javadoc:org.springframework.core.retry.RetryPolicy[] programmatically by declaring a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitListenerRetrySettingsCustomizer[] bean. +You can also customize the javadoc:org.springframework.core.retry.RetryPolicy[] programmatically by declaring a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitListenerRetrySettingsCustomizer[] bean. IMPORTANT: By default, if retries are disabled and the listener throws an exception, the delivery is retried indefinitely. You can modify this behavior in two ways: Set the `defaultRequeueRejected` property to `false` so that zero re-deliveries are attempted or throw an javadoc:org.springframework.amqp.AmqpRejectAndDontRequeueException[] to signal the message should be rejected. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc index 4142d8eead44..e5d6ef85271c 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc @@ -179,11 +179,11 @@ javadoc:org.testcontainers.clickhouse.ClickHouseContainer[], javadoc:{url-testcontainers-mariadb-javadoc}/org.testcontainers.mariadb.MariaDBContainer[], javadoc:{url-testcontainers-mssqlserver-javadoc}/org.testcontainers.mssqlserver.MSSQLServerContainer[], javadoc:{url-testcontainers-mysql-javadoc}/org.testcontainers.mysql.MySQLContainer[], javadoc:org.testcontainers.oracle.OracleContainer[OracleContainer (free)], javadoc:{url-testcontainers-oracle-xe-javadoc}/org.testcontainers.containers.OracleContainer[OracleContainer (XE)] or javadoc:{url-testcontainers-postgresql-javadoc}/org.testcontainers.postgresql.PostgreSQLContainer[] -| javadoc:org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails[] +| javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails[] | Containers of type javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] -| javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] -| Containers of type javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] when the `@ServiceConnection` `type` attribute includes javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] +| javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[] +| Containers of type javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] when the `@ServiceConnection` `type` attribute includes javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[] | javadoc:org.springframework.boot.data.redis.autoconfigure.DataRedisConnectionDetails[] | Containers of type javadoc:com.redis.testcontainers.RedisContainer[] or javadoc:com.redis.testcontainers.RedisStackContainer[], or containers named "redis", "redis/redis-stack" or "redis/redis-stack-server" @@ -194,12 +194,12 @@ javadoc:org.testcontainers.oracle.OracleContainer[OracleContainer (free)], javad [TIP] ==== -By default, with the exception of javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[], all applicable connection details beans will be created for a given javadoc:org.testcontainers.containers.Container[]. +By default, with the exception of javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[], all applicable connection details beans will be created for a given javadoc:org.testcontainers.containers.Container[]. For example, a javadoc:{url-testcontainers-postgresql-javadoc}/org.testcontainers.postgresql.PostgreSQLContainer[] will create both javadoc:org.springframework.boot.jdbc.autoconfigure.JdbcConnectionDetails[] and javadoc:org.springframework.boot.r2dbc.autoconfigure.R2dbcConnectionDetails[]. If you want to create only a subset of the applicable types, you can use the `type` attribute of javadoc:org.springframework.boot.testcontainers.service.connection.ServiceConnection[format=annotation]. -To create a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails[] bean from a javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[], you must opt in using the `type` attribute of javadoc:org.springframework.boot.testcontainers.service.connection.ServiceConnection[format=annotation]. +To create a javadoc:org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails[] bean from a javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[], you must opt in using the `type` attribute of javadoc:org.springframework.boot.testcontainers.service.connection.ServiceConnection[format=annotation]. The container must also expose port 5552, or port 5551 when SSL is configured, for RabbitMQ Streams. ==== diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java index e109d5de244a..172c9cbba0d1 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java @@ -18,7 +18,7 @@ import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.boot.amqp.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer; +import org.springframework.boot.rabbitmq.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt index 4f1c80f38666..95ca6f610039 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt @@ -19,7 +19,7 @@ package org.springframework.boot.docs.messaging.amqp.receiving.custom import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory import org.springframework.amqp.rabbit.connection.CachingConnectionFactory import org.springframework.amqp.rabbit.connection.ConnectionFactory -import org.springframework.boot.amqp.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer +import org.springframework.boot.rabbitmq.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories b/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 17fb2095415a..000000000000 --- a/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,8 +0,0 @@ -# Connection Details Factories -org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ -org.springframework.boot.amqp.docker.compose.RabbitDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.amqp.docker.compose.RabbitStreamDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.amqp.testcontainers.DeprecatedRabbitContainerConnectionDetailsFactory,\ -org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory,\ -org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory - diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index daf8ae3ba45f..000000000000 --- a/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1,3 +0,0 @@ -org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration -org.springframework.boot.amqp.autoconfigure.health.RabbitHealthContributorAutoConfiguration -org.springframework.boot.amqp.autoconfigure.metrics.RabbitMetricsAutoConfiguration \ No newline at end of file diff --git a/module/spring-boot-autoconfigure-classic-modules/build.gradle b/module/spring-boot-autoconfigure-classic-modules/build.gradle index 461466400f43..88fdf6e2a8c3 100644 --- a/module/spring-boot-autoconfigure-classic-modules/build.gradle +++ b/module/spring-boot-autoconfigure-classic-modules/build.gradle @@ -28,9 +28,6 @@ dependencies { api(project(":module:spring-boot-activemq")) { transitive = false } - api(project(":module:spring-boot-amqp")) { - transitive = false - } api(project(":module:spring-boot-artemis")) { transitive = false } @@ -226,6 +223,9 @@ dependencies { api(project(":module:spring-boot-r2dbc")) { transitive = false } + api(project(":module:spring-boot-rabbitmq")) { + transitive = false + } api(project(":module:spring-boot-reactor")) { transitive = false } diff --git a/module/spring-boot-amqp/build.gradle b/module/spring-boot-rabbitmq/build.gradle similarity index 98% rename from module/spring-boot-amqp/build.gradle rename to module/spring-boot-rabbitmq/build.gradle index d264876a319d..e36a07f0ef78 100644 --- a/module/spring-boot-amqp/build.gradle +++ b/module/spring-boot-rabbitmq/build.gradle @@ -23,7 +23,7 @@ plugins { id "org.springframework.boot.optional-dependencies" } -description = "Spring Boot AMQP" +description = "Spring Boot RabbitMQ" dependencies { api(project(":core:spring-boot")) diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 74% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index 5b1c7797bbac..0c4902328afb 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -14,25 +14,25 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory} and - * {@link RabbitStreamDockerComposeConnectionDetailsFactory} when used in combination. + * Integration tests for {@link RabbitMqDockerComposeConnectionDetailsFactory} and + * {@link RabbitMqStreamDockerComposeConnectionDetailsFactory} when used in combination. * * @author Andy Wilkinson */ -class RabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests { +class RabbitMqAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests { - @DockerComposeTest(composeFile = "rabbit-and-rabbit-stream-separate-services-compose.yaml", + @DockerComposeTest(composeFile = "rabbitmq-and-rabbitmq-stream-separate-services-compose.yaml", image = TestImage.RABBITMQ) void runCreatesConnectionDetailsFromSeparateServices(RabbitConnectionDetails connectionDetails, RabbitStreamConnectionDetails streamConnectionDetails) { @@ -40,7 +40,8 @@ void runCreatesConnectionDetailsFromSeparateServices(RabbitConnectionDetails con assertConnectionDetails(streamConnectionDetails); } - @DockerComposeTest(composeFile = "rabbit-and-rabbit-stream-single-service-compose.yaml", image = TestImage.RABBITMQ) + @DockerComposeTest(composeFile = "rabbitmq-and-rabbitmq-stream-single-service-compose.yaml", + image = TestImage.RABBITMQ) void runCreatesConnectionDetailsFromSingleService(RabbitConnectionDetails connectionDetails, RabbitStreamConnectionDetails streamConnectionDetails) { assertConnectionDetails(connectionDetails); diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 78% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java index 4d644b742e48..fb482a6cbb83 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -14,33 +14,33 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory}. + * Integration tests for {@link RabbitMqDockerComposeConnectionDetailsFactory}. * * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb * @author Scott Frederick */ -class RabbitDockerComposeConnectionDetailsFactoryIntegrationTests { +class RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests { - @DockerComposeTest(composeFile = "rabbit-compose.yaml", image = TestImage.RABBITMQ) + @DockerComposeTest(composeFile = "rabbitmq-compose.yaml", image = TestImage.RABBITMQ) void runCreatesConnectionDetails(RabbitConnectionDetails connectionDetails) { assertConnectionDetails(connectionDetails); assertThat(connectionDetails.getSslBundle()).isNull(); } - @DockerComposeTest(composeFile = "rabbit-ssl-compose.yaml", image = TestImage.RABBITMQ, + @DockerComposeTest(composeFile = "rabbitmq-ssl-compose.yaml", image = TestImage.RABBITMQ, additionalResources = { "../../ca.crt", "../../server.crt", "../../server.key", "../../client.crt", "../../client.key", "rabbitmq-ssl.conf" }) void runWithSslCreatesConnectionDetails(RabbitConnectionDetails connectionDetails) { diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 78% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index 67f8fad358e6..1a4d0fdf9817 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -14,28 +14,28 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link RabbitStreamDockerComposeConnectionDetailsFactory}. + * Integration tests for {@link RabbitMqStreamDockerComposeConnectionDetailsFactory}. * * @author Andy Wilkinson */ -class RabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests { +class RabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests { - @DockerComposeTest(composeFile = "rabbit-stream-compose.yaml", image = TestImage.RABBITMQ) + @DockerComposeTest(composeFile = "rabbitmq-stream-compose.yaml", image = TestImage.RABBITMQ) void runCreatesConnectionDetails(RabbitStreamConnectionDetails connectionDetails) { assertConnectionDetails(connectionDetails); } - @DockerComposeTest(composeFile = "rabbit-stream-ssl-compose.yaml", image = TestImage.RABBITMQ, + @DockerComposeTest(composeFile = "rabbitmq-stream-ssl-compose.yaml", image = TestImage.RABBITMQ, additionalResources = { "../../ca.crt", "../../server.crt", "../../server.key", "../../client.crt", "../../client.key", "rabbitmq-stream-ssl.conf" }) void runWithSslCreatesConnectionDetails(RabbitStreamConnectionDetails connectionDetails) { diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 74% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java index d9f077bc21cd..af67b52c44b0 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/docker/compose/SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -14,26 +14,27 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory} and - * {@link RabbitStreamDockerComposeConnectionDetailsFactory} when using a single service + * Integration tests for {@link RabbitMqDockerComposeConnectionDetailsFactory} and + * {@link RabbitMqStreamDockerComposeConnectionDetailsFactory} when using a single service * for standard and stream-based messaging. * * @author Andy Wilkinson */ -class SingleServiceRabbitAndRabbitStreamDockerComposeConnectionDetailsFactoryIntegrationTests { +class SingleServiceRabbitAndRabbitMqStreamDockerComposeConnectionDetailsFactoryIntegrationTests { - @DockerComposeTest(composeFile = "rabbit-and-rabbit-stream-single-service-compose.yaml", image = TestImage.RABBITMQ) + @DockerComposeTest(composeFile = "rabbitmq-and-rabbitmq-stream-single-service-compose.yaml", + image = TestImage.RABBITMQ) void runCreatesConnectionDetails(RabbitConnectionDetails connectionDetails, RabbitStreamConnectionDetails streamConnectionDetails) { assertConnectionDetails(connectionDetails); diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java similarity index 91% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java index 51e32711184d..61b0a43c5311 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.time.Duration; import java.util.ArrayList; @@ -30,9 +30,9 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; @@ -48,7 +48,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link RabbitContainerConnectionDetailsFactoryIntegrationTests}. + * {@link RabbitMqContainerConnectionDetailsFactoryIntegrationTests}. */ @SpringJUnitConfig @Testcontainers(disabledWithoutDocker = true) diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java similarity index 89% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java index ea80da8d9bf3..0de6be2fad4a 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.time.Duration; import java.util.ArrayList; @@ -30,9 +30,9 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; @@ -42,7 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitContainerConnectionDetailsFactory}. + * Tests for {@link RabbitMqContainerConnectionDetailsFactory}. * * @author Moritz Halbritter * @author Andy Wilkinson @@ -50,7 +50,7 @@ */ @SpringJUnitConfig @Testcontainers(disabledWithoutDocker = true) -class RabbitContainerConnectionDetailsFactoryIntegrationTests { +class RabbitMqContainerConnectionDetailsFactoryIntegrationTests { @Container @ServiceConnection diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 83% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java index 1e65d213f2ac..3f3981b727d9 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.time.Duration; import java.util.ArrayList; @@ -31,13 +31,13 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; @@ -50,8 +50,8 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitStreamContainerConnectionDetailsFactory} with a single container - * that's only used for streams. + * Tests for {@link RabbitMqStreamContainerConnectionDetailsFactory} with a single + * container that's only used for streams. * * @author Eddú Meléndez * @author Andy Wilkinson @@ -60,7 +60,7 @@ @TestPropertySource( properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) @Testcontainers(disabledWithoutDocker = true) -class RabbitStreamContainerConnectionDetailsFactoryIntegrationTests { +class RabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests { private static final int RABBITMQ_STREAMS_PORT = 5552; diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java similarity index 80% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java index d439b0fdbf48..c13ddc3eae7a 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.time.Duration; import java.util.ArrayList; @@ -32,13 +32,13 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.PemKeyStore; import org.springframework.boot.testcontainers.service.connection.PemTrustStore; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -53,7 +53,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitStreamContainerConnectionDetailsFactory} with SSL. + * Tests for {@link RabbitMqStreamContainerConnectionDetailsFactory} with SSL. * * @author Eddú Meléndez * @author Andy Wilkinson @@ -62,15 +62,15 @@ @TestPropertySource( properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) @Testcontainers(disabledWithoutDocker = true) -class RabbitStreamWithSslContainerConnectionDetailsFactoryIntegrationTests { +class RabbitMqStreamWithSslContainerConnectionDetailsFactoryIntegrationTests { private static final int RABBITMQ_STREAMS_TLS_PORT = 5551; @Container @ServiceConnection(type = RabbitStreamConnectionDetails.class) - @PemTrustStore(certificate = "classpath:org/springframework/boot/amqp/ca.crt") - @PemKeyStore(certificate = "classpath:org/springframework/boot/amqp/client.crt", - privateKey = "classpath:org/springframework/boot/amqp/client.key") + @PemTrustStore(certificate = "classpath:org/springframework/boot/rabbitmq/ca.crt") + @PemKeyStore(certificate = "classpath:org/springframework/boot/rabbitmq/client.crt", + privateKey = "classpath:org/springframework/boot/rabbitmq/client.key") static final RabbitMQContainer rabbit = getRabbitMqStreamContainer(); private static RabbitMQContainer getRabbitMqStreamContainer() { @@ -80,15 +80,15 @@ private static RabbitMQContainer getRabbitMqStreamContainer() { container.withCopyToContainer(Transferable.of(enabledPlugins), "/etc/rabbitmq/enabled_plugins"); container.withCopyFileToContainer( MountableFile - .forClasspathResource("org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf"), + .forClasspathResource("org/springframework/boot/rabbitmq/testcontainers/rabbitmq-stream-ssl.conf"), "/etc/rabbitmq/rabbitmq.conf"); - container.withCopyFileToContainer(MountableFile.forClasspathResource("org/springframework/boot/amqp/ca.crt"), - "/etc/rabbitmq/ca.crt"); container.withCopyFileToContainer( - MountableFile.forClasspathResource("org/springframework/boot/amqp/server.key"), + MountableFile.forClasspathResource("org/springframework/boot/rabbitmq/ca.crt"), "/etc/rabbitmq/ca.crt"); + container.withCopyFileToContainer( + MountableFile.forClasspathResource("org/springframework/boot/rabbitmq/server.key"), "/etc/rabbitmq/server.key"); container.withCopyFileToContainer( - MountableFile.forClasspathResource("org/springframework/boot/amqp/server.crt"), + MountableFile.forClasspathResource("org/springframework/boot/rabbitmq/server.crt"), "/etc/rabbitmq/server.crt"); return container; } diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 86% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java index 778f6536bd57..099cd3816864 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -35,13 +35,13 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; @@ -54,7 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitStreamContainerConnectionDetailsFactory} with two containers, + * Tests for {@link RabbitMqStreamContainerConnectionDetailsFactory} with two containers, * one for streams and one for standard messaging. * * @author Eddú Meléndez @@ -64,7 +64,7 @@ @TestPropertySource( properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) @Testcontainers(disabledWithoutDocker = true) -class SeparateContainersRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests { +class SeparateContainersRabbitMqAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests { private static final int RABBITMQ_STREAMS_PORT = 5552; diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java similarity index 85% rename from module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java index 6dc8b12291c3..686a447bc2e5 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -35,13 +35,13 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.amqp.autoconfigure.EnvironmentBuilderCustomizer; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; -import org.springframework.boot.amqp.testcontainers.RabbitStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.EnvironmentBuilderCustomizer; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory.RabbitMqContainerConnectionDetails; +import org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory.RabbitMqStreamContainerConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; @@ -54,8 +54,8 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitStreamContainerConnectionDetailsFactory} with a single container - * used for both streams and standard messaging. + * Tests for {@link RabbitMqStreamContainerConnectionDetailsFactory} with a single + * container used for both streams and standard messaging. * * @author Eddú Meléndez * @author Andy Wilkinson @@ -64,7 +64,7 @@ @TestPropertySource( properties = { "spring.rabbitmq.stream.name=stream.queue1", "spring.rabbitmq.listener.type=stream" }) @Testcontainers(disabledWithoutDocker = true) -class SingleContainerRabbitAndRabbitStreamContainerConnectionDetailsFactoryIntegrationTests { +class SingleContainerRabbitAndRabbitMqStreamContainerConnectionDetailsFactoryIntegrationTests { private static final int RABBITMQ_STREAMS_PORT = 5552; diff --git a/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml b/module/spring-boot-rabbitmq/src/dockerTest/resources/logback-test.xml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/logback-test.xml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/ca.crt b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/ca.crt similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/ca.crt rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/ca.crt diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.crt b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/client.crt similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.crt rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/client.crt diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.key b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/client.key similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/client.key rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/client.key diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-and-rabbitmq-stream-separate-services-compose.yaml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-separate-services-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-and-rabbitmq-stream-separate-services-compose.yaml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-and-rabbitmq-stream-single-service-compose.yaml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-and-rabbit-stream-single-service-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-and-rabbitmq-stream-single-service-compose.yaml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-compose.yaml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-compose.yaml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-ssl-compose.yaml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-ssl-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-ssl-compose.yaml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-ssl.conf b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-ssl.conf similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-ssl.conf rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-ssl.conf diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-compose.yaml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-compose.yaml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-ssl-compose.yaml similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-stream-ssl-compose.yaml rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-ssl-compose.yaml diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-ssl.conf similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-stream-ssl.conf rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/docker/compose/rabbitmq-stream-ssl.conf diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.crt b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/server.crt similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.crt rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/server.crt diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.key b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/server.key similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/server.key rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/server.key diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf b/module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/testcontainers/rabbitmq-stream-ssl.conf similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/testcontainers/rabbitmq-stream-ssl.conf rename to module/spring-boot-rabbitmq/src/dockerTest/resources/org/springframework/boot/rabbitmq/testcontainers/rabbitmq-stream-ssl.conf diff --git a/module/spring-boot-amqp/src/dockerTest/resources/spring.properties b/module/spring-boot-rabbitmq/src/dockerTest/resources/spring.properties similarity index 100% rename from module/spring-boot-amqp/src/dockerTest/resources/spring.properties rename to module/spring-boot-rabbitmq/src/dockerTest/resources/spring.properties diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractConnectionFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractConnectionFactoryConfigurer.java similarity index 98% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractConnectionFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractConnectionFactoryConfigurer.java index 07b17ce5a9a1..3a0a8a959c6e 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractConnectionFactoryConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractConnectionFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.util.stream.Collectors; @@ -33,7 +33,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb - * @since 4.0.0 + * @since 4.2.0 */ public abstract class AbstractConnectionFactoryConfigurer { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java similarity index 96% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java index ffed69eda93e..16d9d1800ca2 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.util.List; import java.util.concurrent.Executor; @@ -28,8 +28,8 @@ import org.springframework.amqp.rabbit.retry.RejectAndDontRequeueRecoverer; import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention; import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.boot.amqp.autoconfigure.RabbitProperties.ListenerRetry; -import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Retry; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.ListenerRetry; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.Retry; import org.springframework.boot.retry.RetryPolicySettings; import org.springframework.core.retry.RetryPolicy; import org.springframework.util.Assert; @@ -41,7 +41,7 @@ * @param the container factory type. * @author Gary Russell * @author Stephane Nicoll - * @since 4.0.0 + * @since 4.2.0 */ public abstract class AbstractRabbitListenerContainerFactoryConfigurer> { @@ -102,7 +102,7 @@ public void setTaskExecutor(@Nullable Executor taskExecutor) { /** * Sets the observation convention to use. * @param observationConvention the observation convention to use - * @since 4.1.0 + * @since 4.2.0 */ protected void setObservationConvention(@Nullable RabbitListenerObservationConvention observationConvention) { this.observationConvention = observationConvention; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/CachingConnectionFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/CachingConnectionFactoryConfigurer.java similarity index 97% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/CachingConnectionFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/CachingConnectionFactoryConfigurer.java index 2221595f78d1..52ccba6d45dd 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/CachingConnectionFactoryConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/CachingConnectionFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.time.Duration; @@ -33,7 +33,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb - * @since 4.0.0 + * @since 4.2.0 */ public class CachingConnectionFactoryConfigurer extends AbstractConnectionFactoryConfigurer { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionFactoryCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/ConnectionFactoryCustomizer.java similarity index 93% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionFactoryCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/ConnectionFactoryCustomizer.java index 15f2e14dfff8..3efbfc5ef516 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionFactoryCustomizer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/ConnectionFactoryCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import com.rabbitmq.client.ConnectionFactory; @@ -23,7 +23,7 @@ * {@link ConnectionFactory} to fine-tune its auto-configuration. * * @author Andy Wilkinson - * @since 4.0.0 + * @since 4.2.0 */ @FunctionalInterface public interface ConnectionFactoryCustomizer { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java similarity index 96% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java index e4057b5c2ecf..54590c07c786 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/DirectRabbitListenerContainerFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -30,7 +30,7 @@ * * @author Gary Russell * @author Stephane Nicoll - * @since 4.0.0 + * @since 4.2.0 */ public final class DirectRabbitListenerContainerFactoryConfigurer extends AbstractRabbitListenerContainerFactoryConfigurer { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/EnvironmentBuilderCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/EnvironmentBuilderCustomizer.java similarity index 94% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/EnvironmentBuilderCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/EnvironmentBuilderCustomizer.java index 8d5f2373cd8e..52ac68749ffd 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/EnvironmentBuilderCustomizer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/EnvironmentBuilderCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import com.rabbitmq.stream.Environment; import com.rabbitmq.stream.EnvironmentBuilder; @@ -25,7 +25,7 @@ * {@link Environment}. * * @author Andy Wilkinson - * @since 4.0.0 + * @since 4.2.0 */ @FunctionalInterface public interface EnvironmentBuilderCustomizer { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetails.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetails.java similarity index 94% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetails.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetails.java index e15052bf3303..3e3900f8a475 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetails.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetails.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.util.ArrayList; import java.util.List; import org.jspecify.annotations.Nullable; -import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Ssl; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.Ssl; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.util.Assert; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAnnotationDrivenConfiguration.java similarity index 99% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAnnotationDrivenConfiguration.java index 2c1b260abe0a..a2e01ef3942e 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAnnotationDrivenConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.amqp.rabbit.config.ContainerCustomizer; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfiguration.java similarity index 99% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfiguration.java index e6f98bcbd2a8..001aef04c192 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import com.rabbitmq.client.Channel; import com.rabbitmq.client.impl.CredentialsProvider; @@ -72,7 +72,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Scott Frederick - * @since 4.0.0 + * @since 4.2.0 */ @AutoConfiguration @ConditionalOnClass({ RabbitTemplate.class, Channel.class }) diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionDetails.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionDetails.java similarity index 97% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionDetails.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionDetails.java index d6cb1be0512f..80c7a5108967 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionDetails.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionDetails.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.util.List; @@ -30,7 +30,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb - * @since 4.0.0 + * @since 4.2.0 */ public interface RabbitConnectionDetails extends ConnectionDetails { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java similarity index 97% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java index 4657603a65fd..c44e3e3b5268 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.time.Duration; @@ -23,8 +23,8 @@ import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.core.io.ResourceLoader; @@ -44,7 +44,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @author Scott Frederick - * @since 4.0.0 + * @since 4.2.0 */ public class RabbitConnectionFactoryBeanConfigurer { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitListenerRetrySettingsCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitListenerRetrySettingsCustomizer.java similarity index 94% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitListenerRetrySettingsCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitListenerRetrySettingsCustomizer.java index 8a3bc26dd23e..e8a5bf42b270 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitListenerRetrySettingsCustomizer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitListenerRetrySettingsCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.springframework.boot.retry.RetryPolicySettings; import org.springframework.core.retry.RetryPolicy; @@ -24,7 +24,7 @@ * settings} used by message listeners. * * @author Stephane Nicoll - * @since 4.0.0 + * @since 4.2.0 * @see RetryPolicy */ @FunctionalInterface diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitProperties.java similarity index 99% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitProperties.java index c23d78798293..48449dca89e8 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitProperties.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.time.Duration; import java.time.temporal.ChronoUnit; @@ -53,7 +53,7 @@ * @author Lasse Wulff * @author Yanming Zhou * @author Jay Choi - * @since 4.0.0 + * @since 4.2.0 */ @ConfigurationProperties("spring.rabbitmq") public class RabbitProperties { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitRetryTemplateSettings.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitRetryTemplateSettings.java similarity index 95% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitRetryTemplateSettings.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitRetryTemplateSettings.java index 8e29c1d440e0..97d6961575b1 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitRetryTemplateSettings.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitRetryTemplateSettings.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.util.LinkedList; @@ -26,7 +26,7 @@ * Define the settings of a {@link RetryTemplate}. * * @author Stephane Nicoll - * @since 4.0.0 + * @since 4.2.0 */ public class RabbitRetryTemplateSettings { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfiguration.java similarity index 96% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfiguration.java index 416a23d8196f..e2545e8f7d68 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import javax.net.ssl.SSLException; @@ -27,13 +27,13 @@ import org.springframework.amqp.rabbit.config.ContainerCustomizer; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Stream; -import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Stream.Ssl; -import org.springframework.boot.amqp.autoconfigure.RabbitProperties.StreamContainer; 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.PropertyMapper; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.Stream; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.Stream.Ssl; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.StreamContainer; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.ssl.SslManagerBundle; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConnectionDetails.java similarity index 96% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConnectionDetails.java index 5de8fc8301cd..e37512cae9a2 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConnectionDetails.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConnectionDetails.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.jspecify.annotations.Nullable; @@ -26,7 +26,7 @@ * * @author Eddú Meléndez * @author Jay Choi - * @since 4.1.0 + * @since 4.2.0 */ public interface RabbitStreamConnectionDetails extends ConnectionDetails { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamTemplateConfigurer.java similarity index 97% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamTemplateConfigurer.java index fe7306d3e473..52ddf6e81502 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamTemplateConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamTemplateConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.jspecify.annotations.Nullable; @@ -32,7 +32,7 @@ * auto-configuration. * * @author Eddú Meléndez - * @since 4.0.0 + * @since 4.2.0 */ public class RabbitStreamTemplateConfigurer { @@ -73,7 +73,7 @@ public void setProducerCustomizer(@Nullable ProducerCustomizer producerCustomize /** * Set the observation convention to use. * @param observationConvention the observation convention to use - * @since 4.1.0 + * @since 4.2.0 */ public void setObservationConvention(@Nullable RabbitStreamTemplateObservationConvention observationConvention) { this.observationConvention = observationConvention; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateConfigurer.java similarity index 98% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateConfigurer.java index 52823cb48d9a..252f2b7afcc8 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.time.Duration; import java.util.List; @@ -45,7 +45,7 @@ * * @author Stephane Nicoll * @author Yanming Zhou - * @since 4.0.0 + * @since 4.2.0 */ public class RabbitTemplateConfigurer { @@ -87,7 +87,7 @@ public void setRetrySettingsCustomizers( /** * Sets the observation convention to use. * @param observationConvention the observation convention to use - * @since 4.1.0 + * @since 4.2.0 */ public void setObservationConvention(@Nullable RabbitTemplateObservationConvention observationConvention) { this.observationConvention = observationConvention; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateCustomizer.java similarity index 93% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateCustomizer.java index 9015fa8ca49d..a75614857bcf 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateCustomizer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -22,7 +22,7 @@ * Callback interface that can be used to customize a {@link RabbitTemplate}. * * @author dang zhicairang - * @since 4.0.0 + * @since 4.2.0 */ @FunctionalInterface public interface RabbitTemplateCustomizer { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java similarity index 94% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java index 099b510132bb..d72ad89c47bc 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitTemplateRetrySettingsCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.core.retry.RetryTemplate; @@ -24,7 +24,7 @@ * {@linkplain RabbitRetryTemplateSettings retry settings} used by {@link RabbitTemplate}. * * @author Stephane Nicoll - * @since 4.0.0 + * @since 4.2.0 * @see RabbitTemplateConfigurer * @see RetryTemplate */ diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java similarity index 96% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java index 10fac5395c1d..ecc05752b1fb 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SimpleRabbitListenerContainerFactoryConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -30,7 +30,7 @@ * * @author Stephane Nicoll * @author Gary Russell - * @since 4.0.0 + * @since 4.2.0 */ public final class SimpleRabbitListenerContainerFactoryConfigurer extends AbstractRabbitListenerContainerFactoryConfigurer { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SslBundleRabbitConnectionFactoryBean.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SslBundleRabbitConnectionFactoryBean.java similarity index 96% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SslBundleRabbitConnectionFactoryBean.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SslBundleRabbitConnectionFactoryBean.java index d681e44e11d4..c3f0b389ea99 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/SslBundleRabbitConnectionFactoryBean.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SslBundleRabbitConnectionFactoryBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.jspecify.annotations.Nullable; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java similarity index 91% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java index 0abccb7e34cf..29a481161192 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfiguration.java @@ -14,12 +14,10 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure.health; +package org.springframework.boot.rabbitmq.autoconfigure.health; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.health.RabbitHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -28,13 +26,15 @@ import org.springframework.boot.health.autoconfigure.contributor.CompositeHealthContributorConfiguration; import org.springframework.boot.health.autoconfigure.contributor.ConditionalOnEnabledHealthIndicator; import org.springframework.boot.health.contributor.HealthContributor; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.health.RabbitHealthIndicator; import org.springframework.context.annotation.Bean; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link RabbitHealthIndicator}. * * @author Christian Dupuis - * @since 4.0.0 + * @since 4.2.0 */ @AutoConfiguration(after = RabbitAutoConfiguration.class) @ConditionalOnClass({ RabbitHealthIndicator.class, RabbitTemplate.class, ConditionalOnEnabledHealthIndicator.class }) diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/package-info.java similarity index 91% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/package-info.java index bbd0d5e86d39..27f98c583ad9 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/health/package-info.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/health/package-info.java @@ -18,6 +18,6 @@ * Auto-configuration for RabbitMQ health. */ @NullMarked -package org.springframework.boot.amqp.autoconfigure.health; +package org.springframework.boot.rabbitmq.autoconfigure.health; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java similarity index 96% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java index 9841b953e937..2f9f18802c76 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitConnectionFactoryMetricsPostProcessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure.metrics; +package org.springframework.boot.rabbitmq.autoconfigure.metrics; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.MetricsCollector; @@ -24,7 +24,7 @@ import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory; import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.boot.amqp.metrics.RabbitMetrics; +import org.springframework.boot.rabbitmq.metrics.RabbitMetrics; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; import org.springframework.util.StringUtils; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java similarity index 92% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java index 27b5b6ad36ed..9f5756f2ad54 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfiguration.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure.metrics; +package org.springframework.boot.rabbitmq.autoconfigure.metrics; import com.rabbitmq.client.ConnectionFactory; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; 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.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -33,7 +33,7 @@ * {@link ConnectionFactory connection factories}. * * @author Stephane Nicoll - * @since 4.0.0 + * @since 4.2.0 */ @AutoConfiguration( afterName = "org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration", diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/package-info.java similarity index 91% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/package-info.java index ab0814c6f728..94b2c885530b 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/metrics/package-info.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/package-info.java @@ -18,6 +18,6 @@ * Auto-configuration for RabbitMQ metrics. */ @NullMarked -package org.springframework.boot.amqp.autoconfigure.metrics; +package org.springframework.boot.rabbitmq.autoconfigure.metrics; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/package-info.java similarity index 92% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/package-info.java index b519c35a2cae..8b021e23bb05 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/package-info.java @@ -18,6 +18,6 @@ * Auto-configuration for RabbitMQ. */ @NullMarked -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java similarity index 85% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java index daa79344ae75..30b7ab878cb1 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; import java.util.List; import org.jspecify.annotations.Nullable; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; 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; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.ssl.SslBundle; /** @@ -33,12 +33,12 @@ * * @author Andy Wilkinson */ -class RabbitDockerComposeConnectionDetailsFactory +class RabbitMqDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { private static final int RABBITMQ_PORT = 5672; - protected RabbitDockerComposeConnectionDetailsFactory() { + protected RabbitMqDockerComposeConnectionDetailsFactory() { super("rabbitmq"); } @@ -60,7 +60,7 @@ protected RabbitDockerComposeConnectionDetailsFactory() { static class RabbitDockerComposeConnectionDetails extends DockerComposeConnectionDetails implements RabbitConnectionDetails { - private final RabbitEnvironment environment; + private final RabbitMqEnvironment environment; private final List
    addresses; @@ -68,7 +68,7 @@ static class RabbitDockerComposeConnectionDetails extends DockerComposeConnectio protected RabbitDockerComposeConnectionDetails(RunningService service) { super(service); - this.environment = new RabbitEnvironment(service.env()); + this.environment = new RabbitMqEnvironment(service.env()); this.sslBundle = getSslBundle(service); this.addresses = List.of(new Address(service.host(), service.ports().get(RABBITMQ_PORT))); } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironment.java similarity index 89% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironment.java index bc776b015dbf..a71a8207ec71 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironment.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; import java.util.Map; @@ -28,13 +28,13 @@ * @author Phillip Webb * @author Scott Frederick */ -class RabbitEnvironment { +class RabbitMqEnvironment { private final @Nullable String username; private final @Nullable String password; - RabbitEnvironment(Map env) { + RabbitMqEnvironment(Map env) { this.username = env.getOrDefault("RABBITMQ_DEFAULT_USER", env.getOrDefault("RABBITMQ_USERNAME", "guest")); this.password = env.getOrDefault("RABBITMQ_DEFAULT_PASS", env.getOrDefault("RABBITMQ_PASSWORD", "guest")); } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactory.java similarity index 86% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactory.java index 6b19483ff4a9..9a0b8f22bc31 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitStreamDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqStreamDockerComposeConnectionDetailsFactory.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; import org.jspecify.annotations.Nullable; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; 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; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.ssl.SslBundle; /** @@ -35,10 +35,10 @@ * @author Scott Frederick * @author Jay Choi */ -class RabbitStreamDockerComposeConnectionDetailsFactory +class RabbitMqStreamDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { - protected RabbitStreamDockerComposeConnectionDetailsFactory() { + protected RabbitMqStreamDockerComposeConnectionDetailsFactory() { super("rabbitmq"); } @@ -64,7 +64,7 @@ static class RabbitStreamDockerComposeConnectionDetails extends DockerComposeCon private static final int STREAMS_TLS_PORT = 5551; - private final RabbitEnvironment environment; + private final RabbitMqEnvironment environment; private final String host; @@ -74,7 +74,7 @@ static class RabbitStreamDockerComposeConnectionDetails extends DockerComposeCon protected RabbitStreamDockerComposeConnectionDetails(RunningService service) { super(service); - this.environment = new RabbitEnvironment(service.env()); + this.environment = new RabbitMqEnvironment(service.env()); this.host = service.host(); this.sslBundle = getSslBundle(service); this.port = service.ports().get((this.sslBundle != null) ? STREAMS_TLS_PORT : STREAMS_PORT); diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/package-info.java similarity index 92% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/package-info.java index c2d355c0797e..7fffb63a7c4c 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/docker/compose/package-info.java @@ -18,6 +18,6 @@ * Support for Docker Compose RabbitMQ service connections. */ @NullMarked -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/RabbitHealthIndicator.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicator.java similarity index 96% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/RabbitHealthIndicator.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicator.java index 045ab2f89c27..28726d12d8e1 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/RabbitHealthIndicator.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicator.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.health; +package org.springframework.boot.rabbitmq.health; import org.jspecify.annotations.Nullable; @@ -29,7 +29,7 @@ * RabbitMQ messaging system. * * @author Christian Dupuis - * @since 4.0.0 + * @since 4.2.0 */ public class RabbitHealthIndicator extends AbstractHealthIndicator { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/package-info.java similarity index 93% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/package-info.java index ecb6ea0b1649..83f913732e91 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/health/package-info.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/health/package-info.java @@ -18,6 +18,6 @@ * Health integration for AMQP and RabbitMQ. */ @NullMarked -package org.springframework.boot.amqp.health; +package org.springframework.boot.rabbitmq.health; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/RabbitMetrics.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/RabbitMetrics.java similarity index 96% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/RabbitMetrics.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/RabbitMetrics.java index 20ae4111b162..ab7cd3b5f7fa 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/RabbitMetrics.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/RabbitMetrics.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.metrics; +package org.springframework.boot.rabbitmq.metrics; import java.util.Collections; @@ -32,7 +32,7 @@ * * @author Arnaud Cogoluègnes * @author Stephane Nicoll - * @since 4.0.0 + * @since 4.2.0 */ public class RabbitMetrics implements MeterBinder { diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/package-info.java similarity index 93% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/package-info.java index e63026906cd8..c40e5aed68fc 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/metrics/package-info.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/metrics/package-info.java @@ -18,6 +18,6 @@ * Metrics for AMQP and RabbitMQ. */ @NullMarked -package org.springframework.boot.amqp.metrics; +package org.springframework.boot.rabbitmq.metrics; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java similarity index 93% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java index b641c37e4395..750d611deb67 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.net.URI; import java.util.List; @@ -22,7 +22,7 @@ import org.jspecify.annotations.Nullable; import org.testcontainers.containers.RabbitMQContainer; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; @@ -37,7 +37,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link RabbitContainerConnectionDetailsFactory}. + * {@link RabbitMqContainerConnectionDetailsFactory}. */ @Deprecated(since = "4.0.0", forRemoval = true) class DeprecatedRabbitContainerConnectionDetailsFactory diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactory.java similarity index 93% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactory.java index fd47f98fd478..5e13fc5d999c 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqContainerConnectionDetailsFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import java.net.URI; import java.util.List; @@ -22,7 +22,7 @@ import org.jspecify.annotations.Nullable; import org.testcontainers.rabbitmq.RabbitMQContainer; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; @@ -37,7 +37,7 @@ * @author Andy Wilkinson * @author Phillip Webb */ -class RabbitContainerConnectionDetailsFactory +class RabbitMqContainerConnectionDetailsFactory extends ContainerConnectionDetailsFactory { @Override diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactory.java similarity index 92% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactory.java index 5fc1321b7427..17171157b33f 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitStreamContainerConnectionDetailsFactory.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/RabbitMqStreamContainerConnectionDetailsFactory.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import org.jspecify.annotations.Nullable; import org.testcontainers.rabbitmq.RabbitMQContainer; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConnectionDetails; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConnectionDetails; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; @@ -33,10 +33,10 @@ * @author Eddú Meléndez * @author Jay Choi */ -class RabbitStreamContainerConnectionDetailsFactory +class RabbitMqStreamContainerConnectionDetailsFactory extends ContainerConnectionDetailsFactory { - RabbitStreamContainerConnectionDetailsFactory() { + RabbitMqStreamContainerConnectionDetailsFactory() { super(ANY_CONNECTION_NAME, "org.springframework.rabbit.stream.producer.RabbitStreamTemplate"); } diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/package-info.java similarity index 92% rename from module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java rename to module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/package-info.java index 69c6364ef32f..272d19a6b982 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/package-info.java @@ -18,6 +18,6 @@ * Support for testcontainers RabbitMQ service connections. */ @NullMarked -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.rabbitmq.testcontainers; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-rabbitmq/src/main/resources/META-INF/additional-spring-configuration-metadata.json similarity index 100% rename from module/spring-boot-amqp/src/main/resources/META-INF/additional-spring-configuration-metadata.json rename to module/spring-boot-rabbitmq/src/main/resources/META-INF/additional-spring-configuration-metadata.json diff --git a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..a7a1d458e73e --- /dev/null +++ b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories @@ -0,0 +1,8 @@ +# Connection Details Factories +org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ +org.springframework.boot.rabbitmq.docker.compose.RabbitMqDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.rabbitmq.docker.compose.RabbitMqStreamDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.rabbitmq.testcontainers.DeprecatedRabbitContainerConnectionDetailsFactory,\ +org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory,\ +org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory + diff --git a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000000..9ec393f97b32 --- /dev/null +++ b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration +org.springframework.boot.rabbitmq.autoconfigure.health.RabbitHealthContributorAutoConfiguration +org.springframework.boot.rabbitmq.autoconfigure.metrics.RabbitMetricsAutoConfiguration \ No newline at end of file diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetailsTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetailsTests.java similarity index 95% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetailsTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetailsTests.java index fb37bd6f2712..aed57edc4e04 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitConnectionDetailsTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/PropertiesRabbitConnectionDetailsTests.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; import org.springframework.boot.ssl.DefaultSslBundleRegistry; import org.springframework.boot.ssl.SslBundle; diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfigurationTests.java similarity index 99% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfigurationTests.java index 6073381f4b57..787f078c9857 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfigurationTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.io.IOException; import java.security.NoSuchAlgorithmException; @@ -961,7 +961,7 @@ void enableSslWithInvalidTrustStoreTypeShouldFail() { void enableSslWithBundle() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.bundle=test-bundle", - "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.ssl.bundle.jks.test-bundle.keystore.password=secret") .run((context) -> { com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); @@ -974,7 +974,7 @@ void enableSslWithBundle() { void enableSslWithBundleAndWithoutHostnameVerification() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.bundle=test-bundle", "spring.rabbitmq.ssl.verify-hostname=false", - "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.ssl.bundle.jks.test-bundle.keystore.password=secret") .run((context) -> { com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); @@ -987,9 +987,9 @@ void enableSslWithBundleAndWithoutHostnameVerification() { void enableSslWithKeystoreTypeAndTrustStoreTypeShouldWork() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.enabled:true", - "spring.rabbitmq.ssl.key-store=/org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.rabbitmq.ssl.key-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.rabbitmq.ssl.key-store-type=jks", "spring.rabbitmq.ssl.key-store-password=secret", - "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.rabbitmq.ssl.trust-store-type=jks", "spring.rabbitmq.ssl.trust-store-password=secret") .run((context) -> assertThat(context).hasNotFailed()); } @@ -1021,10 +1021,10 @@ void enableSslWithValidateServerCertificateDefault(CapturedOutput output) { void enableSslWithValidStoreAlgorithmShouldWork() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.enabled:true", - "spring.rabbitmq.ssl.key-store=/org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.rabbitmq.ssl.key-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.rabbitmq.ssl.key-store-type=jks", "spring.rabbitmq.ssl.key-store-password=secret", "spring.rabbitmq.ssl.key-store-algorithm=PKIX", - "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.rabbitmq.ssl.trust-store-type=jks", "spring.rabbitmq.ssl.trust-store-password=secret", "spring.rabbitmq.ssl.trust-store-algorithm=PKIX") .run((context) -> assertThat(context).hasNotFailed()); @@ -1034,7 +1034,7 @@ void enableSslWithValidStoreAlgorithmShouldWork() { void enableSslWithInvalidKeyStoreAlgorithmShouldFail() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.enabled:true", - "spring.rabbitmq.ssl.key-store=/org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.rabbitmq.ssl.key-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.rabbitmq.ssl.key-store-type=jks", "spring.rabbitmq.ssl.key-store-password=secret", "spring.rabbitmq.ssl.key-store-algorithm=test-invalid-algo") .run((context) -> { @@ -1048,7 +1048,7 @@ void enableSslWithInvalidKeyStoreAlgorithmShouldFail() { void enableSslWithInvalidTrustStoreAlgorithmShouldFail() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.enabled:true", - "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.rabbitmq.ssl.trust-store=/org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.rabbitmq.ssl.trust-store-type=jks", "spring.rabbitmq.ssl.trust-store-password=secret", "spring.rabbitmq.ssl.trust-store-algorithm=test-invalid-algo") .run((context) -> { diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitPropertiesTests.java similarity index 99% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitPropertiesTests.java index 81ecdf6f625e..5c2a7bff6641 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitPropertiesTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitPropertiesTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.util.List; diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfigurationTests.java similarity index 98% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfigurationTests.java index 87230b0618c9..6ca687352abf 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitStreamConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.rabbitmq.autoconfigure; import java.time.Duration; import java.util.List; @@ -34,9 +34,9 @@ import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.boot.amqp.autoconfigure.RabbitStreamConfiguration.PropertiesRabbitStreamConnectionDetails; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitStreamConfiguration.PropertiesRabbitStreamConnectionDetails; import org.springframework.boot.ssl.NoSuchSslBundleException; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; @@ -352,7 +352,7 @@ void whenStreamSslIsEnabledThenTlsIsUsed() { @Test void whenStreamSslBundleIsConfiguredThenTlsIsUsed() { this.contextRunner.withPropertyValues("spring.rabbitmq.stream.ssl.bundle=test-bundle", - "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.ssl.bundle.jks.test-bundle.keystore.password=secret") .run((context) -> { assertThat(context).hasNotFailed(); @@ -388,7 +388,7 @@ void whenStreamSslIsDisabledWithBundleThenTlsIsStillUsed() { void whenConnectionDetailsSslBundleIsProvidedThenTlsIsUsedWithoutProperties() { this.contextRunner.withUserConfiguration(CustomConnectionDetails.class) .withPropertyValues( - "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/amqp/autoconfigure/test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/rabbitmq/autoconfigure/test.jks", "spring.ssl.bundle.jks.test-bundle.keystore.password=secret") .run((context) -> { assertThat(context).hasNotFailed(); diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java similarity index 89% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java index 8739399d0aae..4b3571c2c8c6 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/health/RabbitHealthContributorAutoConfigurationTests.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure.health; +package org.springframework.boot.rabbitmq.autoconfigure.health; import org.junit.jupiter.api.Test; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.amqp.health.RabbitHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.health.autoconfigure.contributor.HealthContributorAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; +import org.springframework.boot.rabbitmq.health.RabbitHealthIndicator; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java similarity index 94% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java index 8c8c31a499e0..81928ab80091 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationMeterBinderCycleIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure.metrics; +package org.springframework.boot.rabbitmq.autoconfigure.metrics; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.MeterBinder; @@ -22,8 +22,8 @@ import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java similarity index 95% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java index b75b0e1857d9..937a1203a096 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/metrics/RabbitMetricsAutoConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure.metrics; +package org.springframework.boot.rabbitmq.autoconfigure.metrics; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; @@ -22,9 +22,9 @@ import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironmentTests.java similarity index 57% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironmentTests.java index 1ecb0b4d6ff4..533ee69f8c24 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/docker/compose/RabbitMqEnvironmentTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.rabbitmq.docker.compose; import java.util.Collections; import java.util.Map; @@ -24,48 +24,48 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitEnvironment}. + * Tests for {@link RabbitMqEnvironment}. * * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb * @author Scott Frederick */ -class RabbitEnvironmentTests { +class RabbitMqEnvironmentTests { @Test - void getUsernameWhenNoRabbitmqDefaultUser() { - RabbitEnvironment environment = new RabbitEnvironment(Collections.emptyMap()); + void getUsernameWhenNoRabbitMqDefaultUser() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Collections.emptyMap()); assertThat(environment.getUsername()).isEqualTo("guest"); } @Test - void getUsernameWhenHasRabbitmqDefaultUser() { - RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_DEFAULT_USER", "me")); + void getUsernameWhenHasRabbitMqDefaultUser() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_DEFAULT_USER", "me")); assertThat(environment.getUsername()).isEqualTo("me"); } @Test - void getUsernameWhenHasRabbitmqUsername() { - RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_USERNAME", "me")); + void getUsernameWhenHasRabbitMqUsername() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_USERNAME", "me")); assertThat(environment.getUsername()).isEqualTo("me"); } @Test - void getUsernameWhenNoRabbitmqDefaultPass() { - RabbitEnvironment environment = new RabbitEnvironment(Collections.emptyMap()); + void getUsernameWhenNoRabbitMqDefaultPass() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Collections.emptyMap()); assertThat(environment.getPassword()).isEqualTo("guest"); } @Test - void getUsernameWhenHasRabbitmqDefaultPass() { - RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); + void getUsernameWhenHasRabbitMqDefaultPass() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); assertThat(environment.getPassword()).isEqualTo("secret"); } @Test - void getUsernameWhenHasRabbitmqPassword() { - RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_PASSWORD", "secret")); + void getUsernameWhenHasRabbitMqPassword() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_PASSWORD", "secret")); assertThat(environment.getPassword()).isEqualTo("secret"); } diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/health/RabbitHealthIndicatorTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicatorTests.java similarity index 98% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/health/RabbitHealthIndicatorTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicatorTests.java index 5ce2f8e64a20..161cb2fa6e94 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/health/RabbitHealthIndicatorTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/health/RabbitHealthIndicatorTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.health; +package org.springframework.boot.rabbitmq.health; import java.util.Collections; diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/metrics/RabbitMetricsTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/metrics/RabbitMetricsTests.java similarity index 97% rename from module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/metrics/RabbitMetricsTests.java rename to module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/metrics/RabbitMetricsTests.java index 780de803e4f6..a8b3f5b3c1d3 100644 --- a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/metrics/RabbitMetricsTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/metrics/RabbitMetricsTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.metrics; +package org.springframework.boot.rabbitmq.metrics; import com.rabbitmq.client.ConnectionFactory; import io.micrometer.core.instrument.Tags; diff --git a/module/spring-boot-amqp/src/test/resources/logback-test.xml b/module/spring-boot-rabbitmq/src/test/resources/logback-test.xml similarity index 100% rename from module/spring-boot-amqp/src/test/resources/logback-test.xml rename to module/spring-boot-rabbitmq/src/test/resources/logback-test.xml diff --git a/module/spring-boot-amqp/src/test/resources/org/springframework/boot/amqp/autoconfigure/test.jks b/module/spring-boot-rabbitmq/src/test/resources/org/springframework/boot/rabbitmq/autoconfigure/test.jks similarity index 100% rename from module/spring-boot-amqp/src/test/resources/org/springframework/boot/amqp/autoconfigure/test.jks rename to module/spring-boot-rabbitmq/src/test/resources/org/springframework/boot/rabbitmq/autoconfigure/test.jks diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index f585495dfbc2..fc8e69bab79a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2055,7 +2055,7 @@ bom { releaseNotes("https://github.com/r2dbc/r2dbc-spi/releases/tag/v{version}") } } - library("Rabbit AMQP Client", "5.30.0") { + library("RabbitMQ Client", "5.30.0") { group("com.rabbitmq") { modules = [ "amqp-client" @@ -2067,7 +2067,7 @@ bom { releaseNotes("https://github.com/rabbitmq/rabbitmq-java-client/releases/tag/v{version}") } } - library("Rabbit Stream Client", "1.6.0") { + library("RabbitMQ Stream Client", "1.6.0") { group("com.rabbitmq") { modules = [ "stream-client" @@ -2123,7 +2123,6 @@ bom { "spring-boot-activemq", "spring-boot-actuator", "spring-boot-actuator-autoconfigure", - "spring-boot-amqp", "spring-boot-artemis", "spring-boot-autoconfigure", "spring-boot-autoconfigure-classic", @@ -2219,6 +2218,7 @@ bom { "spring-boot-pulsar", "spring-boot-quartz", "spring-boot-r2dbc", + "spring-boot-rabbitmq", "spring-boot-reactor", "spring-boot-reactor-netty", "spring-boot-restclient", @@ -2244,8 +2244,6 @@ bom { "spring-boot-starter-activemq-test", "spring-boot-starter-actuator", "spring-boot-starter-actuator-test", - "spring-boot-starter-amqp", - "spring-boot-starter-amqp-test", "spring-boot-starter-artemis", "spring-boot-starter-artemis-test", "spring-boot-starter-aspectj", @@ -2364,6 +2362,8 @@ bom { "spring-boot-starter-quartz-test", "spring-boot-starter-r2dbc", "spring-boot-starter-r2dbc-test", + "spring-boot-starter-rabbitmqmq", + "spring-boot-starter-rabbitmqmq-test", "spring-boot-starter-reactor-netty", "spring-boot-starter-restclient", "spring-boot-starter-restclient-test", diff --git a/settings.gradle b/settings.gradle index 696ee76c44e6..bee3c7607724 100644 --- a/settings.gradle +++ b/settings.gradle @@ -80,7 +80,6 @@ include "core:spring-boot-testcontainers" include "module:spring-boot-activemq" include "module:spring-boot-actuator" include "module:spring-boot-actuator-autoconfigure" -include "module:spring-boot-amqp" include "module:spring-boot-artemis" include "module:spring-boot-autoconfigure-classic" include "module:spring-boot-autoconfigure-classic-modules" @@ -167,6 +166,7 @@ include "module:spring-boot-persistence" include "module:spring-boot-pulsar" include "module:spring-boot-quartz" include "module:spring-boot-r2dbc" +include "module:spring-boot-rabbitmq" include "module:spring-boot-reactor" include "module:spring-boot-reactor-netty" include "module:spring-boot-restclient" @@ -211,8 +211,6 @@ include "starter:spring-boot-starter-activemq" include "starter:spring-boot-starter-activemq-test" include "starter:spring-boot-starter-actuator" include "starter:spring-boot-starter-actuator-test" -include "starter:spring-boot-starter-amqp" -include "starter:spring-boot-starter-amqp-test" include "starter:spring-boot-starter-artemis" include "starter:spring-boot-starter-artemis-test" include "starter:spring-boot-starter-aspectj" @@ -332,6 +330,8 @@ include "starter:spring-boot-starter-quartz" include "starter:spring-boot-starter-quartz-test" include "starter:spring-boot-starter-r2dbc" include "starter:spring-boot-starter-r2dbc-test" +include "starter:spring-boot-starter-rabbitmq" +include "starter:spring-boot-starter-rabbitmq-test" include "starter:spring-boot-starter-reactor-netty" include "starter:spring-boot-starter-restclient" include "starter:spring-boot-starter-restclient-test" @@ -401,7 +401,6 @@ include ":smoke-test:spring-boot-smoke-test-actuator-extension" include ":smoke-test:spring-boot-smoke-test-actuator-log4j2" include ":smoke-test:spring-boot-smoke-test-actuator-noweb" include ":smoke-test:spring-boot-smoke-test-actuator-ui" -include ":smoke-test:spring-boot-smoke-test-amqp" include ":smoke-test:spring-boot-smoke-test-ant" include ":smoke-test:spring-boot-smoke-test-artemis" include ":smoke-test:spring-boot-smoke-test-aspectj" @@ -458,6 +457,7 @@ include ":smoke-test:spring-boot-smoke-test-prometheus" include ":smoke-test:spring-boot-smoke-test-property-validation" include ":smoke-test:spring-boot-smoke-test-pulsar" include ":smoke-test:spring-boot-smoke-test-quartz" +include ":smoke-test:spring-boot-smoke-test-rabbitmq" include ":smoke-test:spring-boot-smoke-test-reactive-oauth2-client" include ":smoke-test:spring-boot-smoke-test-reactive-oauth2-resource-server" include ":smoke-test:spring-boot-smoke-test-restclient" diff --git a/smoke-test/spring-boot-smoke-test-amqp/build.gradle b/smoke-test/spring-boot-smoke-test-rabbitmq/build.gradle similarity index 90% rename from smoke-test/spring-boot-smoke-test-amqp/build.gradle rename to smoke-test/spring-boot-smoke-test-rabbitmq/build.gradle index 5c5d50459647..58a7af072b54 100644 --- a/smoke-test/spring-boot-smoke-test-amqp/build.gradle +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/build.gradle @@ -19,13 +19,13 @@ plugins { id "org.springframework.boot.docker-test" } -description = "Spring Boot AMQP smoke test" +description = "Spring Boot RabbitMQ smoke test" dependencies { - implementation(project(":starter:spring-boot-starter-amqp")) + implementation(project(":starter:spring-boot-starter-rabbitmq")) dockerTestImplementation(project(":core:spring-boot-testcontainers")) - dockerTestImplementation(project(":starter:spring-boot-starter-amqp-test")) + dockerTestImplementation(project(":starter:spring-boot-starter-rabbitmq-test")) dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) dockerTestImplementation("org.awaitility:awaitility") dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationSslTests.java similarity index 97% rename from smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationSslTests.java index e1acf6319d04..f846f70ae495 100644 --- a/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationSslTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.amqp; +package smoketest.rabbitmq; import java.time.Duration; @@ -43,7 +43,7 @@ @SpringBootTest @Testcontainers(disabledWithoutDocker = true) @ExtendWith(OutputCaptureExtension.class) -class SampleAmqpSimpleApplicationSslTests { +class SampleRabbitApplicationSslTests { @Container @ServiceConnection diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationTests.java similarity index 96% rename from smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationTests.java index e763ade7aa09..626348198713 100644 --- a/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SampleRabbitApplicationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.amqp; +package smoketest.rabbitmq; import java.time.Duration; @@ -37,7 +37,7 @@ @SpringBootTest @Testcontainers(disabledWithoutDocker = true) @ExtendWith(OutputCaptureExtension.class) -class SampleAmqpSimpleApplicationTests { +class SampleRabbitApplicationTests { @Container @ServiceConnection diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SecureRabbitMqContainer.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SecureRabbitMqContainer.java similarity index 98% rename from smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SecureRabbitMqContainer.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SecureRabbitMqContainer.java index 7599d2d4ff21..5dfb9e776c39 100644 --- a/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SecureRabbitMqContainer.java +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/src/dockerTest/java/smoketest/rabbitmq/SecureRabbitMqContainer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.amqp; +package smoketest.rabbitmq; import org.testcontainers.rabbitmq.RabbitMQContainer; import org.testcontainers.utility.DockerImageName; diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/SampleRabbitApplication.java similarity index 87% rename from smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/SampleRabbitApplication.java index 7c01337d3cba..da492b1fe4f5 100644 --- a/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/SampleRabbitApplication.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.amqp; +package smoketest.rabbitmq; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -30,9 +30,9 @@ @SpringBootApplication @RabbitListener(queues = "foo") -public class SampleAmqpSimpleApplication { +public class SampleRabbitApplication { - private static final Log logger = LogFactory.getLog(SampleAmqpSimpleApplication.class); + private static final Log logger = LogFactory.getLog(SampleRabbitApplication.class); @Bean public Sender mySender() { @@ -55,7 +55,7 @@ public ApplicationRunner runner(Sender sender) { } public static void main(String[] args) { - SpringApplication.run(SampleAmqpSimpleApplication.class, args); + SpringApplication.run(SampleRabbitApplication.class, args); } } diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Sender.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/Sender.java similarity index 97% rename from smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Sender.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/Sender.java index 078f2416966c..c3fd88bbed28 100644 --- a/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Sender.java +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/Sender.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.amqp; +package smoketest.rabbitmq; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/package-info.java b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/package-info.java similarity index 96% rename from smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/package-info.java rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/package-info.java index bc9e41a079d7..a078a228474f 100644 --- a/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/package-info.java +++ b/smoke-test/spring-boot-smoke-test-rabbitmq/src/main/java/smoketest/rabbitmq/package-info.java @@ -15,6 +15,6 @@ */ @NullMarked -package smoketest.amqp; +package smoketest.rabbitmq; import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/rabbitmq.conf b/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/rabbitmq.conf similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/rabbitmq.conf rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/rabbitmq.conf diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.crt b/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-ca.crt similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.crt rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-ca.crt diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.key b/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-ca.key similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.key rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-ca.key diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.crt b/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-client.crt similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.crt rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-client.crt diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.key b/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-client.key similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.key rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-client.key diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.crt b/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-server.crt similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.crt rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-server.crt diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.key b/smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-server.key similarity index 100% rename from smoke-test/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.key rename to smoke-test/spring-boot-smoke-test-rabbitmq/src/test/resources/ssl/test-server.key diff --git a/starter/spring-boot-starter-amqp-test/build.gradle b/starter/spring-boot-starter-rabbitmq-test/build.gradle similarity index 85% rename from starter/spring-boot-starter-amqp-test/build.gradle rename to starter/spring-boot-starter-rabbitmq-test/build.gradle index 567bff1c0b71..aa2b7da78b3a 100644 --- a/starter/spring-boot-starter-amqp-test/build.gradle +++ b/starter/spring-boot-starter-rabbitmq-test/build.gradle @@ -18,10 +18,10 @@ plugins { id "org.springframework.boot.starter" } -description = "Starter for testing Spring AMQP and Rabbit MQ" +description = "Starter for testing RabbitMQ messaging and streaming broker" dependencies { - api(project(":starter:spring-boot-starter-amqp")) + api(project(":starter:spring-boot-starter-rabbitmq")) api(project(":starter:spring-boot-starter-test")) api("org.springframework.amqp:spring-rabbit-test") diff --git a/starter/spring-boot-starter-amqp/build.gradle b/starter/spring-boot-starter-rabbitmq/build.gradle similarity index 86% rename from starter/spring-boot-starter-amqp/build.gradle rename to starter/spring-boot-starter-rabbitmq/build.gradle index 5c3afd03ab22..351bcaf77eaa 100644 --- a/starter/spring-boot-starter-amqp/build.gradle +++ b/starter/spring-boot-starter-rabbitmq/build.gradle @@ -18,10 +18,10 @@ plugins { id "org.springframework.boot.starter" } -description = "Starter for using Spring AMQP and Rabbit MQ" +description = "Starter for using RabbitMQ messaging and streaming broker" dependencies { api(project(":starter:spring-boot-starter")) - api(project(":module:spring-boot-amqp")) + api(project(":module:spring-boot-rabbitmq")) } From b5ba1ec85c1cbcf6b66ba2558f3bed8814b00c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 30 Mar 2026 11:58:00 +0200 Subject: [PATCH 696/930] Add AMQP 1.0 generic auto-configuration This commit adds auto-configuration for the generic AMQP 1.0 client in Spring AMQP 4.1, using Qpid ProtonJ. Support for connection factory, client and listener endpoints are available. The "spring.amqp" namespace exposes settings to connect to an AMQP 1.0 compliant broker and customize client and listeners settings. Docker compose and testcontainers support using RabbitMQ, ActiveMQ, and Artemis have been added too. Closes gh-49678 --- .../DocumentConfigurationProperties.java | 1 + documentation/spring-boot-docs/build.gradle | 1 + .../antora/modules/ROOT/pages/redirect.adoc | 14 +- .../pages/features/dev-services.adoc | 3 + .../reference/pages/messaging/amqp.adoc | 89 +++- .../pages/testing/testcontainers.adoc | 3 + .../amqp/generic/receiving/MyBean.java | 30 ++ .../receiving/custom/MyAmqpConfiguration.java | 38 ++ .../amqp/generic/receiving/custom/MyBean.java | 30 ++ .../receiving/custom/MyMessageConverter.java | 2 +- .../amqp/generic/sending/MyBean.java | 37 ++ .../amqp/{ => rabbitmq}/receiving/MyBean.java | 2 +- .../receiving/custom/MyBean.java | 2 +- .../receiving/custom/MyMessageConverter.java | 36 ++ .../custom/MyRabbitConfiguration.java | 2 +- .../amqp/{ => rabbitmq}/sending/MyBean.java | 2 +- .../amqp/generic/receiving/MyBean.kt | 31 ++ .../receiving/custom/MyAmqpConfiguration.kt | 39 ++ .../amqp/generic/receiving/custom/MyBean.kt | 31 ++ .../receiving/custom/MyMessageConverter.kt | 3 +- .../messaging/amqp/generic/sending/MyBean.kt | 32 ++ .../amqp/{ => rabbitmq}/receiving/MyBean.kt | 2 +- .../{ => rabbitmq}/receiving/custom/MyBean.kt | 2 +- .../receiving/custom/MyMessageConverter.kt | 34 ++ .../receiving/custom/MyRabbitConfiguration.kt | 2 +- .../amqp/{ => rabbitmq}/sending/MyBean.kt | 2 +- module/spring-boot-amqp/build.gradle | 61 +++ ...AmqpAutoConfigurationIntegrationTests.java | 130 ++++++ ...nectionDetailsFactoryIntegrationTests.java | 42 ++ ...nectionDetailsFactoryIntegrationTests.java | 42 ++ ...nectionDetailsFactoryIntegrationTests.java | 42 ++ ...nectionDetailsFactoryIntegrationTests.java | 73 ++++ ...nectionDetailsFactoryIntegrationTests.java | 94 +++++ ...nectionDetailsFactoryIntegrationTests.java | 71 ++++ .../src/dockerTest/resources/logback-test.xml | 4 + .../amqp/docker/compose/activemq-compose.yaml | 9 + .../amqp/docker/compose/artemis-compose.yaml | 9 + .../amqp/docker/compose/rabbitmq-compose.yaml | 8 + .../dockerTest/resources/spring.properties | 1 + .../AmqpAnnotationDrivenConfiguration.java | 100 +++++ .../autoconfigure/AmqpAutoConfiguration.java | 134 ++++++ .../autoconfigure/AmqpClientCustomizer.java | 37 ++ .../autoconfigure/AmqpConnectionDetails.java | 62 +++ ...ageListenerContainerFactoryConfigurer.java | 86 ++++ .../amqp/autoconfigure/AmqpProperties.java | 213 ++++++++++ .../ConnectionOptionsCustomizer.java | 39 ++ .../PropertiesAmqpConnectionDetails.java | 49 +++ .../boot/amqp/autoconfigure/package-info.java | 23 ++ ...DockerComposeConnectionDetailsFactory.java | 89 ++++ ...DockerComposeConnectionDetailsFactory.java | 46 +++ .../amqp/docker/compose/AmqpEnvironment.java | 45 +++ ...DockerComposeConnectionDetailsFactory.java | 45 +++ ...DockerComposeConnectionDetailsFactory.java | 47 +++ .../amqp/docker/compose/package-info.java | 23 ++ ...AmqpContainerConnectionDetailsFactory.java | 53 +++ ...AmqpContainerConnectionDetailsFactory.java | 60 +++ ...AmqpContainerConnectionDetailsFactory.java | 60 +++ ...AmqpContainerConnectionDetailsFactory.java | 71 ++++ .../amqp/testcontainers/package-info.java | 23 ++ .../main/resources/META-INF/spring.factories | 8 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../AmqpAutoConfigurationTests.java | 381 ++++++++++++++++++ ...stenerContainerFactoryConfigurerTests.java | 101 +++++ ...rComposeConnectionDetailsFactoryTests.java | 67 +++ ...rComposeConnectionDetailsFactoryTests.java | 67 +++ ...rComposeConnectionDetailsFactoryTests.java | 79 ++++ .../build.gradle | 3 + .../spring-boot-dependencies/build.gradle | 7 +- settings.gradle | 4 + .../spring-boot-smoke-test-amqp/build.gradle | 41 ++ .../amqp/SampleAmqpApplicationTests.java | 57 +++ .../main/java/smoketest/amqp/Listener.java | 35 ++ .../smoketest/amqp/SampleAmqpApplication.java | 36 ++ .../src/main/java/smoketest/amqp/Sender.java | 35 ++ .../java/smoketest/amqp/package-info.java | 20 + .../build.gradle | 26 ++ starter/spring-boot-starter-amqp/build.gradle | 27 ++ .../RabbitMqManagementContainer.java | 57 +++ .../boot/testsupport/container/TestImage.java | 5 +- 79 files changed, 3387 insertions(+), 31 deletions(-) create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/MyBean.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyAmqpConfiguration.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyBean.java rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => generic}/receiving/custom/MyMessageConverter.java (93%) create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/MyBean.java (92%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyBean.java (91%) create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.java rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyRabbitConfiguration.java (95%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/sending/MyBean.java (94%) create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/MyBean.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyAmqpConfiguration.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyBean.kt rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => generic}/receiving/custom/MyMessageConverter.kt (93%) create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/MyBean.kt (92%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyBean.kt (92%) create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyRabbitConfiguration.kt (95%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/sending/MyBean.kt (93%) create mode 100644 module/spring-boot-amqp/build.gradle create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/ActiveMqAmqpDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/ArtemisAmqpDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqAmqpDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/ActiveMqAmqpContainerConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/ArtemisAmqpContainerConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqAmqpContainerConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/activemq-compose.yaml create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/artemis-compose.yaml create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/spring.properties create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAnnotationDrivenConfiguration.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpMessageListenerContainerFactoryConfigurer.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/AbstractAmqpDockerComposeConnectionDetailsFactory.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/ActiveMqAmqpDockerComposeConnectionDetailsFactory.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/AmqpEnvironment.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/ArtemisAmqpDockerComposeConnectionDetailsFactory.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqAmqpDockerComposeConnectionDetailsFactory.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/AbstractAmqpContainerConnectionDetailsFactory.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/ActiveMqAmqpContainerConnectionDetailsFactory.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/ArtemisAmqpContainerConnectionDetailsFactory.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqAmqpContainerConnectionDetailsFactory.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java create mode 100644 module/spring-boot-amqp/src/main/resources/META-INF/spring.factories create mode 100644 module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java create mode 100644 module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpMessageListenerContainerFactoryConfigurerTests.java create mode 100644 module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/ActiveMqAmqpDockerComposeConnectionDetailsFactoryTests.java create mode 100644 module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/ArtemisAmqpDockerComposeConnectionDetailsFactoryTests.java create mode 100644 module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqAmqpDockerComposeConnectionDetailsFactoryTests.java create mode 100644 smoke-test/spring-boot-smoke-test-amqp/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpApplicationTests.java create mode 100644 smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Listener.java create mode 100644 smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Sender.java create mode 100644 smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/package-info.java create mode 100644 starter/spring-boot-starter-amqp-test/build.gradle create mode 100644 starter/spring-boot-starter-amqp/build.gradle create mode 100644 test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.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 5051fdcbfa35..5eb829029896 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 dataMigrationPrefixes(Config prefix) { private void integrationPrefixes(Config prefix) { prefix.accept("spring.activemq"); + prefix.accept("spring.amqp"); prefix.accept("spring.artemis"); prefix.accept("spring.batch"); prefix.accept("spring.integration"); diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index 86ccc81a5673..88c66a91cce7 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -94,6 +94,7 @@ dependencies { implementation(project(path: ":loader:spring-boot-loader-tools")) implementation(project(path: ":module:spring-boot-actuator")) implementation(project(path: ":module:spring-boot-actuator-autoconfigure")) + implementation(project(path: ":module:spring-boot-amqp")) implementation(project(path: ":module:spring-boot-cache")) implementation(project(path: ":module:spring-boot-cache-test")) implementation(project(path: ":module:spring-boot-data-cassandra")) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc index 1ce0969553fe..0194ea8b3d1a 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc @@ -1716,13 +1716,13 @@ * xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#features.messaging.amqp.rabbit] * xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#messaging.amqp.rabbit] * xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#messaging.amqp.rabbitmq] -* xref:reference:messaging/amqp.adoc#messaging.amqp.receiving[#boot-features-using-amqp-receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.receiving[#features.messaging.amqp.receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.receiving[#messaging.amqp.receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.sending-stream[#messaging.amqp.sending-stream] -* xref:reference:messaging/amqp.adoc#messaging.amqp.sending[#boot-features-using-amqp-sending] -* xref:reference:messaging/amqp.adoc#messaging.amqp.sending[#features.messaging.amqp.sending] -* xref:reference:messaging/amqp.adoc#messaging.amqp.sending[#messaging.amqp.sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#boot-features-using-amqp-receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#features.messaging.amqp.receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#messaging.amqp.receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending-stream[#messaging.amqp.sending-stream] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#boot-features-using-amqp-sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#features.messaging.amqp.sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#messaging.amqp.sending] * xref:reference:messaging/amqp.adoc#messaging.amqp[#boot-features-amqp] * xref:reference:messaging/amqp.adoc#messaging.amqp[#features.messaging.amqp] * xref:reference:messaging/amqp.adoc#messaging.amqp[#messaging.amqp] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc index d73f66e962e7..496364b53a4e 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc @@ -86,6 +86,9 @@ The following service connections are currently supported: | javadoc:org.springframework.boot.activemq.autoconfigure.ActiveMQConnectionDetails[] | Containers named "symptoma/activemq", "apache/activemq-classic" or "apache/activemq" +| javadoc:org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails[] +| Containers named "rabbitmq" or "apache/activemq-classic" or "apache/activemq-artemis" + | javadoc:org.springframework.boot.artemis.autoconfigure.ArtemisConnectionDetails[] | Containers named "apache/activemq-artemis" or "apache/artemis" diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index 3864f7ebe9ea..4aad8ad62903 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc @@ -3,7 +3,78 @@ The Advanced Message Queuing Protocol (AMQP) is a platform-neutral, wire-level protocol for message-oriented middleware. The Spring AMQP project applies core Spring concepts to the development of AMQP-based messaging solutions. -Spring Boot offers several conveniences for working with AMQP through RabbitMQ, including the `spring-boot-starter-rabbitmq` starter. +Spring Boot offers several conveniences for working with AMQP: generic AMQP 1.0 support is provided by the `spring-boot-starter-amqp` starter while specific RabbitMQ support is available via the `spring-boot-starter-rabbitmq` starter. + + + +[[messaging.amqp.generic]] +== Generic AMQP 1.0 Support + +AMQP 1.0 is supported by several brokers and messaging services beyond RabbitMQ, including ActiveMQ, Azure Service Bus, and others. +Spring AMQP provides {url-spring-amqp-docs}/amqp10-client.html[generic support for AMQP 1.0] via `org.springframework.amqp:spring-amqp-client` that is based on the https://github.com/apache/qpid-protonj2/blob/main/protonj2-client/README.md[Qpid ProtonJ2 Client Library]. + +AMQP configuration is controlled by external configuration properties in `+spring.amqp.*+`. +For example, you might declare the following in your configuration: + +[configprops,yaml] +---- +spring: + amqp: + host: "localhost" + port: 5672 + username: "admin" + password: "secret" +---- + +To configure javadoc:org.apache.qpid.protonj2.client.ConnectionOptions[connection options] of the auto-configured javadoc:org.springframework.amqp.client.AmqpConnectionFactory[], define a javadoc:org.springframework.boot.amqp.autoconfigure.ConnectionOptionsCustomizer[] bean. + +[[messaging.amqp.generic.sending]] +=== Sending a Message + +Spring's javadoc:org.springframework.amqp.client.AmqpClient[] is auto-configured, and you can autowire it directly into your own beans, as shown in the following example: + +include-code::MyBean[] + +If a javadoc:org.springframework.amqp.support.converter.MessageConverter[] bean is defined, it is associated automatically with the auto-configured javadoc:org.springframework.amqp.client.AmqpClient[]. +If no such converter is defined and Jackson is available, javadoc:org.springframework.amqp.support.converter.JacksonJsonMessageConverter[] is used. + +Client-specific settings can be configured as follows: + +[configprops,yaml] +---- +spring: + amqp: + client: + default-to-address: "/queues/default_queue" + completion-timeout: "500ms" +---- + +To further configure the auto-configured javadoc:org.springframework.amqp.client.AmqpClient[], define a javadoc:org.springframework.boot.amqp.autoconfigure.AmqpClientCustomizer[] bean. + + + +[[messaging.amqp.generic.receiving]] +=== Receiving a Message + +When the generic AMQP 1.0 infrastructure is present, any bean can be annotated with javadoc:org.springframework.amqp.client.annotation.AmqpListener[format=annotation] to create a listener endpoint. +If no javadoc:org.springframework.amqp.client.config.MethodAmqpMessageListenerContainerFactory[] has been defined, a default one is automatically configured. +If a javadoc:org.springframework.amqp.support.converter.MessageConverter[] or a javadoc:org.springframework.amqp.client.listener.AmqpListenerErrorHandler[] bean is defined, it is automatically associated with the default factory. + +The following sample component creates a listener endpoint on the `/queues/someQueue` address: + +include-code::MyBean[] + +TIP: See javadoc:org.springframework.amqp.client.config.EnableAmqp[format=annotation] for more details. + +If you need to create more javadoc:org.springframework.amqp.client.config.MethodAmqpMessageListenerContainerFactory[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.amqp.autoconfigure.AmqpMessageListenerContainerFactoryConfigurer[] that you can use to initialize a javadoc:org.springframework.amqp.client.config.MethodAmqpMessageListenerContainerFactory[] with the same settings as the factory used by the auto-configuration. + +For instance, the following configuration class exposes another factory that uses a specific javadoc:org.springframework.amqp.support.converter.MessageConverter[]: + +include-code::custom/MyAmqpConfiguration[] + +Then you can use the factory in any javadoc:org.springframework.amqp.client.annotation.AmqpListener[format=annotation]-annotated method, as follows: + +include-code::custom/MyBean[] @@ -49,8 +120,8 @@ TIP: See https://spring.io/blog/2010/06/14/understanding-amqp-the-protocol-used- -[[messaging.amqp.sending]] -== Sending a Message +[[messaging.amqp.rabbitmq.sending]] +=== Sending a Message Spring's javadoc:org.springframework.amqp.core.AmqpTemplate[] and javadoc:org.springframework.amqp.core.AmqpAdmin[] are auto-configured, and you can autowire them directly into your own beans, as shown in the following example: @@ -82,8 +153,8 @@ If there's a bean of type javadoc:org.springframework.amqp.rabbit.support.microm -[[messaging.amqp.sending-stream]] -== Sending a Message To A Stream +[[messaging.amqp.rabbitmq.sending-stream]] +=== Sending a Message To A Stream To send a message to a particular stream, specify the name of the stream, as shown in the following example: @@ -101,14 +172,14 @@ If you need to create more javadoc:org.springframework.rabbit.stream.producer.Ra -[[messaging.amqp.sending-stream.ssl]] -=== SSL +[[messaging.amqp.rabbitmq.sending-stream.ssl]] +==== SSL To use SSL with RabbitMQ Streams, set configprop:spring.rabbitmq.stream.ssl.enabled[] to `true` or set configprop:spring.rabbitmq.stream.ssl.bundle[] to configure the xref:features/ssl.adoc#features.ssl.bundles[SSL bundle] to use. -[[messaging.amqp.receiving]] -== Receiving a Message +[[messaging.amqp.rabbitmq.receiving]] +=== Receiving a Message When the Rabbit infrastructure is present, any bean can be annotated with javadoc:org.springframework.amqp.rabbit.annotation.RabbitListener[format=annotation] to create a listener endpoint. If no javadoc:org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory[] has been defined, a default javadoc:org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory[] is automatically configured and you can switch to a direct container using the configprop:spring.rabbitmq.listener.type[] property. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc index e5d6ef85271c..a0bb6cd9f073 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc @@ -128,6 +128,9 @@ The following service connection factories are provided in the `spring-boot-test | javadoc:org.springframework.boot.activemq.autoconfigure.ActiveMQConnectionDetails[] | Containers named "symptoma/activemq" or javadoc:org.testcontainers.activemq.ActiveMQContainer[] +| javadoc:org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails[] +| Containers of types javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] or avadoc:org.testcontainers.activemq.ActiveMQContainer[] or javadoc:org.testcontainers.activemq.ArtemisContainer[] + | javadoc:org.springframework.boot.artemis.autoconfigure.ArtemisConnectionDetails[] | Containers of type javadoc:org.testcontainers.activemq.ArtemisContainer[] diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/MyBean.java new file mode 100644 index 000000000000..655956f5de0f --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/MyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.generic.receiving; + +import org.springframework.amqp.client.annotation.AmqpListener; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @AmqpListener(addresses = "/queues/someQueue") + public void processMessage(String content) { + // ... + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyAmqpConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyAmqpConfiguration.java new file mode 100644 index 000000000000..689f09ab03f1 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyAmqpConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.generic.receiving.custom; + +import org.springframework.amqp.client.AmqpConnectionFactory; +import org.springframework.amqp.client.config.MethodAmqpMessageListenerContainerFactory; +import org.springframework.boot.amqp.autoconfigure.AmqpMessageListenerContainerFactoryConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyAmqpConfiguration { + + @Bean + public MethodAmqpMessageListenerContainerFactory myFactory(AmqpMessageListenerContainerFactoryConfigurer configurer, + AmqpConnectionFactory connectionFactory) { + MethodAmqpMessageListenerContainerFactory factory = new MethodAmqpMessageListenerContainerFactory( + connectionFactory); + configurer.configure(factory); + factory.setMessageConverter(new MyMessageConverter()); + return factory; + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyBean.java new file mode 100644 index 000000000000..18b959e94ddb --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.generic.receiving.custom; + +import org.springframework.amqp.client.annotation.AmqpListener; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @AmqpListener(addresses = "/queues/someQueue", containerFactory = "myFactory") + public void processMessage(String content) { + // ... + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyMessageConverter.java similarity index 93% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyMessageConverter.java index ba8d391a53f3..81ca9b551c52 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyMessageConverter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom; +package org.springframework.boot.docs.messaging.amqp.generic.receiving.custom; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java new file mode 100644 index 000000000000..8f7ffc09ff5c --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.generic.sending; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final AmqpClient amqpClient; + + public MyBean(AmqpClient amqpClient) { + this.amqpClient = amqpClient; + } + + // @fold:on // ... + public void sendMessage(String msg) { + this.amqpClient.to("/queues/test").body(msg).send(); + } + // @fold:off + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java similarity index 92% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java index d5fd3014a058..56064bfb94ce 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving; +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.java similarity index 91% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.java index 0f0c59f87c99..671de83f0e3d 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom; +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.java new file mode 100644 index 000000000000..18169c7e7aba --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.rabbitmq.receiving.custom; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.support.converter.MessageConverter; + +class MyMessageConverter implements MessageConverter { + + @Override + public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { + return null; + } + + @Override + public Object fromMessage(Message message) throws MessageConversionException { + return null; + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java similarity index 95% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java index 172c9cbba0d1..318428b08be0 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom; +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/sending/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java similarity index 94% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/sending/MyBean.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java index f2adb052cecb..51b91d35769b 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/sending/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.sending; +package org.springframework.boot.docs.messaging.amqp.rabbitmq.sending; import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.AmqpTemplate; diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/MyBean.kt new file mode 100644 index 000000000000..20f997bbc84f --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/MyBean.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.generic.receiving + +import org.springframework.amqp.client.annotation.AmqpListener +import org.springframework.stereotype.Component + +@Suppress("UNUSED_PARAMETER") +@Component +class MyBean { + + @AmqpListener(addresses = ["/queues/someQueue"]) + fun processMessage(content: String?) { + // ... + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyAmqpConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyAmqpConfiguration.kt new file mode 100644 index 000000000000..9b08e17d070f --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyAmqpConfiguration.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.generic.receiving.custom + +import org.springframework.amqp.client.AmqpConnectionFactory +import org.springframework.amqp.client.config.MethodAmqpMessageListenerContainerFactory +import org.springframework.boot.amqp.autoconfigure.AmqpMessageListenerContainerFactoryConfigurer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration(proxyBeanMethods = false) +class MyAmqpConfiguration { + + @Bean + fun myFactory( + configurer: AmqpMessageListenerContainerFactoryConfigurer, + connectionFactory: AmqpConnectionFactory + ): MethodAmqpMessageListenerContainerFactory { + val factory = MethodAmqpMessageListenerContainerFactory(connectionFactory) + configurer.configure(factory) + factory.setMessageConverter(MyMessageConverter()) + return factory + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyBean.kt new file mode 100644 index 000000000000..629f4c440f6b --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyBean.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.generic.receiving.custom + +import org.springframework.amqp.client.annotation.AmqpListener +import org.springframework.stereotype.Component + +@Suppress("UNUSED_PARAMETER") +@Component +class MyBean { + + @AmqpListener(addresses = ["/queues/someQueue"], containerFactory = "myFactory") + fun processMessage(content: String?) { + // ... + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyMessageConverter.kt similarity index 93% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyMessageConverter.kt index 7b6265875ccb..d29113704b22 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/receiving/custom/MyMessageConverter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom +package org.springframework.boot.docs.messaging.amqp.generic.receiving.custom import org.springframework.amqp.core.Message import org.springframework.amqp.core.MessageProperties @@ -31,4 +31,3 @@ internal class MyMessageConverter : MessageConverter { } } - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt new file mode 100644 index 000000000000..64c5174f4177 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.generic.sending + +import org.springframework.amqp.client.AmqpClient +import org.springframework.stereotype.Component + +@Component +class MyBean(private val amqpClient: AmqpClient) { + + // @fold:on // ... + fun someOtherMethod(msg: String) { + amqpClient.to("/queues/test").body(msg).send() + } + // @fold:off + +} + diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt similarity index 92% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt index a3241ddb95b8..9bac053496d2 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving import org.springframework.amqp.rabbit.annotation.RabbitListener import org.springframework.stereotype.Component diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt similarity index 92% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt index f92ffc012497..3b90bf3dc42d 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom import org.springframework.amqp.rabbit.annotation.RabbitListener import org.springframework.stereotype.Component diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt new file mode 100644 index 000000000000..03b165448e08 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.rabbitmq.receiving.custom + +import org.springframework.amqp.core.Message +import org.springframework.amqp.core.MessageProperties +import org.springframework.amqp.support.converter.MessageConverter + +internal class MyMessageConverter : MessageConverter { + + override fun toMessage(`object`: Any, messageProperties: MessageProperties): Message { + return Message(byteArrayOf()) + } + + override fun fromMessage(message: Message): Any { + return Any() + } + +} + diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt similarity index 95% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt index 95ca6f610039..900866bf2464 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory import org.springframework.amqp.rabbit.connection.CachingConnectionFactory diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/sending/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt similarity index 93% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/sending/MyBean.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt index e8ec5274920e..6d21de9bce0a 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/sending/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.sending +package org.springframework.boot.docs.messaging.amqp.rabbitmq.sending import org.springframework.amqp.core.AmqpAdmin import org.springframework.amqp.core.AmqpTemplate diff --git a/module/spring-boot-amqp/build.gradle b/module/spring-boot-amqp/build.gradle new file mode 100644 index 000000000000..891deb186196 --- /dev/null +++ b/module/spring-boot-amqp/build.gradle @@ -0,0 +1,61 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java-library" + id "org.springframework.boot.docker-test" + id "org.springframework.boot.auto-configuration" + id "org.springframework.boot.configuration-properties" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot support for AMQP 1.0" + +dependencies { + api(project(":core:spring-boot")) + api("org.apache.qpid:protonj2-client") + api("org.springframework:spring-messaging") + api("org.springframework.amqp:spring-amqp-client") + + implementation(project(":module:spring-boot-transaction")) + + optional(project(":core:spring-boot-autoconfigure")) + optional(project(":core:spring-boot-docker-compose")) + optional(project(":module:spring-boot-jackson")) + optional(project(":core:spring-boot-testcontainers")) + optional("org.testcontainers:testcontainers-activemq") + optional("org.testcontainers:testcontainers-rabbitmq") + + dockerTestImplementation(project(":core:spring-boot-test")) + dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) + dockerTestImplementation(testFixtures(project(":core:spring-boot-docker-compose"))) + dockerTestImplementation("ch.qos.logback:logback-classic") + dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") + + testImplementation(project(":core:spring-boot-test")) + testImplementation(project(":test-support:spring-boot-test-support")) + + testRuntimeOnly("ch.qos.logback:logback-classic") +} + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +tasks.named("compileDockerTestJava") { + options.nullability.checking = "tests" +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java new file mode 100644 index 000000000000..c709c114218b --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +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.amqp.client.AmqpClient; +import org.springframework.amqp.client.annotation.AmqpListener; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.container.RabbitMqManagementContainer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link AmqpAutoConfiguration}. + * + * @author Stephane Nicoll + */ +@Testcontainers(disabledWithoutDocker = true) +class AmqpAutoConfigurationIntegrationTests { + + @Container + static final RabbitMqManagementContainer container = new RabbitMqManagementContainer(); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AmqpAutoConfiguration.class)) + .withPropertyValues("spring.amqp.host=" + container.getHost(), "spring.amqp.port=" + container.getAmqpPort()); + + @Test + void sendAndReceiveUsingAmqpClient() { + String queue = container.createRandomQueue(); + this.contextRunner.run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient.to(queue).body("Hello World").send().get(1, TimeUnit.SECONDS)).isTrue(); + assertThat(amqpClient.from(queue).receiveAndConvert().get(1, TimeUnit.SECONDS)).isEqualTo("Hello World"); + }); + } + + @Test + void sendAndReceiveUsingJson() { + String queue = container.createRandomQueue(); + this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)).run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient.to(queue).body(new TestMessage("hello", 42)).send().get(1, TimeUnit.SECONDS)) + .isTrue(); + assertThat(amqpClient.from(queue).receiveAndConvert().get(1, TimeUnit.SECONDS)) + .isEqualTo(new TestMessage("hello", 42)); + }); + } + + @Test + void sendAndReceiveUsingListener() { + String queue = container.createRandomQueue(); + this.contextRunner.withPropertyValues("test.queue=" + queue) + .withUserConfiguration(TestListener.class) + .run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + TestListener listener = context.getBean(TestListener.class); + amqpClient.to(queue).body("Hello World").send(); + Awaitility.waitAtMost(Duration.ofMinutes(1)) + .untilAsserted(() -> assertThat(listener.messages).containsOnly("Hello World")); + }); + } + + @Test + void sendAndReceiveUsingListenerAndJson() { + String queue = container.createRandomQueue(); + this.contextRunner.withPropertyValues("test.queue=" + queue) + .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) + .withUserConfiguration(TestMessageListener.class) + .run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + TestMessageListener listener = context.getBean(TestMessageListener.class); + amqpClient.to(queue).body(new TestMessage("hello", 42)).send(); + Awaitility.waitAtMost(Duration.ofMinutes(1)) + .untilAsserted(() -> assertThat(listener.messages).containsOnly(new TestMessage("hello", 42))); + }); + } + + record TestMessage(String value, int counter) { + + } + + static class TestListener { + + private final List messages = new ArrayList<>(); + + @AmqpListener(addresses = "${test.queue}") + void processMessage(String message) { + this.messages.add(message); + } + + } + + static class TestMessageListener { + + private final List messages = new ArrayList<>(); + + @AmqpListener(addresses = "${test.queue}") + void processMessage(TestMessage message) { + this.messages.add(message); + } + + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/ActiveMqAmqpDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/ActiveMqAmqpDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..72ceb9f00a70 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/ActiveMqAmqpDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails.Address; +import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.testsupport.container.TestImage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ActiveMqAmqpDockerComposeConnectionDetailsFactory}. + * + * @author Stephane Nicoll + */ +class ActiveMqAmqpDockerComposeConnectionDetailsFactoryIntegrationTests { + + @DockerComposeTest(composeFile = "activemq-compose.yaml", image = TestImage.ACTIVE_MQ_CLASSIC) + void runCreatesConnectionDetails(AmqpConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUsername()).isEqualTo("root"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + Address address = connectionDetails.getAddress(); + assertThat(address.host()).isNotNull(); + assertThat(address.port()).isGreaterThan(0); + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/ArtemisAmqpDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/ArtemisAmqpDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..4f8129bb3d51 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/ArtemisAmqpDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails.Address; +import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.testsupport.container.TestImage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ArtemisAmqpDockerComposeConnectionDetailsFactory}. + * + * @author Stephane Nicoll + */ +class ArtemisAmqpDockerComposeConnectionDetailsFactoryIntegrationTests { + + @DockerComposeTest(composeFile = "artemis-compose.yaml", image = TestImage.ARTEMIS) + void runCreatesConnectionDetails(AmqpConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUsername()).isEqualTo("root"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + Address address = connectionDetails.getAddress(); + assertThat(address.host()).isNotNull(); + assertThat(address.port()).isGreaterThan(0); + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqAmqpDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqAmqpDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..9709dac09e6e --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqAmqpDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails.Address; +import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.testsupport.container.TestImage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link RabbitMqAmqpDockerComposeConnectionDetailsFactory}. + * + * @author Stephane Nicoll + */ +class RabbitMqAmqpDockerComposeConnectionDetailsFactoryIntegrationTests { + + @DockerComposeTest(composeFile = "rabbitmq-compose.yaml", image = TestImage.RABBITMQ) + void runCreatesConnectionDetails(AmqpConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + Address address = connectionDetails.getAddress(); + assertThat(address.host()).isNotNull(); + assertThat(address.port()).isGreaterThan(0); + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/ActiveMqAmqpContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/ActiveMqAmqpContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..0e835388d0a9 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/ActiveMqAmqpContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.AmqpAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.ActiveMQClassicContainer; +import org.springframework.boot.testsupport.container.TestImage; +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 ActiveMqAmqpContainerConnectionDetailsFactory}. + * + * @author Stephane Nicoll + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class ActiveMqAmqpContainerConnectionDetailsFactoryIntegrationTests { + + @Container + @ServiceConnection + static final ActiveMQClassicContainer container = TestImage.container(ActiveMQClassicContainer.class); + + @Autowired(required = false) + private AmqpConnectionDetails connectionDetails; + + @Autowired + private AmqpClient amqpClient; + + @Test + void connectionCanBeMadeToActiveMqContainer() throws Exception { + assertThat(this.connectionDetails).isNotNull(); + String address = UUID.randomUUID().toString(); + this.amqpClient.to(address).body("test message").send(); + Object message = this.amqpClient.from(address).receiveAndConvert().get(4, TimeUnit.MINUTES); + assertThat(message).isEqualTo("test message"); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(AmqpAutoConfiguration.class) + static class TestConfiguration { + + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/ArtemisAmqpContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/ArtemisAmqpContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..d5439c03cdea --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/ArtemisAmqpContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.Container.ExecResult; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.AmqpAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.ArtemisLegacyContainer; +import org.springframework.boot.testsupport.container.TestImage; +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 ArtemisAmqpContainerConnectionDetailsFactory}. + * + * @author Stephane Nicoll + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class ArtemisAmqpContainerConnectionDetailsFactoryIntegrationTests { + + @Container + @ServiceConnection + static final ArtemisLegacyContainer container = TestImage.container(ArtemisLegacyContainer.class); + + @Autowired(required = false) + private AmqpConnectionDetails connectionDetails; + + @Autowired + private AmqpClient amqpClient; + + @Test + void connectionCanBeMadeToArtemisContainer() throws Exception { + assertThat(this.connectionDetails).isNotNull(); + String address = createRandomQueue(); + this.amqpClient.to(address).body("test message").send(); + Object message = this.amqpClient.from(address).receiveAndConvert().get(4, TimeUnit.MINUTES); + assertThat(message).isEqualTo("test message"); + } + + private String createRandomQueue() { + String name = UUID.randomUUID().toString(); + createQueue(name); + return name; + } + + private void createQueue(String name) { + try { + ExecResult execResult = container.execInContainer("/var/lib/artemis-instance/bin/artemis", "queue", + "create", "--name=" + name, "--auto-create-address", "--anycast", "--silent", + "--user=" + container.getUser(), "--password=" + container.getPassword()); + if (execResult.getExitCode() != 0) { + throw new IllegalStateException("Failed to create queue: " + execResult); + } + } + catch (Exception ex) { + throw new IllegalStateException("Failed to create queue " + name, ex); + } + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(AmqpAutoConfiguration.class) + static class TestConfiguration { + + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqAmqpContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqAmqpContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..7f86b6d9ab7f --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqAmqpContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.AmqpAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.RabbitMqManagementContainer; +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 RabbitMqAmqpContainerConnectionDetailsFactory}. + * + * @author Stephane Nicoll + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class RabbitMqAmqpContainerConnectionDetailsFactoryIntegrationTests { + + @Container + @ServiceConnection + static final RabbitMqManagementContainer container = new RabbitMqManagementContainer(); + + @Autowired(required = false) + private AmqpConnectionDetails connectionDetails; + + @Autowired + private AmqpClient amqpClient; + + @Test + void connectionCanBeMadeToRabbitMqContainer() throws Exception { + assertThat(this.connectionDetails).isNotNull(); + String address = container.createRandomQueue(); + this.amqpClient.to(address).body("test message").send(); + Object message = this.amqpClient.from(address).receiveAndConvert().get(4, TimeUnit.MINUTES); + assertThat(message).isEqualTo("test message"); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(AmqpAutoConfiguration.class) + static class TestConfiguration { + + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml b/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml new file mode 100644 index 000000000000..b8a41480d7d6 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml @@ -0,0 +1,4 @@ + + + + diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/activemq-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/activemq-compose.yaml new file mode 100644 index 000000000000..5727d2d68e11 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/activemq-compose.yaml @@ -0,0 +1,9 @@ +services: + activemq: + image: '{imageName}' + ports: + - '61616' + - '5672' + environment: + ACTIVEMQ_CONNECTION_USER: 'root' + ACTIVEMQ_CONNECTION_PASSWORD: 'secret' diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/artemis-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/artemis-compose.yaml new file mode 100644 index 000000000000..8322326b8c76 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/artemis-compose.yaml @@ -0,0 +1,9 @@ +services: + artemis: + image: '{imageName}' + ports: + - '61616' + - '5672' + environment: + ARTEMIS_USER: 'root' + ARTEMIS_PASSWORD: 'secret' diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml new file mode 100644 index 000000000000..1951fba4bb08 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml @@ -0,0 +1,8 @@ +services: + rabbitmq: + image: '{imageName}' + environment: + - 'RABBITMQ_DEFAULT_USER=myuser' + - 'RABBITMQ_DEFAULT_PASS=secret' + ports: + - '5672' diff --git a/module/spring-boot-amqp/src/dockerTest/resources/spring.properties b/module/spring-boot-amqp/src/dockerTest/resources/spring.properties new file mode 100644 index 000000000000..47dff33f0bb5 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/spring.properties @@ -0,0 +1 @@ +spring.test.context.cache.maxSize=1 \ No newline at end of file diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAnnotationDrivenConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAnnotationDrivenConfiguration.java new file mode 100644 index 000000000000..10b328a23d1a --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAnnotationDrivenConfiguration.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.springframework.amqp.client.AmqpConnectionFactory; +import org.springframework.amqp.client.annotation.AmqpListener; +import org.springframework.amqp.client.config.AmqpDefaultConfiguration; +import org.springframework.amqp.client.config.EnableAmqp; +import org.springframework.amqp.client.config.MethodAmqpMessageListenerContainerFactory; +import org.springframework.amqp.client.listener.AmqpListenerErrorHandler; +import org.springframework.amqp.support.converter.MessageConverter; +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.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 using {@link AmqpListener}. + * + * @author Stephane Nicoll + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(EnableAmqp.class) +class AmqpAnnotationDrivenConfiguration { + + private static final String AMQP_LISTENER_CONTAINER_FACTORY_CONFIGURER_BEAN_NAME = "amqpMessageListenerContainerFactoryConfigurer"; + + private final AmqpProperties properties; + + private final ObjectProvider messageConverter; + + private final ObjectProvider listenerErrorHandler; + + AmqpAnnotationDrivenConfiguration(AmqpProperties properties, ObjectProvider messageConverter, + ObjectProvider listenerErrorHandler) { + this.properties = properties; + this.messageConverter = messageConverter; + this.listenerErrorHandler = listenerErrorHandler; + } + + @Bean(name = AMQP_LISTENER_CONTAINER_FACTORY_CONFIGURER_BEAN_NAME) + @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.PLATFORM) + AmqpMessageListenerContainerFactoryConfigurer amqpMessageListenerContainerFactoryConfigurer() { + return messageListenerConfigurer(); + } + + @Bean(name = AMQP_LISTENER_CONTAINER_FACTORY_CONFIGURER_BEAN_NAME) + @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.VIRTUAL) + AmqpMessageListenerContainerFactoryConfigurer amqpMessageListenerContainerFactoryConfigurerVirtualThreads() { + AmqpMessageListenerContainerFactoryConfigurer configurer = messageListenerConfigurer(); + configurer.setTaskExecutor(new VirtualThreadTaskExecutor("amqp-simple-")); + return configurer; + } + + @Bean(name = AmqpDefaultConfiguration.DEFAULT_AMQP_LISTENER_CONTAINER_FACTORY_BEAN_NAME) + @ConditionalOnMissingBean(name = AmqpDefaultConfiguration.DEFAULT_AMQP_LISTENER_CONTAINER_FACTORY_BEAN_NAME) + MethodAmqpMessageListenerContainerFactory amqpMessageListenerContainerFactory( + AmqpMessageListenerContainerFactoryConfigurer configurer, AmqpConnectionFactory connectionFactory) { + MethodAmqpMessageListenerContainerFactory factory = new MethodAmqpMessageListenerContainerFactory( + connectionFactory); + configurer.configure(factory); + return factory; + } + + private AmqpMessageListenerContainerFactoryConfigurer messageListenerConfigurer() { + AmqpMessageListenerContainerFactoryConfigurer configurer = new AmqpMessageListenerContainerFactoryConfigurer( + this.properties); + configurer.setMessageConverter(this.messageConverter.getIfUnique()); + configurer.setListenerErrorHandler(this.listenerErrorHandler.getIfUnique()); + return configurer; + } + + @Configuration(proxyBeanMethods = false) + @EnableAmqp + @ConditionalOnMissingBean(name = AmqpDefaultConfiguration.AMQP_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME) + static class EnableAmqpConfiguration { + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java new file mode 100644 index 000000000000..8d8b8aae5fa7 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java @@ -0,0 +1,134 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import java.util.stream.Stream; + +import org.apache.qpid.protonj2.client.Client; +import org.apache.qpid.protonj2.client.ClientOptions; +import org.apache.qpid.protonj2.client.ConnectionOptions; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.amqp.client.AmqpConnectionFactory; +import org.springframework.amqp.client.SingleAmqpConnectionFactory; +import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails.Address; +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.ConditionalOnSingleCandidate; +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.Configuration; +import org.springframework.context.annotation.Import; + +/** + * Auto-configuration for generic AMQP 1.0 support. + * + * @author Stephane Nicoll + * @since 4.2.0 + */ +@AutoConfiguration(afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration") +@ConditionalOnClass({ Client.class, AmqpConnectionFactory.class }) +@EnableConfigurationProperties(AmqpProperties.class) +@Import(AmqpAnnotationDrivenConfiguration.class) +public final class AmqpAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + static class AmqpConnectionFactoryConfiguration { + + @Bean + @ConditionalOnMissingBean + Client protonClient() { + return Client.create(new ClientOptions()); + } + + @Bean + @ConditionalOnMissingBean + AmqpConnectionDetails amqpConnectionDetails(AmqpProperties properties) { + return new PropertiesAmqpConnectionDetails(properties); + } + + @Bean + @ConditionalOnMissingBean + AmqpConnectionFactory amqpConnectionFactory(AmqpConnectionDetails connectionDetails, Client protonClient, + ObjectProvider connectionOptionsCustomizers) { + ConnectionOptions connectionOptions = createConnectionOptions(connectionDetails, + connectionOptionsCustomizers.orderedStream()); + return createAmqpConnectionFactory(connectionDetails, protonClient).setConnectionOptions(connectionOptions); + } + + private SingleAmqpConnectionFactory createAmqpConnectionFactory(AmqpConnectionDetails connectionDetails, + Client protonClient) { + SingleAmqpConnectionFactory connectionFactory = new SingleAmqpConnectionFactory(protonClient); + PropertyMapper map = PropertyMapper.get(); + Address address = connectionDetails.getAddress(); + map.from(address::host).to(connectionFactory::setHost); + map.from(address::port).to(connectionFactory::setPort); + return connectionFactory; + } + + private ConnectionOptions createConnectionOptions(AmqpConnectionDetails connectionDetails, + Stream customizers) { + ConnectionOptions connectionOptions = new ConnectionOptions(); + PropertyMapper map = PropertyMapper.get(); + map.from(connectionDetails::getUsername).to(connectionOptions::user); + map.from(connectionDetails::getPassword).to(connectionOptions::password); + customizers.forEach((customizer) -> customizer.customize(connectionOptions)); + return connectionOptions; + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(JsonMapper.class) + @ConditionalOnSingleCandidate(JsonMapper.class) + static class JacksonMessageConverterConfiguration { + + @Bean + @ConditionalOnMissingBean(MessageConverter.class) + JacksonJsonMessageConverter jacksonJsonMessageConverter(JsonMapper jsonMapper) { + return new JacksonJsonMessageConverter(jsonMapper); + } + + } + + @Configuration(proxyBeanMethods = false) + static class AmqpClientConfiguration { + + @Bean + @ConditionalOnMissingBean + AmqpClient amqpClient(AmqpConnectionFactory amqpConnectionFactory, AmqpProperties properties, + ObjectProvider messageConverter, ObjectProvider customizers) { + AmqpClient.Builder builder = AmqpClient.builder(amqpConnectionFactory); + AmqpProperties.Client client = properties.getClient(); + PropertyMapper map = PropertyMapper.get(); + map.from(client.getDefaultToAddress()).to(builder::defaultToAddress); + map.from(client.getCompletionTimeout()).to(builder::completionTimeout); + messageConverter.ifAvailable(builder::messageConverter); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder.build(); + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java new file mode 100644 index 000000000000..c29984516004 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.springframework.amqp.client.AmqpClient; + +/** + * Callback interface that can be used to customize the auto-configured + * {@link AmqpClient.Builder}. + * + * @author Stephane Nicoll + * @since 4.2.0 + */ +@FunctionalInterface +public interface AmqpClientCustomizer { + + /** + * Callback to customize a {@link AmqpClient.Builder} instance. + * @param amqpClientBuilder the client builder to customize + */ + void customize(AmqpClient.Builder amqpClientBuilder); + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java new file mode 100644 index 000000000000..f2e9f38309c6 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to an AMQP broker. + * + * @author Stephane Nicoll + * @since 4.2.0 + */ +public interface AmqpConnectionDetails extends ConnectionDetails { + + /** + * Return the {@link Address} of the broker. + * @return the address + */ + Address getAddress(); + + /** + * Login user to authenticate to the broker. + * @return the login user to authenticate to the broker or {@code null} + */ + default @Nullable String getUsername() { + return null; + } + + /** + * Password used to authenticate to the broker. + * @return the password to authenticate to the broker or {@code null} + */ + default @Nullable String getPassword() { + return null; + } + + /** + * An AMQP broker address. + * + * @param host the host + * @param port the port + */ + record Address(String host, int port) { + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpMessageListenerContainerFactoryConfigurer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpMessageListenerContainerFactoryConfigurer.java new file mode 100644 index 000000000000..da56dfd0b19b --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpMessageListenerContainerFactoryConfigurer.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import java.util.concurrent.Executor; + +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.client.config.AmqpMessageListenerContainerFactory; +import org.springframework.amqp.client.config.MethodAmqpMessageListenerContainerFactory; +import org.springframework.amqp.client.listener.AmqpListenerErrorHandler; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.amqp.autoconfigure.AmqpProperties.Listener; +import org.springframework.boot.context.properties.PropertyMapper; + +/** + * Configure {@link AmqpMessageListenerContainerFactory} with sensible defaults tuned + * using configuration properties. Handle additional settings exposed by + * {@link MethodAmqpMessageListenerContainerFactory} as well. + *

    + * Can be injected into application code and used to define a custom + * {@code AmqpMessageListenerContainerFactory} whose configuration is based upon that + * produced by auto-configuration. + * + * @author Stephane Nicoll + * @since 4.2.0 + */ +public class AmqpMessageListenerContainerFactoryConfigurer { + + private final AmqpProperties amqpProperties; + + private @Nullable Executor taskExecutor; + + private @Nullable MessageConverter messageConverter; + + private @Nullable AmqpListenerErrorHandler listenerErrorHandler; + + public AmqpMessageListenerContainerFactoryConfigurer(AmqpProperties amqpProperties) { + this.amqpProperties = amqpProperties; + } + + protected void setTaskExecutor(@Nullable Executor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + protected void setMessageConverter(@Nullable MessageConverter messageConverter) { + this.messageConverter = messageConverter; + } + + protected void setListenerErrorHandler(@Nullable AmqpListenerErrorHandler listenerErrorHandler) { + this.listenerErrorHandler = listenerErrorHandler; + } + + public void configure(AmqpMessageListenerContainerFactory factory) { + PropertyMapper map = PropertyMapper.get(); + Listener listener = this.amqpProperties.getListener(); + + map.from(listener::isAutoStartup).to(factory::setAutoStartup); + map.from(listener::getConcurrency).to(factory::setConcurrency); + map.from(listener::getInitialCredits).to(factory::setInitialCredits); + map.from(listener::getReceiveTimeout).to(factory::setReceiveTimeout); + map.from(listener::getGracefulShutdownPeriod).to(factory::setGracefulShutdownPeriod); + map.from(this.taskExecutor).to(factory::setTaskExecutor); + + if (factory instanceof MethodAmqpMessageListenerContainerFactory methodFactory) { + map.from(listener::isDefaultRequeueRejected).to(methodFactory::setDefaultRequeueRejected); + map.from(this.messageConverter).to(methodFactory::setMessageConverter); + map.from(this.listenerErrorHandler).to(methodFactory::setListenerErrorHandler); + } + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java new file mode 100644 index 000000000000..4ae5278c7f87 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java @@ -0,0 +1,213 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import java.time.Duration; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for AMQP 1.0 connectivity. + * + * @author Stephane Nicoll + * @since 4.2.0 + */ +@ConfigurationProperties("spring.amqp") +public class AmqpProperties { + + /** + * AMQP broker host. + */ + private String host = "localhost"; + + /** + * AMQP broker port. + */ + private int port = 5672; + + /** + * Login user to authenticate to the broker. + */ + private @Nullable String username; + + /** + * Password used to authenticate to the broker. + */ + private @Nullable String password; + + private final Client client = new Client(); + + private final Listener listener = new Listener(); + + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return this.port; + } + + public void setPort(int port) { + this.port = port; + } + + public @Nullable String getUsername() { + return this.username; + } + + public void setUsername(@Nullable String username) { + this.username = username; + } + + public @Nullable String getPassword() { + return this.password; + } + + public void setPassword(@Nullable String password) { + this.password = password; + } + + public Client getClient() { + return this.client; + } + + public Listener getListener() { + return this.listener; + } + + /** + * Client-level settings. + */ + public static class Client { + + /** + * Default destination address for send operations when none is specified. + */ + private @Nullable String defaultToAddress; + + /** + * Maximum time to wait for request operations to complete. + */ + private Duration completionTimeout = Duration.ofSeconds(60); + + public @Nullable String getDefaultToAddress() { + return this.defaultToAddress; + } + + public void setDefaultToAddress(@Nullable String defaultToAddress) { + this.defaultToAddress = defaultToAddress; + } + + public Duration getCompletionTimeout() { + return this.completionTimeout; + } + + public void setCompletionTimeout(Duration completionTimeout) { + this.completionTimeout = completionTimeout; + } + + } + + public static class Listener { + + /** + * Whether to start the container automatically on startup. + */ + private boolean autoStartup = true; + + /** + * Number of consumers. + */ + private int concurrency = 1; + + /** + * Initial number of credits to grant to the AMQP receiver. + */ + private int initialCredits = 100; + + /** + * Timeout for receive operations. + */ + private Duration receiveTimeout = Duration.ofSeconds(1); + + /** + * How long to wait for all consumers to shut down successfully. + */ + private Duration gracefulShutdownPeriod = Duration.ofSeconds(30); + + /** + * Whether to requeue messages in case of error. + */ + private boolean defaultRequeueRejected = true; + + public boolean isAutoStartup() { + return this.autoStartup; + } + + public void setAutoStartup(boolean autoStartup) { + this.autoStartup = autoStartup; + } + + public int getConcurrency() { + return this.concurrency; + } + + public void setConcurrency(int concurrency) { + this.concurrency = concurrency; + } + + public int getInitialCredits() { + return this.initialCredits; + } + + public void setInitialCredits(int initialCredits) { + this.initialCredits = initialCredits; + } + + public Duration getReceiveTimeout() { + return this.receiveTimeout; + } + + public void setReceiveTimeout(Duration receiveTimeout) { + this.receiveTimeout = receiveTimeout; + } + + public Duration getGracefulShutdownPeriod() { + return this.gracefulShutdownPeriod; + } + + public void setGracefulShutdownPeriod(Duration gracefulShutdownPeriod) { + this.gracefulShutdownPeriod = gracefulShutdownPeriod; + } + + public boolean isDefaultRequeueRejected() { + return this.defaultRequeueRejected; + } + + public void setDefaultRequeueRejected(boolean defaultRequeueRejected) { + this.defaultRequeueRejected = defaultRequeueRejected; + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java new file mode 100644 index 000000000000..1ee95b94a643 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.apache.qpid.protonj2.client.ConnectionOptions; + +import org.springframework.amqp.client.AmqpConnectionFactory; + +/** + * Callback interface for customizing {@link ConnectionOptions} on the auto-configured + * {@link AmqpConnectionFactory}. + * + * @author Stephane Nicoll + * @since 4.2.0 + */ +@FunctionalInterface +public interface ConnectionOptionsCustomizer { + + /** + * Customize the {@link ConnectionOptions}. + * @param connectionOptions the connection options to customize + */ + void customize(ConnectionOptions connectionOptions); + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java new file mode 100644 index 000000000000..13e47d047a5f --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.Nullable; + +/** + * Adapts {@link AmqpProperties} to {@link AmqpConnectionDetails}. + * + * @author Stephane Nicoll + */ +class PropertiesAmqpConnectionDetails implements AmqpConnectionDetails { + + private final AmqpProperties properties; + + PropertiesAmqpConnectionDetails(AmqpProperties properties) { + this.properties = properties; + } + + @Override + public Address getAddress() { + return new Address(this.properties.getHost(), this.properties.getPort()); + } + + @Override + public @Nullable String getUsername() { + return this.properties.getUsername(); + } + + @Override + public @Nullable String getPassword() { + return this.properties.getPassword(); + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java new file mode 100644 index 000000000000..fef2b40e8d3f --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 generic AMQP 1.0 support. + */ +@NullMarked +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/AbstractAmqpDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/AbstractAmqpDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..25af101b99a4 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/AbstractAmqpDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +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; + +/** + * Base {@link DockerComposeConnectionDetailsFactory} to create + * {@link AmqpConnectionDetails} from an AMQP 1.0-compliant service. + * + * @author Stephane Nicoll + */ +abstract class AbstractAmqpDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + protected AbstractAmqpDockerComposeConnectionDetailsFactory(String connectionName) { + super(connectionName); + } + + protected AbstractAmqpDockerComposeConnectionDetailsFactory(String[] connectionNames, + String... requiredClassNames) { + super(connectionNames, requiredClassNames); + } + + @Override + protected final @Nullable AmqpConnectionDetails getDockerComposeConnectionDetails( + DockerComposeConnectionSource source) { + RunningService runningService = source.getRunningService(); + AmqpEnvironment environment = getAmqpEnvironment(runningService); + return new AmqpDockerComposeConnectionDetails(runningService, environment); + } + + protected abstract AmqpEnvironment getAmqpEnvironment(RunningService service); + + static class AmqpDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements AmqpConnectionDetails { + + private static final int AMQP_PORT = 5672; + + private final Address address; + + private final @Nullable String username; + + private final @Nullable String password; + + AmqpDockerComposeConnectionDetails(RunningService runningService, AmqpEnvironment environment) { + super(runningService); + this.address = new Address(runningService.host(), runningService.ports().get(AMQP_PORT)); + this.username = environment.getUsername(); + this.password = environment.getPassword(); + } + + @Override + public Address getAddress() { + return this.address; + } + + @Override + public @Nullable String getUsername() { + return this.username; + } + + @Override + public @Nullable String getPassword() { + return this.password; + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/ActiveMqAmqpDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/ActiveMqAmqpDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..6e6951de8acd --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/ActiveMqAmqpDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link AmqpConnectionDetails} + * for an {@code activemq} service. + * + * @author Stephane Nicoll + * @author Eddú Meléndez + */ +class ActiveMqAmqpDockerComposeConnectionDetailsFactory extends AbstractAmqpDockerComposeConnectionDetailsFactory { + + ActiveMqAmqpDockerComposeConnectionDetailsFactory() { + super(new String[] { "apache/activemq-classic", "apache/activemq" }); + } + + @Override + protected AmqpEnvironment getAmqpEnvironment(RunningService service) { + Map env = service.env(); + return new AmqpEnvironment(env.get("ACTIVEMQ_CONNECTION_USER"), env.get("ACTIVEMQ_CONNECTION_PASSWORD")); + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/AmqpEnvironment.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/AmqpEnvironment.java new file mode 100644 index 000000000000..c803f4e14536 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/AmqpEnvironment.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.jspecify.annotations.Nullable; + +/** + * Generic AMQP environment details. + * + * @author Stephane Nicoll + */ +class AmqpEnvironment { + + private final @Nullable String username; + + private final @Nullable String password; + + AmqpEnvironment(@Nullable String username, @Nullable String password) { + this.username = username; + this.password = password; + } + + @Nullable String getUsername() { + return this.username; + } + + @Nullable String getPassword() { + return this.password; + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/ArtemisAmqpDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/ArtemisAmqpDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..55b17ad8ec4b --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/ArtemisAmqpDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link AmqpConnectionDetails} + * for a {@code artemis} service. + * + * @author Stephane Nicoll + */ +class ArtemisAmqpDockerComposeConnectionDetailsFactory extends AbstractAmqpDockerComposeConnectionDetailsFactory { + + ArtemisAmqpDockerComposeConnectionDetailsFactory() { + super(new String[] { "apache/activemq-artemis", "apache/artemis" }); + } + + @Override + protected AmqpEnvironment getAmqpEnvironment(RunningService service) { + Map env = service.env(); + return new AmqpEnvironment(env.get("ARTEMIS_USER"), env.get("ARTEMIS_PASSWORD")); + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqAmqpDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqAmqpDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..15f0e2d112d0 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqAmqpDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link AmqpConnectionDetails} + * for a {@code rabbitmq} service. + * + * @author Stephane Nicoll + */ +class RabbitMqAmqpDockerComposeConnectionDetailsFactory extends AbstractAmqpDockerComposeConnectionDetailsFactory { + + RabbitMqAmqpDockerComposeConnectionDetailsFactory() { + super("rabbitmq"); + } + + @Override + protected AmqpEnvironment getAmqpEnvironment(RunningService service) { + Map env = service.env(); + String username = env.getOrDefault("RABBITMQ_DEFAULT_USER", env.getOrDefault("RABBITMQ_USERNAME", "guest")); + String password = env.getOrDefault("RABBITMQ_DEFAULT_PASS", env.getOrDefault("RABBITMQ_PASSWORD", "guest")); + return new AmqpEnvironment(username, password); + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java new file mode 100644 index 000000000000..6408a2ff0030 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 generic AMQP service connections. + */ +@NullMarked +package org.springframework.boot.amqp.docker.compose; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/AbstractAmqpContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/AbstractAmqpContainerConnectionDetailsFactory.java new file mode 100644 index 000000000000..eaaa63764de4 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/AbstractAmqpContainerConnectionDetailsFactory.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +import org.testcontainers.containers.Container; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; + +/** + * Base {@link ContainerConnectionDetailsFactory} to create {@link AmqpConnectionDetails} + * from a {@link ServiceConnection @ServiceConnection}-annotated container that is + * compliant with AMQP 1.0. + * + * @param the container type + * @author Stephane Nicoll + */ +abstract class AbstractAmqpContainerConnectionDetailsFactory> + extends ContainerConnectionDetailsFactory { + + protected static class AmqpContainerConnectionDetails> extends ContainerConnectionDetails + implements AmqpConnectionDetails { + + private static final int AMQP_PORT = 5672; + + protected AmqpContainerConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + @Override + public Address getAddress() { + return new Address(getContainer().getHost(), getContainer().getMappedPort(AMQP_PORT)); + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/ActiveMqAmqpContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/ActiveMqAmqpContainerConnectionDetailsFactory.java new file mode 100644 index 000000000000..48ebb52eb83a --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/ActiveMqAmqpContainerConnectionDetailsFactory.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +import org.jspecify.annotations.Nullable; +import org.testcontainers.activemq.ActiveMQContainer; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +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 AmqpConnectionDetails} from + * a {@link ServiceConnection @ServiceConnection}-annotated {@link ActiveMQContainer}. + * + * @author Stephane Nicoll + */ +class ActiveMqAmqpContainerConnectionDetailsFactory + extends AbstractAmqpContainerConnectionDetailsFactory { + + @Override + protected @Nullable AmqpConnectionDetails getContainerConnectionDetails( + ContainerConnectionSource source) { + return new ActiveMqAmqpContainerConnectionDetails(source); + } + + static class ActiveMqAmqpContainerConnectionDetails extends AmqpContainerConnectionDetails { + + ActiveMqAmqpContainerConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + @Override + public @Nullable String getUsername() { + return getContainer().getUser(); + } + + @Override + public @Nullable String getPassword() { + return getContainer().getPassword(); + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/ArtemisAmqpContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/ArtemisAmqpContainerConnectionDetailsFactory.java new file mode 100644 index 000000000000..15f6831a59e5 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/ArtemisAmqpContainerConnectionDetailsFactory.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +import org.jspecify.annotations.Nullable; +import org.testcontainers.activemq.ArtemisContainer; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +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 AmqpConnectionDetails} from + * a {@link ServiceConnection @ServiceConnection}-annotated {@link ArtemisContainer}. + * + * @author Stephane Nicoll + */ +class ArtemisAmqpContainerConnectionDetailsFactory + extends AbstractAmqpContainerConnectionDetailsFactory { + + @Override + protected @Nullable AmqpConnectionDetails getContainerConnectionDetails( + ContainerConnectionSource source) { + return new ArtemisAmqpContainerConnectionDetails(source); + } + + static class ArtemisAmqpContainerConnectionDetails extends AmqpContainerConnectionDetails { + + ArtemisAmqpContainerConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + @Override + public @Nullable String getUsername() { + return getContainer().getUser(); + } + + @Override + public @Nullable String getPassword() { + return getContainer().getPassword(); + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqAmqpContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqAmqpContainerConnectionDetailsFactory.java new file mode 100644 index 000000000000..c3abbd1eb2be --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqAmqpContainerConnectionDetailsFactory.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +import java.net.URI; + +import org.testcontainers.rabbitmq.RabbitMQContainer; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +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 AmqpConnectionDetails} from + * a {@link ServiceConnection @ServiceConnection}-annotated {@link RabbitMQContainer}. + * + * @author Stephane Nicoll + */ +class RabbitMqAmqpContainerConnectionDetailsFactory + extends AbstractAmqpContainerConnectionDetailsFactory { + + @Override + protected AmqpConnectionDetails getContainerConnectionDetails(ContainerConnectionSource source) { + return new RabbitMqAmqpContainerConnectionDetails(source); + } + + /** + * {@link AmqpConnectionDetails} for RabbitMQ backed by a + * {@link ContainerConnectionSource}. + */ + static final class RabbitMqAmqpContainerConnectionDetails + extends AmqpContainerConnectionDetails { + + private RabbitMqAmqpContainerConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + @Override + public Address getAddress() { + URI uri = URI.create(getContainer().getAmqpUrl()); + return new Address(uri.getHost(), uri.getPort()); + } + + @Override + public String getUsername() { + return getContainer().getAdminUsername(); + } + + @Override + public String getPassword() { + return getContainer().getAdminPassword(); + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java new file mode 100644 index 000000000000..398ec731847a --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 generic AMQP service connections. + */ +@NullMarked +package org.springframework.boot.amqp.testcontainers; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories b/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..38a7d1dda308 --- /dev/null +++ b/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories @@ -0,0 +1,8 @@ +# Connection Details Factories +org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ +org.springframework.boot.amqp.docker.compose.ActiveMqAmqpDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.amqp.docker.compose.ArtemisAmqpDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.amqp.docker.compose.RabbitMqAmqpDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.amqp.testcontainers.ActiveMqAmqpContainerConnectionDetailsFactory,\ +org.springframework.boot.amqp.testcontainers.ArtemisAmqpContainerConnectionDetailsFactory,\ +org.springframework.boot.amqp.testcontainers.RabbitMqAmqpContainerConnectionDetailsFactory diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000000..1bbaad209b67 --- /dev/null +++ b/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springframework.boot.amqp.autoconfigure.AmqpAutoConfiguration diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java new file mode 100644 index 000000000000..a02837b4a4ee --- /dev/null +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java @@ -0,0 +1,381 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import java.time.Duration; +import java.util.Objects; +import java.util.function.Consumer; + +import org.apache.qpid.protonj2.client.Client; +import org.apache.qpid.protonj2.client.ConnectionOptions; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.amqp.client.AmqpConnectionFactory; +import org.springframework.amqp.client.annotation.AmqpListener; +import org.springframework.amqp.client.config.AmqpDefaultConfiguration; +import org.springframework.amqp.client.config.AmqpMessageListenerContainerFactory; +import org.springframework.amqp.client.config.MethodAmqpListenerEndpoint; +import org.springframework.amqp.client.config.MethodAmqpMessageListenerContainerFactory; +import org.springframework.amqp.client.listener.AmqpMessageListenerContainer; +import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails.Address; +import org.springframework.boot.amqp.autoconfigure.AmqpProperties.Listener; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; +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.util.ReflectionUtils; + +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; + +/** + * Tests for {@link AmqpAutoConfiguration}. + * + * @author Stephane Nicoll + */ +class AmqpAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AmqpAutoConfiguration.class)); + + @Test + void autoConfigurationConfiguresConnectionFactoryWithDefaultSettings() { + this.contextRunner.run((context) -> { + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("host", "localhost"); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("port", 5672); + assertThat(connectionFactory) + .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) + .satisfies((connectOptions) -> { + assertThat(connectOptions).hasFieldOrPropertyWithValue("user", null); + assertThat(connectOptions).hasFieldOrPropertyWithValue("password", null); + }); + }); + } + + @Test + void autoConfigurationConfiguresConnectionFactoryWithOverrides() { + this.contextRunner + .withPropertyValues("spring.amqp.host=amqp.example.com", "spring.amqp.port=1234", + "spring.amqp.username=user", "spring.amqp.password=secret") + .run((context) -> { + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("host", "amqp.example.com"); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("port", 1234); + assertThat(connectionFactory) + .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) + .satisfies((connectOptions) -> { + assertThat(connectOptions).hasFieldOrPropertyWithValue("user", "user"); + assertThat(connectOptions).hasFieldOrPropertyWithValue("password", "secret"); + }); + }); + } + + @Test + void autoConfigurationUsesAmqpConnectionDetailsForConnectionFactory() { + AmqpConnectionDetails connectionDetails = mock(); + given(connectionDetails.getAddress()).willReturn(new Address("details.example.com", 9999)); + given(connectionDetails.getUsername()).willReturn("from-details"); + given(connectionDetails.getPassword()).willReturn("details-secret"); + this.contextRunner.withBean(AmqpConnectionDetails.class, () -> connectionDetails) + .withPropertyValues("spring.amqp.host=ignored.example.com", "spring.amqp.port=1111", + "spring.amqp.username=ignored", "spring.amqp.password=ignored") + .run((context) -> { + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("host", "details.example.com"); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("port", 9999); + assertThat(connectionFactory) + .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) + .satisfies((connectOptions) -> { + assertThat(connectOptions).hasFieldOrPropertyWithValue("user", "from-details"); + assertThat(connectOptions).hasFieldOrPropertyWithValue("password", "details-secret"); + }); + }); + } + + @Test + void autoConfigurationAppliesConnectionOptionsCustomizer() { + this.contextRunner + .withBean(ConnectionOptionsCustomizer.class, () -> (connectOptions) -> connectOptions.user("custom-user")) + .withPropertyValues("spring.amqp.username=user") + .run((context) -> { + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + assertThat(connectionFactory) + .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) + .satisfies((connectOptions) -> assertThat(connectOptions).hasFieldOrPropertyWithValue("user", + "custom-user")); + }); + } + + @Test + void autoConfigurationAppliesConnectionOptionsCustomizersInOrder() { + this.contextRunner.withUserConfiguration(ConnectionOptionsCustomizersConfiguration.class).run((context) -> { + ConnectionOptionsCustomizer first = context.getBean("first", ConnectionOptionsCustomizer.class); + ConnectionOptionsCustomizer second = context.getBean("second", ConnectionOptionsCustomizer.class); + InOrder ordered = inOrder(first, second); + ordered.verify(first).customize(any()); + ordered.verify(second).customize(any()); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesProtonClient() { + Client protonClient = mock(); + this.contextRunner.withBean(Client.class, () -> protonClient).run((context) -> { + assertThat(context).hasSingleBean(Client.class); + assertThat(context.getBean(Client.class)).isSameAs(protonClient); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesAmqpConnectionDetails() { + AmqpConnectionDetails connectionDetails = mock(); + given(connectionDetails.getAddress()).willReturn(new Address("details.example.com", 9999)); + this.contextRunner.withBean(AmqpConnectionDetails.class, () -> connectionDetails).run((context) -> { + assertThat(context).hasSingleBean(AmqpConnectionDetails.class); + assertThat(context.getBean(AmqpConnectionDetails.class)).isSameAs(connectionDetails); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesAmqpConnectionFactory() { + AmqpConnectionFactory amqpConnectionFactory = mock(); + this.contextRunner.withBean(AmqpConnectionFactory.class, () -> amqpConnectionFactory).run((context) -> { + assertThat(context).hasSingleBean(AmqpConnectionFactory.class); + assertThat(context.getBean(AmqpConnectionFactory.class)).isSameAs(amqpConnectionFactory); + }); + } + + @Test + void autoConfigurationConfiguresAmqpClientWithOverrides() { + this.contextRunner + .withPropertyValues("spring.amqp.client.default-to-address=/queues/default_queue", + "spring.amqp.client.completion-timeout=20s") + .run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient).hasFieldOrPropertyWithValue("defaultToAddress", "/queues/default_queue"); + assertThat(amqpClient).hasFieldOrPropertyWithValue("completionTimeout", Duration.ofSeconds(20)); + }); + } + + @Test + void autoConfigurationConfiguresAmqpClientWithJacksonMessageConverter() { + this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)).run((context) -> { + assertThat(context).hasSingleBean(JsonMapper.class); + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient) + .extracting("messageConverter", InstanceOfAssertFactories.type(MessageConverter.class)) + .satisfies((messageConverter) -> assertThat(messageConverter) + .isInstanceOf(JacksonJsonMessageConverter.class) + .hasFieldOrPropertyWithValue("objectMapper", context.getBean(JsonMapper.class))); + }); + } + + @Test + void autoConfigurationConfiguresAmqpClientWithMessageConverter() { + MessageConverter messageConverter = mock(MessageConverter.class); + this.contextRunner.withBean(MessageConverter.class, () -> messageConverter).run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient).hasFieldOrPropertyWithValue("messageConverter", messageConverter); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesAmqpClient() { + AmqpClient amqpClient = mock(); + this.contextRunner.withBean(AmqpClient.class, () -> amqpClient).run((context) -> { + assertThat(context).hasSingleBean(AmqpClient.class); + assertThat(context.getBean(AmqpClient.class)).isSameAs(amqpClient); + }); + } + + @Test + void autoConfigurationUsesUserMessageConverterWhenDefinedAlongsideJackson() { + MessageConverter messageConverter = mock(MessageConverter.class); + this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) + .withBean(MessageConverter.class, () -> messageConverter) + .run((context) -> { + assertThat(context).doesNotHaveBean(JacksonJsonMessageConverter.class); + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient).hasFieldOrPropertyWithValue("messageConverter", messageConverter); + }); + } + + @Test + void autoConfigurationAppliesAmqpClientCustomizer() { + this.contextRunner + .withBean(AmqpClientCustomizer.class, + () -> (client) -> client.defaultToAddress("/queues/custom_default_queue")) + .withPropertyValues("spring.amqp.client.default-to-address=/queues/default_queue") + .run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient).hasFieldOrPropertyWithValue("defaultToAddress", "/queues/custom_default_queue"); + }); + } + + @Test + void autoConfigurationAppliesAmqpClientCustomizersInOrder() { + this.contextRunner.withUserConfiguration(AmqpClientCustomizersConfiguration.class).run((context) -> { + AmqpClientCustomizer first = context.getBean("first", AmqpClientCustomizer.class); + AmqpClientCustomizer second = context.getBean("second", AmqpClientCustomizer.class); + InOrder ordered = inOrder(first, second); + ordered.verify(first).customize(any()); + ordered.verify(second).customize(any()); + }); + } + + @Test + void autoConfigureCreatesDefaultContainerFactory() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(AmqpMessageListenerContainerFactory.class) + .hasSingleBean(MethodAmqpMessageListenerContainerFactory.class) + .hasBean(AmqpDefaultConfiguration.DEFAULT_AMQP_LISTENER_CONTAINER_FACTORY_BEAN_NAME)); + } + + @Test + void autoConfigureConfiguresContainerFactoryWithDefaultSettings() { + this.contextRunner.run((context) -> { + AmqpMessageListenerContainerFactory factory = context.getBean(AmqpMessageListenerContainerFactory.class); + TestAmqpListenerEndpoint endpoint = new TestAmqpListenerEndpoint(); + AmqpMessageListenerContainer container = factory.createContainer(endpoint); + Listener listener = new AmqpProperties().getListener(); + assertThat(container).satisfies(listenerContainerConfiguration(listener.isAutoStartup(), + listener.getConcurrency(), listener.getInitialCredits(), listener.getReceiveTimeout(), + listener.getGracefulShutdownPeriod())); + assertThat(endpoint.isDefaultRequeueRejected()).isEqualTo(listener.isDefaultRequeueRejected()); + }); + } + + @Test + void autoConfigureConfiguresContainerFactoryWithOverrides() { + this.contextRunner + .withPropertyValues("spring.amqp.listener.auto-startup=false", "spring.amqp.listener.concurrency=3", + "spring.amqp.listener.initial-credits=500", "spring.amqp.listener.receive-timeout=25s", + "spring.amqp.listener.graceful-shutdown-period=35s", + "spring.amqp.listener.default-requeue-rejected=false") + .run((context) -> { + AmqpMessageListenerContainerFactory factory = context + .getBean(AmqpMessageListenerContainerFactory.class); + TestAmqpListenerEndpoint endpoint = new TestAmqpListenerEndpoint(); + AmqpMessageListenerContainer container = factory.createContainer(endpoint); + assertThat(container).satisfies( + listenerContainerConfiguration(false, 3, 500, Duration.ofSeconds(25), Duration.ofSeconds(35))); + assertThat(endpoint.isDefaultRequeueRejected()).isEqualTo(false); + }); + } + + @Test + void autoConfigurationCreatesContainerUsingAmqpMessageListenerContainerFactory() { + AmqpMessageListenerContainerFactory factory = mock(AmqpMessageListenerContainerFactory.class); + this.contextRunner + .withBean(AmqpDefaultConfiguration.DEFAULT_AMQP_LISTENER_CONTAINER_FACTORY_BEAN_NAME, + AmqpMessageListenerContainerFactory.class, () -> factory) + .withUserConfiguration(TestListener.class) + .run((context) -> then(factory).should().createContainer(any())); + } + + private Consumer listenerContainerConfiguration(boolean autoStartup, int concurrency, + int initialCredit, Duration receiveTimeout, Duration gracefulShutdownPeriod) { + return (container) -> assertThat(container).hasFieldOrPropertyWithValue("autoStartup", autoStartup) + .hasFieldOrPropertyWithValue("consumersPerQueue", concurrency) + .hasFieldOrPropertyWithValue("initialCredits", initialCredit) + .hasFieldOrPropertyWithValue("receiveTimeout", receiveTimeout) + .hasFieldOrPropertyWithValue("gracefulShutdownPeriod", gracefulShutdownPeriod); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesContainerFactory() { + AmqpMessageListenerContainerFactory containerFactory = mock(AmqpMessageListenerContainerFactory.class); + this.contextRunner + .withBean(AmqpDefaultConfiguration.DEFAULT_AMQP_LISTENER_CONTAINER_FACTORY_BEAN_NAME, + AmqpMessageListenerContainerFactory.class, () -> containerFactory) + .run((context) -> { + assertThat(context).hasSingleBean(AmqpMessageListenerContainerFactory.class) + .doesNotHaveBean(MethodAmqpMessageListenerContainerFactory.class); + assertThat(context.getBean(AmqpMessageListenerContainerFactory.class)).isEqualTo(containerFactory); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionOptionsCustomizersConfiguration { + + @Bean + @Order(2) + ConnectionOptionsCustomizer second() { + return mock(); + } + + @Bean + @Order(1) + ConnectionOptionsCustomizer first() { + return mock(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class AmqpClientCustomizersConfiguration { + + @Bean + @Order(2) + AmqpClientCustomizer second() { + return mock(); + } + + @Bean + @Order(1) + AmqpClientCustomizer first() { + return mock(); + } + + } + + static class TestListener { + + @AmqpListener + void listen(String in) { + + } + + } + + static class TestAmqpListenerEndpoint extends MethodAmqpListenerEndpoint { + + TestAmqpListenerEndpoint() { + super(TestListener.class, + Objects.requireNonNull(ReflectionUtils.findMethod(TestListener.class, "listen", String.class))); + } + + @Override + protected boolean isDefaultRequeueRejected() { + return super.isDefaultRequeueRejected(); + } + + } + +} diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpMessageListenerContainerFactoryConfigurerTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpMessageListenerContainerFactoryConfigurerTests.java new file mode 100644 index 000000000000..7219e6cf3716 --- /dev/null +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpMessageListenerContainerFactoryConfigurerTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import java.time.Duration; +import java.util.concurrent.Executor; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.client.config.AmqpMessageListenerContainerFactory; +import org.springframework.amqp.client.config.MethodAmqpMessageListenerContainerFactory; +import org.springframework.amqp.client.listener.AmqpListenerErrorHandler; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.amqp.autoconfigure.AmqpProperties.Listener; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link AmqpMessageListenerContainerFactoryConfigurer}. + * + * @author Stephane Nicoll + */ +class AmqpMessageListenerContainerFactoryConfigurerTests { + + @Test + void configureAppliesListenerPropertiesToAmqpMessageListenerContainerFactory() { + AmqpProperties properties = new AmqpProperties(); + Listener listener = properties.getListener(); + listener.setAutoStartup(false); + listener.setConcurrency(3); + listener.setInitialCredits(50); + listener.setReceiveTimeout(Duration.ofMillis(500)); + listener.setGracefulShutdownPeriod(Duration.ofSeconds(60)); + Executor taskExecutor = mock(Executor.class); + AmqpMessageListenerContainerFactoryConfigurer configurer = new AmqpMessageListenerContainerFactoryConfigurer( + properties); + configurer.setTaskExecutor(taskExecutor); + + AmqpMessageListenerContainerFactory factory = mock(AmqpMessageListenerContainerFactory.class); + configurer.configure(factory); + then(factory).should().setAutoStartup(false); + then(factory).should().setConcurrency(3); + then(factory).should().setInitialCredits(50); + then(factory).should().setReceiveTimeout(Duration.ofMillis(500)); + then(factory).should().setGracefulShutdownPeriod(Duration.ofSeconds(60)); + then(factory).should().setTaskExecutor(taskExecutor); + } + + @Test + void configureAppliesMethodAmqpMessageListenerContainerFactoryProperties() { + AmqpProperties properties = new AmqpProperties(); + properties.getListener().setDefaultRequeueRejected(false); + + AmqpMessageListenerContainerFactoryConfigurer configurer = new AmqpMessageListenerContainerFactoryConfigurer( + properties); + MessageConverter messageConverter = mock(MessageConverter.class); + AmqpListenerErrorHandler errorHandler = mock(AmqpListenerErrorHandler.class); + configurer.setMessageConverter(messageConverter); + configurer.setListenerErrorHandler(errorHandler); + + MethodAmqpMessageListenerContainerFactory factory = mock(MethodAmqpMessageListenerContainerFactory.class); + configurer.configure(factory); + + then(factory).should().setDefaultRequeueRejected(false); + then(factory).should().setMessageConverter(messageConverter); + then(factory).should().setListenerErrorHandler(errorHandler); + } + + @Test + void configureSkipsOptionalDependenciesWhenNotSet() { + AmqpProperties properties = new AmqpProperties(); + AmqpMessageListenerContainerFactoryConfigurer configurer = new AmqpMessageListenerContainerFactoryConfigurer( + properties); + + MethodAmqpMessageListenerContainerFactory factory = mock(MethodAmqpMessageListenerContainerFactory.class); + configurer.configure(factory); + + then(factory).should(never()).setTaskExecutor(any()); + then(factory).should(never()).setMessageConverter(any()); + then(factory).should(never()).setListenerErrorHandler(any()); + then(factory).should().setDefaultRequeueRejected(properties.getListener().isDefaultRequeueRejected()); + } + +} diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/ActiveMqAmqpDockerComposeConnectionDetailsFactoryTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/ActiveMqAmqpDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 000000000000..7748956c7941 --- /dev/null +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/ActiveMqAmqpDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.RunningService; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ActiveMqAmqpDockerComposeConnectionDetailsFactory}. + * + * @author Stephane Nicoll + */ +class ActiveMqAmqpDockerComposeConnectionDetailsFactoryTests { + + @Test + void getUserWhenHasNoActiveMqUser() { + AmqpEnvironment environment = getAmqpEnvironment(Collections.emptyMap()); + assertThat(environment.getUsername()).isNull(); + } + + @Test + void getUserWhenHasActiveMqUser() { + AmqpEnvironment environment = getAmqpEnvironment(Map.of("ACTIVEMQ_CONNECTION_USER", "me")); + assertThat(environment.getUsername()).isEqualTo("me"); + } + + @Test + void getPasswordWhenHasNoActiveMqPassword() { + AmqpEnvironment environment = getAmqpEnvironment(Collections.emptyMap()); + assertThat(environment.getPassword()).isNull(); + } + + @Test + void getPasswordWhenHasActiveMqPassword() { + AmqpEnvironment environment = getAmqpEnvironment(Map.of("ACTIVEMQ_CONNECTION_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + private AmqpEnvironment getAmqpEnvironment(Map env) { + RunningService runningService = mock(RunningService.class); + given(runningService.env()).willReturn(env); + return new ActiveMqAmqpDockerComposeConnectionDetailsFactory().getAmqpEnvironment(runningService); + } + +} diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/ArtemisAmqpDockerComposeConnectionDetailsFactoryTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/ArtemisAmqpDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 000000000000..be4903b824fb --- /dev/null +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/ArtemisAmqpDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.RunningService; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ArtemisAmqpDockerComposeConnectionDetailsFactory}. + * + * @author Stephane Nicoll + */ +class ArtemisAmqpDockerComposeConnectionDetailsFactoryTests { + + @Test + void getUserWhenHasNoActiveMqUser() { + AmqpEnvironment environment = getAmqpEnvironment(Collections.emptyMap()); + assertThat(environment.getUsername()).isNull(); + } + + @Test + void getUserWhenHasActiveMqUser() { + AmqpEnvironment environment = getAmqpEnvironment(Map.of("ARTEMIS_USER", "me")); + assertThat(environment.getUsername()).isEqualTo("me"); + } + + @Test + void getPasswordWhenHasNoActiveMqPassword() { + AmqpEnvironment environment = getAmqpEnvironment(Collections.emptyMap()); + assertThat(environment.getPassword()).isNull(); + } + + @Test + void getPasswordWhenHasActiveMqPassword() { + AmqpEnvironment environment = getAmqpEnvironment(Map.of("ARTEMIS_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + private AmqpEnvironment getAmqpEnvironment(Map env) { + RunningService runningService = mock(RunningService.class); + given(runningService.env()).willReturn(env); + return new ArtemisAmqpDockerComposeConnectionDetailsFactory().getAmqpEnvironment(runningService); + } + +} diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqAmqpDockerComposeConnectionDetailsFactoryTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqAmqpDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 000000000000..d7d4061ba2d3 --- /dev/null +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqAmqpDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.RunningService; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RabbitMqAmqpDockerComposeConnectionDetailsFactory}. + * + * @author Stephane Nicoll + */ +class RabbitMqAmqpDockerComposeConnectionDetailsFactoryTests { + + @Test + void getUsernameWhenNoRabbitMqDefaultUser() { + AmqpEnvironment environment = getAmqpEnvironment(Collections.emptyMap()); + assertThat(environment.getUsername()).isEqualTo("guest"); + } + + @Test + void getUsernameWhenHasRabbitMqDefaultUser() { + AmqpEnvironment environment = getAmqpEnvironment(Map.of("RABBITMQ_DEFAULT_USER", "me")); + assertThat(environment.getUsername()).isEqualTo("me"); + } + + @Test + void getUsernameWhenHasRabbitMqUsername() { + AmqpEnvironment environment = getAmqpEnvironment(Map.of("RABBITMQ_USERNAME", "me")); + assertThat(environment.getUsername()).isEqualTo("me"); + } + + @Test + void getPasswordWhenNoRabbitMqDefaultPass() { + AmqpEnvironment environment = getAmqpEnvironment(Collections.emptyMap()); + assertThat(environment.getPassword()).isEqualTo("guest"); + } + + @Test + void getPasswordWhenHasRabbitMqDefaultPass() { + AmqpEnvironment environment = getAmqpEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getPasswordWhenHasRabbitMqPassword() { + AmqpEnvironment environment = getAmqpEnvironment(Map.of("RABBITMQ_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + private AmqpEnvironment getAmqpEnvironment(Map env) { + RunningService runningService = mock(RunningService.class); + given(runningService.env()).willReturn(env); + return new RabbitMqAmqpDockerComposeConnectionDetailsFactory().getAmqpEnvironment(runningService); + } + +} diff --git a/module/spring-boot-autoconfigure-classic-modules/build.gradle b/module/spring-boot-autoconfigure-classic-modules/build.gradle index 88fdf6e2a8c3..355de273613f 100644 --- a/module/spring-boot-autoconfigure-classic-modules/build.gradle +++ b/module/spring-boot-autoconfigure-classic-modules/build.gradle @@ -28,6 +28,9 @@ dependencies { api(project(":module:spring-boot-activemq")) { transitive = false } + api(project(":module:spring-boot-amqp")) { + transitive = false + } api(project(":module:spring-boot-artemis")) { transitive = false } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index fc8e69bab79a..525f0501a5a9 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2123,6 +2123,7 @@ bom { "spring-boot-activemq", "spring-boot-actuator", "spring-boot-actuator-autoconfigure", + "spring-boot-amqp", "spring-boot-artemis", "spring-boot-autoconfigure", "spring-boot-autoconfigure-classic", @@ -2244,6 +2245,8 @@ bom { "spring-boot-starter-activemq-test", "spring-boot-starter-actuator", "spring-boot-starter-actuator-test", + "spring-boot-starter-amqp", + "spring-boot-starter-amqp-test", "spring-boot-starter-artemis", "spring-boot-starter-artemis-test", "spring-boot-starter-aspectj", @@ -2362,8 +2365,8 @@ bom { "spring-boot-starter-quartz-test", "spring-boot-starter-r2dbc", "spring-boot-starter-r2dbc-test", - "spring-boot-starter-rabbitmqmq", - "spring-boot-starter-rabbitmqmq-test", + "spring-boot-starter-rabbitmq", + "spring-boot-starter-rabbitmq-test", "spring-boot-starter-reactor-netty", "spring-boot-starter-restclient", "spring-boot-starter-restclient-test", diff --git a/settings.gradle b/settings.gradle index bee3c7607724..269c5fccec1a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -80,6 +80,7 @@ include "core:spring-boot-testcontainers" include "module:spring-boot-activemq" include "module:spring-boot-actuator" include "module:spring-boot-actuator-autoconfigure" +include "module:spring-boot-amqp" include "module:spring-boot-artemis" include "module:spring-boot-autoconfigure-classic" include "module:spring-boot-autoconfigure-classic-modules" @@ -211,6 +212,8 @@ include "starter:spring-boot-starter-activemq" include "starter:spring-boot-starter-activemq-test" include "starter:spring-boot-starter-actuator" include "starter:spring-boot-starter-actuator-test" +include "starter:spring-boot-starter-amqp" +include "starter:spring-boot-starter-amqp-test" include "starter:spring-boot-starter-artemis" include "starter:spring-boot-starter-artemis-test" include "starter:spring-boot-starter-aspectj" @@ -401,6 +404,7 @@ include ":smoke-test:spring-boot-smoke-test-actuator-extension" include ":smoke-test:spring-boot-smoke-test-actuator-log4j2" include ":smoke-test:spring-boot-smoke-test-actuator-noweb" include ":smoke-test:spring-boot-smoke-test-actuator-ui" +include ":smoke-test:spring-boot-smoke-test-amqp" include ":smoke-test:spring-boot-smoke-test-ant" include ":smoke-test:spring-boot-smoke-test-artemis" include ":smoke-test:spring-boot-smoke-test-aspectj" diff --git a/smoke-test/spring-boot-smoke-test-amqp/build.gradle b/smoke-test/spring-boot-smoke-test-amqp/build.gradle new file mode 100644 index 000000000000..cadb8379f2ef --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp/build.gradle @@ -0,0 +1,41 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java" + id "org.springframework.boot.docker-test" +} + +description = "Spring Boot AMQP smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-amqp")) + + dockerTestImplementation(project(":core:spring-boot-testcontainers")) + dockerTestImplementation(project(":starter:spring-boot-starter-amqp-test")) + dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) + dockerTestImplementation("org.awaitility:awaitility") + dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") + dockerTestImplementation("org.testcontainers:testcontainers-rabbitmq") +} + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +tasks.named("compileDockerTestJava") { + options.nullability.checking = "tests" +} diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpApplicationTests.java b/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpApplicationTests.java new file mode 100644 index 000000000000..80813c374cca --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpApplicationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.BeforeAll; +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.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.RabbitMqManagementContainer; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Testcontainers(disabledWithoutDocker = true) +@ExtendWith(OutputCaptureExtension.class) +class SampleAmqpApplicationTests { + + @Container + @ServiceConnection + static final RabbitMqManagementContainer container = new RabbitMqManagementContainer(); + + @BeforeAll + static void setupBroker() { + container.createQueue("foo"); + } + + @Test + void sendSimpleMessage(@Autowired Sender sender, CapturedOutput output) { + sender.send("Test message"); + Awaitility.waitAtMost(Duration.ofMinutes(1)).untilAsserted(() -> assertThat(output).contains("Test message")); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Listener.java b/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Listener.java new file mode 100644 index 000000000000..bedbcd0b0cbb --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Listener.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.amqp.client.annotation.AmqpListener; +import org.springframework.stereotype.Component; + +@Component +class Listener { + + private static final Log logger = LogFactory.getLog(Listener.class); + + @AmqpListener(addresses = "/queues/foo") + void process(String foo) { + logger.info(foo); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpApplication.java b/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpApplication.java new file mode 100644 index 000000000000..dc32714fa253 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpApplication.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class SampleAmqpApplication { + + @Bean + public ApplicationRunner runner(Sender sender) { + return (args) -> sender.send("Hello"); + } + + public static void main(String[] args) { + SpringApplication.run(SampleAmqpApplication.class, args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Sender.java b/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Sender.java new file mode 100644 index 000000000000..3e6d0f1cdcd3 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/Sender.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 org.springframework.amqp.client.AmqpClient; +import org.springframework.stereotype.Component; + +@Component +class Sender { + + private final AmqpClient amqpClient; + + Sender(AmqpClient amqpClient) { + this.amqpClient = amqpClient; + } + + void send(String message) { + this.amqpClient.to("/queues/foo").body(message).send(); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/package-info.java b/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/package-info.java new file mode 100644 index 000000000000..bc9e41a079d7 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NullMarked +package smoketest.amqp; + +import org.jspecify.annotations.NullMarked; diff --git a/starter/spring-boot-starter-amqp-test/build.gradle b/starter/spring-boot-starter-amqp-test/build.gradle new file mode 100644 index 000000000000..6991f36fc910 --- /dev/null +++ b/starter/spring-boot-starter-amqp-test/build.gradle @@ -0,0 +1,26 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for testing generic AMQP 1.0 support" + +dependencies { + api(project(":starter:spring-boot-starter-amqp")) + api(project(":starter:spring-boot-starter-test")) +} diff --git a/starter/spring-boot-starter-amqp/build.gradle b/starter/spring-boot-starter-amqp/build.gradle new file mode 100644 index 000000000000..39de12fb1139 --- /dev/null +++ b/starter/spring-boot-starter-amqp/build.gradle @@ -0,0 +1,27 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for generic AMQP 1.0 support" + +dependencies { + api(project(":starter:spring-boot-starter")) + + api(project(":module:spring-boot-amqp")) +} diff --git a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.java b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.java new file mode 100644 index 000000000000..baea0dcddd5f --- /dev/null +++ b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.container; + +import java.util.UUID; + +import org.testcontainers.rabbitmq.RabbitMQContainer; + +/** + * A {@link RabbitMQContainer} with management plugin enabled. + * + * @author Stephane Nicoll + */ +public class RabbitMqManagementContainer extends RabbitMQContainer { + + public RabbitMqManagementContainer() { + super(TestImage.RABBITMQ.toString()); + } + + /** + * Create a queue with the given name. + * @param name the name of the queue + * @return the AMQP target of the queue + */ + public String createQueue(String name) { + try { + execInContainer("rabbitmqadmin", "queues", "declare", "--name", name); + return "/queues/" + name; + } + catch (Exception ex) { + throw new RuntimeException("Could not create queue", ex); + } + } + + /** + * Create a queue with a random, unique, name. + * @return the AMQP target of the queue + */ + public String createRandomQueue() { + return createQueue(UUID.randomUUID().toString()); + } + +} diff --git a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java index 6fc763b3e744..a47e4aefde26 100644 --- a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java +++ b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java @@ -260,9 +260,10 @@ public enum TestImage { .withStartupTimeout(Duration.ofMinutes(3))), /** - * A container image suitable for testing RabbitMQ. + * A container image suitable for testing RabbitMQ with support for both AMQP 0.9 and + * 1.0. Queues can be declared using {@code rabbitmqadmin}. */ - RABBITMQ("rabbitmq", "3.11-alpine", () -> RabbitMQContainer.class, + RABBITMQ("rabbitmq", "4.2-management", () -> RabbitMQContainer.class, (container) -> ((RabbitMQContainer) container).withStartupTimeout(Duration.ofMinutes(4))), /** From ebac87ae65d9a75e2265208d4b57d3edddb1a2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 15 Jul 2026 16:53:47 +0200 Subject: [PATCH 697/930] Configure trust packages in AMQP test that accepts JSON See gh-49678 --- .../AmqpAutoConfigurationIntegrationTests.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java index c709c114218b..31dfa212c441 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java @@ -28,6 +28,7 @@ import org.springframework.amqp.client.AmqpClient; import org.springframework.amqp.client.annotation.AmqpListener; +import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -63,13 +64,16 @@ void sendAndReceiveUsingAmqpClient() { @Test void sendAndReceiveUsingJson() { String queue = container.createRandomQueue(); - this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)).run((context) -> { - AmqpClient amqpClient = context.getBean(AmqpClient.class); - assertThat(amqpClient.to(queue).body(new TestMessage("hello", 42)).send().get(1, TimeUnit.SECONDS)) - .isTrue(); - assertThat(amqpClient.from(queue).receiveAndConvert().get(1, TimeUnit.SECONDS)) - .isEqualTo(new TestMessage("hello", 42)); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) + .withBean(JacksonJsonMessageConverter.class, + () -> new JacksonJsonMessageConverter(getClass().getPackageName())) + .run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient.to(queue).body(new TestMessage("hello", 42)).send().get(1, TimeUnit.SECONDS)) + .isTrue(); + assertThat(amqpClient.from(queue).receiveAndConvert().get(1, TimeUnit.SECONDS)) + .isEqualTo(new TestMessage("hello", 42)); + }); } @Test From 0ed57a61095e040a59ea5739f3b13b18517b1f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Thu, 19 Mar 2026 11:39:33 -0600 Subject: [PATCH 698/930] Add AMQP 1.0 RabbitMQ auto-configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See gh-49857 Signed-off-by: Eddú Meléndez --- documentation/spring-boot-docs/build.gradle | 1 + .../reference/pages/messaging/amqp.adoc | 82 ++++ .../amqp/rabbitmqamqp/receiving/MyBean.java | 32 ++ .../amqp/rabbitmqamqp/sending/MyBean.java | 41 ++ module/spring-boot-amqp-rabbitmq/build.gradle | 65 +++ ...nectionDetailsFactoryIntegrationTests.java | 48 +++ ...nectionDetailsFactoryIntegrationTests.java | 99 +++++ .../src/dockerTest/resources/logback-test.xml | 4 + .../amqp/docker/compose/rabbit-compose.yaml | 8 + .../dockerTest/resources/spring.properties | 1 + ...PropertiesRabbitAmqpConnectionDetails.java | 58 +++ .../RabbitAmqpAutoConfiguration.java | 132 ++++++ .../RabbitAmqpConnectionDetails.java | 71 ++++ ...abbitAmqpEnvironmentBuilderCustomizer.java | 39 ++ .../autoconfigure/RabbitAmqpProperties.java | 383 ++++++++++++++++++ .../RabbitAmqpTemplateCustomizer.java | 36 ++ .../RabbitAnnotationDrivenConfiguration.java | 41 ++ .../boot/amqp/autoconfigure/package-info.java | 23 ++ ...DockerComposeConnectionDetailsFactory.java | 91 +++++ .../docker/compose/RabbitEnvironment.java | 50 +++ .../amqp/docker/compose/package-info.java | 23 ++ ...bbitContainerConnectionDetailsFactory.java | 79 ++++ .../amqp/testcontainers/package-info.java | 23 ++ .../main/resources/META-INF/spring.factories | 4 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../RabbitAmqpAutoConfigurationTests.java | 160 ++++++++ .../compose/RabbitEnvironmentTests.java | 72 ++++ .../spring-boot-dependencies/build.gradle | 2 + settings.gradle | 3 + .../build.gradle | 41 ++ ...ampleRabbitAmqpSimpleApplicationTests.java | 55 +++ .../SampleRabbitAmqpSimpleApplication.java | 61 +++ .../src/main/java/smoketest/amqp/Sender.java | 31 ++ .../java/smoketest/amqp/package-info.java | 20 + .../build.gradle | 27 ++ 35 files changed, 1907 insertions(+) create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/receiving/MyBean.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/sending/MyBean.java create mode 100644 module/spring-boot-amqp-rabbitmq/build.gradle create mode 100644 module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/logback-test.xml create mode 100644 module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml create mode 100644 module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/spring.properties create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitAmqpConnectionDetails.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfiguration.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpConnectionDetails.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpEnvironmentBuilderCustomizer.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpProperties.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpTemplateCustomizer.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring.factories create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfigurationTests.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java create mode 100644 smoke-test/spring-boot-smoke-test-amqp-rabbitmq/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleRabbitAmqpSimpleApplicationTests.java create mode 100644 smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/SampleRabbitAmqpSimpleApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/Sender.java create mode 100644 smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/package-info.java create mode 100644 starter/spring-boot-starter-amqp-rabbitmq/build.gradle diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index 88c66a91cce7..e1604ce7a37e 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -95,6 +95,7 @@ dependencies { implementation(project(path: ":module:spring-boot-actuator")) implementation(project(path: ":module:spring-boot-actuator-autoconfigure")) implementation(project(path: ":module:spring-boot-amqp")) + implementation(project(path: ":module:spring-boot-amqp-rabbitmq")) implementation(project(path: ":module:spring-boot-cache")) implementation(project(path: ":module:spring-boot-cache-test")) implementation(project(path: ":module:spring-boot-data-cassandra")) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index 4aad8ad62903..235a7f0170bd 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc @@ -213,3 +213,85 @@ You can also customize the javadoc:org.springframework.core.retry.RetryPolicy[] IMPORTANT: By default, if retries are disabled and the listener throws an exception, the delivery is retried indefinitely. You can modify this behavior in two ways: Set the `defaultRequeueRejected` property to `false` so that zero re-deliveries are attempted or throw an javadoc:org.springframework.amqp.AmqpRejectAndDontRequeueException[] to signal the message should be rejected. The latter is the mechanism used when retries are enabled and the maximum number of delivery attempts is reached. + + + +[[messaging.amqp.rabbitmq-amqp]] +== RabbitMQ AMQP 1.0 Support + +https://www.rabbitmq.com/[RabbitMQ] supports the AMQP 1.0 protocol via the https://github.com/rabbitmq/rabbitmq-amqp-java-client[RabbitMQ AMQP 1.0 Java client]. +Spring AMQP provides support for this client through `spring-rabbitmq-client`. +Spring Boot offers auto-configuration for this support via the `spring-boot-starter-amqp-rabbitmq` starter. + +RabbitMQ AMQP 1.0 configuration is controlled by external configuration properties in `+spring.amqp.rabbitmq.*+`. +For example, you might declare the following section in `application.properties`: + +[configprops,yaml] +---- +spring: + amqp: + rabbitmq: + host: "localhost" + port: 5672 + username: "admin" + password: "secret" +---- + +Alternatively, you could configure the same connection using the `address` attribute: + +[configprops,yaml] +---- +spring: + amqp: + rabbitmq: + address: "amqp://admin:secret@localhost" +---- + +NOTE: When specifying an address that way, the `host` and `port` properties are ignored. + +See javadoc:org.springframework.boot.amqp.autoconfigure.RabbitAmqpProperties[] for more of the supported property-based configuration options. +To configure lower-level details of the auto-configured javadoc:com.rabbitmq.client.amqp.Environment[], define a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitAmqpEnvironmentBuilderCustomizer[] bean. + +If a javadoc:com.rabbitmq.client.amqp.CredentialsProvider[] bean exists in the context, it will be automatically used by the auto-configured javadoc:com.rabbitmq.client.amqp.Environment[]. + + + +[[messaging.amqp.rabbitmq-amqp.sending]] +=== Sending a Message + +Spring's javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate[] and javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin[] are auto-configured, and you can autowire them directly into your own beans, as shown in the following example: + +include-code::MyBean[] + +If a javadoc:org.springframework.amqp.support.converter.MessageConverter[] bean is defined, it is associated automatically to the auto-configured javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate[]. + +You can set properties for the template as follows: + +[configprops,yaml] +---- +spring: + amqp: + rabbitmq: + template: + exchange: "my-exchange" + routing-key: "my-key" + default-receive-queue: "my-queue" +---- + +You can also customize the javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate[] programmatically by declaring a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitAmqpTemplateCustomizer[] bean. + + + +[[messaging.amqp.rabbitmq-amqp.receiving]] +=== Receiving a Message + +When the RabbitMQ AMQP 1.0 infrastructure is present, any bean can be annotated with javadoc:org.springframework.amqp.rabbit.annotation.RabbitListener[format=annotation] to create a listener endpoint. +If no javadoc:org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory[] has been defined, a default one is automatically configured. + +The following sample component creates a listener endpoint on the `someQueue` queue: + +include-code::MyBean[] + +TIP: See javadoc:org.springframework.amqp.rabbit.annotation.EnableRabbit[format=annotation] for more details. + +To customize the listener container, define a javadoc:org.springframework.amqp.rabbit.config.ContainerCustomizer[] bean parameterized with javadoc:org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer[]. diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/receiving/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/receiving/MyBean.java new file mode 100644 index 000000000000..83ac5b882b0a --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/receiving/MyBean.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.rabbitmqamqp.receiving; + +import org.springframework.amqp.rabbit.annotation.RabbitHandler; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +@RabbitListener(queues = "someQueue") +public class MyBean { + + @RabbitHandler + public void processMessage(String content) { + // ... + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/sending/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/sending/MyBean.java new file mode 100644 index 000000000000..a05a238bf68d --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/sending/MyBean.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.rabbitmqamqp.sending; + +import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final RabbitAmqpAdmin amqpAdmin; + + private final RabbitAmqpTemplate amqpTemplate; + + public MyBean(RabbitAmqpAdmin amqpAdmin, RabbitAmqpTemplate amqpTemplate) { + this.amqpAdmin = amqpAdmin; + this.amqpTemplate = amqpTemplate; + } + + // @fold:on // ... + public void someOtherMethod() { + this.amqpTemplate.convertAndSend("hello"); + } + // @fold:off + +} diff --git a/module/spring-boot-amqp-rabbitmq/build.gradle b/module/spring-boot-amqp-rabbitmq/build.gradle new file mode 100644 index 000000000000..edd172e77c6b --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/build.gradle @@ -0,0 +1,65 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java-library" + id "org.springframework.boot.docker-test" + id "org.springframework.boot.auto-configuration" + id "org.springframework.boot.configuration-properties" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot AMQP" + +dependencies { + api(project(":core:spring-boot")) + api("org.springframework:spring-messaging") + api("org.springframework.amqp:spring-rabbitmq-client") + + compileOnly("com.fasterxml.jackson.core:jackson-annotations") + + implementation(project(":module:spring-boot-transaction")) + + optional(project(":core:spring-boot-autoconfigure")) + optional(project(":core:spring-boot-docker-compose")) + optional(project(":core:spring-boot-testcontainers")) + optional(project(":module:spring-boot-health")) + optional(project(":module:spring-boot-micrometer-metrics")) + optional("io.micrometer:micrometer-core") + optional("org.springframework.amqp:spring-rabbit-stream") + optional("org.testcontainers:testcontainers-rabbitmq") + + dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) + dockerTestImplementation(testFixtures(project(":core:spring-boot-docker-compose"))) + dockerTestImplementation("ch.qos.logback:logback-classic") + dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") + + testCompileOnly("com.fasterxml.jackson.core:jackson-annotations") + + testImplementation(project(":core:spring-boot-test")) + testImplementation(project(":test-support:spring-boot-test-support")) + + testRuntimeOnly("ch.qos.logback:logback-classic") +} + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +tasks.named("compileDockerTestJava") { + options.nullability.checking = "tests" +} diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..77baad2a4bf4 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails.Address; +import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.testsupport.container.TestImage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Scott Frederick + * @author Eddú Meléndez + */ +class RabbitDockerComposeConnectionDetailsFactoryIntegrationTests { + + @DockerComposeTest(composeFile = "rabbit-compose.yaml", image = TestImage.RABBITMQ) + void runCreatesConnectionDetails(RabbitAmqpConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getVirtualHost()).isEqualTo("/"); + assertThat(connectionDetails.getAddress()).isNotNull(); + Address address = connectionDetails.getAddress(); + assertThat(address.host()).isNotNull(); + assertThat(address.port()).isGreaterThan(0); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..34487fddcf6e --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +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.testcontainers.rabbitmq.RabbitMQContainer; + +import org.springframework.amqp.rabbit.annotation.Queue; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.RabbitAmqpAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.TestImage; +import org.springframework.context.annotation.Bean; +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 RabbitContainerConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Eddú Meléndez + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class RabbitContainerConnectionDetailsFactoryIntegrationTests { + + @Container + @ServiceConnection + static final RabbitMQContainer rabbit = TestImage.container(RabbitMQContainer.class); + + @Autowired(required = false) + private RabbitAmqpConnectionDetails connectionDetails; + + @Autowired + private RabbitAmqpTemplate rabbitAmqpTemplate; + + @Autowired + private TestListener listener; + + @Test + void connectionCanBeMadeToRabbitContainer() { + assertThat(this.connectionDetails).isNotNull(); + this.rabbitAmqpTemplate.convertAndSend("test", "message"); + Awaitility.waitAtMost(Duration.ofMinutes(4)) + .untilAsserted(() -> assertThat(this.listener.messages).containsExactly("message")); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(RabbitAmqpAutoConfiguration.class) + static class TestConfiguration { + + @Bean + TestListener testListener() { + return new TestListener(); + } + + } + + static class TestListener { + + private final List messages = new ArrayList<>(); + + @RabbitListener(queuesToDeclare = @Queue("test")) + void processMessage(String message) { + this.messages.add(message); + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/logback-test.xml b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/logback-test.xml new file mode 100644 index 000000000000..b8a41480d7d6 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/logback-test.xml @@ -0,0 +1,4 @@ + + + + diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml new file mode 100644 index 000000000000..1951fba4bb08 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml @@ -0,0 +1,8 @@ +services: + rabbitmq: + image: '{imageName}' + environment: + - 'RABBITMQ_DEFAULT_USER=myuser' + - 'RABBITMQ_DEFAULT_PASS=secret' + ports: + - '5672' diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/spring.properties b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/spring.properties new file mode 100644 index 000000000000..47dff33f0bb5 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/spring.properties @@ -0,0 +1 @@ +spring.test.context.cache.maxSize=1 \ No newline at end of file diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitAmqpConnectionDetails.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitAmqpConnectionDetails.java new file mode 100644 index 000000000000..2eb06294eb97 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitAmqpConnectionDetails.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.Nullable; + +/** + * Adapts {@link RabbitAmqpProperties} to {@link RabbitAmqpConnectionDetails}. + * + * @author Eddú Meléndez + */ +class PropertiesRabbitAmqpConnectionDetails implements RabbitAmqpConnectionDetails { + + private final RabbitAmqpProperties properties; + + PropertiesRabbitAmqpConnectionDetails(RabbitAmqpProperties properties) { + this.properties = properties; + } + + @Override + public String getUsername() { + return this.properties.determineUsername(); + } + + @Override + public @Nullable String getPassword() { + return this.properties.determinePassword(); + } + + @Override + public @Nullable String getVirtualHost() { + return this.properties.determineVirtualHost(); + } + + @Override + public Address getAddress() { + String address = this.properties.determineAddress(); + int portSeparatorIndex = address.lastIndexOf(':'); + String host = address.substring(0, portSeparatorIndex); + String port = address.substring(portSeparatorIndex + 1); + return new Address(host, Integer.parseInt(port)); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfiguration.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfiguration.java new file mode 100644 index 000000000000..a433060acc31 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfiguration.java @@ -0,0 +1,132 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.CredentialsProvider; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; +import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder.EnvironmentConnectionSettings; + +import org.springframework.amqp.rabbit.config.ContainerCustomizer; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.amqp.rabbitmq.client.SingleAmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails.Address; +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.boot.context.properties.PropertyMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link RabbitAmqpTemplate}. + * + * @author Eddú Meléndez + * @since 4.1.0 + */ +@AutoConfiguration +@ConditionalOnClass({ RabbitAmqpTemplate.class, Connection.class }) +@EnableConfigurationProperties(RabbitAmqpProperties.class) +@Import(RabbitAnnotationDrivenConfiguration.class) +public final class RabbitAmqpAutoConfiguration { + + private final RabbitAmqpProperties properties; + + RabbitAmqpAutoConfiguration(RabbitAmqpProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean + RabbitAmqpConnectionDetails rabbitConnectionDetails() { + return new PropertiesRabbitAmqpConnectionDetails(this.properties); + } + + @Bean(name = "rabbitListenerContainerFactory") + @ConditionalOnMissingBean(name = "rabbitListenerContainerFactory") + RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(AmqpConnectionFactory connectionFactory, + ObjectProvider> amqpContainerCustomizer) { + RabbitAmqpListenerContainerFactory factory = new RabbitAmqpListenerContainerFactory(connectionFactory); + amqpContainerCustomizer.ifUnique(factory::setContainerCustomizer); + + RabbitAmqpProperties.AmqpContainer configuration = this.properties.getListener().getAmqp(); + factory.setObservationEnabled(configuration.isObservationEnabled()); + return factory; + } + + @Bean + @ConditionalOnMissingBean + Environment rabbitAmqpEnvironment(RabbitAmqpConnectionDetails connectionDetails, + ObjectProvider customizers, + ObjectProvider credentialsProvider) { + PropertyMapper map = PropertyMapper.get(); + EnvironmentConnectionSettings environmentConnectionSettings = new AmqpEnvironmentBuilder().connectionSettings(); + Address address = connectionDetails.getAddress(); + map.from(address::host).to(environmentConnectionSettings::host); + map.from(address::port).to(environmentConnectionSettings::port); + map.from(connectionDetails::getUsername).to(environmentConnectionSettings::username); + map.from(connectionDetails::getPassword).to(environmentConnectionSettings::password); + map.from(connectionDetails::getVirtualHost).to(environmentConnectionSettings::virtualHost); + map.from(credentialsProvider::getIfAvailable).to(environmentConnectionSettings::credentialsProvider); + + AmqpEnvironmentBuilder builder = environmentConnectionSettings.environmentBuilder(); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder.build(); + } + + @Bean + @ConditionalOnMissingBean + AmqpConnectionFactory amqpConnectionFactory(Environment environment) { + return new SingleAmqpConnectionFactory(environment); + } + + @Bean + @ConditionalOnMissingBean + RabbitAmqpTemplate rabbitAmqpTemplate(AmqpConnectionFactory connectionFactory, + ObjectProvider customizers, + ObjectProvider messageConverter) { + RabbitAmqpTemplate rabbitAmqpTemplate = new RabbitAmqpTemplate(connectionFactory); + if (messageConverter.getIfAvailable() != null) { + rabbitAmqpTemplate.setMessageConverter(messageConverter.getIfAvailable()); + } + RabbitAmqpProperties.Template templateProperties = this.properties.getTemplate(); + + PropertyMapper map = PropertyMapper.get(); + map.from(templateProperties::getDefaultReceiveQueue).to(rabbitAmqpTemplate::setReceiveQueue); + map.from(templateProperties::getExchange).to(rabbitAmqpTemplate::setExchange); + map.from(templateProperties::getRoutingKey).to(rabbitAmqpTemplate::setRoutingKey); + + customizers.orderedStream().forEach((customizer) -> customizer.customize(rabbitAmqpTemplate)); + return rabbitAmqpTemplate; + } + + @Bean + @ConditionalOnMissingBean + RabbitAmqpAdmin rabbitAmqpAdmin(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpAdmin(connectionFactory); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpConnectionDetails.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpConnectionDetails.java new file mode 100644 index 000000000000..344909fffb00 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpConnectionDetails.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to a RabbitMQ AMQP service. + * + * @author Eddú Meléndez + * @since 4.1.0 + */ +public interface RabbitAmqpConnectionDetails extends ConnectionDetails { + + /** + * Login user to authenticate to the broker. + * @return the login user to authenticate to the broker or {@code null} + */ + default @Nullable String getUsername() { + return null; + } + + /** + * Login to authenticate against the broker. + * @return the login to authenticate against the broker or {@code null} + */ + default @Nullable String getPassword() { + return null; + } + + /** + * Virtual host to use when connecting to the broker. + * @return the virtual host to use when connecting to the broker or {@code null} + */ + default @Nullable String getVirtualHost() { + return null; + } + + /** + * Returns the address. + * @return the address + * @throws IllegalStateException if the address list is empty + */ + Address getAddress(); + + /** + * A RabbitMQ address. + * + * @param host the host + * @param port the port + */ + record Address(String host, int port) { + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpEnvironmentBuilderCustomizer.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpEnvironmentBuilderCustomizer.java new file mode 100644 index 000000000000..807ead65def3 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpEnvironmentBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * auto-configured {@link Environment} that is created by an + * {@link AmqpEnvironmentBuilder}. + * + * @author Eddú Meléndez + * @since 4.1.0 + */ +@FunctionalInterface +public interface RabbitAmqpEnvironmentBuilderCustomizer { + + /** + * Customize the {@code AmqpEnvironmentBuilder}. + * @param builder the builder to customize + */ + void customize(AmqpEnvironmentBuilder builder); + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpProperties.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpProperties.java new file mode 100644 index 000000000000..87ddff5492b1 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpProperties.java @@ -0,0 +1,383 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.util.StringUtils; + +/** + * Configuration properties for Rabbit AMQP. + * + * @author Eddú Meléndez + * @since 4.1.0 + */ +@ConfigurationProperties("spring.amqp.rabbitmq") +public class RabbitAmqpProperties { + + private static final int DEFAULT_PORT = 5672; + + /** + * RabbitMQ host. Ignored if an address is set. + */ + private String host = "localhost"; + + /** + * RabbitMQ port. Ignored if an address is set. Default to 5672, or 5671 if SSL is + * enabled. + */ + private @Nullable Integer port; + + /** + * Login user to authenticate to the broker. + */ + private String username = "guest"; + + /** + * Login to authenticate against the broker. + */ + private String password = "guest"; + + /** + * Virtual host to use when connecting to the broker. + */ + private @Nullable String virtualHost; + + /** + * The address to which the client should connect. When set, the host and port are + * ignored. + */ + private @Nullable String address; + + /** + * Listener container configuration. + */ + private final Listener listener = new Listener(); + + private final Template template = new Template(); + + private @Nullable Address parsedAddress; + + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + public @Nullable Integer getPort() { + return this.port; + } + + /** + * Returns the port from the address, or the configured port if no address have been + * set. + * @return the port + * @see #setAddress(String) + * @see #getPort() + */ + public int determinePort() { + if (this.parsedAddress == null) { + Integer port = getPort(); + if (port != null) { + return port; + } + return DEFAULT_PORT; + } + return this.parsedAddress.port; + } + + public void setPort(@Nullable Integer port) { + this.port = port; + } + + public @Nullable String getAddress() { + return this.address; + } + + /** + * Returns the configured address ({@code host:port}) created from the configured host + * and port. + * @return the address + */ + public String determineAddress() { + if (this.parsedAddress == null) { + if (this.host.contains(",")) { + throw new InvalidConfigurationPropertyValueException("spring.amqp.host", this.host, + "Invalid character ','. Value must be a single host. For multiple hosts, use property 'spring.amqp.address' instead."); + } + return this.host + ":" + determinePort(); + } + return this.parsedAddress.host + ":" + this.parsedAddress.port; + } + + public void setAddress(String address) { + this.address = address; + this.parsedAddress = parseAddress(address); + } + + private Address parseAddress(String address) { + return new Address(address); + } + + public String getUsername() { + return this.username; + } + + /** + * If address has been set and has a username it is returned. Otherwise returns the + * result of calling {@code getUsername()}. + * @return the username + * @see #setAddress(String) + * @see #getUsername() + */ + public String determineUsername() { + if (this.parsedAddress == null) { + return this.username; + } + Address address = this.parsedAddress; + return (address.username != null) ? address.username : this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + /** + * If address has been set and has a password it is returned. Otherwise returns the + * result of calling {@code getPassword()}. + * @return the password or {@code null} + * @see #setAddress(String) + * @see #getPassword() + */ + public @Nullable String determinePassword() { + if (this.parsedAddress == null) { + return getPassword(); + } + Address address = this.parsedAddress; + return (address.password != null) ? address.password : getPassword(); + } + + public void setPassword(String password) { + this.password = password; + } + + public @Nullable String getVirtualHost() { + return this.virtualHost; + } + + /** + * If address has been set and has a virtual host it is returned. Otherwise returns + * the result of calling {@code getVirtualHost()}. + * @return the virtual host or {@code null} + * @see #setAddress(String) + * @see #getVirtualHost() + */ + public @Nullable String determineVirtualHost() { + if (this.parsedAddress == null) { + return getVirtualHost(); + } + Address address = this.parsedAddress; + return (address.virtualHost != null) ? address.virtualHost : getVirtualHost(); + } + + public void setVirtualHost(@Nullable String virtualHost) { + this.virtualHost = StringUtils.hasText(virtualHost) ? virtualHost : "/"; + } + + public Listener getListener() { + return this.listener; + } + + public Template getTemplate() { + return this.template; + } + + public static class Listener { + + private final AmqpContainer amqp = new AmqpContainer(); + + public AmqpContainer getAmqp() { + return this.amqp; + } + + } + + /** + * Configuration properties for {@code RabbitAmqpListenerContainer}. + */ + public static class AmqpContainer { + + /** + * Whether to enable observation. + */ + private boolean observationEnabled; + + /** + * Batch size, expressed as the number of physical messages, to be used by the + * container. + */ + private @Nullable Integer batchSize; + + public boolean isObservationEnabled() { + return this.observationEnabled; + } + + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + public @Nullable Integer getBatchSize() { + return this.batchSize; + } + + public void setBatchSize(@Nullable Integer batchSize) { + this.batchSize = batchSize; + } + + } + + public static class Template { + + /** + * Name of the default exchange to use for send operations. + */ + private String exchange = ""; + + /** + * Value of a default routing key to use for send operations. + */ + private String routingKey = ""; + + /** + * Name of the default queue to receive messages from when none is specified + * explicitly. + */ + private @Nullable String defaultReceiveQueue; + + public String getExchange() { + return this.exchange; + } + + public void setExchange(String exchange) { + this.exchange = exchange; + } + + public String getRoutingKey() { + return this.routingKey; + } + + public void setRoutingKey(String routingKey) { + this.routingKey = routingKey; + } + + public @Nullable String getDefaultReceiveQueue() { + return this.defaultReceiveQueue; + } + + public void setDefaultReceiveQueue(@Nullable String defaultReceiveQueue) { + this.defaultReceiveQueue = defaultReceiveQueue; + } + + } + + private static final class Address { + + private static final String PREFIX_AMQP = "amqp://"; + + private static final String PREFIX_AMQP_SECURE = "amqps://"; + + private String host; + + private int port; + + private @Nullable String username; + + private @Nullable String password; + + private @Nullable String virtualHost; + + private Address(String input) { + input = input.trim(); + input = trimPrefix(input); + input = parseUsernameAndPassword(input); + input = parseVirtualHost(input); + parseHostAndPort(input); + } + + private String trimPrefix(String input) { + if (input.startsWith(PREFIX_AMQP_SECURE)) { + return input.substring(PREFIX_AMQP_SECURE.length()); + } + if (input.startsWith(PREFIX_AMQP)) { + return input.substring(PREFIX_AMQP.length()); + } + return input; + } + + private String parseUsernameAndPassword(String input) { + String[] splitInput = StringUtils.split(input, "@"); + if (splitInput == null) { + return input; + } + String credentials = splitInput[0]; + String[] splitCredentials = StringUtils.split(credentials, ":"); + if (splitCredentials == null) { + this.username = credentials; + } + else { + this.username = splitCredentials[0]; + this.password = splitCredentials[1]; + } + return splitInput[1]; + } + + private String parseVirtualHost(String input) { + int hostIndex = input.indexOf('/'); + if (hostIndex >= 0) { + this.virtualHost = input.substring(hostIndex + 1); + if (this.virtualHost.isEmpty()) { + this.virtualHost = "/"; + } + input = input.substring(0, hostIndex); + } + return input; + } + + private void parseHostAndPort(String input) { + int bracketIndex = input.lastIndexOf(']'); + int colonIndex = input.lastIndexOf(':'); + if (colonIndex == -1 || colonIndex < bracketIndex) { + this.host = input; + this.port = DEFAULT_PORT; + } + else { + this.host = input.substring(0, colonIndex); + this.port = Integer.parseInt(input.substring(colonIndex + 1)); + } + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpTemplateCustomizer.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpTemplateCustomizer.java new file mode 100644 index 000000000000..e1eb2404cbb2 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpTemplateCustomizer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; + +/** + * Callback interface that can be used to customize a {@link RabbitAmqpTemplate}. + * + * @author Eddú Meléndez + * @since 4.0.0 + */ +@FunctionalInterface +public interface RabbitAmqpTemplateCustomizer { + + /** + * Callback to customize a {@link RabbitAmqpTemplate} instance. + * @param rabbitAmqpTemplate the rabbitAmqpTemplate to customize + */ + void customize(RabbitAmqpTemplate rabbitAmqpTemplate); + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java new file mode 100644 index 000000000000..c2c766c096e7 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.config.RabbitListenerConfigUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for Spring AMQP annotation driven endpoints. + * + * @author Eddú Meléndez + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(EnableRabbit.class) +class RabbitAnnotationDrivenConfiguration { + + @Configuration(proxyBeanMethods = false) + @EnableRabbit + @ConditionalOnMissingBean(name = RabbitListenerConfigUtils.RABBIT_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME) + static class EnableRabbitConfiguration { + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java new file mode 100644 index 000000000000..b519c35a2cae --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 RabbitMQ. + */ +@NullMarked +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..289fbe1cd703 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails; +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 RabbitAmqpConnectionDetails} for a {@code rabbitmq} service. + * + * @author Eddú Meléndez + */ +class RabbitDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int RABBITMQ_PORT = 5672; + + protected RabbitDockerComposeConnectionDetailsFactory() { + super("rabbitmq"); + } + + @Override + protected @Nullable RabbitAmqpConnectionDetails getDockerComposeConnectionDetails( + DockerComposeConnectionSource source) { + try { + return new RabbitAmqpDockerComposeConnectionDetails(source.getRunningService()); + } + catch (IllegalStateException ex) { + return null; + } + } + + /** + * {@link RabbitAmqpConnectionDetails} backed by a {@code rabbitmq} + * {@link RunningService}. + */ + static class RabbitAmqpDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements RabbitAmqpConnectionDetails { + + private final RabbitEnvironment environment; + + private final Address address; + + protected RabbitAmqpDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new RabbitEnvironment(service.env()); + this.address = new Address(service.host(), service.ports().get(RABBITMQ_PORT)); + } + + @Override + public @Nullable String getUsername() { + return this.environment.getUsername(); + } + + @Override + public @Nullable String getPassword() { + return this.environment.getPassword(); + } + + @Override + public String getVirtualHost() { + return "/"; + } + + @Override + public Address getAddress() { + return this.address; + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java new file mode 100644 index 000000000000..bc776b015dbf --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +/** + * RabbitMQ environment details. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Scott Frederick + */ +class RabbitEnvironment { + + private final @Nullable String username; + + private final @Nullable String password; + + RabbitEnvironment(Map env) { + this.username = env.getOrDefault("RABBITMQ_DEFAULT_USER", env.getOrDefault("RABBITMQ_USERNAME", "guest")); + this.password = env.getOrDefault("RABBITMQ_DEFAULT_PASS", env.getOrDefault("RABBITMQ_PASSWORD", "guest")); + } + + @Nullable String getUsername() { + return this.username; + } + + @Nullable String getPassword() { + return this.password; + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java new file mode 100644 index 000000000000..c2d355c0797e --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 RabbitMQ service connections. + */ +@NullMarked +package org.springframework.boot.amqp.docker.compose; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java new file mode 100644 index 000000000000..58d6560e6b8f --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.testcontainers; + +import java.net.URI; + +import org.jspecify.annotations.Nullable; +import org.testcontainers.rabbitmq.RabbitMQContainer; + +import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails; +import org.springframework.boot.ssl.SslBundle; +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 RabbitAmqpConnectionDetails} + * from a {@link ServiceConnection @ServiceConnection}-annotated + * {@link RabbitMQContainer}. + * + * @author Eddú Meléndez + */ +class RabbitContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory { + + @Override + protected RabbitAmqpConnectionDetails getContainerConnectionDetails( + ContainerConnectionSource source) { + return new RabbitAmqpMqContainerConnectionDetails(source); + } + + /** + * {@link RabbitAmqpConnectionDetails} backed by a {@link ContainerConnectionSource}. + */ + static final class RabbitAmqpMqContainerConnectionDetails extends ContainerConnectionDetails + implements RabbitAmqpConnectionDetails { + + private RabbitAmqpMqContainerConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + @Override + public String getUsername() { + return getContainer().getAdminUsername(); + } + + @Override + public String getPassword() { + return getContainer().getAdminPassword(); + } + + @Override + public Address getAddress() { + URI uri = URI.create((getSslBundle() != null) ? getContainer().getAmqpsUrl() : getContainer().getAmqpUrl()); + return new Address(uri.getHost(), uri.getPort()); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return super.getSslBundle(); + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java new file mode 100644 index 000000000000..69c6364ef32f --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 RabbitMQ service connections. + */ +@NullMarked +package org.springframework.boot.amqp.testcontainers; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring.factories b/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..412b6813fab6 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +# Connection Details Factories +org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ +org.springframework.boot.amqp.docker.compose.RabbitDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory diff --git a/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000000..050f5729e283 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springframework.boot.amqp.autoconfigure.RabbitAmqpAutoConfiguration \ No newline at end of file diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfigurationTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfigurationTests.java new file mode 100644 index 000000000000..e1c5835ff6cc --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfigurationTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.ContainerCustomizer; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer; +import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.support.converter.MessageConverter; +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 org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RabbitAmqpAutoConfiguration}. + * + * @author Eddú Meléndez + */ +class RabbitAmqpAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(RabbitAmqpAutoConfiguration.class)); + + @Test + void testDefaultRabbitAmqpConfiguration() { + this.contextRunner.run((context) -> { + RabbitAmqpListenerContainerFactory listenerContainerFactory = context + .getBean(RabbitAmqpListenerContainerFactory.class); + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + RabbitAmqpTemplate rabbitAmqpTemplate = context.getBean(RabbitAmqpTemplate.class); + RabbitAmqpAdmin rabbitAmqpAdmin = context.getBean(RabbitAmqpAdmin.class); + + assertThat(listenerContainerFactory).isNotNull(); + assertThat(listenerContainerFactory).extracting("containerCustomizer").isNull(); + assertThat(connectionFactory).isNotNull(); + assertThat(rabbitAmqpTemplate).isNotNull(); + assertThat(rabbitAmqpAdmin).isNotNull(); + }); + } + + @Test + void whenMultipleRabbitAmqpTemplateCustomizersAreDefinedThenTheyAreCalledInOrder() { + this.contextRunner.withUserConfiguration(MultipleRabbitAmqpTemplateCustomizersConfiguration.class) + .run((context) -> { + RabbitAmqpTemplateCustomizer firstCustomizer = context.getBean("firstCustomizer", + RabbitAmqpTemplateCustomizer.class); + RabbitAmqpTemplateCustomizer secondCustomizer = context.getBean("secondCustomizer", + RabbitAmqpTemplateCustomizer.class); + InOrder inOrder = inOrder(firstCustomizer, secondCustomizer); + RabbitAmqpTemplate template = context.getBean(RabbitAmqpTemplate.class); + then(firstCustomizer).should(inOrder).customize(template); + then(secondCustomizer).should(inOrder).customize(template); + inOrder.verifyNoMoreInteractions(); + }); + } + + @Test + void testListenerContainerFactoryWithContainerCustomizer() { + this.contextRunner.withUserConfiguration(AmqpContainerCustomizerConfiguration.class).run((context) -> { + RabbitAmqpListenerContainerFactory listenerContainerFactory = context + .getBean(RabbitAmqpListenerContainerFactory.class); + + assertThat(listenerContainerFactory).isNotNull(); + assertThat(listenerContainerFactory).extracting("containerCustomizer").isNotNull(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class MultipleRabbitAmqpTemplateCustomizersConfiguration { + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + RabbitAmqpTemplateCustomizer secondCustomizer() { + return mock(RabbitAmqpTemplateCustomizer.class); + } + + @Bean + @Order(0) + RabbitAmqpTemplateCustomizer firstCustomizer() { + return mock(RabbitAmqpTemplateCustomizer.class); + } + + } + + @Import(TestListener.class) + @Configuration(proxyBeanMethods = false) + static class AmqpContainerCustomizerConfiguration { + + @Bean + @SuppressWarnings("unchecked") + ContainerCustomizer customizer() { + return mock(ContainerCustomizer.class); + } + + } + + @Configuration + static class CustomMessageConverterConfiguration { + + @Bean + MessageConverter messageConverter() { + return new MessageConverter() { + + @Override + public Message toMessage(Object object, MessageProperties messageProperties) + throws MessageConversionException { + return new Message(object.toString().getBytes()); + } + + @Override + public Object fromMessage(Message message) throws MessageConversionException { + return new String(message.getBody()); + } + + }; + } + + } + + static class TestListener { + + @RabbitListener(queues = "test", autoStartup = "false") + void listen(String in) { + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java new file mode 100644 index 000000000000..1ecb0b4d6ff4 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +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 RabbitEnvironment}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Scott Frederick + */ +class RabbitEnvironmentTests { + + @Test + void getUsernameWhenNoRabbitmqDefaultUser() { + RabbitEnvironment environment = new RabbitEnvironment(Collections.emptyMap()); + assertThat(environment.getUsername()).isEqualTo("guest"); + } + + @Test + void getUsernameWhenHasRabbitmqDefaultUser() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_DEFAULT_USER", "me")); + 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()); + assertThat(environment.getPassword()).isEqualTo("guest"); + } + + @Test + void getUsernameWhenHasRabbitmqDefaultPass() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); + 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/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 525f0501a5a9..a89833cb5a0d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2124,6 +2124,7 @@ bom { "spring-boot-actuator", "spring-boot-actuator-autoconfigure", "spring-boot-amqp", + "spring-boot-amqp-rabbitmq", "spring-boot-artemis", "spring-boot-autoconfigure", "spring-boot-autoconfigure-classic", @@ -2246,6 +2247,7 @@ bom { "spring-boot-starter-actuator", "spring-boot-starter-actuator-test", "spring-boot-starter-amqp", + "spring-boot-starter-amqp-rabbitmq", "spring-boot-starter-amqp-test", "spring-boot-starter-artemis", "spring-boot-starter-artemis-test", diff --git a/settings.gradle b/settings.gradle index 269c5fccec1a..ce1e29e4c9b5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -81,6 +81,7 @@ include "module:spring-boot-activemq" include "module:spring-boot-actuator" include "module:spring-boot-actuator-autoconfigure" include "module:spring-boot-amqp" +include "module:spring-boot-amqp-rabbitmq" include "module:spring-boot-artemis" include "module:spring-boot-autoconfigure-classic" include "module:spring-boot-autoconfigure-classic-modules" @@ -213,6 +214,7 @@ include "starter:spring-boot-starter-activemq-test" include "starter:spring-boot-starter-actuator" include "starter:spring-boot-starter-actuator-test" include "starter:spring-boot-starter-amqp" +include "starter:spring-boot-starter-amqp-rabbitmq" include "starter:spring-boot-starter-amqp-test" include "starter:spring-boot-starter-artemis" include "starter:spring-boot-starter-artemis-test" @@ -405,6 +407,7 @@ include ":smoke-test:spring-boot-smoke-test-actuator-log4j2" include ":smoke-test:spring-boot-smoke-test-actuator-noweb" include ":smoke-test:spring-boot-smoke-test-actuator-ui" include ":smoke-test:spring-boot-smoke-test-amqp" +include ":smoke-test:spring-boot-smoke-test-amqp-rabbitmq" include ":smoke-test:spring-boot-smoke-test-ant" include ":smoke-test:spring-boot-smoke-test-artemis" include ":smoke-test:spring-boot-smoke-test-aspectj" diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/build.gradle b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/build.gradle new file mode 100644 index 000000000000..ac586791eb04 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/build.gradle @@ -0,0 +1,41 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java" + id "org.springframework.boot.docker-test" +} + +description = "Spring Boot RabbitMQ AMQP smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-amqp-rabbitmq")) + + dockerTestImplementation(project(":starter:spring-boot-starter-test")) + dockerTestImplementation(project(":core:spring-boot-testcontainers")) + dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) + dockerTestImplementation("org.awaitility:awaitility") + dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") + dockerTestImplementation("org.testcontainers:testcontainers-rabbitmq") +} + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +tasks.named("compileDockerTestJava") { + options.nullability.checking = "tests" +} \ No newline at end of file diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleRabbitAmqpSimpleApplicationTests.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleRabbitAmqpSimpleApplicationTests.java new file mode 100644 index 000000000000..bd9f51c562a5 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleRabbitAmqpSimpleApplicationTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.testcontainers.rabbitmq.RabbitMQContainer; + +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.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.TestImage; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Testcontainers(disabledWithoutDocker = true) +@ExtendWith(OutputCaptureExtension.class) +class SampleRabbitAmqpSimpleApplicationTests { + + @Container + @ServiceConnection + static final RabbitMQContainer rabbit = TestImage.container(RabbitMQContainer.class); + + @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/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/SampleRabbitAmqpSimpleApplication.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/SampleRabbitAmqpSimpleApplication.java new file mode 100644 index 000000000000..aff9bc342e3b --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/SampleRabbitAmqpSimpleApplication.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.annotation.RabbitHandler; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +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.messaging.handler.annotation.Payload; + +@SpringBootApplication +@RabbitListener(queues = "foo") +public class SampleRabbitAmqpSimpleApplication { + + private static final Log logger = LogFactory.getLog(SampleRabbitAmqpSimpleApplication.class); + + @Bean + public Sender mySender() { + return new Sender(); + } + + @Bean + public Queue fooQueue() { + return new Queue("foo"); + } + + @RabbitHandler + public void process(@Payload String foo) { + logger.info(foo); + } + + @Bean + public ApplicationRunner runner(Sender sender) { + return (args) -> sender.send("Hello"); + } + + public static void main(String[] args) { + SpringApplication.run(SampleRabbitAmqpSimpleApplication.class, args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/Sender.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/Sender.java new file mode 100644 index 000000000000..6639bc0316ce --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/Sender.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.beans.factory.annotation.Autowired; + +public class Sender { + + @Autowired + private RabbitAmqpTemplate rabbitAmqpTemplate; + + public void send(String message) { + this.rabbitAmqpTemplate.convertAndSend("foo", message); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/package-info.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/package-info.java new file mode 100644 index 000000000000..bc9e41a079d7 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NullMarked +package smoketest.amqp; + +import org.jspecify.annotations.NullMarked; diff --git a/starter/spring-boot-starter-amqp-rabbitmq/build.gradle b/starter/spring-boot-starter-amqp-rabbitmq/build.gradle new file mode 100644 index 000000000000..cb36be1a7d1c --- /dev/null +++ b/starter/spring-boot-starter-amqp-rabbitmq/build.gradle @@ -0,0 +1,27 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring AMQP with Rabbit MQ over AMQP 1.0 protocol" + +dependencies { + api(project(":starter:spring-boot-starter")) + + api(project(":module:spring-boot-amqp-rabbitmq")) +} From 21f73909443f8094d8216612ec0a12437c840d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 31 Mar 2026 14:57:05 +0200 Subject: [PATCH 699/930] Polish "Add AMQP 1.0 RabbitMQ auto-configuration" See gh-49857 --- .../antora/modules/ROOT/pages/redirect.adoc | 22 +- .../reference/pages/messaging/amqp.adoc | 201 ++++++------ .../amqp/rabbitmq/receiving/MyBean.java | 4 +- .../custom/MyRabbitAmqpConfiguration.java | 37 +++ .../amqp/rabbitmq/sending/MyBean.java | 14 +- .../receiving/MyBean.java | 6 +- .../rabbitmq09/receiving/custom/MyBean.java | 30 ++ .../receiving/custom/MyMessageConverter.java | 36 +++ .../custom/MyRabbitConfiguration.java | 2 +- .../sending/MyBean.java | 16 +- .../amqp/rabbitmq/receiving/MyBean.kt | 5 +- .../amqp/rabbitmq/receiving/custom/MyBean.kt | 1 - .../receiving/custom/MyMessageConverter.kt | 1 - .../custom/MyRabbitAmqpConfiguration.kt | 39 +++ .../messaging/amqp/rabbitmq/sending/MyBean.kt | 11 +- .../amqp/rabbitmq09/receiving/MyBean.kt | 32 ++ .../rabbitmq09/receiving/custom/MyBean.kt | 32 ++ .../receiving/custom/MyMessageConverter.kt | 34 ++ .../receiving/custom/MyRabbitConfiguration.kt | 2 +- .../amqp/rabbitmq09/sending/MyBean.kt | 37 +++ module/spring-boot-amqp-rabbitmq/build.gradle | 5 +- ...ectionDetailsFactoryIntegrationTests.java} | 30 +- ...ectionDetailsFactoryIntegrationTests.java} | 14 +- ...nectionDetailsFactoryIntegrationTests.java | 124 ++++++++ .../springframework/boot/amqp/rabbitmq/ca.crt | 32 ++ .../boot/amqp/rabbitmq/client.crt | 26 ++ .../boot/amqp/rabbitmq/client.key | 28 ++ .../docker/compose/rabbitmq-compose.yaml} | 0 .../docker/compose/rabbitmq-ssl-compose.yaml | 26 ++ .../rabbitmq/docker/compose/rabbitmq-ssl.conf | 8 + .../boot/amqp/rabbitmq/server.crt | 26 ++ .../boot/amqp/rabbitmq/server.key | 28 ++ .../rabbitmq/testcontainers/rabbitmq-ssl.conf | 8 + .../RabbitAnnotationDrivenConfiguration.java | 41 --- .../AmqpEnvironmentCustomizer.java} | 15 +- ...qpRabbitAnnotationDrivenConfiguration.java | 72 +++++ .../AmqpRabbitAutoConfiguration.java} | 69 ++-- .../AmqpRabbitConnectionDetails.java} | 26 +- .../autoconfigure/AmqpRabbitProperties.java} | 88 ++++-- ...ropertiesAmqpRabbitConnectionDetails.java} | 40 ++- ...mqpListenerContainerFactoryConfigurer.java | 92 ++++++ .../RabbitAmqpTemplateCustomizer.java | 4 +- ...bitHealthContributorAutoConfiguration.java | 61 ++++ .../autoconfigure/health/package-info.java | 23 ++ .../rabbitmq/autoconfigure/package-info.java | 23 ++ ...ockerComposeConnectionDetailsFactory.java} | 44 ++- .../docker/compose/RabbitEnvironment.java | 7 +- .../docker/compose/package-info.java | 2 +- .../health/AmqpRabbitHealthIndicator.java | 50 +++ .../health}/package-info.java | 4 +- ...tMqContainerConnectionDetailsFactory.java} | 34 +- .../testcontainers/package-info.java | 2 +- .../main/resources/META-INF/spring.factories | 4 +- ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../RabbitAmqpAutoConfigurationTests.java | 160 ---------- .../AmqpRabbitAutoConfigurationTests.java | 294 ++++++++++++++++++ .../AmqpRabbitPropertiesTests.java | 213 +++++++++++++ ...rtiesAmqpRabbitConnectionDetailsTests.java | 163 ++++++++++ ...stenerContainerFactoryConfigurerTests.java | 106 +++++++ ...althContributorAutoConfigurationTests.java | 51 +++ .../compose/RabbitEnvironmentTests.java | 8 +- .../AmqpRabbitHealthIndicatorTests.java | 86 +++++ .../build.gradle | 3 + .../spring-boot-dependencies/build.gradle | 15 +- settings.gradle | 1 + ...ampleRabbitAmqpSimpleApplicationTests.java | 5 +- .../SampleRabbitAmqpSimpleApplication.java | 14 +- .../smoketest/amqp/{ => rabbitmq}/Sender.java | 9 +- .../amqp/{ => rabbitmq}/package-info.java | 2 +- .../build.gradle | 26 ++ 70 files changed, 2268 insertions(+), 509 deletions(-) create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitAmqpConfiguration.java rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{rabbitmqamqp => rabbitmq09}/receiving/MyBean.java (81%) create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyBean.java create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyMessageConverter.java rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{rabbitmq => rabbitmq09}/receiving/custom/MyRabbitConfiguration.java (95%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{rabbitmqamqp => rabbitmq09}/sending/MyBean.java (69%) create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitAmqpConfiguration.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/MyBean.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyBean.kt create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyMessageConverter.kt rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{rabbitmq => rabbitmq09}/receiving/custom/MyRabbitConfiguration.kt (95%) create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/sending/MyBean.kt rename module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/{docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java => rabbitmq/docker/compose/AmqpRabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java} (51%) rename module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/{testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java => rabbitmq/testcontainers/AmqpRabbitMqContainerConnectionDetailsFactoryIntegrationTests.java} (84%) create mode 100644 module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerWithSslConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/ca.crt create mode 100644 module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/client.crt create mode 100644 module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/client.key rename module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/{docker/compose/rabbit-compose.yaml => rabbitmq/docker/compose/rabbitmq-compose.yaml} (100%) create mode 100644 module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-ssl-compose.yaml create mode 100644 module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-ssl.conf create mode 100644 module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/server.crt create mode 100644 module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/server.key create mode 100644 module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/testcontainers/rabbitmq-ssl.conf delete mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java rename module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/{autoconfigure/RabbitAmqpEnvironmentBuilderCustomizer.java => rabbitmq/autoconfigure/AmqpEnvironmentCustomizer.java} (70%) create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAnnotationDrivenConfiguration.java rename module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/{autoconfigure/RabbitAmqpAutoConfiguration.java => rabbitmq/autoconfigure/AmqpRabbitAutoConfiguration.java} (66%) rename module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/{autoconfigure/RabbitAmqpConnectionDetails.java => rabbitmq/autoconfigure/AmqpRabbitConnectionDetails.java} (75%) rename module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/{autoconfigure/RabbitAmqpProperties.java => rabbitmq/autoconfigure/AmqpRabbitProperties.java} (86%) rename module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/{autoconfigure/PropertiesRabbitAmqpConnectionDetails.java => rabbitmq/autoconfigure/PropertiesAmqpRabbitConnectionDetails.java} (59%) create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpListenerContainerFactoryConfigurer.java rename module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/{ => rabbitmq}/autoconfigure/RabbitAmqpTemplateCustomizer.java (93%) create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/AmqpRabbitHealthContributorAutoConfiguration.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/package-info.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/package-info.java rename module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/{docker/compose/RabbitDockerComposeConnectionDetailsFactory.java => rabbitmq/docker/compose/AmqpRabbitMqDockerComposeConnectionDetailsFactory.java} (60%) rename module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/{ => rabbitmq}/docker/compose/RabbitEnvironment.java (88%) rename module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/{ => rabbitmq}/docker/compose/package-info.java (92%) create mode 100644 module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/health/AmqpRabbitHealthIndicator.java rename module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/{autoconfigure => rabbitmq/health}/package-info.java (85%) rename module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/{testcontainers/RabbitContainerConnectionDetailsFactory.java => rabbitmq/testcontainers/AmqpRabbitMqContainerConnectionDetailsFactory.java} (68%) rename module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/{ => rabbitmq}/testcontainers/package-info.java (92%) delete mode 100644 module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfigurationTests.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAutoConfigurationTests.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitPropertiesTests.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/PropertiesAmqpRabbitConnectionDetailsTests.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpListenerContainerFactoryConfigurerTests.java create mode 100644 module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/AmqpRabbitHealthContributorAutoConfigurationTests.java rename module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/{ => rabbitmq}/docker/compose/RabbitEnvironmentTests.java (91%) create mode 100644 module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/health/AmqpRabbitHealthIndicatorTests.java rename smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/{ => rabbitmq}/SampleRabbitAmqpSimpleApplicationTests.java (91%) rename smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/{ => rabbitmq}/SampleRabbitAmqpSimpleApplication.java (82%) rename smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/{ => rabbitmq}/Sender.java (78%) rename smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/{ => rabbitmq}/package-info.java (95%) create mode 100644 starter/spring-boot-starter-amqp-rabbitmq-test/build.gradle diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc index 0194ea8b3d1a..f5de88d29eb6 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc @@ -1712,17 +1712,17 @@ * xref:reference:io/webservices.adoc#io.webservices[#boot-features-webservices] * xref:reference:io/webservices.adoc#io.webservices[#features.webservices] * xref:reference:io/webservices.adoc#io.webservices[#io.webservices] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#boot-features-rabbitmq] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#features.messaging.amqp.rabbit] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#messaging.amqp.rabbit] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#messaging.amqp.rabbitmq] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#boot-features-using-amqp-receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#features.messaging.amqp.receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#messaging.amqp.receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending-stream[#messaging.amqp.sending-stream] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#boot-features-using-amqp-sending] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#features.messaging.amqp.sending] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#messaging.amqp.sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09[#boot-features-rabbitmq] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09[#features.messaging.amqp.rabbit] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09[#messaging.amqp.rabbit] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09[#messaging.amqp.rabbitmq09] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09.receiving[#boot-features-using-amqp-receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09.receiving[#features.messaging.amqp.receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09.receiving[#messaging.amqp.receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09.sending-stream[#messaging.amqp.sending-stream] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09.sending[#boot-features-using-amqp-sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09.sending[#features.messaging.amqp.sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09.sending[#messaging.amqp.sending] * xref:reference:messaging/amqp.adoc#messaging.amqp[#boot-features-amqp] * xref:reference:messaging/amqp.adoc#messaging.amqp[#features.messaging.amqp] * xref:reference:messaging/amqp.adoc#messaging.amqp[#messaging.amqp] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index 235a7f0170bd..1718f0abaa7d 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc @@ -3,7 +3,14 @@ The Advanced Message Queuing Protocol (AMQP) is a platform-neutral, wire-level protocol for message-oriented middleware. The Spring AMQP project applies core Spring concepts to the development of AMQP-based messaging solutions. -Spring Boot offers several conveniences for working with AMQP: generic AMQP 1.0 support is provided by the `spring-boot-starter-amqp` starter while specific RabbitMQ support is available via the `spring-boot-starter-rabbitmq` starter. +Spring Boot provides auto-configuration for three different AMQP styles: + +* xref:messaging/amqp.adoc#messaging.amqp.generic[Generic AMQP 1.0] (`spring-boot-starter-amqp`) — broker-agnostic AMQP 1.0 support based on the Qpid ProtonJ2 client library. + Use this when portability across AMQP 1.0-compatible brokers such as ActiveMQ, Azure Service Bus, or RabbitMQ is important. +* xref:messaging/amqp.adoc#messaging.amqp.rabbitmq[RabbitMQ AMQP 1.0] (`spring-boot-starter-amqp-rabbitmq`) — RabbitMQ-specific support via the native https://github.com/rabbitmq/rabbitmq-amqp-java-client[RabbitMQ AMQP 1.0 Java client]. + Use this when your application targets RabbitMQ exclusively and you want to use the AMQP 1.0 protocol. +* xref:messaging/amqp.adoc#messaging.amqp.rabbitmq09[RabbitMQ AMQP 0.9] (`spring-boot-starter-rabbitmq`) — RabbitMQ support via the classic AMQP 0.9.1 protocol. + Use this for the most established and feature-rich RabbitMQ integration, including Streams support. @@ -77,9 +84,107 @@ Then you can use the factory in any javadoc:org.springframework.amqp.client.anno include-code::custom/MyBean[] - [[messaging.amqp.rabbitmq]] -== RabbitMQ Support +== RabbitMQ AMQP 1.0 Support + +Spring AMQP provides {url-spring-amqp-docs}/rabbitmq-amqp-client.html[RabbitMQ dedicated support for AMQP 1.0] via `org.springframework.amqp:spring-rabbitmq-client`. + +RabbitMQ AMQP 1.0 configuration is controlled by external configuration properties in `+spring.amqp.rabbitmq.*+`. +For example, you might declare the following section in `application.properties`: + +[configprops,yaml] +---- +spring: + amqp: + rabbitmq: + host: "localhost" + port: 5672 + username: "admin" + password: "secret" +---- + +Alternatively, you could configure the same connection using the `address` attribute: + +[configprops,yaml] +---- +spring: + amqp: + rabbitmq: + address: "amqp://admin:secret@localhost" +---- + +NOTE: When specifying an address that way, the `host` and `port` properties are ignored. +If the address uses the `amqps` scheme, an xref:features/ssl.adoc#features.ssl.bundles[SSL bundle] must be configured. + +See javadoc:org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitProperties[] for more of the supported property-based configuration options. +To configure lower-level details of the auto-configured javadoc:com.rabbitmq.client.amqp.Environment[], define a javadoc:org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpEnvironmentCustomizer[] bean. + +If a javadoc:com.rabbitmq.client.amqp.CredentialsProvider[] bean exists in the context, it will be automatically used by the auto-configured javadoc:com.rabbitmq.client.amqp.Environment[]. + + + +[[messaging.amqp.rabbitmq.ssl]] +=== SSL + +To use SSL with RabbitMQ AMQP 1.0, set configprop:spring.amqp.rabbitmq.ssl.bundle[] to configure the xref:features/ssl.adoc#features.ssl.bundles[SSL bundle] to use. + + + +[[messaging.amqp.rabbitmq.sending]] +=== Sending a Message + +Spring's javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate[] and javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin[] are auto-configured, and you can autowire them directly into your own beans, as shown in the following example: + +include-code::MyBean[] + +If a javadoc:org.springframework.amqp.support.converter.MessageConverter[] bean is defined, it is associated automatically to the auto-configured javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate[]. + +You can set properties for the template as follows: + +[configprops,yaml] +---- +spring: + amqp: + rabbitmq: + template: + exchange: "my-exchange" + routing-key: "my-key" + default-receive-queue: "my-queue" +---- + +To further configure the auto-configured javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate[], declare a javadoc:org.springframework.boot.amqp.rabbitmq.autoconfigure.RabbitAmqpTemplateCustomizer[] bean. + + + +[[messaging.amqp.rabbitmq.receiving]] +=== Receiving a Message + +When the RabbitMQ AMQP 1.0 infrastructure is present, any bean can be annotated with javadoc:org.springframework.amqp.rabbit.annotation.RabbitListener[format=annotation] to create a listener endpoint. +If no javadoc:org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory[] has been defined, a default one is automatically configured. +If a javadoc:org.springframework.amqp.support.converter.MessageConverter[] bean is defined, it is associated automatically with the default factory. + +The following sample component creates a listener endpoint on the `someQueue` queue: + +include-code::MyBean[] + +TIP: See javadoc:org.springframework.amqp.rabbit.annotation.EnableRabbit[format=annotation] for more details. + +If you need to create more javadoc:org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.amqp.rabbitmq.autoconfigure.RabbitAmqpListenerContainerFactoryConfigurer[] that you can use to initialize a javadoc:org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory[] with the same settings as the factory used by the auto-configuration. + +For instance, the following configuration class exposes another factory that uses a specific javadoc:org.springframework.amqp.support.converter.MessageConverter[]: + +include-code::custom/MyRabbitAmqpConfiguration[] + +Then you can use the factory in any javadoc:org.springframework.amqp.rabbit.annotation.RabbitListener[format=annotation]-annotated method, as follows: + +include-code::custom/MyBean[] + +To customize the listener container, define a javadoc:org.springframework.amqp.rabbit.config.ContainerCustomizer[] bean parameterized with javadoc:org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer[]. + + + +[[messaging.amqp.rabbitmq09]] +== RabbitMQ AMQP 0.9 Support https://www.rabbitmq.com/[RabbitMQ] is a lightweight, reliable, scalable, and portable message broker based on the AMQP protocol. Spring uses RabbitMQ to communicate through the AMQP protocol. @@ -120,7 +225,7 @@ TIP: See https://spring.io/blog/2010/06/14/understanding-amqp-the-protocol-used- -[[messaging.amqp.rabbitmq.sending]] +[[messaging.amqp.rabbitmq09.sending]] === Sending a Message Spring's javadoc:org.springframework.amqp.core.AmqpTemplate[] and javadoc:org.springframework.amqp.core.AmqpAdmin[] are auto-configured, and you can autowire them directly into your own beans, as shown in the following example: @@ -153,7 +258,7 @@ If there's a bean of type javadoc:org.springframework.amqp.rabbit.support.microm -[[messaging.amqp.rabbitmq.sending-stream]] +[[messaging.amqp.rabbitmq09.sending-stream]] === Sending a Message To A Stream To send a message to a particular stream, specify the name of the stream, as shown in the following example: @@ -172,13 +277,13 @@ If you need to create more javadoc:org.springframework.rabbit.stream.producer.Ra -[[messaging.amqp.rabbitmq.sending-stream.ssl]] +[[messaging.amqp.rabbitmq09.sending-stream.ssl]] ==== SSL To use SSL with RabbitMQ Streams, set configprop:spring.rabbitmq.stream.ssl.enabled[] to `true` or set configprop:spring.rabbitmq.stream.ssl.bundle[] to configure the xref:features/ssl.adoc#features.ssl.bundles[SSL bundle] to use. -[[messaging.amqp.rabbitmq.receiving]] +[[messaging.amqp.rabbitmq09.receiving]] === Receiving a Message When the Rabbit infrastructure is present, any bean can be annotated with javadoc:org.springframework.amqp.rabbit.annotation.RabbitListener[format=annotation] to create a listener endpoint. @@ -213,85 +318,3 @@ You can also customize the javadoc:org.springframework.core.retry.RetryPolicy[] IMPORTANT: By default, if retries are disabled and the listener throws an exception, the delivery is retried indefinitely. You can modify this behavior in two ways: Set the `defaultRequeueRejected` property to `false` so that zero re-deliveries are attempted or throw an javadoc:org.springframework.amqp.AmqpRejectAndDontRequeueException[] to signal the message should be rejected. The latter is the mechanism used when retries are enabled and the maximum number of delivery attempts is reached. - - - -[[messaging.amqp.rabbitmq-amqp]] -== RabbitMQ AMQP 1.0 Support - -https://www.rabbitmq.com/[RabbitMQ] supports the AMQP 1.0 protocol via the https://github.com/rabbitmq/rabbitmq-amqp-java-client[RabbitMQ AMQP 1.0 Java client]. -Spring AMQP provides support for this client through `spring-rabbitmq-client`. -Spring Boot offers auto-configuration for this support via the `spring-boot-starter-amqp-rabbitmq` starter. - -RabbitMQ AMQP 1.0 configuration is controlled by external configuration properties in `+spring.amqp.rabbitmq.*+`. -For example, you might declare the following section in `application.properties`: - -[configprops,yaml] ----- -spring: - amqp: - rabbitmq: - host: "localhost" - port: 5672 - username: "admin" - password: "secret" ----- - -Alternatively, you could configure the same connection using the `address` attribute: - -[configprops,yaml] ----- -spring: - amqp: - rabbitmq: - address: "amqp://admin:secret@localhost" ----- - -NOTE: When specifying an address that way, the `host` and `port` properties are ignored. - -See javadoc:org.springframework.boot.amqp.autoconfigure.RabbitAmqpProperties[] for more of the supported property-based configuration options. -To configure lower-level details of the auto-configured javadoc:com.rabbitmq.client.amqp.Environment[], define a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitAmqpEnvironmentBuilderCustomizer[] bean. - -If a javadoc:com.rabbitmq.client.amqp.CredentialsProvider[] bean exists in the context, it will be automatically used by the auto-configured javadoc:com.rabbitmq.client.amqp.Environment[]. - - - -[[messaging.amqp.rabbitmq-amqp.sending]] -=== Sending a Message - -Spring's javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate[] and javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin[] are auto-configured, and you can autowire them directly into your own beans, as shown in the following example: - -include-code::MyBean[] - -If a javadoc:org.springframework.amqp.support.converter.MessageConverter[] bean is defined, it is associated automatically to the auto-configured javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate[]. - -You can set properties for the template as follows: - -[configprops,yaml] ----- -spring: - amqp: - rabbitmq: - template: - exchange: "my-exchange" - routing-key: "my-key" - default-receive-queue: "my-queue" ----- - -You can also customize the javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate[] programmatically by declaring a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitAmqpTemplateCustomizer[] bean. - - - -[[messaging.amqp.rabbitmq-amqp.receiving]] -=== Receiving a Message - -When the RabbitMQ AMQP 1.0 infrastructure is present, any bean can be annotated with javadoc:org.springframework.amqp.rabbit.annotation.RabbitListener[format=annotation] to create a listener endpoint. -If no javadoc:org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory[] has been defined, a default one is automatically configured. - -The following sample component creates a listener endpoint on the `someQueue` queue: - -include-code::MyBean[] - -TIP: See javadoc:org.springframework.amqp.rabbit.annotation.EnableRabbit[format=annotation] for more details. - -To customize the listener container, define a javadoc:org.springframework.amqp.rabbit.config.ContainerCustomizer[] bean parameterized with javadoc:org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer[]. diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java index 56064bfb94ce..1d224b158299 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java @@ -16,13 +16,15 @@ package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving; +import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component +@RabbitListener(queues = "someQueue") public class MyBean { - @RabbitListener(queues = "someQueue") + @RabbitHandler public void processMessage(String content) { // ... } diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitAmqpConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitAmqpConfiguration.java new file mode 100644 index 000000000000..ce801674115a --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitAmqpConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.rabbitmq.receiving.custom; + +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.RabbitAmqpListenerContainerFactoryConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyRabbitAmqpConfiguration { + + @Bean + public RabbitAmqpListenerContainerFactory myFactory(RabbitAmqpListenerContainerFactoryConfigurer configurer, + AmqpConnectionFactory connectionFactory) { + RabbitAmqpListenerContainerFactory factory = new RabbitAmqpListenerContainerFactory(connectionFactory); + configurer.configure(factory); + factory.setMessageConverter(new MyMessageConverter()); + return factory; + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java index 51b91d35769b..908114897136 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java @@ -16,27 +16,23 @@ package org.springframework.boot.docs.messaging.amqp.rabbitmq.sending; -import org.springframework.amqp.core.AmqpAdmin; -import org.springframework.amqp.core.AmqpTemplate; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; import org.springframework.stereotype.Component; @Component public class MyBean { - private final AmqpAdmin amqpAdmin; + private final RabbitAmqpAdmin amqpAdmin; - private final AmqpTemplate amqpTemplate; + private final RabbitAmqpTemplate amqpTemplate; - public MyBean(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate) { + public MyBean(RabbitAmqpAdmin amqpAdmin, RabbitAmqpTemplate amqpTemplate) { this.amqpAdmin = amqpAdmin; this.amqpTemplate = amqpTemplate; } // @fold:on // ... - public void someMethod() { - this.amqpAdmin.getQueueInfo("someQueue"); - } - public void someOtherMethod() { this.amqpTemplate.convertAndSend("hello"); } diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/receiving/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/MyBean.java similarity index 81% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/receiving/MyBean.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/MyBean.java index 83ac5b882b0a..6d44d39ec2ea 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/receiving/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/MyBean.java @@ -14,17 +14,15 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmqamqp.receiving; +package org.springframework.boot.docs.messaging.amqp.rabbitmq09.receiving; -import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component -@RabbitListener(queues = "someQueue") public class MyBean { - @RabbitHandler + @RabbitListener(queues = "someQueue") public void processMessage(String content) { // ... } diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyBean.java new file mode 100644 index 000000000000..35031b330ccb --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.rabbitmq09.receiving.custom; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @RabbitListener(queues = "someQueue", containerFactory = "myFactory") + public void processMessage(String content) { + // ... + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyMessageConverter.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyMessageConverter.java new file mode 100644 index 000000000000..e63133f58d08 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyMessageConverter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.rabbitmq09.receiving.custom; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.support.converter.MessageConverter; + +class MyMessageConverter implements MessageConverter { + + @Override + public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { + return null; + } + + @Override + public Object fromMessage(Message message) throws MessageConversionException { + return null; + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyRabbitConfiguration.java similarity index 95% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyRabbitConfiguration.java index 318428b08be0..f47e0d4d1f91 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyRabbitConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom; +package org.springframework.boot.docs.messaging.amqp.rabbitmq09.receiving.custom; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/sending/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/sending/MyBean.java similarity index 69% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/sending/MyBean.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/sending/MyBean.java index a05a238bf68d..a4247919fc6e 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/sending/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/sending/MyBean.java @@ -14,25 +14,29 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmqamqp.sending; +package org.springframework.boot.docs.messaging.amqp.rabbitmq09.sending; -import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin; -import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.AmqpTemplate; import org.springframework.stereotype.Component; @Component public class MyBean { - private final RabbitAmqpAdmin amqpAdmin; + private final AmqpAdmin amqpAdmin; - private final RabbitAmqpTemplate amqpTemplate; + private final AmqpTemplate amqpTemplate; - public MyBean(RabbitAmqpAdmin amqpAdmin, RabbitAmqpTemplate amqpTemplate) { + public MyBean(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate) { this.amqpAdmin = amqpAdmin; this.amqpTemplate = amqpTemplate; } // @fold:on // ... + public void someMethod() { + this.amqpAdmin.getQueueInfo("someQueue"); + } + public void someOtherMethod() { this.amqpTemplate.convertAndSend("hello"); } diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt index 9bac053496d2..a477cb2d84ca 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt @@ -16,17 +16,18 @@ package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving +import org.springframework.amqp.rabbit.annotation.RabbitHandler import org.springframework.amqp.rabbit.annotation.RabbitListener import org.springframework.stereotype.Component @Suppress("UNUSED_PARAMETER") @Component +@RabbitListener(queues = ["someQueue"]) class MyBean { - @RabbitListener(queues = ["someQueue"]) + @RabbitHandler fun processMessage(content: String?) { // ... } } - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt index 3b90bf3dc42d..41e3ab585b02 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt @@ -29,4 +29,3 @@ class MyBean { } } - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt index 03b165448e08..971452683e73 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt @@ -31,4 +31,3 @@ internal class MyMessageConverter : MessageConverter { } } - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitAmqpConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitAmqpConfiguration.kt new file mode 100644 index 000000000000..95249b2409cb --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitAmqpConfiguration.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.rabbitmq.receiving.custom + +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory +import org.springframework.boot.amqp.rabbitmq.autoconfigure.RabbitAmqpListenerContainerFactoryConfigurer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration(proxyBeanMethods = false) +class MyRabbitAmqpConfiguration { + + @Bean + fun myFactory( + configurer: RabbitAmqpListenerContainerFactoryConfigurer, + connectionFactory: AmqpConnectionFactory + ): RabbitAmqpListenerContainerFactory { + val factory = RabbitAmqpListenerContainerFactory(connectionFactory) + configurer.configure(factory) + factory.setMessageConverter(MyMessageConverter()) + return factory + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt index 6d21de9bce0a..cf1223d7105f 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt @@ -16,22 +16,17 @@ package org.springframework.boot.docs.messaging.amqp.rabbitmq.sending -import org.springframework.amqp.core.AmqpAdmin -import org.springframework.amqp.core.AmqpTemplate +import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate import org.springframework.stereotype.Component @Component -class MyBean(private val amqpAdmin: AmqpAdmin, private val amqpTemplate: AmqpTemplate) { +class MyBean(private val amqpAdmin: RabbitAmqpAdmin, private val amqpTemplate: RabbitAmqpTemplate) { // @fold:on // ... - fun someMethod() { - amqpAdmin.getQueueInfo("someQueue") - } - fun someOtherMethod() { amqpTemplate.convertAndSend("hello") } // @fold:off } - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/MyBean.kt new file mode 100644 index 000000000000..99244d15b01c --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/MyBean.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.rabbitmq09.receiving + +import org.springframework.amqp.rabbit.annotation.RabbitListener +import org.springframework.stereotype.Component + +@Suppress("UNUSED_PARAMETER") +@Component +class MyBean { + + @RabbitListener(queues = ["someQueue"]) + fun processMessage(content: String?) { + // ... + } + +} + diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyBean.kt new file mode 100644 index 000000000000..b119e83ccdfa --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyBean.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.rabbitmq09.receiving.custom + +import org.springframework.amqp.rabbit.annotation.RabbitListener +import org.springframework.stereotype.Component + +@Suppress("UNUSED_PARAMETER") +@Component +class MyBean { + + @RabbitListener(queues = ["someQueue"], containerFactory = "myFactory") + fun processMessage(content: String?) { + // ... + } + +} + diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyMessageConverter.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyMessageConverter.kt new file mode 100644 index 000000000000..0005bbb328e2 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyMessageConverter.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.rabbitmq09.receiving.custom + +import org.springframework.amqp.core.Message +import org.springframework.amqp.core.MessageProperties +import org.springframework.amqp.support.converter.MessageConverter + +internal class MyMessageConverter : MessageConverter { + + override fun toMessage(`object`: Any, messageProperties: MessageProperties): Message { + return Message(byteArrayOf()) + } + + override fun fromMessage(message: Message): Any { + return Any() + } + +} + diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyRabbitConfiguration.kt similarity index 95% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyRabbitConfiguration.kt index 900866bf2464..17d5ced6dd09 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyRabbitConfiguration.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom +package org.springframework.boot.docs.messaging.amqp.rabbitmq09.receiving.custom import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory import org.springframework.amqp.rabbit.connection.CachingConnectionFactory diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/sending/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/sending/MyBean.kt new file mode 100644 index 000000000000..07dbdff26adf --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/sending/MyBean.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.amqp.rabbitmq09.sending + +import org.springframework.amqp.core.AmqpAdmin +import org.springframework.amqp.core.AmqpTemplate +import org.springframework.stereotype.Component + +@Component +class MyBean(private val amqpAdmin: AmqpAdmin, private val amqpTemplate: AmqpTemplate) { + + // @fold:on // ... + fun someMethod() { + amqpAdmin.getQueueInfo("someQueue") + } + + fun someOtherMethod() { + amqpTemplate.convertAndSend("hello") + } + // @fold:off + +} + diff --git a/module/spring-boot-amqp-rabbitmq/build.gradle b/module/spring-boot-amqp-rabbitmq/build.gradle index edd172e77c6b..827a5f4a7554 100644 --- a/module/spring-boot-amqp-rabbitmq/build.gradle +++ b/module/spring-boot-amqp-rabbitmq/build.gradle @@ -23,7 +23,7 @@ plugins { id "org.springframework.boot.optional-dependencies" } -description = "Spring Boot AMQP" +description = "Spring Boot AMQP 1.0 support for RabbitMQ" dependencies { api(project(":core:spring-boot")) @@ -38,9 +38,6 @@ dependencies { optional(project(":core:spring-boot-docker-compose")) optional(project(":core:spring-boot-testcontainers")) optional(project(":module:spring-boot-health")) - optional(project(":module:spring-boot-micrometer-metrics")) - optional("io.micrometer:micrometer-core") - optional("org.springframework.amqp:spring-rabbit-stream") optional("org.testcontainers:testcontainers-rabbitmq") dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/docker/compose/AmqpRabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 51% rename from module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/docker/compose/AmqpRabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java index 77baad2a4bf4..3a4cfbd8b2d7 100644 --- a/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/docker/compose/AmqpRabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -14,17 +14,18 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.amqp.rabbitmq.docker.compose; -import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails; -import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails.Address; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails.Address; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory}. + * Integration tests for {@link AmqpRabbitMqDockerComposeConnectionDetailsFactory}. * * @author Moritz Halbritter * @author Andy Wilkinson @@ -32,14 +33,27 @@ * @author Scott Frederick * @author Eddú Meléndez */ -class RabbitDockerComposeConnectionDetailsFactoryIntegrationTests { +class AmqpRabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests { - @DockerComposeTest(composeFile = "rabbit-compose.yaml", image = TestImage.RABBITMQ) - void runCreatesConnectionDetails(RabbitAmqpConnectionDetails connectionDetails) { + @DockerComposeTest(composeFile = "rabbitmq-compose.yaml", image = TestImage.RABBITMQ) + void runCreatesConnectionDetails(AmqpRabbitConnectionDetails connectionDetails) { + assertConnectionDetails(connectionDetails); + assertThat(connectionDetails.getSslBundle()).isNull(); + } + + @DockerComposeTest(composeFile = "rabbitmq-ssl-compose.yaml", image = TestImage.RABBITMQ, + additionalResources = { "../../ca.crt", "../../server.crt", "../../server.key", "../../client.crt", + "../../client.key", "rabbitmq-ssl.conf" }) + void runWithSslCreatesConnectionDetails(AmqpRabbitConnectionDetails connectionDetails) { + assertConnectionDetails(connectionDetails); + SslBundle sslBundle = connectionDetails.getSslBundle(); + assertThat(sslBundle).isNotNull(); + } + + private void assertConnectionDetails(AmqpRabbitConnectionDetails connectionDetails) { assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); assertThat(connectionDetails.getPassword()).isEqualTo("secret"); assertThat(connectionDetails.getVirtualHost()).isEqualTo("/"); - assertThat(connectionDetails.getAddress()).isNotNull(); Address address = connectionDetails.getAddress(); assertThat(address.host()).isNotNull(); assertThat(address.port()).isGreaterThan(0); diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerConnectionDetailsFactoryIntegrationTests.java similarity index 84% rename from module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java rename to module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerConnectionDetailsFactoryIntegrationTests.java index 34487fddcf6e..cbab101ebcaa 100644 --- a/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerConnectionDetailsFactoryIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.amqp.rabbitmq.testcontainers; import java.time.Duration; import java.util.ArrayList; @@ -30,8 +30,8 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.amqp.autoconfigure.RabbitAmqpAutoConfiguration; -import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitAutoConfiguration; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; @@ -42,7 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link RabbitContainerConnectionDetailsFactory}. + * Tests for {@link AmqpRabbitMqContainerConnectionDetailsFactory}. * * @author Moritz Halbritter * @author Andy Wilkinson @@ -51,14 +51,14 @@ */ @SpringJUnitConfig @Testcontainers(disabledWithoutDocker = true) -class RabbitContainerConnectionDetailsFactoryIntegrationTests { +class AmqpRabbitMqContainerConnectionDetailsFactoryIntegrationTests { @Container @ServiceConnection static final RabbitMQContainer rabbit = TestImage.container(RabbitMQContainer.class); @Autowired(required = false) - private RabbitAmqpConnectionDetails connectionDetails; + private AmqpRabbitConnectionDetails connectionDetails; @Autowired private RabbitAmqpTemplate rabbitAmqpTemplate; @@ -75,7 +75,7 @@ void connectionCanBeMadeToRabbitContainer() { } @Configuration(proxyBeanMethods = false) - @ImportAutoConfiguration(RabbitAmqpAutoConfiguration.class) + @ImportAutoConfiguration(AmqpRabbitAutoConfiguration.class) static class TestConfiguration { @Bean diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerWithSslConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerWithSslConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..9517219297ef --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerWithSslConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.rabbitmq.testcontainers; + +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.testcontainers.rabbitmq.RabbitMQContainer; +import org.testcontainers.utility.MountableFile; + +import org.springframework.amqp.rabbit.annotation.Queue; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitAutoConfiguration; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.PemKeyStore; +import org.springframework.boot.testcontainers.service.connection.PemTrustStore; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.TestImage; +import org.springframework.context.annotation.Bean; +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 AmqpRabbitMqContainerConnectionDetailsFactory} with SSL. + * + * @author Stephane Nicoll + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class AmqpRabbitMqContainerWithSslConnectionDetailsFactoryIntegrationTests { + + private static final int RABBITMQ_TLS_PORT = 5671; + + @Container + @ServiceConnection + @PemTrustStore(certificate = "classpath:org/springframework/boot/amqp/rabbitmq/ca.crt") + @PemKeyStore(certificate = "classpath:org/springframework/boot/amqp/rabbitmq/client.crt", + privateKey = "classpath:org/springframework/boot/amqp/rabbitmq/client.key") + static final RabbitMQContainer rabbit = getRabbitMqContainer(); + + private static RabbitMQContainer getRabbitMqContainer() { + RabbitMQContainer container = TestImage.container(RabbitMQContainer.class); + container.addExposedPorts(RABBITMQ_TLS_PORT); + container.withCopyFileToContainer( + MountableFile + .forClasspathResource("org/springframework/boot/amqp/rabbitmq/testcontainers/rabbitmq-ssl.conf"), + "/etc/rabbitmq/rabbitmq.conf"); + container.withCopyFileToContainer( + MountableFile.forClasspathResource("org/springframework/boot/amqp/rabbitmq/ca.crt"), + "/etc/rabbitmq/ca.crt"); + container.withCopyFileToContainer( + MountableFile.forClasspathResource("org/springframework/boot/amqp/rabbitmq/server.key"), + "/etc/rabbitmq/server.key"); + container.withCopyFileToContainer( + MountableFile.forClasspathResource("org/springframework/boot/amqp/rabbitmq/server.crt"), + "/etc/rabbitmq/server.crt"); + return container; + } + + @Autowired(required = false) + private AmqpRabbitConnectionDetails connectionDetails; + + @Autowired + private RabbitAmqpTemplate rabbitAmqpTemplate; + + @Autowired + private TestListener listener; + + @Test + void connectionCanBeMadeToRabbitContainerWithSsl() { + assertThat(this.connectionDetails).isNotNull(); + assertThat(this.connectionDetails.getSslBundle()).isNotNull(); + this.rabbitAmqpTemplate.convertAndSend("test", "message"); + Awaitility.waitAtMost(Duration.ofMinutes(4)) + .untilAsserted(() -> assertThat(this.listener.messages).containsExactly("message")); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(AmqpRabbitAutoConfiguration.class) + static class TestConfiguration { + + @Bean + TestListener testListener() { + return new TestListener(); + } + + } + + static class TestListener { + + private final List messages = new ArrayList<>(); + + @RabbitListener(queuesToDeclare = @Queue("test")) + void processMessage(String message) { + this.messages.add(message); + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/ca.crt b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/ca.crt new file mode 100644 index 000000000000..beed250b132b --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/ca.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFhjCCA26gAwIBAgIUfIkk29IT9OpbgfjL8oRIPSLjUcAwDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDUwMTE2NTMyNVoXDTM0MDQyOTE2NTMyNVow +OzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNh +dGUgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAusN2 +KzQQUUxZSiI3ZZuZohFwq2KXSUNPdJ6rgD3/YKNTDSZXKZPO53kYPP0DXf0sm3CH +cyWSWVabyimZYuPWena1MElSL4ZpJ9WwkZoOQ3bPFK1utz6kMOwrgAUcky8H/rIK +j2JEBhkSHUIGr57NjUEwG1ygaSerM8RzWw1PtMq+C8LOu3v94qzE3NDg1QRpyvV9 +OmsLsjISd0ZmAJNi9vmiEH923KnPyiqnQmWKpYicdgQmX1GXylS22jZqAwaOkYGj +X8UdeyvrohkZkM0hn9uaSufQGEW4yKACn3PkjJtzi8drBIyjIi9YcAzBxZB9oVKq +XZMlltgO2fDMmIJi0Ngt0Ci7fCoEMqSocKyDKML6YLr9UWtx4bfsrk+rVO9Q/D/v +8RKgstv7dCf2KWRX3ZJEC0IBHS5gLNq0qqqVcGx3LcSyhdiKJOtSwAnNkHMh+jSQ +xLSlBjcSqTPiGTRK/Rddl+xnU/mBgk7ZBGNrUFaD5McMFjddS7Ih82aHnpQ1gekW +nUGv+Tm/G68h2BvZ5U2q+RfeOCgRW9i/AYW2jgT7IFnfjyUXgBQveauMAchomqFE +VLe95ZgViF6vmH34EKo3w9L5TQiwk/r53YlM7TSOTyDqx66t4zGYDsVMicpKmzi4 +2Rp8EpErARRyREUIKSvWs9O9+uT3+7arNLgHe5ECAwEAAaOBgTB/MB0GA1UdDgQW +BBRVMLDVqPECWaH6GruL9E52VcTrPjAfBgNVHSMEGDAWgBRVMLDVqPECWaH6GruL +9E52VcTrPjAPBgNVHRMBAf8EBTADAQH/MCwGA1UdEQQlMCOCC2V4YW1wbGUuY29t +gglsb2NhbGhvc3SCCTEyNy4wLjAuMTANBgkqhkiG9w0BAQsFAAOCAgEAeSpjCL3j +2GIFBNKr/5amLOYa0kZ6r1dJs+K6xvMsUvsBJ/QQsV5nYDMIoV/NYUd8SyYV4lEj +7LHX5ZbmJrvPk30LGEBG/5Vy2MIATrQrQ14S4nXtEdSnBvTQwPOOaHc+2dTp3YpM +f4ffELKWyispTifx1eqdiUJhURKeQBh+3W7zpyaiN4vJaqEDKGgFQtHA/OyZL2hZ +BpxHB0zpb2iDHV8MeyfOT7HQWUk6p13vdYm6EnyJT8fzWvE+TqYNbqFmB+CLRSXy +R3p1yaeTd4LnVknJ0UBKqEyul3ziHZDhKhBpwdglYOQz4eWjSFhikX9XZ8NaI38Q +QqLZVn0DsH2ztkjrQrUVgK2xn4aUuqoLDk4Hu6h5baUn+f2GLuzx+EXc/i3ikYvw +Y3JyufOgw6nGGFG+/QXEj85XtLPhN7Wm42z2e/BGzi0MLl65sfpEDXvFTA72Yzws +OYaeg/HxeYwUHQgs2fKl/LgV4chntSCvTqfNl6OnQafD/ISJNpx3xWR3HwF+ypFG +UaLE+e1soqEJbzL31U/6pypHLsj8Y8r9hJbZXo2ibnhjFV6fypUAP0rbIzaoWcrJ +T0Sbliz+KQTMzCcubiAi4bI/kZ5FJ4kkaHqUpIWzlx1h2WVJ65ASFDjBWb8eVmB6 +Dyno/RVFR/rUL5091gjGRXhLsi1oUHKdEzU= +-----END CERTIFICATE----- diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/client.crt b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/client.crt new file mode 100644 index 000000000000..811d880fcbd3 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/client.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEWjCCAkKgAwIBAgIURBZvq442tp+/K9TZII5Vy/LzVx0wDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDUwMTE2NTMyNVoXDTM0MDQyOTE2NTMyNVow +LzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDESMBAGA1UEAwwJbG9jYWxob3N0 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvGb7tu0odSuOjeY1lHlh +sRR4PayAvlryjfrrp49hjoVTiL3d/Jo6Po5HlqwJcYuclm0EWQR5Vur/zYJpfUE7 +b8+E9Qwe50+YzfQ2tVFEdq/VfqemrYRGee+pMelOCI90enOKCxfpo6EHbz+WnUP0 +mnD8OAF9QpolSdWAMOGJoPdWX65KQvyMXvQbj9VIHmsx7NCaIOYxjHXB/dI2FmXV ++m4VT6mb8he9dXmgK/ozMq6XIPOAXe0n3dlfMTSEddeNeVwnBpr/n5e0cpwGFhdf +NNu5CI4ecipBhXljJi/4/47M/6hd69HwE05C4zyH4ZDZ2JTfaSKOLV+jYdBUqJP5 +dwIDAQABo2IwYDALBgNVHQ8EBAMCBaAwEQYJYIZIAYb4QgEBBAQDAgeAMB0GA1Ud +DgQWBBRWiWOo9cm2IF/ZlhWLVjifLzYa/DAfBgNVHSMEGDAWgBRVMLDVqPECWaH6 +GruL9E52VcTrPjANBgkqhkiG9w0BAQsFAAOCAgEAA5Wphtu2nBhY+QNOBOwXq4zF +N5qt2IYTLfR7xqpKhhXx9VkIjdPWpcsGuCuMmfPVNvQWE6iK0/jMMqToTj4H6K7e +MN74j0GwwcknT1P42tUzEpg8LKR8VMdhWhyqdniCDNWWuaz1iVSoF0S2i4jFSzH5 +1q3KMKMZ4niK5aJI0fAGa4fCjyuun1Mfg/qGBGwLnqDkIXjeAopZf4Jb64TtzjAs +j9NT6mYbe3E0tw3fHT9ihYdbZDZgSjeCsuq9OiRMVb0DWWmRoLmmOrlN8IJlHV/3 +WyI/ta4Cw5EZ0oaOg0lIyOxXyvElth1xIvh+kdqZSBsU0gNBri6ZIzYbbTh2KTTO +BJHQt9L5naWG27pDrIxBicWXS/MIYonktm3YgCLfuW3kWcVk8bIlNhfcoAYBBgfM +IEYSYEq+bH2IQ+YoWQz3AxjJ8gEuuSUP6R6mYY65FfpjkKgcpGBvw4EIAmqKDtPS +hlLY/F0XVj9KZzrMyH4/vonu+DAb/P7Zmt2fyk/dQO6bAc3ltRmJbJm4VJ2v/T8I +LVu2FtcUYgtLNtkWUPfdb3GSUUgkKlUpWSty31TKSUszJjW1oRykQhEko6o5U3S8 +ptQzXdApsb1lGOqewkubE25tIu2RLiNkKcjFOjJ/lu0vP9k76wWwRVnFLFvfo4lW +pgywiOifs5JbcCt0ZQ0= +-----END CERTIFICATE----- diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/client.key b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/client.key new file mode 100644 index 000000000000..2ae0f49bf4a4 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8Zvu27Sh1K46N +5jWUeWGxFHg9rIC+WvKN+uunj2GOhVOIvd38mjo+jkeWrAlxi5yWbQRZBHlW6v/N +gml9QTtvz4T1DB7nT5jN9Da1UUR2r9V+p6athEZ576kx6U4Ij3R6c4oLF+mjoQdv +P5adQ/SacPw4AX1CmiVJ1YAw4Ymg91ZfrkpC/Ixe9BuP1UgeazHs0Jog5jGMdcH9 +0jYWZdX6bhVPqZvyF711eaAr+jMyrpcg84Bd7Sfd2V8xNIR11415XCcGmv+fl7Ry +nAYWF18027kIjh5yKkGFeWMmL/j/jsz/qF3r0fATTkLjPIfhkNnYlN9pIo4tX6Nh +0FSok/l3AgMBAAECggEABXnBe3MwXAMQENzNypOiXK4VE3XMYkePfdsSK163byOD +w3ZeTgQNfU4g8LJK8/homzO0SQIJAdz2+ZFbpsp4A2W2zJ+1jvN5RuX/8/UcVhmk +tb1IL/LWCvx5/aoYBWkgIA70UfQJa2jDbdM0v5j/Gu9yE7GI14jh6DFC3xGMGV3b +fOwManxf7sDibCI1nGjnFYNGxninRr+tpb+a1KNbVzhett68LrgPmtph6B3HCPAJ +zBigk1Phgb8WHozTXxnLyw9/RdKJ0Ro4PFmtQv0EvCSlytptnF+0nXkqr3f851XS +bUWwYFchIFWPMhPfD5B3niNWCV42/sU/bQlk+BMQAQKBgQD6NvMq8EdYy2Y7fXT5 +FgB4s+7EkLgI2d5LUaCXCFgc6iZtCTQKUXj1rIWeRfGrFVCCe8qV+XIMKt/G5eEi +tn5ifHhktA2A8GK1scj026qHP3bVn0hMaUnkCF1UpDRKPiEO5G/apPtav8PbCNaX +GAimLGw+WZNZuv7+T33bEBeUdwKBgQDAwiidayLXkRkz2deefdDKcXQsB7RHFGGy +vfZPBCGqizxml+6ojJkkDsVUKL1IXFfyK9KpQAI6tezn4oktgu4jAQqkYY7QZobs +RpQx1dR+KxEm7ISDBTq/B1Q9cFKUKVvQQy8N2pnIbCdzb6MTOKLmJqFGTjr+5T8q +F32B5vkDAQKBgDCKfH42AwFc5EZiPlEcTZcdARMtKCa/bXqbKVZjjgR+AFpi0K+3 +womWoI1l8E5KYkYOEe0qaU+m+aaybgy37qjYkNqoe34qJFwvU1b9ToXScBFdRz9b +pbQRU1naSTKl/u/OrUxzeTfPwAU8H7VMOlFSiOVHp2he+J0JetcGtixdAoGBAIJQ +QMj7rxhxHcqyEVUy1b6nKNTDeJs9Kjd+uU/+CQyVCQaK3GvScY2w9rLIv/51f3dX +LRoDDf7HExxJSFgeVgQQJjOvSK+XQMvngzSVzQxm7TeVWpiBJpAS0l6e2xUTSODp +KpyBFsoqZBlkdaj+9xIFN66iILxGG4fHTbBOiDYBAoGBAOZMKjM5N/hGcCmik/6t +p/zBA2pN9O6zwPndITTsdyVWSlVqCZhXlRX47CerAN+/WVCidlh7Vp5Tuy75Wa77 +v16IDLO01txgWNobcLaM4VgFsyLi5JuxK73S18Vb1cKWdHFRF0LH3cUIq20fjpv6 +Odl4vjNOncXMZCLPHQ+bKWaf +-----END PRIVATE KEY----- diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-compose.yaml similarity index 100% rename from module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml rename to module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-compose.yaml diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-ssl-compose.yaml b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-ssl-compose.yaml new file mode 100644 index 000000000000..69179aa1efed --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-ssl-compose.yaml @@ -0,0 +1,26 @@ +services: + rabbitmq: + image: '{imageName}' + environment: + - 'RABBITMQ_DEFAULT_USER=myuser' + - 'RABBITMQ_DEFAULT_PASS=secret' + ports: + - '5672' + - '5671' + secrets: + - ssl-ca + - ssl-key + - ssl-cert + volumes: + - ./rabbitmq-ssl.conf:/etc/rabbitmq/rabbitmq.conf:ro + labels: + - 'org.springframework.boot.sslbundle.pem.keystore.certificate=client.crt' + - 'org.springframework.boot.sslbundle.pem.keystore.private-key=client.key' + - 'org.springframework.boot.sslbundle.pem.truststore.certificate=ca.crt' +secrets: + ssl-ca: + file: 'ca.crt' + ssl-key: + file: 'server.key' + ssl-cert: + file: 'server.crt' diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-ssl.conf b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-ssl.conf new file mode 100644 index 000000000000..55b842131ca6 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-ssl.conf @@ -0,0 +1,8 @@ +listeners.ssl.default=5671 + +ssl_options.cacertfile=/run/secrets/ssl-ca +ssl_options.certfile=/run/secrets/ssl-cert +ssl_options.keyfile=/run/secrets/ssl-key + +ssl_options.verify=verify_peer +ssl_options.fail_if_no_peer_cert=true diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/server.crt b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/server.crt new file mode 100644 index 000000000000..57c66cc78a3b --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/server.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEWjCCAkKgAwIBAgIURBZvq442tp+/K9TZII5Vy/LzVxwwDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDUwMTE2NTMyNVoXDTM0MDQyOTE2NTMyNVow +LzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDESMBAGA1UEAwwJbG9jYWxob3N0 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsllxsSQzTTJlNHMfXC2b +CIXCPsfCgCBl7FbPz828jwJk+EYcXh0+WTFGks0WxSwb8NQza5UtyCUDEueZj9fV +j5mWBY97WCu01Sl/3xClHmYisXfyyv27GKec7PaSOurCm2JDkyHRNumiJROa4jte +N0GOHzw7FYsM3779TuNw14/gtW+eBrGnvgrpU7fbUvx42Di6ftGYQUwIi+3uIaqT +//i7ktDMaAQJtkL6haTzZ5JN2qKO5a34/WRz/ApvPw3lpDV8c4qoTk3C0Bg9MP+a +DnZtjtLBSN9CJWwr+n11QaMgHTotEKsOahGdi3J2zYxCvJP0LT+hjN2O9aRzSMIs +MwIDAQABo2IwYDALBgNVHQ8EBAMCBaAwEQYJYIZIAYb4QgEBBAQDAgZAMB0GA1Ud +DgQWBBS9XQHGwJZhG0olAGM1UMNuwZ65DzAfBgNVHSMEGDAWgBRVMLDVqPECWaH6 +GruL9E52VcTrPjANBgkqhkiG9w0BAQsFAAOCAgEAhBcqm5UQahn8iFMETXvfLMR6 +OOPijsHQ5lVfhig08s46a9O5eaJ9EYSYyiDnxYvZ4gYVH03f/kPwNLamvGR5KIBQ +R0DltkPPX4a11/vjwlSq1cXAt9r59nY+sNcVXWgIWH7zNodL8lyTpYhqvB2wEQkx +t2/JKZ8A0sGjed4S6I5HofYd7bnBxQZgfZShQ2SdDbzbcyg4SCEb8ghwnsH0KNZo +jJF+20RpK2VMViE6lylLTEMd/PyAdST/NPoqVxyva3QjTrKt+tkkFTsmNVMXcmYC +f1xo1/YFp73FFE63VYFI+Yw+Ajau8sYSo4+YvgFCy+Efhf3h3GFDtaiNod56uX9G +9M/cu8XsFzFP2e/0YWY3XL+v7ESOdc3g7yS4FQZ7Z6YvfAed9hCB25cDECvZXqJG +HSYDR38NHyAPROuCwlEwDyVmWRl9bpwZt+hr9kaTQScIDx+rV/EF3o0GKIwtR7AK +jaPAta0f4/Uu+EuWAcccSRUMtfx5/Jse/6iliBvy7JXmA+Y0PrT7K4uHO7iktdI+ +x8WbfZKfnLVuqw5fneTjC1n48Ltjis/f8DgO7BuWTmLdZXddjqqxzBSukFTBn4Hg +/oSg3XiMywOAVrRCNJehcdTG0u/BqZsrRjcYAJaf5qG/0tMLNsuF9Y53XQQAeezE +etL+7y0mkeQhVF+Kmy4= +-----END CERTIFICATE----- diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/server.key b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/server.key new file mode 100644 index 000000000000..95e2ef3e8b31 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQCyWXGxJDNNMmU0 +cx9cLZsIhcI+x8KAIGXsVs/PzbyPAmT4RhxeHT5ZMUaSzRbFLBvw1DNrlS3IJQMS +55mP19WPmZYFj3tYK7TVKX/fEKUeZiKxd/LK/bsYp5zs9pI66sKbYkOTIdE26aIl +E5riO143QY4fPDsViwzfvv1O43DXj+C1b54Gsae+CulTt9tS/HjYOLp+0ZhBTAiL +7e4hqpP/+LuS0MxoBAm2QvqFpPNnkk3aoo7lrfj9ZHP8Cm8/DeWkNXxziqhOTcLQ +GD0w/5oOdm2O0sFI30IlbCv6fXVBoyAdOi0Qqw5qEZ2LcnbNjEK8k/QtP6GM3Y71 +pHNIwiwzAgMBAAECgf9REZuCvy2Bi8SoTnjqQuHG5FuA6cPuisuFZr1k88IO+zJQ +uY3WKNs29BV+LcxnoK29W8jQnjqPHXcMfrF5dVWmkrrJdu8JLaGWVHF+uBq8nRb0 +2LvREh5XhZTGzIESNdc/7GIxdouag/8FlzCUYQGuT3v9+wUCiim+4CuIuPvv7ncD +8vANe3Ua5G0mHjVshOiMNpegg45zYlzYpMtUFPs+asLilW6A7UlgC+pLZ1cHUUlU +ZB7KOGT9JdrZpilTidl6LLvDDQK30TSWz8A26SuEAE71DR2VEjLVpjTNS76vlx+c +CrYr/WwpMb0xul+e/uHiNgo+51FiTiJ/IfuGeskCgYEA804CXQM6i5m4/Upps2yG +aTae5xBaYUquZREp5Zb054U6lUAHI41iTMTIwTTvWn5ogNojgi+YjljkzRj2RQ5k +NccBkjBBwwUNVWpBoGeZ73KAdejNB4C4ucGc2kkqEDo4MU5x3IE4JK1Yi1jl9mKb +IR6m3pqb2PCQHjO8sqKNHYkCgYEAu6fH/qUd/XGmCZJWY5K6jg3dISXH16MTO5M+ +jetprkGMMybWKZQa1GedXurPexE48oRlRhkjdQkW6Wcj1Qh6OKp6N2Zx8sY4dLeQ +yVChnMPFE2LK+UlRCKJUZi+rzX415ML6pZg+yW7O2cHpMKv7PlXISw2YDqtboCAi +Y+doqNsCgYBE1yqmBJbZDuqfiCF2KduyA0lcmWzpIEdNw1h2ZIrwwup7dj1O2t8Y +V4lx2TdsBF4vLwli+XKRvCcovMpZaaQC70bLhSnmMxS9uS3OY+HTNTORqQfx+oLJ +1DU8Mf1b0A08LjTbLhijkASAkOuoFehMq66NR3OXIyGz2fGnHYUN+QKBgCC47SL2 +X/hl7PIWVoIef/FtcXXqRKLRiPUGhA3zUwZT38K7rvSpItSPDN4UTAHFywxfEdnb +YFd0Mk6Y8aKgS8+9ynoGnzAaaJXRvKmeKdBQQvlSbNpzcnHy/IylG2xF6dfuOA7Q +MYKmk+Nc8PDPzIveIYMU58MHFn8hm12YaKOpAoGAV1CE8hFkEK9sbRGoKNJkx9nm +CZTv7PybaG/RN4ZrBSwVmnER0FEagA/Tzrlp1pi3sC8ZsC9onSOf6Btq8ZE0zbO1 +vsAm3gTBXcrCJxzw0Wjt8pzEbk3yELm4WE6VDEx4da2jWocdspslpIwdjHnPwsbH +r5O3ZAgigZs/ZtKW/U4= +-----END PRIVATE KEY----- diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/testcontainers/rabbitmq-ssl.conf b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/testcontainers/rabbitmq-ssl.conf new file mode 100644 index 000000000000..b954a8b729e1 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/testcontainers/rabbitmq-ssl.conf @@ -0,0 +1,8 @@ +listeners.ssl.default=5671 + +ssl_options.cacertfile=/etc/rabbitmq/ca.crt +ssl_options.certfile=/etc/rabbitmq/server.crt +ssl_options.keyfile=/etc/rabbitmq/server.key + +ssl_options.verify=verify_peer +ssl_options.fail_if_no_peer_cert=true diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java deleted file mode 100644 index c2c766c096e7..000000000000 --- a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.autoconfigure; - -import org.springframework.amqp.rabbit.annotation.EnableRabbit; -import org.springframework.amqp.rabbit.config.RabbitListenerConfigUtils; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Configuration; - -/** - * Configuration for Spring AMQP annotation driven endpoints. - * - * @author Eddú Meléndez - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(EnableRabbit.class) -class RabbitAnnotationDrivenConfiguration { - - @Configuration(proxyBeanMethods = false) - @EnableRabbit - @ConditionalOnMissingBean(name = RabbitListenerConfigUtils.RABBIT_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME) - static class EnableRabbitConfiguration { - - } - -} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpEnvironmentBuilderCustomizer.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpEnvironmentCustomizer.java similarity index 70% rename from module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpEnvironmentBuilderCustomizer.java rename to module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpEnvironmentCustomizer.java index 807ead65def3..3bc6fdac9936 100644 --- a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpEnvironmentBuilderCustomizer.java +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpEnvironmentCustomizer.java @@ -14,26 +14,27 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.amqp.rabbitmq.autoconfigure; import com.rabbitmq.client.amqp.Environment; import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; /** * Callback interface that can be implemented by beans wishing to customize the - * auto-configured {@link Environment} that is created by an + * auto-configured AMQP {@link Environment} that is created by an * {@link AmqpEnvironmentBuilder}. * * @author Eddú Meléndez - * @since 4.1.0 + * @author Stephane Nicoll + * @since 4.2.0 */ @FunctionalInterface -public interface RabbitAmqpEnvironmentBuilderCustomizer { +public interface AmqpEnvironmentCustomizer { /** - * Customize the {@code AmqpEnvironmentBuilder}. - * @param builder the builder to customize + * Customize the {@link AmqpEnvironmentBuilder}. + * @param environmentBuilder the environment builder to customize */ - void customize(AmqpEnvironmentBuilder builder); + void customize(AmqpEnvironmentBuilder environmentBuilder); } diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAnnotationDrivenConfiguration.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAnnotationDrivenConfiguration.java new file mode 100644 index 000000000000..244c4aa849a7 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAnnotationDrivenConfiguration.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.rabbitmq.autoconfigure; + +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.config.ContainerCustomizer; +import org.springframework.amqp.rabbit.config.RabbitListenerConfigUtils; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.ObjectProvider; +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.scheduling.TaskScheduler; + +/** + * Configuration for Spring AMQP annotation-driven endpoints using RabbitMQ. + * + * @author Eddú Meléndez + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(EnableRabbit.class) +class AmqpRabbitAnnotationDrivenConfiguration { + + @Bean + @ConditionalOnMissingBean + RabbitAmqpListenerContainerFactoryConfigurer rabbitAmqpListenerContainerFactoryConfigurer( + AmqpRabbitProperties properties, ObjectProvider messageConverter, + ObjectProvider taskSchedulers) { + RabbitAmqpListenerContainerFactoryConfigurer configurer = new RabbitAmqpListenerContainerFactoryConfigurer( + properties); + messageConverter.ifAvailable(configurer::setMessageConverter); + taskSchedulers.ifAvailable(configurer::setTaskScheduler); + return configurer; + } + + @Bean(name = "rabbitListenerContainerFactory") + @ConditionalOnMissingBean(name = "rabbitListenerContainerFactory") + RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory( + RabbitAmqpListenerContainerFactoryConfigurer configurer, AmqpConnectionFactory connectionFactory, + ObjectProvider> amqpContainerCustomizer) { + RabbitAmqpListenerContainerFactory factory = new RabbitAmqpListenerContainerFactory(connectionFactory); + configurer.configure(factory); + amqpContainerCustomizer.ifUnique(factory::setContainerCustomizer); + return factory; + } + + @Configuration(proxyBeanMethods = false) + @EnableRabbit + @ConditionalOnMissingBean(name = RabbitListenerConfigUtils.RABBIT_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME) + static class EnableRabbitConfiguration { + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfiguration.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAutoConfiguration.java similarity index 66% rename from module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfiguration.java rename to module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAutoConfiguration.java index a433060acc31..a5ad684f756f 100644 --- a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfiguration.java +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAutoConfiguration.java @@ -14,73 +14,58 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.amqp.rabbitmq.autoconfigure; + +import javax.net.ssl.SSLContext; import com.rabbitmq.client.amqp.Connection; import com.rabbitmq.client.amqp.CredentialsProvider; import com.rabbitmq.client.amqp.Environment; import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder.EnvironmentConnectionSettings; +import org.jspecify.annotations.Nullable; -import org.springframework.amqp.rabbit.config.ContainerCustomizer; import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin; import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; import org.springframework.amqp.rabbitmq.client.SingleAmqpConnectionFactory; -import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; -import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails.Address; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails.Address; 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.TaskSchedulingAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; /** - * {@link EnableAutoConfiguration Auto-configuration} for {@link RabbitAmqpTemplate}. + * Auto-configuration for AMQP 1.0 support with RabbitMQ. * * @author Eddú Meléndez - * @since 4.1.0 + * @since 4.2.0 */ -@AutoConfiguration +@AutoConfiguration(after = TaskSchedulingAutoConfiguration.class) @ConditionalOnClass({ RabbitAmqpTemplate.class, Connection.class }) -@EnableConfigurationProperties(RabbitAmqpProperties.class) -@Import(RabbitAnnotationDrivenConfiguration.class) -public final class RabbitAmqpAutoConfiguration { - - private final RabbitAmqpProperties properties; - - RabbitAmqpAutoConfiguration(RabbitAmqpProperties properties) { - this.properties = properties; - } +@EnableConfigurationProperties(AmqpRabbitProperties.class) +@Import(AmqpRabbitAnnotationDrivenConfiguration.class) +public final class AmqpRabbitAutoConfiguration { @Bean @ConditionalOnMissingBean - RabbitAmqpConnectionDetails rabbitConnectionDetails() { - return new PropertiesRabbitAmqpConnectionDetails(this.properties); - } - - @Bean(name = "rabbitListenerContainerFactory") - @ConditionalOnMissingBean(name = "rabbitListenerContainerFactory") - RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(AmqpConnectionFactory connectionFactory, - ObjectProvider> amqpContainerCustomizer) { - RabbitAmqpListenerContainerFactory factory = new RabbitAmqpListenerContainerFactory(connectionFactory); - amqpContainerCustomizer.ifUnique(factory::setContainerCustomizer); - - RabbitAmqpProperties.AmqpContainer configuration = this.properties.getListener().getAmqp(); - factory.setObservationEnabled(configuration.isObservationEnabled()); - return factory; + AmqpRabbitConnectionDetails amqpRabbitConnectionDetails(AmqpRabbitProperties properties, + @Nullable SslBundles sslBundles) { + return new PropertiesAmqpRabbitConnectionDetails(properties, sslBundles); } @Bean @ConditionalOnMissingBean - Environment rabbitAmqpEnvironment(RabbitAmqpConnectionDetails connectionDetails, - ObjectProvider customizers, + Environment amqpRabbitEnvironment(AmqpRabbitConnectionDetails connectionDetails, + ObjectProvider customizers, ObjectProvider credentialsProvider) { PropertyMapper map = PropertyMapper.get(); EnvironmentConnectionSettings environmentConnectionSettings = new AmqpEnvironmentBuilder().connectionSettings(); @@ -91,7 +76,11 @@ Environment rabbitAmqpEnvironment(RabbitAmqpConnectionDetails connectionDetails, map.from(connectionDetails::getPassword).to(environmentConnectionSettings::password); map.from(connectionDetails::getVirtualHost).to(environmentConnectionSettings::virtualHost); map.from(credentialsProvider::getIfAvailable).to(environmentConnectionSettings::credentialsProvider); - + SslBundle sslBundle = connectionDetails.getSslBundle(); + if (sslBundle != null) { + SSLContext sslContext = sslBundle.createSslContext(); + environmentConnectionSettings.tls().sslContext(sslContext); + } AmqpEnvironmentBuilder builder = environmentConnectionSettings.environmentBuilder(); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); @@ -99,20 +88,18 @@ Environment rabbitAmqpEnvironment(RabbitAmqpConnectionDetails connectionDetails, @Bean @ConditionalOnMissingBean - AmqpConnectionFactory amqpConnectionFactory(Environment environment) { + AmqpConnectionFactory amqpRabbitConnectionFactory(Environment environment) { return new SingleAmqpConnectionFactory(environment); } @Bean @ConditionalOnMissingBean - RabbitAmqpTemplate rabbitAmqpTemplate(AmqpConnectionFactory connectionFactory, + RabbitAmqpTemplate rabbitAmqpTemplate(AmqpConnectionFactory connectionFactory, AmqpRabbitProperties properties, ObjectProvider customizers, ObjectProvider messageConverter) { RabbitAmqpTemplate rabbitAmqpTemplate = new RabbitAmqpTemplate(connectionFactory); - if (messageConverter.getIfAvailable() != null) { - rabbitAmqpTemplate.setMessageConverter(messageConverter.getIfAvailable()); - } - RabbitAmqpProperties.Template templateProperties = this.properties.getTemplate(); + messageConverter.ifAvailable(rabbitAmqpTemplate::setMessageConverter); + AmqpRabbitProperties.Template templateProperties = properties.getTemplate(); PropertyMapper map = PropertyMapper.get(); map.from(templateProperties::getDefaultReceiveQueue).to(rabbitAmqpTemplate::setReceiveQueue); diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpConnectionDetails.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitConnectionDetails.java similarity index 75% rename from module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpConnectionDetails.java rename to module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitConnectionDetails.java index 344909fffb00..364918a6c05f 100644 --- a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpConnectionDetails.java +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitConnectionDetails.java @@ -14,19 +14,26 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.amqp.rabbitmq.autoconfigure; import org.jspecify.annotations.Nullable; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.boot.ssl.SslBundle; /** * Details required to establish a connection to a RabbitMQ AMQP service. * * @author Eddú Meléndez - * @since 4.1.0 + * @since 4.2.0 */ -public interface RabbitAmqpConnectionDetails extends ConnectionDetails { +public interface AmqpRabbitConnectionDetails extends ConnectionDetails { + + /** + * Return the address of the broker. + * @return the address + */ + Address getAddress(); /** * Login user to authenticate to the broker. @@ -37,8 +44,8 @@ public interface RabbitAmqpConnectionDetails extends ConnectionDetails { } /** - * Login to authenticate against the broker. - * @return the login to authenticate against the broker or {@code null} + * Password used to authenticate to the broker. + * @return the password to authenticate to the broker or {@code null} */ default @Nullable String getPassword() { return null; @@ -53,11 +60,12 @@ public interface RabbitAmqpConnectionDetails extends ConnectionDetails { } /** - * Returns the address. - * @return the address - * @throws IllegalStateException if the address list is empty + * SSL bundle to use. + * @return the SSL bundle to use or {@code null} */ - Address getAddress(); + default @Nullable SslBundle getSslBundle() { + return null; + } /** * A RabbitMQ address. diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpProperties.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitProperties.java similarity index 86% rename from module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpProperties.java rename to module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitProperties.java index 87ddff5492b1..92aa9c12f14f 100644 --- a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpProperties.java +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitProperties.java @@ -14,7 +14,9 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.amqp.rabbitmq.autoconfigure; + +import java.time.Duration; import org.jspecify.annotations.Nullable; @@ -26,12 +28,12 @@ * Configuration properties for Rabbit AMQP. * * @author Eddú Meléndez - * @since 4.1.0 + * @since 4.2.0 */ @ConfigurationProperties("spring.amqp.rabbitmq") -public class RabbitAmqpProperties { +public class AmqpRabbitProperties { - private static final int DEFAULT_PORT = 5672; + static final int DEFAULT_PORT = 5672; /** * RabbitMQ host. Ignored if an address is set. @@ -65,11 +67,19 @@ public class RabbitAmqpProperties { */ private @Nullable String address; + /** + * SSL configuration. + */ + private final Ssl ssl = new Ssl(); + /** * Listener container configuration. */ private final Listener listener = new Listener(); + /** + * Template configuration. + */ private final Template template = new Template(); private @Nullable Address parsedAddress; @@ -120,7 +130,7 @@ public void setPort(@Nullable Integer port) { public String determineAddress() { if (this.parsedAddress == null) { if (this.host.contains(",")) { - throw new InvalidConfigurationPropertyValueException("spring.amqp.host", this.host, + throw new InvalidConfigurationPropertyValueException("spring.amqp.rabbitmq.host", this.host, "Invalid character ','. Value must be a single host. For multiple hosts, use property 'spring.amqp.address' instead."); } return this.host + ":" + determinePort(); @@ -206,6 +216,10 @@ public void setVirtualHost(@Nullable String virtualHost) { this.virtualHost = StringUtils.hasText(virtualHost) ? virtualHost : "/"; } + public Ssl getSsl() { + return this.ssl; + } + public Listener getListener() { return this.listener; } @@ -214,31 +228,34 @@ public Template getTemplate() { return this.template; } - public static class Listener { + /** + * SSL configuration. + */ + public static class Ssl { + + /** + * SSL bundle name. + */ + private @Nullable String bundle; - private final AmqpContainer amqp = new AmqpContainer(); + public @Nullable String getBundle() { + return this.bundle; + } - public AmqpContainer getAmqp() { - return this.amqp; + public void setBundle(@Nullable String bundle) { + this.bundle = bundle; } } - /** - * Configuration properties for {@code RabbitAmqpListenerContainer}. - */ - public static class AmqpContainer { + public static class Listener { /** * Whether to enable observation. */ private boolean observationEnabled; - /** - * Batch size, expressed as the number of physical messages, to be used by the - * container. - */ - private @Nullable Integer batchSize; + private final Batch batch = new Batch(); public boolean isObservationEnabled() { return this.observationEnabled; @@ -248,12 +265,39 @@ public void setObservationEnabled(boolean observationEnabled) { this.observationEnabled = observationEnabled; } - public @Nullable Integer getBatchSize() { - return this.batchSize; + public Batch getBatch() { + return this.batch; } - public void setBatchSize(@Nullable Integer batchSize) { - this.batchSize = batchSize; + public static class Batch { + + /** + * Batch size, expressed as the number of physical messages, to be used by the + * container. + */ + private @Nullable Integer size; + + /** + * Timeout for gathering batch messages. + */ + private Duration receiveTimeout = Duration.ofSeconds(30); + + public @Nullable Integer getSize() { + return this.size; + } + + public void setSize(@Nullable Integer size) { + this.size = size; + } + + public Duration getReceiveTimeout() { + return this.receiveTimeout; + } + + public void setReceiveTimeout(Duration receiveTimeout) { + this.receiveTimeout = receiveTimeout; + } + } } diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitAmqpConnectionDetails.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/PropertiesAmqpRabbitConnectionDetails.java similarity index 59% rename from module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitAmqpConnectionDetails.java rename to module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/PropertiesAmqpRabbitConnectionDetails.java index 2eb06294eb97..9f1d9b71ed3f 100644 --- a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitAmqpConnectionDetails.java +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/PropertiesAmqpRabbitConnectionDetails.java @@ -14,21 +14,38 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.amqp.rabbitmq.autoconfigure; import org.jspecify.annotations.Nullable; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + /** - * Adapts {@link RabbitAmqpProperties} to {@link RabbitAmqpConnectionDetails}. + * Adapts {@link AmqpRabbitProperties} to {@link AmqpRabbitConnectionDetails}. * * @author Eddú Meléndez */ -class PropertiesRabbitAmqpConnectionDetails implements RabbitAmqpConnectionDetails { +class PropertiesAmqpRabbitConnectionDetails implements AmqpRabbitConnectionDetails { + + private final AmqpRabbitProperties properties; - private final RabbitAmqpProperties properties; + private final @Nullable SslBundles sslBundles; - PropertiesRabbitAmqpConnectionDetails(RabbitAmqpProperties properties) { + PropertiesAmqpRabbitConnectionDetails(AmqpRabbitProperties properties, @Nullable SslBundles sslBundles) { this.properties = properties; + this.sslBundles = sslBundles; + } + + @Override + public Address getAddress() { + String address = this.properties.determineAddress(); + int portSeparatorIndex = address.lastIndexOf(':'); + String host = address.substring(0, portSeparatorIndex); + String port = address.substring(portSeparatorIndex + 1); + return new Address(host, Integer.parseInt(port)); } @Override @@ -47,12 +64,13 @@ public String getUsername() { } @Override - public Address getAddress() { - String address = this.properties.determineAddress(); - int portSeparatorIndex = address.lastIndexOf(':'); - String host = address.substring(0, portSeparatorIndex); - String port = address.substring(portSeparatorIndex + 1); - return new Address(host, Integer.parseInt(port)); + public @Nullable SslBundle getSslBundle() { + String bundle = this.properties.getSsl().getBundle(); + if (StringUtils.hasLength(bundle)) { + Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context"); + return this.sslBundles.getBundle(bundle); + } + return null; } } diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpListenerContainerFactoryConfigurer.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpListenerContainerFactoryConfigurer.java new file mode 100644 index 000000000000..deae4330a183 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpListenerContainerFactoryConfigurer.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.rabbitmq.autoconfigure; + +import java.time.Duration; + +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitProperties.Listener; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitProperties.Listener.Batch; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.util.Assert; + +/** + * Configure {@link RabbitAmqpListenerContainerFactory} with sensible defaults tuned using + * configuration properties. + *

    + * Can be injected into application code and used to define a custom + * {@code RabbitAmqpListenerContainerFactory} whose configuration is based upon that + * produced by auto-configuration. + * + * @author Stephane Nicoll + * @since 4.2.0 + */ +public final class RabbitAmqpListenerContainerFactoryConfigurer { + + private final AmqpRabbitProperties properties; + + private @Nullable MessageConverter messageConverter; + + private @Nullable TaskScheduler taskScheduler; + + /** + * Creates a new configurer that will use the given {@code properties}. + * @param properties the properties to use + */ + public RabbitAmqpListenerContainerFactoryConfigurer(AmqpRabbitProperties properties) { + this.properties = properties; + } + + /** + * Set the {@link MessageConverter} to use or {@code null} if the out-of-the-box + * converter should be used. + * @param messageConverter the {@link MessageConverter} + */ + protected void setMessageConverter(@Nullable MessageConverter messageConverter) { + this.messageConverter = messageConverter; + } + + /** + * Set the {@link TaskScheduler} to use or {@code null} if the default should be used. + * @param taskScheduler the {@link TaskScheduler} + */ + protected void setTaskScheduler(@Nullable TaskScheduler taskScheduler) { + this.taskScheduler = taskScheduler; + } + + /** + * Configure the specified {@link RabbitAmqpListenerContainerFactory}. The factory can + * be further tuned and default settings can be overridden. + * @param factory the {@link RabbitAmqpListenerContainerFactory} instance to configure + */ + public void configure(RabbitAmqpListenerContainerFactory factory) { + Assert.notNull(factory, "'factory' must not be null"); + PropertyMapper map = PropertyMapper.get(); + map.from(this.messageConverter).to(factory::setMessageConverter); + map.from(this.taskScheduler).to(factory::setTaskScheduler); + Listener listenerProperties = this.properties.getListener(); + map.from(listenerProperties.isObservationEnabled()).to(factory::setObservationEnabled); + Batch batchProperties = listenerProperties.getBatch(); + map.from(batchProperties.getSize()).to(factory::setBatchSize); + map.from(batchProperties.getReceiveTimeout()).as(Duration::toMillis).to(factory::setBatchReceiveTimeout); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpTemplateCustomizer.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpTemplateCustomizer.java similarity index 93% rename from module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpTemplateCustomizer.java rename to module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpTemplateCustomizer.java index e1eb2404cbb2..f47c3a02843c 100644 --- a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpTemplateCustomizer.java +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpTemplateCustomizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.amqp.rabbitmq.autoconfigure; import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; @@ -22,7 +22,7 @@ * Callback interface that can be used to customize a {@link RabbitAmqpTemplate}. * * @author Eddú Meléndez - * @since 4.0.0 + * @since 4.2.0 */ @FunctionalInterface public interface RabbitAmqpTemplateCustomizer { diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/AmqpRabbitHealthContributorAutoConfiguration.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/AmqpRabbitHealthContributorAutoConfiguration.java new file mode 100644 index 000000000000..d4d9b367768b --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/AmqpRabbitHealthContributorAutoConfiguration.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.rabbitmq.autoconfigure.health; + +import com.rabbitmq.client.amqp.Connection; + +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitAutoConfiguration; +import org.springframework.boot.amqp.rabbitmq.health.AmqpRabbitHealthIndicator; +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.health.autoconfigure.contributor.CompositeHealthContributorConfiguration; +import org.springframework.boot.health.autoconfigure.contributor.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.health.contributor.HealthContributor; +import org.springframework.context.annotation.Bean; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for + * {@link AmqpRabbitHealthIndicator}. + * + * @author Stephane Nicoll + * @since 4.2.0 + */ +@AutoConfiguration(after = AmqpRabbitAutoConfiguration.class) +@ConditionalOnClass({ AmqpRabbitHealthIndicator.class, RabbitAmqpTemplate.class, Connection.class, + ConditionalOnEnabledHealthIndicator.class }) +@ConditionalOnBean(AmqpConnectionFactory.class) +@ConditionalOnEnabledHealthIndicator("rabbit") +public final class AmqpRabbitHealthContributorAutoConfiguration + extends CompositeHealthContributorConfiguration { + + AmqpRabbitHealthContributorAutoConfiguration() { + super(AmqpRabbitHealthIndicator::new); + } + + @Bean + @ConditionalOnMissingBean(name = { "rabbitHealthIndicator", "rabbitHealthContributor" }) + HealthContributor rabbitHealthContributor(ConfigurableListableBeanFactory beanFactory) { + return createContributor(beanFactory, AmqpConnectionFactory.class); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/package-info.java new file mode 100644 index 000000000000..dfd3495d0d23 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 AMQP 1.0 support with RabbitMQ. + */ +@NullMarked +package org.springframework.boot.amqp.rabbitmq.autoconfigure.health; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/package-info.java new file mode 100644 index 000000000000..e2b848d419a4 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 AMQP 1.0 support with RabbitMQ. + */ +@NullMarked +package org.springframework.boot.amqp.rabbitmq.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/AmqpRabbitMqDockerComposeConnectionDetailsFactory.java similarity index 60% rename from module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java rename to module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/AmqpRabbitMqDockerComposeConnectionDetailsFactory.java index 289fbe1cd703..4c2a4d16e838 100644 --- a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/AmqpRabbitMqDockerComposeConnectionDetailsFactory.java @@ -14,35 +14,38 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.amqp.rabbitmq.docker.compose; import org.jspecify.annotations.Nullable; -import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails; 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; +import org.springframework.boot.ssl.SslBundle; /** * {@link DockerComposeConnectionDetailsFactory} to create - * {@link RabbitAmqpConnectionDetails} for a {@code rabbitmq} service. + * {@link AmqpRabbitConnectionDetails} for a {@code rabbitmq} service. * * @author Eddú Meléndez */ -class RabbitDockerComposeConnectionDetailsFactory - extends DockerComposeConnectionDetailsFactory { +class AmqpRabbitMqDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { private static final int RABBITMQ_PORT = 5672; - protected RabbitDockerComposeConnectionDetailsFactory() { + private static final int RABBITMQ_TLS_PORT = 5671; + + protected AmqpRabbitMqDockerComposeConnectionDetailsFactory() { super("rabbitmq"); } @Override - protected @Nullable RabbitAmqpConnectionDetails getDockerComposeConnectionDetails( + protected @Nullable AmqpRabbitConnectionDetails getDockerComposeConnectionDetails( DockerComposeConnectionSource source) { try { - return new RabbitAmqpDockerComposeConnectionDetails(source.getRunningService()); + return new AmqpRabbitMqDockerComposeRabbitConnectionDetails(source.getRunningService()); } catch (IllegalStateException ex) { return null; @@ -50,20 +53,29 @@ protected RabbitDockerComposeConnectionDetailsFactory() { } /** - * {@link RabbitAmqpConnectionDetails} backed by a {@code rabbitmq} + * {@link AmqpRabbitConnectionDetails} backed by a {@code rabbitmq} * {@link RunningService}. */ - static class RabbitAmqpDockerComposeConnectionDetails extends DockerComposeConnectionDetails - implements RabbitAmqpConnectionDetails { + static class AmqpRabbitMqDockerComposeRabbitConnectionDetails extends DockerComposeConnectionDetails + implements AmqpRabbitConnectionDetails { + + private final Address address; private final RabbitEnvironment environment; - private final Address address; + private final @Nullable SslBundle sslBundle; - protected RabbitAmqpDockerComposeConnectionDetails(RunningService service) { + protected AmqpRabbitMqDockerComposeRabbitConnectionDetails(RunningService service) { super(service); + this.sslBundle = getSslBundle(service); + int containerPort = (this.sslBundle != null) ? RABBITMQ_TLS_PORT : RABBITMQ_PORT; + this.address = new Address(service.host(), service.ports().get(containerPort)); this.environment = new RabbitEnvironment(service.env()); - this.address = new Address(service.host(), service.ports().get(RABBITMQ_PORT)); + } + + @Override + public Address getAddress() { + return this.address; } @Override @@ -82,8 +94,8 @@ public String getVirtualHost() { } @Override - public Address getAddress() { - return this.address; + public @Nullable SslBundle getSslBundle() { + return this.sslBundle; } } diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/RabbitEnvironment.java similarity index 88% rename from module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java rename to module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/RabbitEnvironment.java index bc776b015dbf..709ccb58991c 100644 --- a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/RabbitEnvironment.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.amqp.rabbitmq.docker.compose; import java.util.Map; @@ -23,10 +23,7 @@ /** * RabbitMQ environment details. * - * @author Moritz Halbritter - * @author Andy Wilkinson - * @author Phillip Webb - * @author Scott Frederick + * @author Eddú Meléndez */ class RabbitEnvironment { diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/package-info.java similarity index 92% rename from module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java rename to module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/package-info.java index c2d355c0797e..94e20efb5373 100644 --- a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/package-info.java @@ -18,6 +18,6 @@ * Support for Docker Compose RabbitMQ service connections. */ @NullMarked -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.amqp.rabbitmq.docker.compose; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/health/AmqpRabbitHealthIndicator.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/health/AmqpRabbitHealthIndicator.java new file mode 100644 index 000000000000..5e8d7b0b8a02 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/health/AmqpRabbitHealthIndicator.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.rabbitmq.health; + +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.boot.health.contributor.AbstractHealthIndicator; +import org.springframework.boot.health.contributor.Health; +import org.springframework.boot.health.contributor.HealthIndicator; +import org.springframework.util.Assert; + +/** + * {@link HealthIndicator} for AMQP 1.0 support with RabbitMQ. + * + * @author Stephane Nicoll + * @since 4.2.0 + */ +public class AmqpRabbitHealthIndicator extends AbstractHealthIndicator { + + private final AmqpConnectionFactory amqpConnectionFactory; + + public AmqpRabbitHealthIndicator(AmqpConnectionFactory amqpConnectionFactory) { + super("Rabbit health check failed"); + Assert.notNull(amqpConnectionFactory, "'amqpConnectionFactory' must not be null"); + this.amqpConnectionFactory = amqpConnectionFactory; + } + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + builder.up(); + String version = this.amqpConnectionFactory.getConnection().connectionInfo().brokerVersion(); + if (version != null) { + builder.withDetail("version", version); + } + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/health/package-info.java similarity index 85% rename from module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java rename to module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/health/package-info.java index b519c35a2cae..4883951f3506 100644 --- a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/health/package-info.java @@ -15,9 +15,9 @@ */ /** - * Auto-configuration for RabbitMQ. + * Health integration for AMQP 1.0 support with RabbitMQ. */ @NullMarked -package org.springframework.boot.amqp.autoconfigure; +package org.springframework.boot.amqp.rabbitmq.health; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerConnectionDetailsFactory.java similarity index 68% rename from module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java rename to module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerConnectionDetailsFactory.java index 58d6560e6b8f..717049be83ed 100644 --- a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerConnectionDetailsFactory.java @@ -14,45 +14,51 @@ * limitations under the License. */ -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.amqp.rabbitmq.testcontainers; import java.net.URI; import org.jspecify.annotations.Nullable; import org.testcontainers.rabbitmq.RabbitMQContainer; -import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails; import org.springframework.boot.ssl.SslBundle; 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 RabbitAmqpConnectionDetails} + * {@link ContainerConnectionDetailsFactory} to create {@link AmqpRabbitConnectionDetails} * from a {@link ServiceConnection @ServiceConnection}-annotated * {@link RabbitMQContainer}. * * @author Eddú Meléndez */ -class RabbitContainerConnectionDetailsFactory - extends ContainerConnectionDetailsFactory { +class AmqpRabbitMqContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory { @Override - protected RabbitAmqpConnectionDetails getContainerConnectionDetails( + protected AmqpRabbitConnectionDetails getContainerConnectionDetails( ContainerConnectionSource source) { - return new RabbitAmqpMqContainerConnectionDetails(source); + return new AmqpRabbitMqContainerRabbitConnectionDetails(source); } /** - * {@link RabbitAmqpConnectionDetails} backed by a {@link ContainerConnectionSource}. + * {@link AmqpRabbitConnectionDetails} backed by a {@link ContainerConnectionSource}. */ - static final class RabbitAmqpMqContainerConnectionDetails extends ContainerConnectionDetails - implements RabbitAmqpConnectionDetails { + static final class AmqpRabbitMqContainerRabbitConnectionDetails + extends ContainerConnectionDetails implements AmqpRabbitConnectionDetails { - private RabbitAmqpMqContainerConnectionDetails(ContainerConnectionSource source) { + private AmqpRabbitMqContainerRabbitConnectionDetails(ContainerConnectionSource source) { super(source); } + @Override + public Address getAddress() { + URI uri = URI.create((getSslBundle() != null) ? getContainer().getAmqpsUrl() : getContainer().getAmqpUrl()); + return new Address(uri.getHost(), uri.getPort()); + } + @Override public String getUsername() { return getContainer().getAdminUsername(); @@ -63,12 +69,6 @@ public String getPassword() { return getContainer().getAdminPassword(); } - @Override - public Address getAddress() { - URI uri = URI.create((getSslBundle() != null) ? getContainer().getAmqpsUrl() : getContainer().getAmqpUrl()); - return new Address(uri.getHost(), uri.getPort()); - } - @Override public @Nullable SslBundle getSslBundle() { return super.getSslBundle(); diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/testcontainers/package-info.java similarity index 92% rename from module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java rename to module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/testcontainers/package-info.java index 69c6364ef32f..dc19f3dd3426 100644 --- a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/testcontainers/package-info.java @@ -18,6 +18,6 @@ * Support for testcontainers RabbitMQ service connections. */ @NullMarked -package org.springframework.boot.amqp.testcontainers; +package org.springframework.boot.amqp.rabbitmq.testcontainers; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring.factories b/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring.factories index 412b6813fab6..b6129e3103c9 100644 --- a/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring.factories @@ -1,4 +1,4 @@ # Connection Details Factories org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ -org.springframework.boot.amqp.docker.compose.RabbitDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory +org.springframework.boot.amqp.rabbitmq.docker.compose.AmqpRabbitMqDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.amqp.rabbitmq.testcontainers.AmqpRabbitMqContainerConnectionDetailsFactory diff --git a/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 050f5729e283..aa0a1c854fe4 100644 --- a/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1,2 @@ -org.springframework.boot.amqp.autoconfigure.RabbitAmqpAutoConfiguration \ No newline at end of file +org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitAutoConfiguration +org.springframework.boot.amqp.rabbitmq.autoconfigure.health.AmqpRabbitHealthContributorAutoConfiguration diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfigurationTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfigurationTests.java deleted file mode 100644 index e1c5835ff6cc..000000000000 --- a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfigurationTests.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.amqp.autoconfigure; - -import org.junit.jupiter.api.Test; -import org.mockito.InOrder; - -import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.rabbit.annotation.RabbitListener; -import org.springframework.amqp.rabbit.config.ContainerCustomizer; -import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; -import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin; -import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; -import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; -import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer; -import org.springframework.amqp.support.converter.MessageConversionException; -import org.springframework.amqp.support.converter.MessageConverter; -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 org.springframework.context.annotation.Import; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link RabbitAmqpAutoConfiguration}. - * - * @author Eddú Meléndez - */ -class RabbitAmqpAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(RabbitAmqpAutoConfiguration.class)); - - @Test - void testDefaultRabbitAmqpConfiguration() { - this.contextRunner.run((context) -> { - RabbitAmqpListenerContainerFactory listenerContainerFactory = context - .getBean(RabbitAmqpListenerContainerFactory.class); - AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); - RabbitAmqpTemplate rabbitAmqpTemplate = context.getBean(RabbitAmqpTemplate.class); - RabbitAmqpAdmin rabbitAmqpAdmin = context.getBean(RabbitAmqpAdmin.class); - - assertThat(listenerContainerFactory).isNotNull(); - assertThat(listenerContainerFactory).extracting("containerCustomizer").isNull(); - assertThat(connectionFactory).isNotNull(); - assertThat(rabbitAmqpTemplate).isNotNull(); - assertThat(rabbitAmqpAdmin).isNotNull(); - }); - } - - @Test - void whenMultipleRabbitAmqpTemplateCustomizersAreDefinedThenTheyAreCalledInOrder() { - this.contextRunner.withUserConfiguration(MultipleRabbitAmqpTemplateCustomizersConfiguration.class) - .run((context) -> { - RabbitAmqpTemplateCustomizer firstCustomizer = context.getBean("firstCustomizer", - RabbitAmqpTemplateCustomizer.class); - RabbitAmqpTemplateCustomizer secondCustomizer = context.getBean("secondCustomizer", - RabbitAmqpTemplateCustomizer.class); - InOrder inOrder = inOrder(firstCustomizer, secondCustomizer); - RabbitAmqpTemplate template = context.getBean(RabbitAmqpTemplate.class); - then(firstCustomizer).should(inOrder).customize(template); - then(secondCustomizer).should(inOrder).customize(template); - inOrder.verifyNoMoreInteractions(); - }); - } - - @Test - void testListenerContainerFactoryWithContainerCustomizer() { - this.contextRunner.withUserConfiguration(AmqpContainerCustomizerConfiguration.class).run((context) -> { - RabbitAmqpListenerContainerFactory listenerContainerFactory = context - .getBean(RabbitAmqpListenerContainerFactory.class); - - assertThat(listenerContainerFactory).isNotNull(); - assertThat(listenerContainerFactory).extracting("containerCustomizer").isNotNull(); - }); - } - - @Configuration(proxyBeanMethods = false) - static class MultipleRabbitAmqpTemplateCustomizersConfiguration { - - @Bean - @Order(Ordered.LOWEST_PRECEDENCE) - RabbitAmqpTemplateCustomizer secondCustomizer() { - return mock(RabbitAmqpTemplateCustomizer.class); - } - - @Bean - @Order(0) - RabbitAmqpTemplateCustomizer firstCustomizer() { - return mock(RabbitAmqpTemplateCustomizer.class); - } - - } - - @Import(TestListener.class) - @Configuration(proxyBeanMethods = false) - static class AmqpContainerCustomizerConfiguration { - - @Bean - @SuppressWarnings("unchecked") - ContainerCustomizer customizer() { - return mock(ContainerCustomizer.class); - } - - } - - @Configuration - static class CustomMessageConverterConfiguration { - - @Bean - MessageConverter messageConverter() { - return new MessageConverter() { - - @Override - public Message toMessage(Object object, MessageProperties messageProperties) - throws MessageConversionException { - return new Message(object.toString().getBytes()); - } - - @Override - public Object fromMessage(Message message) throws MessageConversionException { - return new String(message.getBody()); - } - - }; - } - - } - - static class TestListener { - - @RabbitListener(queues = "test", autoStartup = "false") - void listen(String in) { - } - - } - -} diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAutoConfigurationTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAutoConfigurationTests.java new file mode 100644 index 000000000000..f485de17b054 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAutoConfigurationTests.java @@ -0,0 +1,294 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.rabbitmq.autoconfigure; + +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.ContainerCustomizer; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer; +import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails.Address; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +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 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; + +/** + * Tests for {@link AmqpRabbitAutoConfiguration}. + * + * @author Eddú Meléndez + */ +class AmqpRabbitAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AmqpRabbitAutoConfiguration.class)); + + @Test + void testDefaultRabbitAmqpConfiguration() { + this.contextRunner.run((context) -> { + RabbitAmqpListenerContainerFactory listenerContainerFactory = context + .getBean(RabbitAmqpListenerContainerFactory.class); + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + RabbitAmqpTemplate rabbitAmqpTemplate = context.getBean(RabbitAmqpTemplate.class); + RabbitAmqpAdmin rabbitAmqpAdmin = context.getBean(RabbitAmqpAdmin.class); + + assertThat(listenerContainerFactory).isNotNull(); + assertThat(listenerContainerFactory).extracting("containerCustomizer").isNull(); + assertThat(connectionFactory).isNotNull(); + assertThat(rabbitAmqpTemplate).isNotNull(); + assertThat(rabbitAmqpAdmin).isNotNull(); + }); + } + + @Test + void whenMultipleRabbitAmqpTemplateCustomizersAreDefinedThenTheyAreCalledInOrder() { + this.contextRunner.withUserConfiguration(MultipleRabbitAmqpTemplateCustomizersConfiguration.class) + .run((context) -> { + RabbitAmqpTemplateCustomizer firstCustomizer = context.getBean("firstCustomizer", + RabbitAmqpTemplateCustomizer.class); + RabbitAmqpTemplateCustomizer secondCustomizer = context.getBean("secondCustomizer", + RabbitAmqpTemplateCustomizer.class); + InOrder inOrder = inOrder(firstCustomizer, secondCustomizer); + RabbitAmqpTemplate template = context.getBean(RabbitAmqpTemplate.class); + then(firstCustomizer).should(inOrder).customize(template); + then(secondCustomizer).should(inOrder).customize(template); + inOrder.verifyNoMoreInteractions(); + }); + } + + @Test + void testListenerContainerFactoryWithContainerCustomizer() { + this.contextRunner.withUserConfiguration(AmqpContainerCustomizerConfiguration.class).run((context) -> { + RabbitAmqpListenerContainerFactory listenerContainerFactory = context + .getBean(RabbitAmqpListenerContainerFactory.class); + + assertThat(listenerContainerFactory).isNotNull(); + assertThat(listenerContainerFactory).extracting("containerCustomizer").isNotNull(); + }); + } + + @Test + void autoConfigurationConfiguresConnectionDetailsWithDefaultValues() { + this.contextRunner.run((context) -> { + AmqpRabbitConnectionDetails connectionDetails = context.getBean(AmqpRabbitConnectionDetails.class); + AmqpRabbitProperties properties = new AmqpRabbitProperties(); + assertThat(connectionDetails.getAddress().host()).isEqualTo(properties.getHost()); + assertThat(connectionDetails.getAddress().port()).isEqualTo(AmqpRabbitProperties.DEFAULT_PORT); + assertThat(connectionDetails.getUsername()).isEqualTo(properties.getUsername()); + assertThat(connectionDetails.getPassword()).isEqualTo(properties.getPassword()); + assertThat(connectionDetails.getVirtualHost()).isNull(); + }); + } + + @Test + void autoConfigurationConfiguresConnectionDetailsWithPropertyOverrides() { + this.contextRunner + .withPropertyValues("spring.amqp.rabbitmq.host=custom.host", "spring.amqp.rabbitmq.port=1234", + "spring.amqp.rabbitmq.username=user", "spring.amqp.rabbitmq.password=secret") + .run((context) -> { + AmqpRabbitConnectionDetails connectionDetails = context.getBean(AmqpRabbitConnectionDetails.class); + assertThat(connectionDetails.getAddress().host()).isEqualTo("custom.host"); + assertThat(connectionDetails.getAddress().port()).isEqualTo(1234); + assertThat(connectionDetails.getUsername()).isEqualTo("user"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesConnectionDetails() { + AmqpRabbitConnectionDetails connectionDetails = mock(AmqpRabbitConnectionDetails.class); + given(connectionDetails.getAddress()).willReturn(new Address("localhost", 5672)); + this.contextRunner.withBean(AmqpRabbitConnectionDetails.class, () -> connectionDetails).run((context) -> { + assertThat(context).hasSingleBean(AmqpRabbitConnectionDetails.class); + assertThat(context.getBean(AmqpRabbitConnectionDetails.class)).isSameAs(connectionDetails); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesEnvironment() { + Environment environment = mock(Environment.class); + this.contextRunner.withBean(Environment.class, () -> environment).run((context) -> { + assertThat(context).hasSingleBean(Environment.class); + assertThat(context.getBean(Environment.class)).isSameAs(environment); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesConnectionFactory() { + AmqpConnectionFactory connectionFactory = mock(AmqpConnectionFactory.class); + this.contextRunner.withBean(AmqpConnectionFactory.class, () -> connectionFactory).run((context) -> { + assertThat(context).hasSingleBean(AmqpConnectionFactory.class); + assertThat(context.getBean(AmqpConnectionFactory.class)).isSameAs(connectionFactory); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesRabbitAmqpTemplate() { + RabbitAmqpTemplate template = mock(RabbitAmqpTemplate.class); + this.contextRunner.withBean(RabbitAmqpTemplate.class, () -> template).run((context) -> { + assertThat(context).hasSingleBean(RabbitAmqpTemplate.class); + assertThat(context.getBean(RabbitAmqpTemplate.class)).isSameAs(template); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesRabbitAmqpAdmin() { + RabbitAmqpAdmin admin = mock(RabbitAmqpAdmin.class); + this.contextRunner.withBean(RabbitAmqpAdmin.class, () -> admin).run((context) -> { + assertThat(context).hasSingleBean(RabbitAmqpAdmin.class); + assertThat(context.getBean(RabbitAmqpAdmin.class)).isSameAs(admin); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesRabbitListenerContainerFactory() { + RabbitAmqpListenerContainerFactory factory = mock(RabbitAmqpListenerContainerFactory.class); + this.contextRunner + .withBean("rabbitListenerContainerFactory", RabbitAmqpListenerContainerFactory.class, () -> factory) + .run((context) -> { + assertThat(context).hasSingleBean(RabbitAmqpListenerContainerFactory.class); + assertThat(context.getBean(RabbitAmqpListenerContainerFactory.class)).isSameAs(factory); + }); + } + + @Test + void autoConfigurationAppliesEnvironmentCustomizer() { + AmqpEnvironmentCustomizer customizer = mock(AmqpEnvironmentCustomizer.class); + this.contextRunner.withBean(AmqpEnvironmentCustomizer.class, () -> customizer) + .run((context) -> then(customizer).should().customize(any(AmqpEnvironmentBuilder.class))); + } + + @Test + void autoConfigurationConfiguresConnectionDetailsWithSslBundle() { + SslBundle sslBundle = mock(SslBundle.class); + SslBundles sslBundles = mock(SslBundles.class); + given(sslBundles.getBundle("test-bundle")).willReturn(sslBundle); + this.contextRunner.withPropertyValues("spring.amqp.rabbitmq.ssl.bundle=test-bundle") + .withBean(SslBundles.class, () -> sslBundles) + .run((context) -> { + AmqpRabbitConnectionDetails connectionDetails = context.getBean(AmqpRabbitConnectionDetails.class); + assertThat(connectionDetails.getSslBundle()).isSameAs(sslBundle); + }); + } + + @Test + void autoConfigurationConfiguresRabbitAmqpTemplateWithMessageConverter() { + this.contextRunner.withUserConfiguration(CustomMessageConverterConfiguration.class).run((context) -> { + MessageConverter expected = context.getBean(MessageConverter.class); + RabbitAmqpTemplate template = context.getBean(RabbitAmqpTemplate.class); + assertThat(template).hasFieldOrPropertyWithValue("messageConverter", expected); + }); + } + + @Test + void autoConfigurationConfiguresRabbitAmqpTemplateWithTemplateProperties() { + this.contextRunner + .withPropertyValues("spring.amqp.rabbitmq.template.exchange=my-exchange", + "spring.amqp.rabbitmq.template.routing-key=my-routing-key", + "spring.amqp.rabbitmq.template.default-receive-queue=my-queue") + .run((context) -> { + RabbitAmqpTemplate template = context.getBean(RabbitAmqpTemplate.class); + assertThat(template).hasFieldOrPropertyWithValue("defaultExchange", "my-exchange"); + assertThat(template).hasFieldOrPropertyWithValue("defaultRoutingKey", "my-routing-key"); + assertThat(template).hasFieldOrPropertyWithValue("defaultReceiveQueue", "my-queue"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class MultipleRabbitAmqpTemplateCustomizersConfiguration { + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + RabbitAmqpTemplateCustomizer secondCustomizer() { + return mock(RabbitAmqpTemplateCustomizer.class); + } + + @Bean + @Order(0) + RabbitAmqpTemplateCustomizer firstCustomizer() { + return mock(RabbitAmqpTemplateCustomizer.class); + } + + } + + @Import(TestListener.class) + @Configuration(proxyBeanMethods = false) + static class AmqpContainerCustomizerConfiguration { + + @Bean + @SuppressWarnings("unchecked") + ContainerCustomizer customizer() { + return mock(ContainerCustomizer.class); + } + + } + + @Configuration + static class CustomMessageConverterConfiguration { + + @Bean + MessageConverter messageConverter() { + return new MessageConverter() { + + @Override + public Message toMessage(Object object, MessageProperties messageProperties) + throws MessageConversionException { + return new Message(object.toString().getBytes()); + } + + @Override + public Object fromMessage(Message message) throws MessageConversionException { + return new String(message.getBody()); + } + + }; + } + + } + + static class TestListener { + + @RabbitListener(queues = "test", autoStartup = "false") + void listen(String in) { + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitPropertiesTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitPropertiesTests.java new file mode 100644 index 000000000000..1ca065848082 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitPropertiesTests.java @@ -0,0 +1,213 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.rabbitmq.autoconfigure; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link AmqpRabbitProperties}. + * + * @author Stephane Nicoll + */ +class AmqpRabbitPropertiesTests { + + private final AmqpRabbitProperties properties = new AmqpRabbitProperties(); + + @Test + void customHost() { + this.properties.setHost("rabbit.example.com"); + assertThat(this.properties.getHost()).isEqualTo("rabbit.example.com"); + } + + @Test + void customPort() { + this.properties.setPort(1234); + assertThat(this.properties.getPort()).isEqualTo(1234); + } + + @Test + void determinePortDefaultsTo5672() { + assertThat(this.properties.determinePort()).isEqualTo(5672); + } + + @Test + void determinePortReturnsPortFromAddress() { + this.properties.setAddress("rabbit.example.com:1234"); + assertThat(this.properties.determinePort()).isEqualTo(1234); + } + + @Test + void determinePortDefaultsWhenAddressHasNoPort() { + this.properties.setAddress("rabbit.example.com"); + assertThat(this.properties.determinePort()).isEqualTo(5672); + } + + @Test + void customVirtualHost() { + this.properties.setVirtualHost("alpha"); + assertThat(this.properties.getVirtualHost()).isEqualTo("alpha"); + } + + @Test + void virtualHostRetainsALeadingSlash() { + this.properties.setVirtualHost("/alpha"); + assertThat(this.properties.getVirtualHost()).isEqualTo("/alpha"); + } + + @Test + void emptyVirtualHostIsCoercedToASlash() { + this.properties.setVirtualHost(""); + assertThat(this.properties.getVirtualHost()).isEqualTo("/"); + } + + @Test + void customUsername() { + this.properties.setUsername("user"); + assertThat(this.properties.getUsername()).isEqualTo("user"); + } + + @Test + void customPassword() { + this.properties.setPassword("secret"); + assertThat(this.properties.getPassword()).isEqualTo("secret"); + } + + @Test + void customAddress() { + this.properties.setAddress("user:secret@rabbit.example.com:1234/alpha"); + assertThat(this.properties.getAddress()).isEqualTo("user:secret@rabbit.example.com:1234/alpha"); + } + + @Test + void determineAddressUsesHostAndPortDefaults() { + assertThat(this.properties.determineAddress()).isEqualTo("localhost:5672"); + } + + @Test + void determineAddressUsesHostAndPortProperties() { + this.properties.setHost("rabbit.example.com"); + this.properties.setPort(1234); + assertThat(this.properties.determineAddress()).isEqualTo("rabbit.example.com:1234"); + } + + @Test + void determineAddressUsesAddressProperty() { + this.properties.setAddress("rabbit.example.com:1234"); + assertThat(this.properties.determineAddress()).isEqualTo("rabbit.example.com:1234"); + } + + @Test + void determineAddressFromAmqpUrl() { + this.properties.setAddress("amqp://user:secret@rabbit.example.com:1234/alpha"); + assertThat(this.properties.determineAddress()).isEqualTo("rabbit.example.com:1234"); + assertThat(this.properties.determineUsername()).isEqualTo("user"); + assertThat(this.properties.determinePassword()).isEqualTo("secret"); + assertThat(this.properties.determineVirtualHost()).isEqualTo("alpha"); + } + + @Test + void determineAddressFromAmqpsUrl() { + this.properties.setAddress("amqps://user:secret@rabbit.example.com:1234"); + assertThat(this.properties.determineAddress()).isEqualTo("rabbit.example.com:1234"); + } + + @Test + void determineUsernameFromAddress() { + this.properties.setAddress("user:secret@rabbit.example.com:1234"); + assertThat(this.properties.determineUsername()).isEqualTo("user"); + } + + @Test + void determineUsernameReturnsPropertyWhenAddressHasNoUsername() { + this.properties.setUsername("alice"); + this.properties.setAddress("rabbit.example.com:1234"); + assertThat(this.properties.determineUsername()).isEqualTo("alice"); + } + + @Test + void determineUsernameWithoutPassword() { + this.properties.setAddress("user@rabbit.example.com:1234"); + assertThat(this.properties.determineUsername()).isEqualTo("user"); + assertThat(this.properties.determinePassword()).isEqualTo("guest"); + } + + @Test + void determinePasswordFromAddress() { + this.properties.setAddress("user:secret@rabbit.example.com:1234"); + assertThat(this.properties.determinePassword()).isEqualTo("secret"); + } + + @Test + void determinePasswordReturnsPropertyWhenAddressHasNoPassword() { + this.properties.setPassword("12345678"); + this.properties.setAddress("rabbit.example.com:1234"); + assertThat(this.properties.determinePassword()).isEqualTo("12345678"); + } + + @Test + void determineVirtualHostFromAddress() { + this.properties.setAddress("rabbit.example.com:1234/alpha"); + assertThat(this.properties.determineVirtualHost()).isEqualTo("alpha"); + } + + @Test + void determineVirtualHostReturnsPropertyWhenAddressHasNoVirtualHost() { + this.properties.setVirtualHost("alpha"); + this.properties.setAddress("rabbit.example.com:1234"); + assertThat(this.properties.determineVirtualHost()).isEqualTo("alpha"); + } + + @Test + void determineVirtualHostIsSlashWhenAddressHasTrailingSlash() { + this.properties.setAddress("amqp://root:password@otherhost:1111/"); + assertThat(this.properties.determineVirtualHost()).isEqualTo("/"); + } + + @Test + void ipv6Address() { + this.properties.setAddress("amqp://foo:bar@[aaaa:bbbb:cccc::d]:1234"); + assertThat(this.properties.determineAddress()).isEqualTo("[aaaa:bbbb:cccc::d]:1234"); + assertThat(this.properties.determinePort()).isEqualTo(1234); + } + + @Test + void ipv6AddressDefaultPort() { + this.properties.setAddress("amqp://foo:bar@[aaaa:bbbb:cccc::d]"); + assertThat(this.properties.determineAddress()).isEqualTo("[aaaa:bbbb:cccc::d]:5672"); + assertThat(this.properties.determinePort()).isEqualTo(5672); + } + + @Test + void hostPropertyMustBeSingleHost() { + this.properties.setHost("my-rmq-host.net,my-rmq-host-2.net"); + assertThatExceptionOfType(InvalidConfigurationPropertyValueException.class) + .isThrownBy(this.properties::determineAddress) + .withMessageContaining("spring.amqp.rabbitmq.host"); + } + + @Test + void customSslBundle() { + this.properties.getSsl().setBundle("test-bundle"); + assertThat(this.properties.getSsl().getBundle()).isEqualTo("test-bundle"); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/PropertiesAmqpRabbitConnectionDetailsTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/PropertiesAmqpRabbitConnectionDetailsTests.java new file mode 100644 index 000000000000..88d21b6cd271 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/PropertiesAmqpRabbitConnectionDetailsTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.rabbitmq.autoconfigure; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails.Address; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; +import org.springframework.boot.ssl.SslBundle; + +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 PropertiesAmqpRabbitConnectionDetails}. + * + * @author Stephane Nicoll + */ +class PropertiesAmqpRabbitConnectionDetailsTests { + + private AmqpRabbitProperties properties; + + private DefaultSslBundleRegistry sslBundleRegistry; + + private PropertiesAmqpRabbitConnectionDetails connectionDetails; + + @BeforeEach + void setUp() { + this.properties = new AmqpRabbitProperties(); + this.sslBundleRegistry = new DefaultSslBundleRegistry(); + this.connectionDetails = new PropertiesAmqpRabbitConnectionDetails(this.properties, this.sslBundleRegistry); + } + + @Test + void getAddressUsesDefaultHostAndPort() { + Address address = this.connectionDetails.getAddress(); + assertThat(address.host()).isEqualTo("localhost"); + assertThat(address.port()).isEqualTo(5672); + } + + @Test + void getAddressUsesHostAndPortProperties() { + this.properties.setHost("rabbit.example.com"); + this.properties.setPort(1234); + Address address = this.connectionDetails.getAddress(); + assertThat(address.host()).isEqualTo("rabbit.example.com"); + assertThat(address.port()).isEqualTo(1234); + } + + @Test + void getAddressUsesAddressProperty() { + this.properties.setAddress("rabbit.example.com:1234"); + Address address = this.connectionDetails.getAddress(); + assertThat(address.host()).isEqualTo("rabbit.example.com"); + assertThat(address.port()).isEqualTo(1234); + } + + @Test + void getAddressFromAmqpUrl() { + this.properties.setAddress("amqp://user:secret@rabbit.example.com:1234/alpha"); + Address address = this.connectionDetails.getAddress(); + assertThat(address.host()).isEqualTo("rabbit.example.com"); + assertThat(address.port()).isEqualTo(1234); + } + + @Test + void getAddressWithIpv6() { + this.properties.setAddress("amqp://foo:bar@[aaaa:bbbb:cccc::d]:1234"); + Address address = this.connectionDetails.getAddress(); + assertThat(address.host()).isEqualTo("[aaaa:bbbb:cccc::d]"); + assertThat(address.port()).isEqualTo(1234); + } + + @Test + void getUsernameReturnsDefaultGuest() { + assertThat(this.connectionDetails.getUsername()).isEqualTo("guest"); + } + + @Test + void getUsernameUsesProperty() { + this.properties.setUsername("alice"); + assertThat(this.connectionDetails.getUsername()).isEqualTo("alice"); + } + + @Test + void getUsernameFromAddress() { + this.properties.setAddress("user:secret@rabbit.example.com:1234"); + assertThat(this.connectionDetails.getUsername()).isEqualTo("user"); + } + + @Test + void getPasswordReturnsDefaultGuest() { + assertThat(this.connectionDetails.getPassword()).isEqualTo("guest"); + } + + @Test + void getPasswordUsesProperty() { + this.properties.setPassword("secret"); + assertThat(this.connectionDetails.getPassword()).isEqualTo("secret"); + } + + @Test + void getPasswordFromAddress() { + this.properties.setAddress("user:secret@rabbit.example.com:1234"); + assertThat(this.connectionDetails.getPassword()).isEqualTo("secret"); + } + + @Test + void getVirtualHostDefaultsToNull() { + assertThat(this.connectionDetails.getVirtualHost()).isNull(); + } + + @Test + void getVirtualHostUsesProperty() { + this.properties.setVirtualHost("alpha"); + assertThat(this.connectionDetails.getVirtualHost()).isEqualTo("alpha"); + } + + @Test + void getVirtualHostFromAddress() { + this.properties.setAddress("rabbit.example.com:1234/alpha"); + assertThat(this.connectionDetails.getVirtualHost()).isEqualTo("alpha"); + } + + @Test + void getSslBundleDefaultsToNull() { + assertThat(this.connectionDetails.getSslBundle()).isNull(); + } + + @Test + void getSslBundleUsesBundle() { + SslBundle bundle = mock(SslBundle.class); + this.sslBundleRegistry.registerBundle("test-bundle", bundle); + this.properties.getSsl().setBundle("test-bundle"); + assertThat(this.connectionDetails.getSslBundle()).isSameAs(bundle); + } + + @Test + void getSslBundleWithNoSslBundlesThrowsException() { + PropertiesAmqpRabbitConnectionDetails details = new PropertiesAmqpRabbitConnectionDetails(this.properties, + null); + this.properties.getSsl().setBundle("test-bundle"); + assertThatIllegalArgumentException().isThrownBy(details::getSslBundle) + .withMessageContaining("SSL bundle name has been set but no SSL bundles found in context"); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpListenerContainerFactoryConfigurerTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpListenerContainerFactoryConfigurerTests.java new file mode 100644 index 000000000000..16a1aad5153f --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpListenerContainerFactoryConfigurerTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.rabbitmq.autoconfigure; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitProperties.Listener.Batch; +import org.springframework.scheduling.TaskScheduler; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link RabbitAmqpListenerContainerFactoryConfigurer}. + * + * @author Stephane Nicoll + */ +class RabbitAmqpListenerContainerFactoryConfigurerTests { + + @Test + void configureAppliesDefaultListenerProperties() { + AmqpRabbitProperties properties = new AmqpRabbitProperties(); + RabbitAmqpListenerContainerFactoryConfigurer configurer = new RabbitAmqpListenerContainerFactoryConfigurer( + properties); + RabbitAmqpListenerContainerFactory factory = mock(RabbitAmqpListenerContainerFactory.class); + configurer.configure(factory); + then(factory).should().setObservationEnabled(false); + then(factory).should().setBatchReceiveTimeout(Duration.ofSeconds(30).toMillis()); + then(factory).should(never()).setBatchSize(any()); + then(factory).should(never()).setMessageConverter(any()); + then(factory).should(never()).setTaskScheduler(any()); + } + + @Test + void configureAppliesCustomListenerProperties() { + AmqpRabbitProperties properties = new AmqpRabbitProperties(); + properties.getListener().setObservationEnabled(true); + Batch batch = properties.getListener().getBatch(); + batch.setSize(10); + batch.setReceiveTimeout(Duration.ofSeconds(5)); + RabbitAmqpListenerContainerFactoryConfigurer configurer = new RabbitAmqpListenerContainerFactoryConfigurer( + properties); + RabbitAmqpListenerContainerFactory factory = mock(RabbitAmqpListenerContainerFactory.class); + configurer.configure(factory); + then(factory).should().setObservationEnabled(true); + then(factory).should().setBatchSize(10); + then(factory).should().setBatchReceiveTimeout(Duration.ofSeconds(5).toMillis()); + } + + @Test + void configureAppliesMessageConverter() { + AmqpRabbitProperties properties = new AmqpRabbitProperties(); + RabbitAmqpListenerContainerFactoryConfigurer configurer = new RabbitAmqpListenerContainerFactoryConfigurer( + properties); + MessageConverter messageConverter = mock(MessageConverter.class); + configurer.setMessageConverter(messageConverter); + RabbitAmqpListenerContainerFactory factory = mock(RabbitAmqpListenerContainerFactory.class); + configurer.configure(factory); + then(factory).should().setMessageConverter(messageConverter); + } + + @Test + void configureAppliesTaskScheduler() { + AmqpRabbitProperties properties = new AmqpRabbitProperties(); + RabbitAmqpListenerContainerFactoryConfigurer configurer = new RabbitAmqpListenerContainerFactoryConfigurer( + properties); + TaskScheduler taskScheduler = mock(TaskScheduler.class); + configurer.setTaskScheduler(taskScheduler); + RabbitAmqpListenerContainerFactory factory = mock(RabbitAmqpListenerContainerFactory.class); + configurer.configure(factory); + then(factory).should().setTaskScheduler(taskScheduler); + } + + @Test + void configureSkipsOptionalDependenciesWhenNotSet() { + AmqpRabbitProperties properties = new AmqpRabbitProperties(); + RabbitAmqpListenerContainerFactoryConfigurer configurer = new RabbitAmqpListenerContainerFactoryConfigurer( + properties); + RabbitAmqpListenerContainerFactory factory = mock(RabbitAmqpListenerContainerFactory.class); + configurer.configure(factory); + then(factory).should(never()).setMessageConverter(any()); + then(factory).should(never()).setTaskScheduler(any()); + then(factory).should(never()).setBatchSize(any()); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/AmqpRabbitHealthContributorAutoConfigurationTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/AmqpRabbitHealthContributorAutoConfigurationTests.java new file mode 100644 index 000000000000..3c0ee8b23ec3 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/AmqpRabbitHealthContributorAutoConfigurationTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.rabbitmq.autoconfigure.health; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitAutoConfiguration; +import org.springframework.boot.amqp.rabbitmq.health.AmqpRabbitHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.health.autoconfigure.contributor.HealthContributorAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AmqpRabbitHealthContributorAutoConfiguration}. + * + * @author Stephane Nicoll + */ +class AmqpRabbitHealthContributorAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AmqpRabbitAutoConfiguration.class, + AmqpRabbitHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); + + @Test + void runShouldCreateIndicator() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(AmqpRabbitHealthIndicator.class)); + } + + @Test + void runWhenDisabledShouldNotCreateIndicator() { + this.contextRunner.withPropertyValues("management.health.rabbit.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(AmqpRabbitHealthIndicator.class)); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/docker/compose/RabbitEnvironmentTests.java similarity index 91% rename from module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java rename to module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/docker/compose/RabbitEnvironmentTests.java index 1ecb0b4d6ff4..f732c725183d 100644 --- a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/docker/compose/RabbitEnvironmentTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.amqp.docker.compose; +package org.springframework.boot.amqp.rabbitmq.docker.compose; import java.util.Collections; import java.util.Map; @@ -52,19 +52,19 @@ void getUsernameWhenHasRabbitmqUsername() { } @Test - void getUsernameWhenNoRabbitmqDefaultPass() { + void getPasswordWhenNoRabbitmqDefaultPass() { RabbitEnvironment environment = new RabbitEnvironment(Collections.emptyMap()); assertThat(environment.getPassword()).isEqualTo("guest"); } @Test - void getUsernameWhenHasRabbitmqDefaultPass() { + void getPasswordWhenHasRabbitmqDefaultPass() { RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); assertThat(environment.getPassword()).isEqualTo("secret"); } @Test - void getUsernameWhenHasRabbitmqPassword() { + void getPasswordWhenHasRabbitmqPassword() { RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_PASSWORD", "secret")); assertThat(environment.getPassword()).isEqualTo("secret"); } diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/health/AmqpRabbitHealthIndicatorTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/health/AmqpRabbitHealthIndicatorTests.java new file mode 100644 index 000000000000..97de985c768a --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/health/AmqpRabbitHealthIndicatorTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.rabbitmq.health; + +import com.rabbitmq.client.amqp.Connection; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.boot.health.contributor.Health; +import org.springframework.boot.health.contributor.Status; + +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 AmqpRabbitHealthIndicator}. + * + * @author Stephane Nicoll + */ +@ExtendWith(MockitoExtension.class) +class AmqpRabbitHealthIndicatorTests { + + @Mock + @SuppressWarnings("NullAway.Init") + private AmqpConnectionFactory amqpConnectionFactory; + + @Mock + @SuppressWarnings("NullAway.Init") + private Connection connection; + + @Test + @SuppressWarnings("NullAway") // Test null check + void createWhenAmqpConnectionFactoryIsNullShouldThrowException() { + assertThatIllegalArgumentException().isThrownBy(() -> new AmqpRabbitHealthIndicator(null)) + .withMessageContaining("'amqpConnectionFactory' must not be null"); + } + + @Test + void healthWhenConnectionSucceedsShouldReturnUpWithVersion() { + given(this.amqpConnectionFactory.getConnection()).willReturn(this.connection); + Connection.ConnectionInfo connectionInfo = mock(Connection.ConnectionInfo.class); + given(connectionInfo.brokerVersion()).willReturn("123"); + given(this.connection.connectionInfo()).willReturn(connectionInfo); + Health health = new AmqpRabbitHealthIndicator(this.amqpConnectionFactory).health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsEntry("version", "123"); + } + + @Test + void healthWhenVersionIsMissingShouldReturnUpWithoutVersion() { + given(this.amqpConnectionFactory.getConnection()).willReturn(this.connection); + Connection.ConnectionInfo connectionInfo = mock(Connection.ConnectionInfo.class); + given(connectionInfo.brokerVersion()).willReturn(null); + given(this.connection.connectionInfo()).willReturn(connectionInfo); + Health health = new AmqpRabbitHealthIndicator(this.amqpConnectionFactory).health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).doesNotContainKey("version"); + } + + @Test + void healthWhenConnectionFailsShouldReturnDown() { + given(this.amqpConnectionFactory.getConnection()).willThrow(new RuntimeException()); + Health health = new AmqpRabbitHealthIndicator(this.amqpConnectionFactory).health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + } + +} diff --git a/module/spring-boot-autoconfigure-classic-modules/build.gradle b/module/spring-boot-autoconfigure-classic-modules/build.gradle index 355de273613f..cb82401cae75 100644 --- a/module/spring-boot-autoconfigure-classic-modules/build.gradle +++ b/module/spring-boot-autoconfigure-classic-modules/build.gradle @@ -31,6 +31,9 @@ dependencies { api(project(":module:spring-boot-amqp")) { transitive = false } + api(project(":module:spring-boot-amqp-rabbitmq")) { + transitive = false + } api(project(":module:spring-boot-artemis")) { transitive = false } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a89833cb5a0d..18a37bcd2505 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2063,10 +2063,22 @@ bom { } links { site("https://github.com/rabbitmq/rabbitmq-java-client") - javadoc("https://rabbitmq.github.io/rabbitmq-java-client/api/current", "com.rabbitmq") + javadoc("https://rabbitmq.github.io/rabbitmq-java-client/api/current", "com.rabbitmq.client") releaseNotes("https://github.com/rabbitmq/rabbitmq-java-client/releases/tag/v{version}") } } + library("RabbitMQ AMQP Client", "1.2.0") { + group("com.rabbitmq.client") { + modules = [ + "amqp-client" + ] + } + links { + site("https://github.com/rabbitmq/rabbitmq-amqp-java-client") + javadoc("https://rabbitmq.github.io/rabbitmq-amqp-java-client/stable/api", "com.rabbitmq.amqp.client") + releaseNotes("https://github.com/rabbitmq/rabbitmq-amqp-java-client/releases/tag/v{version}") + } + } library("RabbitMQ Stream Client", "1.6.0") { group("com.rabbitmq") { modules = [ @@ -2248,6 +2260,7 @@ bom { "spring-boot-starter-actuator-test", "spring-boot-starter-amqp", "spring-boot-starter-amqp-rabbitmq", + "spring-boot-starter-amqp-rabbitmq-test", "spring-boot-starter-amqp-test", "spring-boot-starter-artemis", "spring-boot-starter-artemis-test", diff --git a/settings.gradle b/settings.gradle index ce1e29e4c9b5..4a5e437de32f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -215,6 +215,7 @@ include "starter:spring-boot-starter-actuator" include "starter:spring-boot-starter-actuator-test" include "starter:spring-boot-starter-amqp" include "starter:spring-boot-starter-amqp-rabbitmq" +include "starter:spring-boot-starter-amqp-rabbitmq-test" include "starter:spring-boot-starter-amqp-test" include "starter:spring-boot-starter-artemis" include "starter:spring-boot-starter-artemis-test" diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleRabbitAmqpSimpleApplicationTests.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/rabbitmq/SampleRabbitAmqpSimpleApplicationTests.java similarity index 91% rename from smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleRabbitAmqpSimpleApplicationTests.java rename to smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/rabbitmq/SampleRabbitAmqpSimpleApplicationTests.java index bd9f51c562a5..e0c7f8e6853b 100644 --- a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleRabbitAmqpSimpleApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/rabbitmq/SampleRabbitAmqpSimpleApplicationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.amqp; +package smoketest.amqp.rabbitmq; import java.time.Duration; @@ -49,7 +49,8 @@ class SampleRabbitAmqpSimpleApplicationTests { @Test void sendSimpleMessage(CapturedOutput output) { this.sender.send("Test message"); - Awaitility.waitAtMost(Duration.ofMinutes(1)).untilAsserted(() -> assertThat(output).contains("Test message")); + Awaitility.waitAtMost(Duration.ofMinutes(1)) + .untilAsserted(() -> assertThat(output).contains("'Test message'").contains("'smoke-test'")); } } diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/SampleRabbitAmqpSimpleApplication.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/SampleRabbitAmqpSimpleApplication.java similarity index 82% rename from smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/SampleRabbitAmqpSimpleApplication.java rename to smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/SampleRabbitAmqpSimpleApplication.java index aff9bc342e3b..83c73d1ed34a 100644 --- a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/SampleRabbitAmqpSimpleApplication.java +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/SampleRabbitAmqpSimpleApplication.java @@ -14,22 +14,20 @@ * limitations under the License. */ -package smoketest.amqp; +package smoketest.amqp.rabbitmq; +import com.rabbitmq.client.amqp.Message; 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; import org.springframework.amqp.rabbit.annotation.RabbitListener; 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.messaging.handler.annotation.Payload; @SpringBootApplication -@RabbitListener(queues = "foo") public class SampleRabbitAmqpSimpleApplication { private static final Log logger = LogFactory.getLog(SampleRabbitAmqpSimpleApplication.class); @@ -44,9 +42,11 @@ public Queue fooQueue() { return new Queue("foo"); } - @RabbitHandler - public void process(@Payload String foo) { - logger.info(foo); + @RabbitListener(queues = "foo") + public void process(Message amqpMessage) { + String body = new String(amqpMessage.body()); + String sourceApp = (String) amqpMessage.property("source-app"); + logger.info("Received '%s' from '%s'".formatted(body, sourceApp)); } @Bean diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/Sender.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/Sender.java similarity index 78% rename from smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/Sender.java rename to smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/Sender.java index 6639bc0316ce..d8b873d8972b 100644 --- a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/Sender.java +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/Sender.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package smoketest.amqp; +package smoketest.amqp.rabbitmq; import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; import org.springframework.beans.factory.annotation.Autowired; @@ -24,8 +24,11 @@ public class Sender { @Autowired private RabbitAmqpTemplate rabbitAmqpTemplate; - public void send(String message) { - this.rabbitAmqpTemplate.convertAndSend("foo", message); + public void send(String payload) { + this.rabbitAmqpTemplate.convertAndSend("foo", payload, (message) -> { + message.getMessageProperties().setHeader("source-app", "smoke-test"); + return message; + }); } } diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/package-info.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/package-info.java similarity index 95% rename from smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/package-info.java rename to smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/package-info.java index bc9e41a079d7..62bf09a6b3df 100644 --- a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/package-info.java +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/package-info.java @@ -15,6 +15,6 @@ */ @NullMarked -package smoketest.amqp; +package smoketest.amqp.rabbitmq; import org.jspecify.annotations.NullMarked; diff --git a/starter/spring-boot-starter-amqp-rabbitmq-test/build.gradle b/starter/spring-boot-starter-amqp-rabbitmq-test/build.gradle new file mode 100644 index 000000000000..c4e673e9c221 --- /dev/null +++ b/starter/spring-boot-starter-amqp-rabbitmq-test/build.gradle @@ -0,0 +1,26 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for testing Spring AMQP with Rabbit MQ over AMQP 1.0 protocol" + +dependencies { + api(project(":starter:spring-boot-starter-amqp-rabbitmq")) + api(project(":starter:spring-boot-starter-test")) +} From aba84a385002b79b41c6168674c5bacb02a51e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 15 Jul 2026 20:13:22 +0200 Subject: [PATCH 700/930] Remove deprecated code Closes gh-50955 --- .../build/architecture/ArchitectureRules.java | 7 +- .../boot/env/EnvironmentPostProcessor.java | 65 ----- ...oriesEnvironmentPostProcessorsFactory.java | 35 +-- .../antora/modules/ROOT/pages/redirect.adoc | 1 - .../reference/pages/actuator/tracing.adoc | 18 -- .../modules/reference/pages/web/servlet.adoc | 16 +- .../build.gradle | 2 - ...OpenTelemetryTracingAutoConfiguration.java | 66 ----- .../autoconfigure/zipkin/package-info.java | 23 -- ...ot.autoconfigure.AutoConfiguration.imports | 1 - .../zipkin/DefaultEncodingConfiguration.java | 38 --- .../autoconfigure/zipkin/NoopSender.java | 44 ---- ...elemetryTracingAutoConfigurationTests.java | 226 ------------------ ...ontainerConnectionDetailsFactoryTests.java | 60 ----- ...goDbContainerConnectionDetailsFactory.java | 43 ---- .../main/resources/META-INF/spring.factories | 1 - ...nectionDetailsFactoryIntegrationTests.java | 62 ----- ...eo4jContainerConnectionDetailsFactory.java | 77 ------ .../main/resources/META-INF/spring.factories | 1 - ...nectionDetailsFactoryIntegrationTests.java | 97 -------- ...lsarContainerConnectionDetailsFactory.java | 65 ----- .../main/resources/META-INF/spring.factories | 1 - ...2dbcContainerConnectionDetailsFactory.java | 68 ------ ...2dbcContainerConnectionDetailsFactory.java | 68 ------ ...2dbcContainerConnectionDetailsFactory.java | 69 ------ ...2dbcContainerConnectionDetailsFactory.java | 70 ------ .../main/resources/META-INF/spring.factories | 4 - ...nectionDetailsFactoryIntegrationTests.java | 102 -------- ...bbitContainerConnectionDetailsFactory.java | 85 ------- .../main/resources/META-INF/spring.factories | 1 - .../autoconfigure/SessionProperties.java | 13 - .../autoconfigure/SessionPropertiesTests.java | 24 -- .../WebMvcAutoConfiguration.java | 27 --- .../autoconfigure/WebMvcProperties.java | 13 +- .../WebMvcAutoConfigurationTests.java | 19 -- .../boot/testsupport/container/TestImage.java | 44 ---- 36 files changed, 9 insertions(+), 1547 deletions(-) delete mode 100644 core/spring-boot/src/main/java/org/springframework/boot/env/EnvironmentPostProcessor.java delete mode 100644 module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfiguration.java delete mode 100644 module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/package-info.java delete mode 100644 module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/DefaultEncodingConfiguration.java delete mode 100644 module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/NoopSender.java delete mode 100644 module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfigurationTests.java delete mode 100644 module/spring-boot-mongodb/src/dockerTest/java/org/springframework/boot/mongodb/testcontainers/DeprecatedMongoDbContainerConnectionDetailsFactoryTests.java delete mode 100644 module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/testcontainers/DeprecatedMongoDbContainerConnectionDetailsFactory.java delete mode 100644 module/spring-boot-neo4j/src/dockerTest/java/org/springframework/boot/neo4j/testcontainers/DeprecatedNeo4jContainerConnectionDetailsFactoryIntegrationTests.java delete mode 100644 module/spring-boot-neo4j/src/main/java/org/springframework/boot/neo4j/testcontainers/DeprecatedNeo4jContainerConnectionDetailsFactory.java delete mode 100644 module/spring-boot-pulsar/src/dockerTest/java/org/springframework/boot/pulsar/testcontainers/DeprecatedPulsarContainerConnectionDetailsFactoryIntegrationTests.java delete mode 100644 module/spring-boot-pulsar/src/main/java/org/springframework/boot/pulsar/testcontainers/DeprecatedPulsarContainerConnectionDetailsFactory.java delete mode 100644 module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedMariaDbR2dbcContainerConnectionDetailsFactory.java delete mode 100644 module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedMySqlR2dbcContainerConnectionDetailsFactory.java delete mode 100644 module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedPostgresR2dbcContainerConnectionDetailsFactory.java delete mode 100644 module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedSqlServerR2dbcContainerConnectionDetailsFactory.java delete mode 100644 module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java delete mode 100644 module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java index b6c2440c4e97..02d3aaace8f6 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java @@ -189,12 +189,7 @@ private static DescribedPredicate doNotReturnSelfType() { } private static ArchRule allPackagesShouldBeFreeOfTangles() { - return SlicesRuleDefinition.slices() - .matching("(**)") - .should() - .beFreeOfCycles() - .ignoreDependency("org.springframework.boot.env.EnvironmentPostProcessor", - "org.springframework.boot.SpringApplication"); + return SlicesRuleDefinition.slices().matching("(**)").should().beFreeOfCycles(); } private static ArchRule allBeanPostProcessorBeanMethodsShouldBeStaticAndNotCausePrematureInitialization() { diff --git a/core/spring-boot/src/main/java/org/springframework/boot/env/EnvironmentPostProcessor.java b/core/spring-boot/src/main/java/org/springframework/boot/env/EnvironmentPostProcessor.java deleted file mode 100644 index 08ccbaf1d7a7..000000000000 --- a/core/spring-boot/src/main/java/org/springframework/boot/env/EnvironmentPostProcessor.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.bootstrap.BootstrapContext; -import org.springframework.boot.bootstrap.BootstrapRegistry; -import org.springframework.boot.bootstrap.ConfigurableBootstrapContext; -import org.springframework.boot.logging.DeferredLogFactory; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; - -/** - * Allows for customization of the application's {@link Environment} prior to the - * application context being refreshed. - *

    - * EnvironmentPostProcessor implementations have to be registered in - * {@code META-INF/spring.factories}, using the fully qualified name of this class as the - * key. Implementations may implement the {@link org.springframework.core.Ordered Ordered} - * interface or use an {@link org.springframework.core.annotation.Order @Order} annotation - * if they wish to be invoked in specific order. - *

    - * Since Spring Boot 2.4, {@code EnvironmentPostProcessor} implementations may optionally - * take the following constructor parameters: - *

      - *
    • {@link DeferredLogFactory} - A factory that can be used to create loggers with - * output deferred until the application has been fully prepared (allowing the environment - * itself to configure logging levels).
    • - *
    • {@link ConfigurableBootstrapContext} - A bootstrap context that can be used to - * store objects that may be expensive to create, or need to be shared - * ({@link BootstrapContext} or {@link BootstrapRegistry} may also be used).
    • - *
    - * - * @author Andy Wilkinson - * @author Stephane Nicoll - * @since 1.3.0 - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link org.springframework.boot.EnvironmentPostProcessor} - */ -@FunctionalInterface -@Deprecated(since = "4.0.0", forRemoval = true) -public interface EnvironmentPostProcessor { - - /** - * Post-process the given {@code environment}. - * @param environment the environment to post-process - * @param application the application to which the environment belongs - */ - void postProcessEnvironment(ConfigurableEnvironment environment, - org.springframework.boot.SpringApplication application); - -} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/support/SpringFactoriesEnvironmentPostProcessorsFactory.java b/core/spring-boot/src/main/java/org/springframework/boot/support/SpringFactoriesEnvironmentPostProcessorsFactory.java index 745686055cf5..f0bc73105407 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/support/SpringFactoriesEnvironmentPostProcessorsFactory.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/support/SpringFactoriesEnvironmentPostProcessorsFactory.java @@ -16,18 +16,13 @@ package org.springframework.boot.support; -import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; import org.springframework.boot.EnvironmentPostProcessor; -import org.springframework.boot.SpringApplication; import org.springframework.boot.bootstrap.BootstrapContext; import org.springframework.boot.bootstrap.BootstrapRegistry; import org.springframework.boot.bootstrap.ConfigurableBootstrapContext; import org.springframework.boot.logging.DeferredLogFactory; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver; @@ -51,35 +46,7 @@ public List getEnvironmentPostProcessors(DeferredLogFa argumentResolver = argumentResolver.and(ConfigurableBootstrapContext.class, bootstrapContext); argumentResolver = argumentResolver.and(BootstrapContext.class, bootstrapContext); argumentResolver = argumentResolver.and(BootstrapRegistry.class, bootstrapContext); - List postProcessors = new ArrayList<>(); - postProcessors.addAll(this.loader.load(EnvironmentPostProcessor.class, argumentResolver)); - postProcessors.addAll(loadDeprecatedPostProcessors(argumentResolver)); - AnnotationAwareOrderComparator.sort(postProcessors); - return postProcessors.stream().map(Adapter::apply).collect(Collectors.toCollection(ArrayList::new)); - } - - @SuppressWarnings("removal") - private List loadDeprecatedPostProcessors( - ArgumentResolver argumentResolver) { - return this.loader.load(org.springframework.boot.env.EnvironmentPostProcessor.class, argumentResolver); - } - - @SuppressWarnings("removal") - record Adapter( - org.springframework.boot.env.EnvironmentPostProcessor postProcessor) implements EnvironmentPostProcessor { - - @Override - public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { - this.postProcessor.postProcessEnvironment(environment, application); - } - - static EnvironmentPostProcessor apply(Object source) { - if (source instanceof EnvironmentPostProcessor environmentPostProcessor) { - return environmentPostProcessor; - } - return new Adapter((org.springframework.boot.env.EnvironmentPostProcessor) source); - } - + return this.loader.load(EnvironmentPostProcessor.class, argumentResolver); } } diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc index f5de88d29eb6..393ca0ded329 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc @@ -1200,7 +1200,6 @@ * xref:reference:actuator/tracing.adoc#actuator.micrometer-tracing.tests[#actuator.micrometer-tracing.tests] * xref:reference:actuator/tracing.adoc#actuator.micrometer-tracing.tracer-implementations.brave-zipkin[#actuator.micrometer-tracing.tracer-implementations.brave-zipkin] * xref:reference:actuator/tracing.adoc#actuator.micrometer-tracing.tracer-implementations.otel-otlp[#actuator.micrometer-tracing.tracer-implementations.otel-otlp] -* xref:reference:actuator/tracing.adoc#actuator.micrometer-tracing.tracer-implementations.otel-zipkin[#actuator.micrometer-tracing.tracer-implementations.otel-zipkin] * xref:reference:actuator/tracing.adoc#actuator.micrometer-tracing.tracer-implementations[#actuator.micrometer-tracing.tracer-implementations] * xref:reference:actuator/tracing.adoc#actuator.micrometer-tracing.tracers[#actuator.micrometer-tracing.tracers] * xref:reference:actuator/tracing.adoc#actuator.micrometer-tracing[#actuator.micrometer-tracing] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc index 744e6fab240b..1d270ff91cf5 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc @@ -125,24 +125,6 @@ The customizers take precedence over anything applied by the auto-configuration. -[[actuator.micrometer-tracing.tracer-implementations.otel-zipkin]] -=== OpenTelemetry With Zipkin - -WARNING: OpenTelemetry has https://opentelemetry.io/docs/specs/otel/trace/sdk_exporters/zipkin/[deprecated their Zipkin support]. -The auto-configuration for it will be removed in Spring Boot 4.2. -Either switch to Brave or consider using https://github.com/openzipkin-contrib/zipkin-otel[the Zipkin OTel module] for ingesting OTLP directly. - -Tracing with OpenTelemetry and reporting to Zipkin requires the following dependencies: - -* `org.springframework.boot:spring-boot-micrometer-tracing-opentelemetry` - Spring Boot's support for Micrometer Tracing over OpenTelemetry. -* `io.micrometer:micrometer-tracing-bridge-otel` - bridges the Micrometer Observation API to OpenTelemetry. -* `org.springframework.boot:spring-boot-zipkin` - Spring Boot's support for Zipkin. -* `io.opentelemetry:opentelemetry-exporter-zipkin` - OpenTelemetry exporter that reports traces to Zipkin. - -Use the `management.tracing.export.zipkin.*` configuration properties to configure reporting to Zipkin. - - - [[actuator.micrometer-tracing.tracer-implementations.brave-zipkin]] === OpenZipkin Brave With Zipkin diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc index 5a01011474c0..f4d095471a03 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc @@ -278,22 +278,10 @@ spring: markdown: "text/markdown" ---- -As of Spring Framework 5.3, Spring MVC supports two strategies for matching request paths to controllers. -By default, Spring Boot uses the javadoc:org.springframework.web.util.pattern.PathPatternParser[] strategy. -javadoc:org.springframework.web.util.pattern.PathPatternParser[] is an https://spring.io/blog/2020/06/30/url-matching-with-pathpattern-in-spring-mvc[optimized implementation] but comes with some restrictions compared to the javadoc:org.springframework.util.AntPathMatcher[] strategy. -javadoc:org.springframework.web.util.pattern.PathPatternParser[] restricts usage of {url-spring-framework-docs}/web/webmvc/mvc-controller/ann-requestmapping.html#mvc-ann-requestmapping-uri-templates[some path pattern variants]. +Spring Boot uses the javadoc:org.springframework.web.util.pattern.PathPatternParser[] strategy for matching request paths to controllers. +javadoc:org.springframework.web.util.pattern.PathPatternParser[] is an https://spring.io/blog/2020/06/30/url-matching-with-pathpattern-in-spring-mvc[optimized implementation] but restricts usage of {url-spring-framework-docs}/web/webmvc/mvc-controller/ann-requestmapping.html#mvc-ann-requestmapping-uri-templates[some path pattern variants]. It is also incompatible with configuring the javadoc:org.springframework.web.servlet.DispatcherServlet[] with a path prefix (configprop:spring.mvc.servlet.path[]). -The strategy can be configured using the configprop:spring.mvc.pathmatch.matching-strategy[] configuration property, as shown in the following example: - -[configprops,yaml] ----- -spring: - mvc: - pathmatch: - matching-strategy: "ant-path-matcher" ----- - Spring MVC will throw a javadoc:org.springframework.web.servlet.NoHandlerFoundException[] if a handler is not found for a request. Note that, by default, the xref:web/servlet.adoc#web.servlet.spring-mvc.static-content[serving of static content] is mapped to `+/**+` and will, therefore, provide a handler for all requests. If no static content is available, javadoc:org.springframework.web.servlet.resource.ResourceHttpRequestHandler[] will throw a javadoc:org.springframework.web.servlet.resource.NoResourceFoundException[]. diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/build.gradle b/module/spring-boot-micrometer-tracing-opentelemetry/build.gradle index 22f1cca8643e..379b51490ee8 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/build.gradle +++ b/module/spring-boot-micrometer-tracing-opentelemetry/build.gradle @@ -37,9 +37,7 @@ dependencies { optional(project(":core:spring-boot-autoconfigure")) optional(project(":core:spring-boot-docker-compose")) optional(project(":core:spring-boot-testcontainers")) - optional(project(":module:spring-boot-zipkin")) optional("io.micrometer:micrometer-tracing-bridge-otel") - optional("io.opentelemetry:opentelemetry-exporter-zipkin") optional("io.opentelemetry:opentelemetry-exporter-otlp") optional("org.junit.platform:junit-platform-launcher") optional("org.testcontainers:testcontainers-grafana") diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfiguration.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfiguration.java deleted file mode 100644 index 2968cac57274..000000000000 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfiguration.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.zipkin; - -import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; -import zipkin2.Span; -import zipkin2.reporter.BytesEncoder; -import zipkin2.reporter.BytesMessageSender; -import zipkin2.reporter.Encoding; -import zipkin2.reporter.SpanBytesEncoder; - -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.micrometer.tracing.autoconfigure.ConditionalOnEnabledTracingExport; -import org.springframework.context.annotation.Bean; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for Zipkin tracing with - * OpenTelemetry. - * - * @author Moritz Halbritter - * @author Stefan Bratanov - * @author Wick Dynex - * @author Phillip Webb - * @since 4.0.0 - * @deprecated since 4.0.4 for removal in 4.2.0 - */ -@SuppressWarnings("deprecation") -@AutoConfiguration(afterName = "org.springframework.boot.zipkin.autoconfigure.ZipkinAutoConfiguration") -@ConditionalOnClass({ ZipkinSpanExporter.class, Span.class }) -@Deprecated(since = "4.0.4", forRemoval = true) -public final class ZipkinWithOpenTelemetryTracingAutoConfiguration { - - @Bean - @ConditionalOnBean(Encoding.class) - @ConditionalOnMissingBean(value = Span.class, parameterizedContainer = BytesEncoder.class) - BytesEncoder spanBytesEncoder(Encoding encoding) { - return SpanBytesEncoder.forEncoding(encoding); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnBean(BytesMessageSender.class) - @ConditionalOnEnabledTracingExport("zipkin") - ZipkinSpanExporter zipkinSpanExporter(BytesMessageSender sender, BytesEncoder spanBytesEncoder) { - return ZipkinSpanExporter.builder().setSender(sender).setEncoder(spanBytesEncoder).build(); - } - -} diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/package-info.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/package-info.java deleted file mode 100644 index 3b758c0e2474..000000000000 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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 tracing with Zipkin. - */ -@NullMarked -package org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.zipkin; - -import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 7eb042722d81..b7824e841fcd 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,2 @@ org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryTracingAutoConfiguration org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingAutoConfiguration -org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.zipkin.ZipkinWithOpenTelemetryTracingAutoConfiguration diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/DefaultEncodingConfiguration.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/DefaultEncodingConfiguration.java deleted file mode 100644 index 55363dd1b511..000000000000 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/DefaultEncodingConfiguration.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.zipkin; - -import zipkin2.reporter.Encoding; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.zipkin.autoconfigure.ZipkinAutoConfiguration; -import org.springframework.context.annotation.Bean; - -/** - * Configures the bean {@linkplain ZipkinAutoConfiguration} would from properties. - */ -@TestConfiguration(proxyBeanMethods = false) -class DefaultEncodingConfiguration { - - @Bean - @ConditionalOnMissingBean - Encoding zipkinReporterEncoding() { - return Encoding.JSON; - } - -} diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/NoopSender.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/NoopSender.java deleted file mode 100644 index 655f6282eba5..000000000000 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/NoopSender.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.zipkin; - -import java.io.IOException; -import java.util.List; - -import zipkin2.reporter.BytesMessageSender; -import zipkin2.reporter.Encoding; - -class NoopSender extends BytesMessageSender.Base { - - NoopSender(Encoding encoding) { - super(encoding); - } - - @Override - public int messageMaxBytes() { - return 1024; - } - - @Override - public void send(List encodedSpans) { - } - - @Override - public void close() throws IOException { - } - -} diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfigurationTests.java deleted file mode 100644 index e423baff4ed7..000000000000 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/ZipkinWithOpenTelemetryTracingAutoConfigurationTests.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.zipkin; - -import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; -import io.opentelemetry.sdk.trace.export.SpanExporter; -import org.junit.jupiter.api.Test; -import zipkin2.Span; -import zipkin2.reporter.BytesEncoder; -import zipkin2.reporter.BytesMessageSender; -import zipkin2.reporter.Encoding; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.boot.zipkin.autoconfigure.ZipkinAutoConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ZipkinWithOpenTelemetryTracingAutoConfiguration}. - * - * @author Moritz Halbritter - */ -@SuppressWarnings({ "removal", "deprecation" }) -class ZipkinWithOpenTelemetryTracingAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ZipkinWithOpenTelemetryTracingAutoConfiguration.class, - DefaultEncodingConfiguration.class)); - - @Test - void shouldNotSupplyBeansIfInfrastructureIsNotAvailable() { - new ApplicationContextRunner().withUserConfiguration(ZipkinWithOpenTelemetryTracingAutoConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean(BytesEncoder.class) - .doesNotHaveBean(SpanExporter.class) - .doesNotHaveBean(ZipkinSpanExporter.class)); - } - - @Test - void shouldSupplyBeansIfInfrastructureIsAvailable() { - this.contextRunner.withConfiguration(AutoConfigurations.of(ZipkinAutoConfiguration.class)).run((context) -> { - assertThat(context).hasSingleBean(SpanExporter.class); - assertThat(context).hasSingleBean(ZipkinSpanExporter.class); - }); - } - - @Test - void shouldNotSupplyBeansIfTracingIsDisabled() { - this.contextRunner.withPropertyValues("management.tracing.export.enabled=false") - .withConfiguration(AutoConfigurations.of(ZipkinAutoConfiguration.class)) - .run((context) -> { - assertThat(context).doesNotHaveBean(SpanExporter.class); - assertThat(context).doesNotHaveBean(ZipkinSpanExporter.class); - }); - } - - @Test - void backsOffWithoutEncoding() { - new ApplicationContextRunner().withUserConfiguration(ZipkinWithOpenTelemetryTracingAutoConfiguration.class) - .run((context) -> { - assertThat(context).hasNotFailed(); - assertThat(context).doesNotHaveBean(ZipkinSpanExporter.class); - assertThat(context).doesNotHaveBean(BytesEncoder.class); - }); - } - - @Test - void shouldSupplyBeans() { - this.contextRunner.withUserConfiguration(SenderConfiguration.class, CustomEncoderConfiguration.class) - .run((context) -> { - assertThat(context).hasSingleBean(ZipkinSpanExporter.class); - assertThat(context).hasBean("customSpanEncoder"); - }); - } - - @Test - void shouldNotSupplyZipkinSpanExporterIfSenderIsMissing() { - this.contextRunner.run((context) -> { - assertThat(context).doesNotHaveBean(ZipkinSpanExporter.class); - assertThat(context).hasBean("spanBytesEncoder"); - }); - } - - @Test - void shouldNotSupplyZipkinSpanExporterIfNotOnClasspath() { - this.contextRunner.withClassLoader(new FilteredClassLoader("io.opentelemetry.exporter.zipkin")) - .withUserConfiguration(SenderConfiguration.class) - .run((context) -> { - assertThat(context).doesNotHaveBean(ZipkinSpanExporter.class); - assertThat(context).doesNotHaveBean("spanBytesEncoder"); - }); - - } - - @Test - void shouldBackOffIfZipkinIsNotOnClasspath() { - this.contextRunner.withClassLoader(new FilteredClassLoader("zipkin2.Span")) - .withUserConfiguration(SenderConfiguration.class) - .run((context) -> { - assertThat(context).doesNotHaveBean(ZipkinSpanExporter.class); - assertThat(context).doesNotHaveBean("spanBytesEncoder"); - }); - } - - @Test - void shouldBackOffOnCustomBeans() { - this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> { - assertThat(context).hasBean("customZipkinSpanExporter"); - assertThat(context).hasSingleBean(ZipkinSpanExporter.class); - }); - } - - @Test - void shouldNotSupplyZipkinSpanExporterIfGlobalTracingIsDisabled() { - this.contextRunner.withPropertyValues("management.tracing.export.enabled=false") - .withUserConfiguration(SenderConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean(ZipkinSpanExporter.class)); - } - - @Test - void shouldNotSupplyZipkinSpanExporterIfZipkinTracingIsDisabled() { - this.contextRunner.withPropertyValues("management.tracing.export.zipkin.enabled=false") - .withUserConfiguration(SenderConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean(ZipkinSpanExporter.class)); - } - - @Test - void shouldUseCustomEncoderBean() { - this.contextRunner.withUserConfiguration(SenderConfiguration.class, CustomEncoderConfiguration.class) - .run((context) -> { - assertThat(context).hasSingleBean(ZipkinSpanExporter.class); - assertThat(context).hasBean("customSpanEncoder"); - assertThat(context.getBean(ZipkinSpanExporter.class)).extracting("encoder") - .isInstanceOf(CustomSpanEncoder.class) - .extracting("encoding") - .isEqualTo(Encoding.JSON); - }); - } - - @Test - void shouldUseCustomEncodingBean() { - this.contextRunner - .withUserConfiguration(SenderConfiguration.class, CustomEncodingConfiguration.class, - CustomEncoderConfiguration.class) - .run((context) -> { - assertThat(context).hasSingleBean(ZipkinSpanExporter.class); - assertThat(context).hasBean("customSpanEncoder"); - assertThat(context.getBean(ZipkinSpanExporter.class)).extracting("encoder") - .isInstanceOf(CustomSpanEncoder.class) - .extracting("encoding") - .isEqualTo(Encoding.PROTO3); - }); - } - - @Configuration(proxyBeanMethods = false) - private static final class CustomEncodingConfiguration { - - @Bean - Encoding encoding() { - return Encoding.PROTO3; - } - - } - - @Configuration(proxyBeanMethods = false) - private static final class SenderConfiguration { - - @Bean - BytesMessageSender sender(Encoding encoding) { - return new NoopSender(encoding); - } - - } - - @Configuration(proxyBeanMethods = false) - private static final class CustomConfiguration { - - @Bean - ZipkinSpanExporter customZipkinSpanExporter() { - return ZipkinSpanExporter.builder().build(); - } - - } - - @Configuration(proxyBeanMethods = false) - private static final class CustomEncoderConfiguration { - - @Bean - BytesEncoder customSpanEncoder(Encoding encoding) { - return new CustomSpanEncoder(encoding); - } - - } - - record CustomSpanEncoder(Encoding encoding) implements BytesEncoder { - - @Override - public int sizeInBytes(Span span) { - throw new UnsupportedOperationException(); - } - - @Override - public byte[] encode(Span span) { - throw new UnsupportedOperationException(); - } - - } - -} diff --git a/module/spring-boot-mongodb/src/dockerTest/java/org/springframework/boot/mongodb/testcontainers/DeprecatedMongoDbContainerConnectionDetailsFactoryTests.java b/module/spring-boot-mongodb/src/dockerTest/java/org/springframework/boot/mongodb/testcontainers/DeprecatedMongoDbContainerConnectionDetailsFactoryTests.java deleted file mode 100644 index 3f2f433632d3..000000000000 --- a/module/spring-boot-mongodb/src/dockerTest/java/org/springframework/boot/mongodb/testcontainers/DeprecatedMongoDbContainerConnectionDetailsFactoryTests.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.mongodb.testcontainers; - -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoClients; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.MongoDBContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.mongodb.autoconfigure.MongoConnectionDetails; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.testsupport.container.TestImage; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration tests for {@link DeprecatedMongoDbContainerConnectionDetailsFactory}. - * - * @author Andy Wilkinson - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link MongoDbContainerConnectionDetailsFactory}. - */ -@SpringJUnitConfig -@Testcontainers(disabledWithoutDocker = true) -@Deprecated(since = "4.0.0", forRemoval = true) -class DeprecatedMongoDbContainerConnectionDetailsFactoryTests { - - @Container - @ServiceConnection - static final MongoDBContainer mongoDb = TestImage.container(MongoDBContainer.class); - - @Autowired(required = false) - private MongoConnectionDetails connectionDetails; - - @Test - void connectionCanBeMadeToContainer() { - assertThat(this.connectionDetails).isNotNull(); - MongoClient client = MongoClients.create(this.connectionDetails.getConnectionString()); - assertThat(client.listDatabaseNames()).containsExactly("admin", "config", "local"); - } - -} diff --git a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/testcontainers/DeprecatedMongoDbContainerConnectionDetailsFactory.java b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/testcontainers/DeprecatedMongoDbContainerConnectionDetailsFactory.java deleted file mode 100644 index 1f4084270e4c..000000000000 --- a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/testcontainers/DeprecatedMongoDbContainerConnectionDetailsFactory.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.mongodb.testcontainers; - -import org.testcontainers.containers.MongoDBContainer; - -import org.springframework.boot.mongodb.autoconfigure.MongoConnectionDetails; -import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; - -/** - * {@link ContainerConnectionDetailsFactory} to create {@link MongoConnectionDetails} from - * a {@link ServiceConnection @ServiceConnection}-annotated {@link MongoDBContainer}. - * - * @author Moritz Halbritter - * @author Andy Wilkinson - * @author Phillip Webb - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link MongoDbContainerConnectionDetailsFactory}. - */ -@Deprecated(since = "4.0.0", forRemoval = true) -class DeprecatedMongoDbContainerConnectionDetailsFactory - extends AbstractMongoContainerConnectionDetailsFactory { - - DeprecatedMongoDbContainerConnectionDetailsFactory() { - super(MongoDBContainer::getReplicaSetUrl); - } - -} diff --git a/module/spring-boot-mongodb/src/main/resources/META-INF/spring.factories b/module/spring-boot-mongodb/src/main/resources/META-INF/spring.factories index a772684516f2..189e62c27c3d 100644 --- a/module/spring-boot-mongodb/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-mongodb/src/main/resources/META-INF/spring.factories @@ -1,6 +1,5 @@ # Connection Details Factories org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ org.springframework.boot.mongodb.docker.compose.MongoDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.mongodb.testcontainers.DeprecatedMongoDbContainerConnectionDetailsFactory,\ org.springframework.boot.mongodb.testcontainers.MongoDbAtlasLocalContainerConnectionDetailsFactory,\ org.springframework.boot.mongodb.testcontainers.MongoDbContainerConnectionDetailsFactory diff --git a/module/spring-boot-neo4j/src/dockerTest/java/org/springframework/boot/neo4j/testcontainers/DeprecatedNeo4jContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-neo4j/src/dockerTest/java/org/springframework/boot/neo4j/testcontainers/DeprecatedNeo4jContainerConnectionDetailsFactoryIntegrationTests.java deleted file mode 100644 index feeb6ad630d7..000000000000 --- a/module/spring-boot-neo4j/src/dockerTest/java/org/springframework/boot/neo4j/testcontainers/DeprecatedNeo4jContainerConnectionDetailsFactoryIntegrationTests.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.neo4j.testcontainers; - -import org.junit.jupiter.api.Test; -import org.neo4j.driver.Driver; -import org.neo4j.driver.GraphDatabase; -import org.testcontainers.containers.Neo4jContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.neo4j.autoconfigure.Neo4jConnectionDetails; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.testsupport.container.TestImage; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration tests for {@link DeprecatedNeo4jContainerConnectionDetailsFactory}. - * - * @author Stephane Nicoll - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link Neo4jContainerConnectionDetailsFactory}. - */ -@SpringJUnitConfig -@Testcontainers(disabledWithoutDocker = true) -@Deprecated(since = "4.0.0", forRemoval = true) -class DeprecatedNeo4jContainerConnectionDetailsFactoryIntegrationTests { - - @Container - @ServiceConnection - static final Neo4jContainer container = TestImage.container(Neo4jContainer.class); - - @Autowired(required = false) - private Neo4jConnectionDetails connectionDetails; - - @Test - void connectionCanBeMadeToContainer() { - assertThat(this.connectionDetails).isNotNull(); - try (Driver driver = GraphDatabase.driver(this.connectionDetails.getUri(), - this.connectionDetails.getAuthToken())) { - driver.verifyConnectivity(); - } - } - -} diff --git a/module/spring-boot-neo4j/src/main/java/org/springframework/boot/neo4j/testcontainers/DeprecatedNeo4jContainerConnectionDetailsFactory.java b/module/spring-boot-neo4j/src/main/java/org/springframework/boot/neo4j/testcontainers/DeprecatedNeo4jContainerConnectionDetailsFactory.java deleted file mode 100644 index d762b04fa7ca..000000000000 --- a/module/spring-boot-neo4j/src/main/java/org/springframework/boot/neo4j/testcontainers/DeprecatedNeo4jContainerConnectionDetailsFactory.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.neo4j.testcontainers; - -import java.net.URI; - -import org.neo4j.driver.AuthToken; -import org.neo4j.driver.AuthTokens; -import org.testcontainers.containers.Neo4jContainer; - -import org.springframework.boot.neo4j.autoconfigure.Neo4jConnectionDetails; -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 Neo4jConnectionDetails} from - * a {@link ServiceConnection @ServiceConnection}-annotated {@link Neo4jContainer}. - * - * @author Moritz Halbritter - * @author Andy Wilkinson - * @author Phillip Webb - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link Neo4jContainerConnectionDetailsFactory}. - */ -@Deprecated(since = "4.0.0", forRemoval = true) -class DeprecatedNeo4jContainerConnectionDetailsFactory - extends ContainerConnectionDetailsFactory, Neo4jConnectionDetails> { - - DeprecatedNeo4jContainerConnectionDetailsFactory() { - super(ANY_CONNECTION_NAME, "org.neo4j.driver.AuthToken"); - } - - @Override - protected Neo4jConnectionDetails getContainerConnectionDetails( - ContainerConnectionSource> source) { - return new Neo4jContainerConnectionDetails(source); - } - - /** - * {@link Neo4jConnectionDetails} backed by a {@link ContainerConnectionSource}. - */ - private static final class Neo4jContainerConnectionDetails extends ContainerConnectionDetails> - implements Neo4jConnectionDetails { - - private Neo4jContainerConnectionDetails(ContainerConnectionSource> source) { - super(source); - } - - @Override - public URI getUri() { - return URI.create(getContainer().getBoltUrl()); - } - - @Override - public AuthToken getAuthToken() { - String password = getContainer().getAdminPassword(); - return (password != null) ? AuthTokens.basic("neo4j", password) : AuthTokens.none(); - } - - } - -} diff --git a/module/spring-boot-neo4j/src/main/resources/META-INF/spring.factories b/module/spring-boot-neo4j/src/main/resources/META-INF/spring.factories index 70daabc504b7..98d3b5dfdbed 100644 --- a/module/spring-boot-neo4j/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-neo4j/src/main/resources/META-INF/spring.factories @@ -1,5 +1,4 @@ # Connection Details Factories org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ org.springframework.boot.neo4j.docker.compose.Neo4jDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.neo4j.testcontainers.DeprecatedNeo4jContainerConnectionDetailsFactory,\ org.springframework.boot.neo4j.testcontainers.Neo4jContainerConnectionDetailsFactory diff --git a/module/spring-boot-pulsar/src/dockerTest/java/org/springframework/boot/pulsar/testcontainers/DeprecatedPulsarContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-pulsar/src/dockerTest/java/org/springframework/boot/pulsar/testcontainers/DeprecatedPulsarContainerConnectionDetailsFactoryIntegrationTests.java deleted file mode 100644 index ec1edb553b34..000000000000 --- a/module/spring-boot-pulsar/src/dockerTest/java/org/springframework/boot/pulsar/testcontainers/DeprecatedPulsarContainerConnectionDetailsFactoryIntegrationTests.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.pulsar.testcontainers; - -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.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.pulsar.autoconfigure.PulsarAutoConfiguration; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.testsupport.container.TestImage; -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 DeprecatedPulsarContainerConnectionDetailsFactory}. - * - * @author Chris Bono - * @author Stephane Nicoll - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link PulsarContainerConnectionDetailsFactory}. - */ -@SpringJUnitConfig -@Testcontainers(disabledWithoutDocker = true) -@TestPropertySource(properties = { "spring.pulsar.consumer.subscription.initial-position=earliest" }) -@Deprecated(since = "4.0.0", forRemoval = true) -class DeprecatedPulsarContainerConnectionDetailsFactoryIntegrationTests { - - @Container - @ServiceConnection - @SuppressWarnings("unused") - static final PulsarContainer pulsar = TestImage.container(PulsarContainer.class); - - @Autowired - private PulsarTemplate pulsarTemplate; - - @Autowired - private TestListener listener; - - @Test - void connectionCanBeMadeToPulsarContainer() { - 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/module/spring-boot-pulsar/src/main/java/org/springframework/boot/pulsar/testcontainers/DeprecatedPulsarContainerConnectionDetailsFactory.java b/module/spring-boot-pulsar/src/main/java/org/springframework/boot/pulsar/testcontainers/DeprecatedPulsarContainerConnectionDetailsFactory.java deleted file mode 100644 index 46ba6a8a2e71..000000000000 --- a/module/spring-boot-pulsar/src/main/java/org/springframework/boot/pulsar/testcontainers/DeprecatedPulsarContainerConnectionDetailsFactory.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.pulsar.testcontainers; - -import org.testcontainers.containers.PulsarContainer; - -import org.springframework.boot.pulsar.autoconfigure.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 - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link PulsarContainerConnectionDetailsFactory}. - */ -@Deprecated(since = "4.0.0", forRemoval = true) -class DeprecatedPulsarContainerConnectionDetailsFactory - 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 getBrokerUrl() { - return getContainer().getPulsarBrokerUrl(); - } - - @Override - public String getAdminUrl() { - return getContainer().getHttpServiceUrl(); - } - - } - -} diff --git a/module/spring-boot-pulsar/src/main/resources/META-INF/spring.factories b/module/spring-boot-pulsar/src/main/resources/META-INF/spring.factories index b04e3949dce3..ceb868e08f08 100644 --- a/module/spring-boot-pulsar/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-pulsar/src/main/resources/META-INF/spring.factories @@ -1,5 +1,4 @@ # Connection Details Factories org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ org.springframework.boot.pulsar.docker.compose.PulsarDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.pulsar.testcontainers.DeprecatedPulsarContainerConnectionDetailsFactory,\ org.springframework.boot.pulsar.testcontainers.PulsarContainerConnectionDetailsFactory diff --git a/module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedMariaDbR2dbcContainerConnectionDetailsFactory.java b/module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedMariaDbR2dbcContainerConnectionDetailsFactory.java deleted file mode 100644 index f25f4f273473..000000000000 --- a/module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedMariaDbR2dbcContainerConnectionDetailsFactory.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.testcontainers; - -import io.r2dbc.spi.ConnectionFactoryOptions; -import org.testcontainers.containers.MariaDBContainer; -import org.testcontainers.containers.MariaDBR2DBCDatabaseContainer; - -import org.springframework.boot.r2dbc.autoconfigure.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 MariaDBContainer}. - * - * @author Moritz Halbritter - * @author Andy Wilkinson - * @author Phillip Webb - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link MariaDbR2dbcContainerConnectionDetailsFactory}. - */ -@Deprecated(since = "4.0.0", forRemoval = true) -class DeprecatedMariaDbR2dbcContainerConnectionDetailsFactory - extends ContainerConnectionDetailsFactory, R2dbcConnectionDetails> { - - DeprecatedMariaDbR2dbcContainerConnectionDetailsFactory() { - super(ANY_CONNECTION_NAME, "io.r2dbc.spi.ConnectionFactoryOptions"); - } - - @Override - public R2dbcConnectionDetails getContainerConnectionDetails(ContainerConnectionSource> source) { - return new MariaDbR2dbcDatabaseContainerConnectionDetails(source); - } - - /** - * {@link R2dbcConnectionDetails} backed by a {@link ContainerConnectionSource}. - */ - private static final class MariaDbR2dbcDatabaseContainerConnectionDetails - extends ContainerConnectionDetails> implements R2dbcConnectionDetails { - - private MariaDbR2dbcDatabaseContainerConnectionDetails(ContainerConnectionSource> source) { - super(source); - } - - @Override - public ConnectionFactoryOptions getConnectionFactoryOptions() { - return MariaDBR2DBCDatabaseContainer.getOptions(getContainer()); - } - - } - -} diff --git a/module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedMySqlR2dbcContainerConnectionDetailsFactory.java b/module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedMySqlR2dbcContainerConnectionDetailsFactory.java deleted file mode 100644 index f926b5bc6dcb..000000000000 --- a/module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedMySqlR2dbcContainerConnectionDetailsFactory.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.testcontainers; - -import io.r2dbc.spi.ConnectionFactoryOptions; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.containers.MySQLR2DBCDatabaseContainer; - -import org.springframework.boot.r2dbc.autoconfigure.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 MySQLContainer}. - * - * @author Moritz Halbritter - * @author Andy Wilkinson - * @author Phillip Webb - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link MySqlR2dbcContainerConnectionDetailsFactory}. - */ -@Deprecated(since = "4.0.0", forRemoval = true) -class DeprecatedMySqlR2dbcContainerConnectionDetailsFactory - extends ContainerConnectionDetailsFactory, R2dbcConnectionDetails> { - - DeprecatedMySqlR2dbcContainerConnectionDetailsFactory() { - super(ANY_CONNECTION_NAME, "io.r2dbc.spi.ConnectionFactoryOptions"); - } - - @Override - public R2dbcConnectionDetails getContainerConnectionDetails(ContainerConnectionSource> source) { - return new MySqlR2dbcDatabaseContainerConnectionDetails(source); - } - - /** - * {@link R2dbcConnectionDetails} backed by a {@link ContainerConnectionSource}. - */ - private static final class MySqlR2dbcDatabaseContainerConnectionDetails - extends ContainerConnectionDetails> implements R2dbcConnectionDetails { - - private MySqlR2dbcDatabaseContainerConnectionDetails(ContainerConnectionSource> source) { - super(source); - } - - @Override - public ConnectionFactoryOptions getConnectionFactoryOptions() { - return MySQLR2DBCDatabaseContainer.getOptions(getContainer()); - } - - } - -} diff --git a/module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedPostgresR2dbcContainerConnectionDetailsFactory.java b/module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedPostgresR2dbcContainerConnectionDetailsFactory.java deleted file mode 100644 index d8ee6ba41f4a..000000000000 --- a/module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedPostgresR2dbcContainerConnectionDetailsFactory.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.testcontainers; - -import io.r2dbc.spi.ConnectionFactoryOptions; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.containers.PostgreSQLR2DBCDatabaseContainer; - -import org.springframework.boot.r2dbc.autoconfigure.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 PostgreSQLContainer}. - * - * @author Moritz Halbritter - * @author Andy Wilkinson - * @author Phillip Webb - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link PostgresR2dbcContainerConnectionDetailsFactory}. - */ -@Deprecated(since = "4.0.0", forRemoval = true) -class DeprecatedPostgresR2dbcContainerConnectionDetailsFactory - extends ContainerConnectionDetailsFactory, R2dbcConnectionDetails> { - - DeprecatedPostgresR2dbcContainerConnectionDetailsFactory() { - super(ANY_CONNECTION_NAME, "io.r2dbc.spi.ConnectionFactoryOptions"); - } - - @Override - public R2dbcConnectionDetails getContainerConnectionDetails( - ContainerConnectionSource> source) { - return new PostgresR2dbcDatabaseContainerConnectionDetails(source); - } - - /** - * {@link R2dbcConnectionDetails} backed by a {@link ContainerConnectionSource}. - */ - private static final class PostgresR2dbcDatabaseContainerConnectionDetails - extends ContainerConnectionDetails> implements R2dbcConnectionDetails { - - PostgresR2dbcDatabaseContainerConnectionDetails(ContainerConnectionSource> source) { - super(source); - } - - @Override - public ConnectionFactoryOptions getConnectionFactoryOptions() { - return PostgreSQLR2DBCDatabaseContainer.getOptions(getContainer()); - } - - } - -} diff --git a/module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedSqlServerR2dbcContainerConnectionDetailsFactory.java b/module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedSqlServerR2dbcContainerConnectionDetailsFactory.java deleted file mode 100644 index b9efcc9457ae..000000000000 --- a/module/spring-boot-r2dbc/src/main/java/org/springframework/boot/r2dbc/testcontainers/DeprecatedSqlServerR2dbcContainerConnectionDetailsFactory.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.testcontainers; - -import io.r2dbc.spi.ConnectionFactoryOptions; -import org.testcontainers.containers.MSSQLR2DBCDatabaseContainer; -import org.testcontainers.containers.MSSQLServerContainer; - -import org.springframework.boot.r2dbc.autoconfigure.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 MSSQLServerContainer}. - * - * @author Moritz Halbritter - * @author Andy Wilkinson - * @author Phillip Webb - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link SqlServerR2dbcContainerConnectionDetailsFactory}. - */ -@Deprecated(since = "4.0.0", forRemoval = true) -class DeprecatedSqlServerR2dbcContainerConnectionDetailsFactory - extends ContainerConnectionDetailsFactory, R2dbcConnectionDetails> { - - DeprecatedSqlServerR2dbcContainerConnectionDetailsFactory() { - super(ANY_CONNECTION_NAME, "io.r2dbc.spi.ConnectionFactoryOptions"); - } - - @Override - public R2dbcConnectionDetails getContainerConnectionDetails( - ContainerConnectionSource> source) { - return new MsSqlServerR2dbcDatabaseContainerConnectionDetails(source); - } - - /** - * {@link R2dbcConnectionDetails} backed by a {@link ContainerConnectionSource}. - */ - private static final class MsSqlServerR2dbcDatabaseContainerConnectionDetails - extends ContainerConnectionDetails> implements R2dbcConnectionDetails { - - private MsSqlServerR2dbcDatabaseContainerConnectionDetails( - ContainerConnectionSource> source) { - super(source); - } - - @Override - public ConnectionFactoryOptions getConnectionFactoryOptions() { - return MSSQLR2DBCDatabaseContainer.getOptions(getContainer()); - } - - } - -} diff --git a/module/spring-boot-r2dbc/src/main/resources/META-INF/spring.factories b/module/spring-boot-r2dbc/src/main/resources/META-INF/spring.factories index b71026ca9c7e..92eacd6c66f2 100644 --- a/module/spring-boot-r2dbc/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-r2dbc/src/main/resources/META-INF/spring.factories @@ -8,10 +8,6 @@ org.springframework.boot.r2dbc.docker.compose.OracleXeR2dbcDockerComposeConnecti org.springframework.boot.r2dbc.docker.compose.PostgresR2dbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.r2dbc.docker.compose.SqlServerR2dbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.r2dbc.testcontainers.ClickHouseR2dbcContainerConnectionDetailsFactory,\ -org.springframework.boot.r2dbc.testcontainers.DeprecatedMariaDbR2dbcContainerConnectionDetailsFactory,\ -org.springframework.boot.r2dbc.testcontainers.DeprecatedMySqlR2dbcContainerConnectionDetailsFactory,\ -org.springframework.boot.r2dbc.testcontainers.DeprecatedPostgresR2dbcContainerConnectionDetailsFactory,\ -org.springframework.boot.r2dbc.testcontainers.DeprecatedSqlServerR2dbcContainerConnectionDetailsFactory,\ org.springframework.boot.r2dbc.testcontainers.MariaDbR2dbcContainerConnectionDetailsFactory,\ org.springframework.boot.r2dbc.testcontainers.MySqlR2dbcContainerConnectionDetailsFactory,\ org.springframework.boot.r2dbc.testcontainers.OracleFreeR2dbcContainerConnectionDetailsFactory,\ diff --git a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java deleted file mode 100644 index 61b0a43c5311..000000000000 --- a/module/spring-boot-rabbitmq/src/dockerTest/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.rabbitmq.testcontainers; - -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.containers.RabbitMQContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import org.springframework.amqp.rabbit.annotation.Queue; -import org.springframework.amqp.rabbit.annotation.RabbitListener; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration; -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.testsupport.container.TestImage; -import org.springframework.context.annotation.Bean; -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 DeprecatedRabbitContainerConnectionDetailsFactory}. - * - * @author Moritz Halbritter - * @author Andy Wilkinson - * @author Phillip Webb - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link RabbitMqContainerConnectionDetailsFactoryIntegrationTests}. - */ -@SpringJUnitConfig -@Testcontainers(disabledWithoutDocker = true) -@Deprecated(since = "4.0.0", forRemoval = true) -class DeprecatedRabbitContainerConnectionDetailsFactoryIntegrationTests { - - @Container - @ServiceConnection - static final RabbitMQContainer rabbit = TestImage.container(RabbitMQContainer.class); - - @Autowired(required = false) - private RabbitConnectionDetails connectionDetails; - - @Autowired - private RabbitTemplate rabbitTemplate; - - @Autowired - private TestListener listener; - - @Test - void connectionCanBeMadeToRabbitContainer() { - assertThat(this.connectionDetails).isNotNull(); - this.rabbitTemplate.convertAndSend("test", "message"); - Awaitility.waitAtMost(Duration.ofMinutes(4)) - .untilAsserted(() -> assertThat(this.listener.messages).containsExactly("message")); - - } - - @Configuration(proxyBeanMethods = false) - @ImportAutoConfiguration(RabbitAutoConfiguration.class) - static class TestConfiguration { - - @Bean - TestListener testListener() { - return new TestListener(); - } - - } - - static class TestListener { - - private final List messages = new ArrayList<>(); - - @RabbitListener(queuesToDeclare = @Queue("test")) - void processMessage(String message) { - this.messages.add(message); - } - - } - -} diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java deleted file mode 100644 index 750d611deb67..000000000000 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/testcontainers/DeprecatedRabbitContainerConnectionDetailsFactory.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.rabbitmq.testcontainers; - -import java.net.URI; -import java.util.List; - -import org.jspecify.annotations.Nullable; -import org.testcontainers.containers.RabbitMQContainer; - -import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails; -import org.springframework.boot.ssl.SslBundle; -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 RabbitConnectionDetails} - * from a {@link ServiceConnection @ServiceConnection}-annotated - * {@link RabbitMQContainer}. - * - * @author Moritz Halbritter - * @author Andy Wilkinson - * @author Phillip Webb - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link RabbitMqContainerConnectionDetailsFactory}. - */ -@Deprecated(since = "4.0.0", forRemoval = true) -class DeprecatedRabbitContainerConnectionDetailsFactory - extends ContainerConnectionDetailsFactory { - - @Override - protected RabbitConnectionDetails getContainerConnectionDetails( - ContainerConnectionSource source) { - return new RabbitMqContainerConnectionDetails(source); - } - - /** - * {@link RabbitConnectionDetails} backed by a {@link ContainerConnectionSource}. - */ - private static final class RabbitMqContainerConnectionDetails extends ContainerConnectionDetails - implements RabbitConnectionDetails { - - private RabbitMqContainerConnectionDetails(ContainerConnectionSource source) { - super(source); - } - - @Override - public String getUsername() { - return getContainer().getAdminUsername(); - } - - @Override - public String getPassword() { - return getContainer().getAdminPassword(); - } - - @Override - public List
    getAddresses() { - URI uri = URI.create((getSslBundle() != null) ? getContainer().getAmqpsUrl() : getContainer().getAmqpUrl()); - return List.of(new Address(uri.getHost(), uri.getPort())); - } - - @Override - public @Nullable SslBundle getSslBundle() { - return super.getSslBundle(); - } - - } - -} diff --git a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories index a7a1d458e73e..8ed1fd222c4c 100644 --- a/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-rabbitmq/src/main/resources/META-INF/spring.factories @@ -2,7 +2,6 @@ org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ org.springframework.boot.rabbitmq.docker.compose.RabbitMqDockerComposeConnectionDetailsFactory,\ org.springframework.boot.rabbitmq.docker.compose.RabbitMqStreamDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.rabbitmq.testcontainers.DeprecatedRabbitContainerConnectionDetailsFactory,\ org.springframework.boot.rabbitmq.testcontainers.RabbitMqContainerConnectionDetailsFactory,\ org.springframework.boot.rabbitmq.testcontainers.RabbitMqStreamContainerConnectionDetailsFactory diff --git a/module/spring-boot-session/src/main/java/org/springframework/boot/session/autoconfigure/SessionProperties.java b/module/spring-boot-session/src/main/java/org/springframework/boot/session/autoconfigure/SessionProperties.java index 30216c894aba..c4d6784e504a 100644 --- a/module/spring-boot-session/src/main/java/org/springframework/boot/session/autoconfigure/SessionProperties.java +++ b/module/spring-boot-session/src/main/java/org/springframework/boot/session/autoconfigure/SessionProperties.java @@ -21,7 +21,6 @@ import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; -import java.util.function.Supplier; import org.jspecify.annotations.Nullable; @@ -65,18 +64,6 @@ public void setServlet(Servlet servlet) { this.servlet = servlet; } - /** - * Determine the session timeout. If no timeout is configured, the - * {@code fallbackTimeout} is used. - * @param fallbackTimeout a fallback timeout value if the timeout isn't configured - * @return the session timeout - * @deprecated since 4.0.1 for removal in 4.2.0 in favor of {@link SessionTimeout} - */ - @Deprecated(since = "4.0.1", forRemoval = true) - public Duration determineTimeout(Supplier fallbackTimeout) { - return (this.timeout != null) ? this.timeout : fallbackTimeout.get(); - } - /** * Servlet-related properties. */ diff --git a/module/spring-boot-session/src/test/java/org/springframework/boot/session/autoconfigure/SessionPropertiesTests.java b/module/spring-boot-session/src/test/java/org/springframework/boot/session/autoconfigure/SessionPropertiesTests.java index 797381e58b05..d9ebd9076b52 100644 --- a/module/spring-boot-session/src/test/java/org/springframework/boot/session/autoconfigure/SessionPropertiesTests.java +++ b/module/spring-boot-session/src/test/java/org/springframework/boot/session/autoconfigure/SessionPropertiesTests.java @@ -16,17 +16,12 @@ package org.springframework.boot.session.autoconfigure; -import java.time.Duration; -import java.util.function.Supplier; - import org.junit.jupiter.api.Test; import org.springframework.core.Ordered; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.within; -import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.mock; /** * Tests for {@link SessionProperties}. @@ -37,25 +32,6 @@ class SessionPropertiesTests { private final SessionProperties properties = new SessionProperties(); - @Test - @SuppressWarnings({ "unchecked", "removal" }) - @Deprecated(since = "4.0.1", forRemoval = true) - void determineTimeoutWithTimeoutIgnoreFallback() { - this.properties.setTimeout(Duration.ofMinutes(1)); - Supplier fallback = mock(Supplier.class); - assertThat(this.properties.determineTimeout(fallback)).isEqualTo(Duration.ofMinutes(1)); - then(fallback).shouldHaveNoInteractions(); - } - - @Test - @SuppressWarnings("removal") - @Deprecated(since = "4.0.1", forRemoval = true) - void determineTimeoutWithNoTimeoutUseFallback() { - this.properties.setTimeout(null); - Duration fallback = Duration.ofMinutes(2); - assertThat(this.properties.determineTimeout(() -> fallback)).isSameAs(fallback); - } - @Test void defaultFilterOrderIsCloseToHighestPrecedence() { assertThat(this.properties.getServlet().getFilterOrder()).isCloseTo(Ordered.HIGHEST_PRECEDENCE, within(50)); diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java index 281b124124a0..fbcb80ae2597 100644 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java @@ -84,7 +84,6 @@ import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; import org.springframework.lang.Contract; -import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -116,7 +115,6 @@ import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.ResourceChainRegistration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; @@ -137,7 +135,6 @@ import org.springframework.web.servlet.view.BeanNameViewResolver; import org.springframework.web.servlet.view.ContentNegotiatingViewResolver; import org.springframework.web.servlet.view.InternalResourceViewResolver; -import org.springframework.web.util.UrlPathHelper; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link EnableWebMvc Web MVC}. @@ -268,30 +265,6 @@ public void configureAsyncSupport(AsyncSupportConfigurer configurer) { } } - @Override - @SuppressWarnings("removal") - public void configurePathMatch(PathMatchConfigurer configurer) { - if (this.mvcProperties.getPathmatch() - .getMatchingStrategy() == WebMvcProperties.MatchingStrategy.ANT_PATH_MATCHER) { - configurer.setPathMatcher(new AntPathMatcher()); - this.dispatcherServletPath.ifAvailable((dispatcherPath) -> { - String servletUrlMapping = dispatcherPath.getServletUrlMapping(); - if (servletUrlMapping.equals("/") && singleDispatcherServlet()) { - UrlPathHelper urlPathHelper = new UrlPathHelper(); - urlPathHelper.setAlwaysUseFullPath(true); - configurer.setUrlPathHelper(urlPathHelper); - } - }); - } - } - - private boolean singleDispatcherServlet() { - return this.servletRegistrations.stream() - .map(ServletRegistrationBean::getServlet) - .filter(DispatcherServlet.class::isInstance) - .count() == 1; - } - @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { WebMvcProperties.Contentnegotiation contentnegotiation = this.mvcProperties.getContentnegotiation(); diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcProperties.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcProperties.java index a95ff9b22310..aa7486556b09 100644 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcProperties.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcProperties.java @@ -25,6 +25,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.context.properties.bind.Name; import org.springframework.http.MediaType; import org.springframework.util.Assert; @@ -96,6 +97,7 @@ public class WebMvcProperties { private final Contentnegotiation contentnegotiation = new Contentnegotiation(); + @Deprecated(since = "4.2.0", forRemoval = true) private final Pathmatch pathmatch = new Pathmatch(); private final Problemdetails problemdetails = new Problemdetails(); @@ -186,6 +188,7 @@ public Contentnegotiation getContentnegotiation() { return this.contentnegotiation; } + @Deprecated(since = "4.2.0", forRemoval = true) public Pathmatch getPathmatch() { return this.pathmatch; } @@ -368,6 +371,7 @@ public void setDefaultContentTypes(List defaultContentTypes) { } + @Deprecated(since = "4.2.0", forRemoval = true) public static class Pathmatch { /** @@ -375,6 +379,7 @@ public static class Pathmatch { */ private MatchingStrategy matchingStrategy = MatchingStrategy.PATH_PATTERN_PARSER; + @DeprecatedConfigurationProperty(reason = "Path matching is no longer configurable", since = "4.2.0") public MatchingStrategy getMatchingStrategy() { return this.matchingStrategy; } @@ -436,14 +441,6 @@ public void setDateTime(@Nullable String dateTime) { */ public enum MatchingStrategy { - /** - * Use the {@code AntPathMatcher} implementation. - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link #PATH_PATTERN_PARSER} - */ - @Deprecated(since = "4.0.0", forRemoval = true) - ANT_PATH_MATCHER, - /** * Use the {@code PathPatternParser} implementation. */ diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java index 2629ccbd0107..4d9661126a16 100644 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java @@ -947,25 +947,6 @@ void customPrinterAndParserShouldBeRegisteredAsConverters() { }); } - @Test - void urlPathHelperUsesFullPathByDefaultWhenAntPathMatchingIsUsed() { - this.contextRunner.withPropertyValues("spring.mvc.pathmatch.matching-strategy:ant-path-matcher") - .run((context) -> { - UrlPathHelper urlPathHelper = context.getBean(UrlPathHelper.class); - assertThat(urlPathHelper).extracting("alwaysUseFullPath").isEqualTo(true); - }); - } - - @Test - void urlPathHelperDoesNotUseFullPathWithServletMapping() { - this.contextRunner.withPropertyValues("spring.mvc.pathmatch.matching-strategy:ant-path-matcher") - .withPropertyValues("spring.mvc.servlet.path=/test/") - .run((context) -> { - UrlPathHelper urlPathHelper = context.getBean(UrlPathHelper.class); - assertThat(urlPathHelper).extracting("alwaysUseFullPath").isEqualTo(false); - }); - } - @Test void urlPathHelperDoesNotUseFullPathWithAdditionalDispatcherServlet() { this.contextRunner.withUserConfiguration(AdditionalDispatcherServletConfiguration.class).run((context) -> { diff --git a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java index a47e4aefde26..ef81e9bb5201 100644 --- a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java +++ b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java @@ -174,17 +174,6 @@ public enum TestImage { (container) -> ((MongoDBContainer) container).withStartupAttempts(5) .withStartupTimeout(Duration.ofMinutes(5))), - /** - * A container image suitable for testing MongoDB using the deprecated - * {@link org.testcontainers.containers.MongoDBContainer}. - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of {@link #MONGODB} - */ - @SuppressWarnings("deprecation") - @Deprecated(since = "3.4.0", forRemoval = true) - MONGODB_DEPRECATED("mongo", "5.0.17", () -> org.testcontainers.containers.MongoDBContainer.class, - (container) -> ((org.testcontainers.containers.MongoDBContainer) container).withStartupAttempts(5) - .withStartupTimeout(Duration.ofMinutes(5))), - /** * A container image suitable for testing MongoDB Atlas. */ @@ -204,17 +193,6 @@ public enum TestImage { (container) -> ((Neo4jContainer) container).withStartupAttempts(5) .withStartupTimeout(Duration.ofMinutes(10))), - /** - * A container image suitable for testing Neo4j using the deprecated - * {@link org.testcontainers.containers.Neo4jContainer}. - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of {@link #NEO4J} - */ - @SuppressWarnings({ "deprecation", "rawtypes" }) - @Deprecated(since = "3.4.0", forRemoval = true) - NEO4J_DEPRECATED("neo4j", "5.26.11", () -> org.testcontainers.containers.Neo4jContainer.class, - (container) -> ((org.testcontainers.containers.Neo4jContainer) container).withStartupAttempts(5) - .withStartupTimeout(Duration.ofMinutes(10))), - /** * A container image suitable for testing Oracle Free. */ @@ -248,17 +226,6 @@ public enum TestImage { .withStartupTimeout(Duration.ofMinutes(3)) .withEnv("PULSAR_PREFIX_advertisedAddress", "localhost")), - /** - * A container image suitable for testing Pulsar using the deprecated - * {@link org.testcontainers.containers.PulsarContainer}. - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of {@link #PULSAR} - */ - @SuppressWarnings("deprecation") - @Deprecated(since = "3.4.0", forRemoval = true) - PULSAR_DEPRECATED("apachepulsar/pulsar", "3.3.3", () -> org.testcontainers.containers.PulsarContainer.class, - (container) -> ((org.testcontainers.containers.PulsarContainer) container).withStartupAttempts(2) - .withStartupTimeout(Duration.ofMinutes(3))), - /** * A container image suitable for testing RabbitMQ with support for both AMQP 0.9 and * 1.0. Queues can be declared using {@code rabbitmqadmin}. @@ -266,17 +233,6 @@ public enum TestImage { RABBITMQ("rabbitmq", "4.2-management", () -> RabbitMQContainer.class, (container) -> ((RabbitMQContainer) container).withStartupTimeout(Duration.ofMinutes(4))), - /** - * A container image suitable for testing RabbitMQ using the deprecated - * {@link org.testcontainers.containers.RabbitMQContainer}. - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of {@link #RABBITMQ} - */ - @SuppressWarnings("deprecation") - @Deprecated(since = "3.4.0", forRemoval = true) - RABBITMQ_DEPRECATED("rabbitmq", "3.11-alpine", () -> org.testcontainers.containers.RabbitMQContainer.class, - (container) -> ((org.testcontainers.containers.RabbitMQContainer) container) - .withStartupTimeout(Duration.ofMinutes(4))), - /** * A container image suitable for testing Redis. */ From 5aec048d4bfb02837022a9a803737905e30e7223 Mon Sep 17 00:00:00 2001 From: sourabh bhattacharjee Date: Tue, 30 Jun 2026 04:53:27 +0200 Subject: [PATCH 701/930] Add plugin management for hibernate-maven-plugin See gh-50892 Signed-off-by: sourabh bhattacharjee --- platform/spring-boot-dependencies/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 18a37bcd2505..54f1608f5fa7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -660,6 +660,9 @@ bom { "hibernate-testing", "hibernate-vector", ] + plugins = [ + "hibernate-maven-plugin" + ] } links { site("https://hibernate.org/orm") From 046d4c64bcc9ac9499963c4575024f321a1141a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 15 Jul 2026 20:43:23 +0200 Subject: [PATCH 702/930] Upgrade to Json Path 3.0.0 Closes gh-50786 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 54f1608f5fa7..12631e99866e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1162,7 +1162,7 @@ bom { releaseNotes("https://github.com/jOOQ/jOOQ/releases/tag/version-{version}") } } - library("Json Path", "2.10.0") { + library("Json Path", "3.0.0") { group("com.jayway.jsonpath") { modules = [ "json-path", From 5f7bdcc029178a98c8be10183778701c88135398 Mon Sep 17 00:00:00 2001 From: Venkata Naga Sai Srikanth Gollapudi <42247688+gollapudisrikanth@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:36:00 -0500 Subject: [PATCH 703/930] Add support for configuring start timeout for JMS health checks See gh-50957 Signed-off-by: Venkata Naga Sai Srikanth Gollapudi <42247688+gollapudisrikanth@users.noreply.github.com> --- ...JmsHealthContributorAutoConfiguration.java | 7 ++- .../health/JmsHealthIndicatorProperties.java | 49 +++++++++++++++++++ .../boot/jms/health/JmsHealthIndicator.java | 21 +++++++- ...althContributorAutoConfigurationTests.java | 24 +++++++++ .../jms/health/JmsHealthIndicatorTests.java | 46 +++++++++++++++-- 5 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthIndicatorProperties.java diff --git a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfiguration.java b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfiguration.java index 6c3642f48a2c..d3c49b689fb4 100644 --- a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfiguration.java +++ b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfiguration.java @@ -24,6 +24,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.context.properties.EnableConfigurationProperties; import org.springframework.boot.health.autoconfigure.contributor.CompositeHealthContributorConfiguration; import org.springframework.boot.health.autoconfigure.contributor.ConditionalOnEnabledHealthIndicator; import org.springframework.boot.health.contributor.HealthContributor; @@ -35,17 +36,19 @@ * {@link EnableAutoConfiguration Auto-configuration} for {@link JmsHealthIndicator}. * * @author Stephane Nicoll + * @author Venkata Naga Sai Srikanth Gollapudi * @since 4.0.0 */ @AutoConfiguration(after = JmsAutoConfiguration.class) @ConditionalOnClass({ ConnectionFactory.class, JmsHealthIndicator.class }) @ConditionalOnBean(ConnectionFactory.class) @ConditionalOnEnabledHealthIndicator("jms") +@EnableConfigurationProperties(JmsHealthIndicatorProperties.class) public final class JmsHealthContributorAutoConfiguration extends CompositeHealthContributorConfiguration { - JmsHealthContributorAutoConfiguration() { - super(JmsHealthIndicator::new); + JmsHealthContributorAutoConfiguration(JmsHealthIndicatorProperties properties) { + super((connectionFactory) -> new JmsHealthIndicator(connectionFactory, properties.getTimeout())); } @Bean diff --git a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthIndicatorProperties.java b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthIndicatorProperties.java new file mode 100644 index 000000000000..d8b4f9ca6b50 --- /dev/null +++ b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthIndicatorProperties.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jms.autoconfigure.health; + +import java.time.Duration; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jms.health.JmsHealthIndicator; +import org.springframework.util.Assert; + +/** + * External configuration properties for {@link JmsHealthIndicator}. + * + * @author Venkata Naga Sai Srikanth Gollapudi + * @since 4.x + */ +@ConfigurationProperties("management.health.jms") +public class JmsHealthIndicatorProperties { + + /** + * Timeout to use when starting a connection for the health check. + */ + private Duration timeout = Duration.ofSeconds(5); + + public Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(Duration timeout) { + Assert.notNull(timeout, "'timeout' must not be null"); + Assert.isTrue(timeout.compareTo(Duration.ZERO) > 0, "'timeout' must be greater than 0"); + this.timeout = timeout; + } + +} diff --git a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/health/JmsHealthIndicator.java b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/health/JmsHealthIndicator.java index ac3681de69bd..625cdf7b68ff 100644 --- a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/health/JmsHealthIndicator.java +++ b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/health/JmsHealthIndicator.java @@ -16,6 +16,7 @@ package org.springframework.boot.jms.health; +import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -25,25 +26,40 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.boot.convert.DurationStyle; import org.springframework.boot.health.contributor.AbstractHealthIndicator; import org.springframework.boot.health.contributor.Health; import org.springframework.boot.health.contributor.HealthIndicator; +import org.springframework.core.log.LogMessage; +import org.springframework.util.Assert; /** * {@link HealthIndicator} for a JMS {@link ConnectionFactory}. * * @author Stephane Nicoll + * @author Venkata Naga Sai Srikanth Gollapudi * @since 4.0.0 */ public class JmsHealthIndicator extends AbstractHealthIndicator { + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5); + private final Log logger = LogFactory.getLog(JmsHealthIndicator.class); private final ConnectionFactory connectionFactory; + private final Duration timeout; + public JmsHealthIndicator(ConnectionFactory connectionFactory) { + this(connectionFactory, DEFAULT_TIMEOUT); + } + + public JmsHealthIndicator(ConnectionFactory connectionFactory, Duration timeout) { super("JMS health check failed"); + Assert.notNull(timeout, "'timeout' must not be null"); + Assert.isTrue(timeout.compareTo(Duration.ZERO) > 0, "'timeout' must be greater than 0"); this.connectionFactory = connectionFactory; + this.timeout = timeout; } @Override @@ -67,9 +83,10 @@ private final class MonitoredConnection { void start() throws JMSException { new Thread(() -> { try { - if (!this.latch.await(5, TimeUnit.SECONDS)) { + if (!this.latch.await(JmsHealthIndicator.this.timeout.toNanos(), TimeUnit.NANOSECONDS)) { 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.", + DurationStyle.SIMPLE.print(JmsHealthIndicator.this.timeout))); closeConnection(); } } diff --git a/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfigurationTests.java b/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfigurationTests.java index 43795bff614f..30b9ab314449 100644 --- a/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfigurationTests.java +++ b/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfigurationTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.jms.autoconfigure.health; +import java.time.Duration; + import jakarta.jms.ConnectionFactory; import org.junit.jupiter.api.Test; @@ -31,6 +33,7 @@ * Tests for {@link JmsHealthContributorAutoConfiguration}. * * @author Phillip Webb + * @author Venkata Naga Sai Srikanth Gollapudi */ class JmsHealthContributorAutoConfigurationTests { @@ -44,6 +47,27 @@ void runShouldCreateIndicator() { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(JmsHealthIndicator.class)); } + @Test + void runWhenTimeoutIsConfiguredShouldCreateIndicatorWithConfiguredTimeout() { + this.contextRunner.withPropertyValues("management.health.jms.timeout=10ms").run((context) -> { + assertThat(context).hasSingleBean(JmsHealthIndicator.class); + assertThat(context).hasSingleBean(JmsHealthIndicatorProperties.class); + assertThat(context.getBean(JmsHealthIndicatorProperties.class).getTimeout()) + .isEqualTo(Duration.ofMillis(10)); + assertThat(context.getBean(JmsHealthIndicator.class)).hasFieldOrPropertyWithValue("timeout", + Duration.ofMillis(10)); + }); + } + + @Test + void runWhenTimeoutIsZeroShouldFail() { + this.contextRunner.withPropertyValues("management.health.jms.timeout=0ms") + .run((context) -> assertThat(context).hasFailed() + .getFailure() + .rootCause() + .hasMessage("'timeout' must be greater than 0")); + } + @Test void runWhenDisabledShouldNotCreateIndicator() { this.contextRunner.withPropertyValues("management.health.jms.enabled:false") diff --git a/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/health/JmsHealthIndicatorTests.java b/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/health/JmsHealthIndicatorTests.java index c454945b5df5..ee9c783b89b3 100644 --- a/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/health/JmsHealthIndicatorTests.java +++ b/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/health/JmsHealthIndicatorTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.jms.health; +import java.time.Duration; + import jakarta.jms.Connection; import jakarta.jms.ConnectionFactory; import jakarta.jms.ConnectionMetaData; @@ -28,6 +30,7 @@ import org.springframework.boot.health.contributor.Status; 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.BDDMockito.then; import static org.mockito.BDDMockito.willAnswer; @@ -38,9 +41,33 @@ * Tests for {@link JmsHealthIndicator}. * * @author Stephane Nicoll + * @author Venkata Naga Sai Srikanth Gollapudi */ class JmsHealthIndicatorTests { + @Test + @SuppressWarnings("NullAway") // Test null check + void createWhenTimeoutIsNullThrowsException() { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + assertThatIllegalArgumentException().isThrownBy(() -> new JmsHealthIndicator(connectionFactory, null)) + .withMessage("'timeout' must not be null"); + } + + @Test + void createWhenTimeoutIsZeroThrowsException() { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + assertThatIllegalArgumentException().isThrownBy(() -> new JmsHealthIndicator(connectionFactory, Duration.ZERO)) + .withMessage("'timeout' must be greater than 0"); + } + + @Test + void createWhenTimeoutIsNegativeThrowsException() { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + assertThatIllegalArgumentException() + .isThrownBy(() -> new JmsHealthIndicator(connectionFactory, Duration.ofMillis(-1))) + .withMessage("'timeout' must be greater than 0"); + } + @Test void jmsBrokerIsUp() throws JMSException { ConnectionMetaData connectionMetaData = mock(ConnectionMetaData.class); @@ -98,6 +125,19 @@ void jmsBrokerUsesFailover() throws JMSException { @Test void whenConnectionStartIsUnresponsiveStatusIsDown() throws JMSException { + Health health = healthWhenConnectionStartIsUnresponsive(Duration.ofSeconds(5)); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat((String) health.getDetails().get("error")).contains("Connection closed"); + } + + @Test + void whenConnectionStartIsUnresponsiveUsesConfiguredTimeout() throws JMSException { + Health health = healthWhenConnectionStartIsUnresponsive(Duration.ofMillis(10)); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat((String) health.getDetails().get("error")).contains("Connection closed"); + } + + private Health healthWhenConnectionStartIsUnresponsive(Duration timeout) throws JMSException { ConnectionMetaData connectionMetaData = mock(ConnectionMetaData.class); given(connectionMetaData.getJMSProviderName()).willReturn("JMS test provider"); Connection connection = mock(Connection.class); @@ -109,10 +149,8 @@ void whenConnectionStartIsUnresponsiveStatusIsDown() throws JMSException { }).given(connection).close(); ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willReturn(connection); - JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory); - Health health = indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.DOWN); - assertThat((String) health.getDetails().get("error")).contains("Connection closed"); + JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory, timeout); + return indicator.health(); } private static final class UnresponsiveStartAnswer implements Answer { From db12d4d940c9fbf01c23ab413d3d553203ecd7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 16 Jul 2026 11:21:45 +0200 Subject: [PATCH 704/930] Polish "Add support for configuring start timeout for JMS health checks" See gh-50957 --- ...JmsHealthContributorAutoConfiguration.java | 2 +- .../health/JmsHealthIndicatorProperties.java | 18 +++++----- .../boot/jms/health/JmsHealthIndicator.java | 34 ++++++++++++++----- ...althContributorAutoConfigurationTests.java | 14 ++++---- .../jms/health/JmsHealthIndicatorTests.java | 18 +++++----- 5 files changed, 50 insertions(+), 36 deletions(-) diff --git a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfiguration.java b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfiguration.java index d3c49b689fb4..2bbe76bc6c87 100644 --- a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfiguration.java +++ b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfiguration.java @@ -48,7 +48,7 @@ public final class JmsHealthContributorAutoConfiguration extends CompositeHealthContributorConfiguration { JmsHealthContributorAutoConfiguration(JmsHealthIndicatorProperties properties) { - super((connectionFactory) -> new JmsHealthIndicator(connectionFactory, properties.getTimeout())); + super((connectionFactory) -> new JmsHealthIndicator(connectionFactory, properties.getStartTimeout())); } @Bean diff --git a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthIndicatorProperties.java b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthIndicatorProperties.java index d8b4f9ca6b50..d0cfcf24b69f 100644 --- a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthIndicatorProperties.java +++ b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthIndicatorProperties.java @@ -20,13 +20,13 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jms.health.JmsHealthIndicator; -import org.springframework.util.Assert; /** - * External configuration properties for {@link JmsHealthIndicator}. + * Configuration properties for {@link JmsHealthIndicator}. * * @author Venkata Naga Sai Srikanth Gollapudi - * @since 4.x + * @author Stephane Nicoll + * @since 4.2.0 */ @ConfigurationProperties("management.health.jms") public class JmsHealthIndicatorProperties { @@ -34,16 +34,14 @@ public class JmsHealthIndicatorProperties { /** * Timeout to use when starting a connection for the health check. */ - private Duration timeout = Duration.ofSeconds(5); + private Duration startTimeout = JmsHealthIndicator.DEFAULT_START_TIMEOUT; - public Duration getTimeout() { - return this.timeout; + public Duration getStartTimeout() { + return this.startTimeout; } - public void setTimeout(Duration timeout) { - Assert.notNull(timeout, "'timeout' must not be null"); - Assert.isTrue(timeout.compareTo(Duration.ZERO) > 0, "'timeout' must be greater than 0"); - this.timeout = timeout; + public void setStartTimeout(Duration startTimeout) { + this.startTimeout = startTimeout; } } diff --git a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/health/JmsHealthIndicator.java b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/health/JmsHealthIndicator.java index 625cdf7b68ff..1c788f3086ba 100644 --- a/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/health/JmsHealthIndicator.java +++ b/module/spring-boot-jms/src/main/java/org/springframework/boot/jms/health/JmsHealthIndicator.java @@ -42,24 +42,39 @@ */ public class JmsHealthIndicator extends AbstractHealthIndicator { - private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5); + /** + * Default timeout to use when starting a connection for the health check. + */ + public static final Duration DEFAULT_START_TIMEOUT = Duration.ofSeconds(5); private final Log logger = LogFactory.getLog(JmsHealthIndicator.class); private final ConnectionFactory connectionFactory; - private final Duration timeout; + private final Duration startTimeout; + /** + * Create a new {@link JmsHealthIndicator} instance with a + * {@linkplain #DEFAULT_START_TIMEOUT default} start timeout. + * @param connectionFactory the connection factory to use + */ public JmsHealthIndicator(ConnectionFactory connectionFactory) { - this(connectionFactory, DEFAULT_TIMEOUT); + this(connectionFactory, DEFAULT_START_TIMEOUT); } - public JmsHealthIndicator(ConnectionFactory connectionFactory, Duration timeout) { + /** + * Create a new {@link JmsHealthIndicator} instance with the given + * {@code startTimeout}. + * @param connectionFactory the connection factory to use + * @param startTimeout timeout to use when starting a connection for the health check + * @since 4.2.0 + */ + public JmsHealthIndicator(ConnectionFactory connectionFactory, Duration startTimeout) { super("JMS health check failed"); - Assert.notNull(timeout, "'timeout' must not be null"); - Assert.isTrue(timeout.compareTo(Duration.ZERO) > 0, "'timeout' must be greater than 0"); + Assert.notNull(startTimeout, "'startTimeout' must not be null"); + Assert.isTrue(startTimeout.compareTo(Duration.ZERO) > 0, "'startTimeout' must be greater than 0"); this.connectionFactory = connectionFactory; - this.timeout = timeout; + this.startTimeout = startTimeout; } @Override @@ -83,10 +98,11 @@ private final class MonitoredConnection { void start() throws JMSException { new Thread(() -> { try { - if (!this.latch.await(JmsHealthIndicator.this.timeout.toNanos(), TimeUnit.NANOSECONDS)) { + Duration startTimeout1 = JmsHealthIndicator.this.startTimeout; + if (!this.latch.await(startTimeout1.toNanos(), TimeUnit.NANOSECONDS)) { JmsHealthIndicator.this.logger .warn(LogMessage.format("Connection failed to start within %s and will be closed.", - DurationStyle.SIMPLE.print(JmsHealthIndicator.this.timeout))); + DurationStyle.SIMPLE.print(startTimeout1))); closeConnection(); } } diff --git a/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfigurationTests.java b/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfigurationTests.java index 30b9ab314449..8b25767f797c 100644 --- a/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfigurationTests.java +++ b/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/autoconfigure/health/JmsHealthContributorAutoConfigurationTests.java @@ -48,24 +48,24 @@ void runShouldCreateIndicator() { } @Test - void runWhenTimeoutIsConfiguredShouldCreateIndicatorWithConfiguredTimeout() { - this.contextRunner.withPropertyValues("management.health.jms.timeout=10ms").run((context) -> { + void runWhenStartTimeoutIsConfiguredShouldCreateIndicatorWithConfiguredStartTimeout() { + this.contextRunner.withPropertyValues("management.health.jms.start-timeout=10ms").run((context) -> { assertThat(context).hasSingleBean(JmsHealthIndicator.class); assertThat(context).hasSingleBean(JmsHealthIndicatorProperties.class); - assertThat(context.getBean(JmsHealthIndicatorProperties.class).getTimeout()) + assertThat(context.getBean(JmsHealthIndicatorProperties.class).getStartTimeout()) .isEqualTo(Duration.ofMillis(10)); - assertThat(context.getBean(JmsHealthIndicator.class)).hasFieldOrPropertyWithValue("timeout", + assertThat(context.getBean(JmsHealthIndicator.class)).hasFieldOrPropertyWithValue("startTimeout", Duration.ofMillis(10)); }); } @Test - void runWhenTimeoutIsZeroShouldFail() { - this.contextRunner.withPropertyValues("management.health.jms.timeout=0ms") + void runWhenStartTimeoutIsZeroShouldFail() { + this.contextRunner.withPropertyValues("management.health.jms.start-timeout=0ms") .run((context) -> assertThat(context).hasFailed() .getFailure() .rootCause() - .hasMessage("'timeout' must be greater than 0")); + .hasMessage("'startTimeout' must be greater than 0")); } @Test diff --git a/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/health/JmsHealthIndicatorTests.java b/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/health/JmsHealthIndicatorTests.java index ee9c783b89b3..cf2a8ac069be 100644 --- a/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/health/JmsHealthIndicatorTests.java +++ b/module/spring-boot-jms/src/test/java/org/springframework/boot/jms/health/JmsHealthIndicatorTests.java @@ -47,25 +47,25 @@ class JmsHealthIndicatorTests { @Test @SuppressWarnings("NullAway") // Test null check - void createWhenTimeoutIsNullThrowsException() { + void createWhenStartTimeoutIsNullThrowsException() { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); assertThatIllegalArgumentException().isThrownBy(() -> new JmsHealthIndicator(connectionFactory, null)) - .withMessage("'timeout' must not be null"); + .withMessage("'startTimeout' must not be null"); } @Test - void createWhenTimeoutIsZeroThrowsException() { + void createWhenStartTimeoutIsZeroThrowsException() { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); assertThatIllegalArgumentException().isThrownBy(() -> new JmsHealthIndicator(connectionFactory, Duration.ZERO)) - .withMessage("'timeout' must be greater than 0"); + .withMessage("'startTimeout' must be greater than 0"); } @Test - void createWhenTimeoutIsNegativeThrowsException() { + void createWhenStartTimeoutIsNegativeThrowsException() { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); assertThatIllegalArgumentException() .isThrownBy(() -> new JmsHealthIndicator(connectionFactory, Duration.ofMillis(-1))) - .withMessage("'timeout' must be greater than 0"); + .withMessage("'startTimeout' must be greater than 0"); } @Test @@ -131,13 +131,13 @@ void whenConnectionStartIsUnresponsiveStatusIsDown() throws JMSException { } @Test - void whenConnectionStartIsUnresponsiveUsesConfiguredTimeout() throws JMSException { + void whenConnectionStartIsUnresponsiveUsesConfiguredStartTimeout() throws JMSException { Health health = healthWhenConnectionStartIsUnresponsive(Duration.ofMillis(10)); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat((String) health.getDetails().get("error")).contains("Connection closed"); } - private Health healthWhenConnectionStartIsUnresponsive(Duration timeout) throws JMSException { + private Health healthWhenConnectionStartIsUnresponsive(Duration startTimeout) throws JMSException { ConnectionMetaData connectionMetaData = mock(ConnectionMetaData.class); given(connectionMetaData.getJMSProviderName()).willReturn("JMS test provider"); Connection connection = mock(Connection.class); @@ -149,7 +149,7 @@ private Health healthWhenConnectionStartIsUnresponsive(Duration timeout) throws }).given(connection).close(); ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willReturn(connection); - JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory, timeout); + JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory, startTimeout); return indicator.health(); } From becdfefbedb4a02a7a83ed54e0f74b3e5262e48f Mon Sep 17 00:00:00 2001 From: Trygve Aasjord Date: Thu, 14 May 2026 08:43:48 +0200 Subject: [PATCH 705/930] Add Kafka Stream's leaveGroupOnClose property This commit makes it possible to set the option to leave the group on close at configuration/deployment time using configuration. See gh-50432 Signed-off-by: Trygve Aasjord --- .../boot/kafka/autoconfigure/KafkaProperties.java | 13 +++++++++++++ .../KafkaStreamsAnnotationDrivenConfiguration.java | 1 + 2 files changed, 14 insertions(+) diff --git a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaProperties.java b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaProperties.java index d622810b5fee..9f66725b7d11 100644 --- a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaProperties.java +++ b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaProperties.java @@ -812,6 +812,11 @@ public static class Streams { */ private @Nullable String stateDir; + /** + * Whether the consumer should leave the group when stopping Kafka Streams. + */ + private boolean leaveGroupOnClose; + /** * Additional Kafka properties used to configure the streams. */ @@ -885,6 +890,14 @@ public void setStateDir(@Nullable String stateDir) { this.stateDir = stateDir; } + public boolean isLeaveGroupOnClose() { + return this.leaveGroupOnClose; + } + + public void setLeaveGroupOnClose(boolean leaveGroupOnClose) { + this.leaveGroupOnClose = leaveGroupOnClose; + } + public Map getProperties() { return this.properties; } diff --git a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaStreamsAnnotationDrivenConfiguration.java b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaStreamsAnnotationDrivenConfiguration.java index 1c7184960063..410d72067254 100644 --- a/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaStreamsAnnotationDrivenConfiguration.java +++ b/module/spring-boot-kafka/src/main/java/org/springframework/boot/kafka/autoconfigure/KafkaStreamsAnnotationDrivenConfiguration.java @@ -101,6 +101,7 @@ public void configure(StreamsBuilderFactoryBean factoryBean) { KafkaProperties.Cleanup cleanup = this.properties.getStreams().getCleanup(); CleanupConfig cleanupConfig = new CleanupConfig(cleanup.isOnStartup(), cleanup.isOnShutdown()); factoryBean.setCleanupConfig(cleanupConfig); + factoryBean.setLeaveGroupOnClose(this.properties.getStreams().isLeaveGroupOnClose()); } @Override From 1383d34d1edb48c5a3adeaed5199c06211f67403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 15 Jul 2026 11:58:00 +0200 Subject: [PATCH 706/930] Polish "Add Kafka Stream's leaveGroupOnClose property" See gh-50432 --- .../autoconfigure/KafkaAutoConfigurationTests.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/module/spring-boot-kafka/src/test/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfigurationTests.java b/module/spring-boot-kafka/src/test/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfigurationTests.java index e65e217272d1..f4cf275aa63b 100644 --- a/module/spring-boot-kafka/src/test/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfigurationTests.java +++ b/module/spring-boot-kafka/src/test/java/org/springframework/boot/kafka/autoconfigure/KafkaAutoConfigurationTests.java @@ -475,6 +475,7 @@ void streamsBuilderFactoryBeanConfigurerIsApplied() { assertThat(factoryBean.isAutoStartup()).isFalse(); assertThat(factoryBean).extracting("cleanupConfig.onStart").isEqualTo(true); assertThat(factoryBean).extracting("cleanupConfig.onStop").isEqualTo(true); + assertThat(factoryBean).extracting("leaveGroupOnClose").isEqualTo(false); factoryBean.addListener(listener); }) .withPropertyValues("spring.kafka.client-id=cid", @@ -696,6 +697,19 @@ void streamsWithCleanupConfig() { }); } + @Test + void streamsWithLeaveGroupOnClose() { + this.contextRunner + .withUserConfiguration(EnableKafkaStreamsConfiguration.class, TestKafkaStreamsConfiguration.class) + .withPropertyValues("spring.application.name=my-test-app", + "spring.kafka.bootstrap-servers=localhost:9092,localhost:9093", + "spring.kafka.streams.auto-startup=false", "spring.kafka.streams.leave-group-on-close=true") + .run((context) -> { + StreamsBuilderFactoryBean streamsBuilderFactoryBean = context.getBean(StreamsBuilderFactoryBean.class); + assertThat(streamsBuilderFactoryBean).extracting("leaveGroupOnClose").isEqualTo(true); + }); + } + @Test void streamsApplicationIdIsMandatory() { this.contextRunner.withUserConfiguration(EnableKafkaStreamsConfiguration.class).run((context) -> { From 8d0ecd95d9ccad04586091e4006b6fc5bcd3d284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:47:19 +0200 Subject: [PATCH 707/930] Upgrade to ActiveMQ 6.2.7 Closes gh-51002 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8818eff7c371..981c10d06541 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -31,7 +31,7 @@ bom { ] } } - library("ActiveMQ", "6.2.6") { + library("ActiveMQ", "6.2.7") { group("org.apache.activemq") { modules = [ "activemq-console", From fbe273a88cba7b6b86d3e0d68647f0f6fe218717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:47:23 +0200 Subject: [PATCH 708/930] Upgrade to Byte Buddy 1.18.11 Closes gh-51003 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 981c10d06541..eb58cc737c66 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -186,7 +186,7 @@ bom { releaseNotes("https://github.com/mojohaus/build-helper-maven-plugin/releases/tag/{version}") } } - library("Byte Buddy", "1.18.10") { + library("Byte Buddy", "1.18.11") { group("net.bytebuddy") { modules = [ "byte-buddy", From 82af80e4c745b6bf99030a0b79e49232df3a032b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:47:27 +0200 Subject: [PATCH 709/930] Upgrade to CycloneDX Maven Plugin 2.9.2 Closes gh-51004 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index eb58cc737c66..5550f70c1f61 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -337,7 +337,7 @@ bom { ] } } - library("CycloneDX Maven Plugin", "2.9.1") { + library("CycloneDX Maven Plugin", "2.9.2") { group("org.cyclonedx") { plugins = [ "cyclonedx-maven-plugin" From a6a5880e93091f98ccdda1c6bbbfa93032ea5a80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:47:32 +0200 Subject: [PATCH 710/930] Upgrade to DB2 JDBC 12.1.5.0 Closes gh-51005 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 5550f70c1f61..1ca82f1edecb 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -347,7 +347,7 @@ bom { releaseNotes("https://github.com/CycloneDX/cyclonedx-maven-plugin/releases/tag/cyclonedx-maven-plugin-{version}") } } - library("DB2 JDBC", "12.1.4.0") { + library("DB2 JDBC", "12.1.5.0") { group("com.ibm.db2") { modules = [ "jcc" From 47c499eeb12a676daf12d811124c5fd009d76677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:47:36 +0200 Subject: [PATCH 711/930] Upgrade to Groovy 5.0.7 Closes gh-51006 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1ca82f1edecb..b2b57e62acc1 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -546,7 +546,7 @@ bom { releaseNotes("https://github.com/graphql-java/graphql-java/releases/tag/v{version}") } } - library("Groovy", "5.0.6") { + library("Groovy", "5.0.7") { prohibit { contains "-alpha-" because "we don't want alpha dependencies" From 11a4ed459930446ada329a1941bd7da117196cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:47:40 +0200 Subject: [PATCH 712/930] Upgrade to Hibernate 7.4.5.Final Closes gh-51007 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b2b57e62acc1..df6d6733ae14 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -641,7 +641,7 @@ bom { releaseNotes("https://github.com/hazelcast/hazelcast/releases/tag/v{version}") } } - library("Hibernate", "7.4.1.Final") { + library("Hibernate", "7.4.5.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From 184ed6d21a30842ad7455a727e79194ca806de2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:47:44 +0200 Subject: [PATCH 713/930] Upgrade to Hibernate Validator 9.1.2.Final Closes gh-51008 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index df6d6733ae14..fc103e22c9fa 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -673,7 +673,7 @@ bom { .formatted(version.major(), version.minor())) } } - library("Hibernate Validator", "9.1.0.Final") { + library("Hibernate Validator", "9.1.2.Final") { group("org.hibernate.validator") { modules = [ "hibernate-validator", From 1da637d5ef1afda1ced033012032f7c6a91b725d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:47:48 +0200 Subject: [PATCH 714/930] Upgrade to HttpClient5 5.6.2 Closes gh-51009 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index fc103e22c9fa..e5afeffc4de3 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -727,7 +727,7 @@ bom { ] } } - library("HttpClient5", "5.6.1") { + library("HttpClient5", "5.6.2") { prohibit { contains "-alpha" contains "-beta" From fc34335a23825b546690b07cd36feec204db245f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:47:52 +0200 Subject: [PATCH 715/930] Upgrade to HttpCore5 5.4.3 Closes gh-51010 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e5afeffc4de3..0b53e2757471 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -751,7 +751,7 @@ bom { ] } } - library("HttpCore5", "5.4.2") { + library("HttpCore5", "5.4.3") { group("org.apache.httpcomponents.core5") { modules = [ "httpcore5", From aa4aa7171dc562f43aa27cff7b5f49d1bf1f233b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:47:57 +0200 Subject: [PATCH 716/930] Upgrade to Jackson 2 Bom 2.21.5 Closes gh-51011 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 50aa13ad7428..b2d307bad637 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ checkstyleToolVersion=10.12.4 commonsCodecVersion=1.21.0 graalVersion=25 hamcrestVersion=3.0 -jackson2Version=2.21.4 +jackson2Version=2.21.5 jacksonVersion=3.1.4 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 From 943ba438ef9305e506af4f900a6ba8341f2c9ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:48:01 +0200 Subject: [PATCH 717/930] Upgrade to Jackson Bom 3.1.5 Closes gh-51012 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b2d307bad637..5b4815d142b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ commonsCodecVersion=1.21.0 graalVersion=25 hamcrestVersion=3.0 jackson2Version=2.21.5 -jacksonVersion=3.1.4 +jacksonVersion=3.1.5 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 kotlinVersion=2.3.21 From 5f1d196884301b52663261ac8dfa3a26c19d16e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:48:05 +0200 Subject: [PATCH 718/930] Upgrade to Jetty 12.1.11 Closes gh-51013 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0b53e2757471..476fe182717e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1121,7 +1121,7 @@ bom { ] } } - library("Jetty", "12.1.10") { + library("Jetty", "12.1.11") { prohibit { contains ".alpha" because "we don't want alpha dependencies" From 7ca7bc06c8b11e64b2071a37690f55aef77eb119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:48:09 +0200 Subject: [PATCH 719/930] Upgrade to jOOQ 3.21.6 Closes gh-51014 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 476fe182717e..aa6b06715dba 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1145,7 +1145,7 @@ bom { ] } } - library("jOOQ", "3.21.5") { + library("jOOQ", "3.21.6") { group("org.jooq") { bom("jooq-bom") plugins = [ From d9db0172c9a1987c687bd4aed11e045f53aedd33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:48:14 +0200 Subject: [PATCH 720/930] Upgrade to Log4j2 2.25.5 Closes gh-51015 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index aa6b06715dba..a02148473f25 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1358,7 +1358,7 @@ bom { releaseNotes("https://github.com/liquibase/liquibase/releases/tag/v{version}") } } - library("Log4j2", "2.25.4") { + library("Log4j2", "2.25.5") { prohibit { contains "-alpha" contains "-beta" From c80497dac1898844312112626ca3d53f9b2abf60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:48:18 +0200 Subject: [PATCH 721/930] Upgrade to Logback 1.5.38 Closes gh-51016 --- .../SpringBootJoranConfiguratorTests.java | 3 +- .../logback/TestPropertyCondition.java | 34 +++++++++++++++++++ .../spring-boot-dependencies/build.gradle | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 core/spring-boot/src/test/java/org/springframework/boot/logging/logback/TestPropertyCondition.java diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java index d3a2b4141dfc..0b08bbe8ac2d 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java @@ -339,7 +339,8 @@ private interface Action { - + + diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/TestPropertyCondition.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/TestPropertyCondition.java new file mode 100644 index 000000000000..e9adc7bdc4c1 --- /dev/null +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/TestPropertyCondition.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 ch.qos.logback.core.boolex.PropertyConditionBase; + +/** + * Test condition used by {@link SpringBootJoranConfiguratorTests}. + * + * @author Stephane Nicoll + */ +@SuppressWarnings("unused") +public class TestPropertyCondition extends PropertyConditionBase { + + @Override + public boolean evaluate() { + return property("MINE").contains("true"); + } + +} diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a02148473f25..3030bf618b6e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1375,7 +1375,7 @@ bom { releaseNotes("https://github.com/apache/logging-log4j2/releases/tag/rel%2F{version}") } } - library("Logback", "1.5.34") { + library("Logback", "1.5.38") { group("ch.qos.logback") { modules = [ "logback-classic", From a7fddce6d260cff97ffce07a8b2ee9d0f89bb6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:48:22 +0200 Subject: [PATCH 722/930] Upgrade to MariaDB 3.5.9 Closes gh-51017 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3030bf618b6e..f251e833731e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1398,7 +1398,7 @@ bom { javadoc("https://projectlombok.org/api") } } - library("MariaDB", "3.5.8") { + library("MariaDB", "3.5.9") { group("org.mariadb.jdbc") { modules = [ "mariadb-java-client" From 56ef9572003a08d76807ed1709140181f70683d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:48:26 +0200 Subject: [PATCH 723/930] Upgrade to Maven Help Plugin 3.5.2 Closes gh-51018 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index f251e833731e..4c2bcf03d215 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1502,7 +1502,7 @@ bom { releaseNotes("https://github.com/apache/maven-surefire/releases/tag/surefire-{version}") } } - library("Maven Help Plugin", "3.5.1") { + library("Maven Help Plugin", "3.5.2") { group("org.apache.maven.plugins") { plugins = [ "maven-help-plugin" From 0fd33b8dbfe5dd65a8bb34a1913860c0b6a962b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:48:30 +0200 Subject: [PATCH 724/930] Upgrade to Native Build Tools Plugin 1.1.5 Closes gh-51019 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5b4815d142b6..cfa5fa127128 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ junitJupiterVersion=6.0.3 kotlinVersion=2.3.21 mavenVersion=3.9.13 mockitoVersion=5.23.0 -nativeBuildToolsVersion=1.1.4 +nativeBuildToolsVersion=1.1.5 nullabilityPluginVersion=0.0.14 snakeYamlVersion=2.6 springGrpcVersion=1.1.0 From d19d3b016e219120e83e4177340f1494f9d605f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:48:34 +0200 Subject: [PATCH 725/930] Upgrade to Netty 4.2.16.Final Closes gh-51020 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4c2bcf03d215..70f4ef6a98ea 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1754,7 +1754,7 @@ bom { releaseNotes("https://github.com/neo4j/neo4j-java-driver/releases/tag/{version}") } } - library("Netty", "4.2.15.Final") { + library("Netty", "4.2.16.Final") { prohibit { contains ".Alpha" contains ".Beta" From af765877626693ca0cbc7ebe7d11e28ba519a28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:48:39 +0200 Subject: [PATCH 726/930] Upgrade to Protobuf Maven Plugin 5.1.7 Closes gh-51021 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 70f4ef6a98ea..3f7778854ae4 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1844,7 +1844,7 @@ bom { releaseNotes("https://github.com/oracle/oracle-r2dbc/releases/tag/{version}") } } - library("Protobuf Maven Plugin", "5.1.4") { + library("Protobuf Maven Plugin", "5.1.7") { group("io.github.ascopes") { plugins = [ "protobuf-maven-plugin" From ecdd0ad3f30851ac9a5395df60325fb4272fbe5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:48:43 +0200 Subject: [PATCH 727/930] Upgrade to Postgresql 42.7.13 Closes gh-51022 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3f7778854ae4..4dd588571b60 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1865,7 +1865,7 @@ bom { javadoc("https://javadoc.io/doc/org.messaginghub/pooled-jms/{version}", "org.messaginghub.pooled.jms") } } - library("Postgresql", "42.7.11") { + library("Postgresql", "42.7.13") { group("org.postgresql") { modules = [ "postgresql" From e522f0855425c9dc1fdfdf505aee6a33be8861c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:48:47 +0200 Subject: [PATCH 728/930] Upgrade to Pulsar 4.2.3 Closes gh-51023 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4dd588571b60..ae23f98c17eb 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1904,7 +1904,7 @@ bom { releaseNotes("https://github.com/prometheus/client_java/releases/tag/parent-{version}") } } - library("Pulsar", "4.2.2") { + library("Pulsar", "4.2.3") { group("org.apache.pulsar") { bom("pulsar-bom") } From c80476555173838eced7ab0a5fa19079c61b81d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:48:51 +0200 Subject: [PATCH 729/930] Upgrade to R2DBC MariaDB 1.4.1 Closes gh-51024 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ae23f98c17eb..3d4f034477bd 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1978,7 +1978,7 @@ bom { releaseNotes("https://github.com/r2dbc/r2dbc-h2/releases/tag/v{version}") } } - library("R2DBC MariaDB", "1.4.0") { + library("R2DBC MariaDB", "1.4.1") { group("org.mariadb") { modules = [ "r2dbc-mariadb" From c8601065c0ac2bca12ca7f7c4094ff55a2819752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:48:56 +0200 Subject: [PATCH 730/930] Upgrade to R2DBC MSSQL 1.0.5.RELEASE Closes gh-51025 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3d4f034477bd..d60f7c4f670a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1988,7 +1988,7 @@ bom { releaseNotes("https://github.com/mariadb-corporation/mariadb-connector-r2dbc/releases/tag/{version}") } } - library("R2DBC MSSQL", "1.0.4.RELEASE") { + library("R2DBC MSSQL", "1.0.5.RELEASE") { group ("io.r2dbc") { modules = [ "r2dbc-mssql" From 99652e09073ca0e1c05345057ef1d2ee6bd2312c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:49:00 +0200 Subject: [PATCH 731/930] Upgrade to R2DBC Postgresql 1.1.2.RELEASE Closes gh-51026 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d60f7c4f670a..8fc4f1bb787e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2020,7 +2020,7 @@ bom { releaseNotes("https://github.com/r2dbc/r2dbc-pool/releases/tag/v{version}") } } - library("R2DBC Postgresql", "1.1.1.RELEASE") { + library("R2DBC Postgresql", "1.1.2.RELEASE") { considerSnapshots() group("org.postgresql") { modules = [ From 3e813527bf066bef84219d76eb8e0dbe4f989c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:49:04 +0200 Subject: [PATCH 732/930] Upgrade to Tomcat 11.0.24 Closes gh-51027 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cfa5fa127128..06222da173fa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,7 +24,7 @@ snakeYamlVersion=2.6 springGrpcVersion=1.1.0 springFrameworkVersion=7.0.8 springFramework60xVersion=6.0.23 -tomcatVersion=11.0.22 +tomcatVersion=11.0.24 protobufGradlePluginVersion=0.9.6 kotlin.stdlib.default.dependency=false From cde4271bd51561a0bb0772b2e3cda9ba929bdb81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:49:08 +0200 Subject: [PATCH 733/930] Upgrade to UnboundID LDAPSDK 7.0.5 Closes gh-51028 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8fc4f1bb787e..c6b7f269a6a2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2879,7 +2879,7 @@ bom { releaseNotes(version -> "https://tomcat.apache.org/tomcat-%s.%s-doc/changelog.html".formatted(version.major(), version.minor())) } } - library("UnboundID LDAPSDK", "7.0.4") { + library("UnboundID LDAPSDK", "7.0.5") { group("com.unboundid") { modules = [ "unboundid-ldapsdk" From b0ec77ff68573e829c03b711739fc813b7b47255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 10:49:12 +0200 Subject: [PATCH 734/930] Upgrade to WebJars Locator Lite 1.1.4 Closes gh-51029 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c6b7f269a6a2..a6bb11551716 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2914,7 +2914,7 @@ bom { ] } } - library("WebJars Locator Lite", "1.1.3") { + library("WebJars Locator Lite", "1.1.4") { group("org.webjars") { modules = [ "webjars-locator-lite" From f558e1d8e50d552d378f01b547b71701a8124e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 13:13:10 +0200 Subject: [PATCH 735/930] Disable Spring Cloud bindings Spring Cloud Bindings still rely on the EnvironmentPostProcessor interface that was deprecated in Spring Boot 4.0. This commit disables this feature and adapts to new deprecation warnings. See gh-50955 --- .../boot/image/paketo/PaketoBuilderTests.java | 1 + .../paketo/PaketoBuilderTests-executableWarApp.gradle | 6 ++++++ .../image/paketo/PaketoBuilderTests-plainWarApp.gradle | 3 ++- .../boot/image/paketo/PaketoBuilderTests.gradle | 8 +++++++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java b/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java index d2e7d5259ed4..37054f2da82b 100644 --- a/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java +++ b/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java @@ -75,6 +75,7 @@ void configureGradleBuild() { this.gradleBuild.expectDeprecationMessages("BPL_SPRING_CLOUD_BINDINGS_ENABLED.*true.*Deprecated"); this.gradleBuild.expectDeprecationMessages("Command \"packages\" is deprecated, use `syft scan` instead"); this.gradleBuild.expectDeprecationMessages("BP_ENABLE_RUNTIME_CERT_BINDING.*true.*Deprecated"); + this.gradleBuild.expectDeprecationMessages("Warning: BOM table is deprecated in this buildpack api version"); this.gradleBuild.gradleVersion(GradleVersions.maximumCompatible()); } diff --git a/system-test/spring-boot-image-system-tests/src/systemTest/resources/org/springframework/boot/image/paketo/PaketoBuilderTests-executableWarApp.gradle b/system-test/spring-boot-image-system-tests/src/systemTest/resources/org/springframework/boot/image/paketo/PaketoBuilderTests-executableWarApp.gradle index e6b2bccdbca9..6044cbaf9bac 100644 --- a/system-test/spring-boot-image-system-tests/src/systemTest/resources/org/springframework/boot/image/paketo/PaketoBuilderTests-executableWarApp.gradle +++ b/system-test/spring-boot-image-system-tests/src/systemTest/resources/org/springframework/boot/image/paketo/PaketoBuilderTests-executableWarApp.gradle @@ -49,3 +49,9 @@ bootWar { ) } } + +tasks.named('bootBuildImage') { + environment = [ + 'BP_SPRING_CLOUD_BINDINGS_DISABLED': 'true' + ] +} diff --git a/system-test/spring-boot-image-system-tests/src/systemTest/resources/org/springframework/boot/image/paketo/PaketoBuilderTests-plainWarApp.gradle b/system-test/spring-boot-image-system-tests/src/systemTest/resources/org/springframework/boot/image/paketo/PaketoBuilderTests-plainWarApp.gradle index 5b1a4eef9a9e..c47e47d828ab 100644 --- a/system-test/spring-boot-image-system-tests/src/systemTest/resources/org/springframework/boot/image/paketo/PaketoBuilderTests-plainWarApp.gradle +++ b/system-test/spring-boot-image-system-tests/src/systemTest/resources/org/springframework/boot/image/paketo/PaketoBuilderTests-plainWarApp.gradle @@ -51,6 +51,7 @@ bootBuildImage { environment = [ 'BP_JVM_VERSION': java.targetCompatibility.getMajorVersion(), 'BP_TOMCAT_VERSION': '11.*', - 'BP_DEPENDENCY_MIRROR_DOWNLOADS_APACHE_ORG': 'https://archive.apache.org/dist' + 'BP_DEPENDENCY_MIRROR_DOWNLOADS_APACHE_ORG': 'https://archive.apache.org/dist', + 'BP_SPRING_CLOUD_BINDINGS_DISABLED': 'true' ] } diff --git a/system-test/spring-boot-image-system-tests/src/systemTest/resources/org/springframework/boot/image/paketo/PaketoBuilderTests.gradle b/system-test/spring-boot-image-system-tests/src/systemTest/resources/org/springframework/boot/image/paketo/PaketoBuilderTests.gradle index 81b8c7f174e2..5ec468592784 100644 --- a/system-test/spring-boot-image-system-tests/src/systemTest/resources/org/springframework/boot/image/paketo/PaketoBuilderTests.gradle +++ b/system-test/spring-boot-image-system-tests/src/systemTest/resources/org/springframework/boot/image/paketo/PaketoBuilderTests.gradle @@ -46,4 +46,10 @@ bootJar { 'Implementation-Title': "Paketo Test" ) } -} \ No newline at end of file +} + +tasks.named('bootBuildImage') { + environment = [ + 'BP_SPRING_CLOUD_BINDINGS_DISABLED': 'true' + ] +} From c6e6d1d06d637594b00e55028f6613e91b116869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 15:51:28 +0200 Subject: [PATCH 736/930] Prevent upgrade to Jackson 2.22 Closes gh-51049 --- platform/spring-boot-dependencies/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 12631e99866e..853e35c716b7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -786,6 +786,10 @@ bom { } } library("Jackson 2 Bom", "${jackson2Version}") { + prohibit { + versionRange "[2.22.0,2.23.0)" + because "it is not LTS (https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.22)" + } group("com.fasterxml.jackson") { bom("jackson-bom") } From 068617aa9c4ddfe50105de66bba36ec4dab10235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:15:01 +0200 Subject: [PATCH 737/930] Prevent upgrade to Jackson 3.2 Closes gh-51032 --- platform/spring-boot-dependencies/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 853e35c716b7..65c5f1f33ae9 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -798,6 +798,10 @@ bom { } } library("Jackson Bom", "${jacksonVersion}") { + prohibit { + versionRange "[3.2.0,3.3.0)" + because "it is not LTS (https://github.com/FasterXML/jackson/wiki/Jackson-Release-3.2)" + } group("tools.jackson") { bom("jackson-bom") { permit("com.fasterxml.jackson.core:jackson-annotations") From ba6ac9c22b6b6a46ed67f9ae80f179a0c9b0de0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:20:41 +0200 Subject: [PATCH 738/930] Upgrade to ActiveMQ 6.2.7 Closes gh-51033 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 65c5f1f33ae9..a9c50d4d473a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -31,7 +31,7 @@ bom { ] } } - library("ActiveMQ", "6.2.6") { + library("ActiveMQ", "6.2.7") { group("org.apache.activemq") { modules = [ "activemq-console", From d556fbd6d5fbce7edc7e8e95c794bb5babfbe669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:20:45 +0200 Subject: [PATCH 739/930] Upgrade to Artemis 2.55.0 Closes gh-51034 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a9c50d4d473a..a1b0e6b03a80 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -65,7 +65,7 @@ bom { releaseNotes("https://github.com/eclipse-ee4j/angus-mail/releases/tag/{version}") } } - library("Artemis", "2.53.0") { + library("Artemis", "2.55.0") { group("org.apache.artemis") { bom("artemis-bom") { permit("org.apache.activemq:artemis-amqp-protocol") From 0b7826bb64537793e021024e58c7670f00336dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:20:49 +0200 Subject: [PATCH 740/930] Upgrade to Byte Buddy 1.18.11 Closes gh-51035 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a1b0e6b03a80..a70d01f04e28 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -186,7 +186,7 @@ bom { releaseNotes("https://github.com/mojohaus/build-helper-maven-plugin/releases/tag/{version}") } } - library("Byte Buddy", "1.18.10") { + library("Byte Buddy", "1.18.11") { group("net.bytebuddy") { modules = [ "byte-buddy", From 128c19dd0712b59f3659701c847f68fb7e99b6a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:20:53 +0200 Subject: [PATCH 741/930] Upgrade to Commons Codec 1.22.0 Closes gh-51036 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cb764f2f0704..eea119077e7a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8 assertjVersion=3.27.7 checkstyleToolVersion=10.12.4 -commonsCodecVersion=1.21.0 +commonsCodecVersion=1.22.0 graalVersion=25 hamcrestVersion=3.0 jackson2Version=2.21.4 From 060f217ee1858725e10a3fb30fc2f4de9bb858a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:20:58 +0200 Subject: [PATCH 742/930] Upgrade to Commons Logging 1.4.0 Closes gh-51037 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a70d01f04e28..8b4b55fbf35e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -284,7 +284,7 @@ bom { releaseNotes("https://commons.apache.org/proper/commons-lang/changes.html#a{version}") } } - library("Commons Logging", "1.3.6") { + library("Commons Logging", "1.4.0") { group("commons-logging") { modules = [ "commons-logging" From 78ae79da2df9a1fb374f5a030dc18a1c1020908c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:21:02 +0200 Subject: [PATCH 743/930] Upgrade to CycloneDX Maven Plugin 2.9.2 Closes gh-51038 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8b4b55fbf35e..47b2cbfc01be 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -337,7 +337,7 @@ bom { ] } } - library("CycloneDX Maven Plugin", "2.9.1") { + library("CycloneDX Maven Plugin", "2.9.2") { group("org.cyclonedx") { plugins = [ "cyclonedx-maven-plugin" From 4d354c41bf04755891a5447a465deb36ebd4bc35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:21:06 +0200 Subject: [PATCH 744/930] Upgrade to DB2 JDBC 12.1.5.0 Closes gh-51039 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 47b2cbfc01be..bde0650164d7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -347,7 +347,7 @@ bom { releaseNotes("https://github.com/CycloneDX/cyclonedx-maven-plugin/releases/tag/cyclonedx-maven-plugin-{version}") } } - library("DB2 JDBC", "12.1.4.0") { + library("DB2 JDBC", "12.1.5.0") { group("com.ibm.db2") { modules = [ "jcc" From d898c8e48d5224fe263bc0215b66151a03a3b5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:21:10 +0200 Subject: [PATCH 745/930] Upgrade to Flyway 12.11.0 Closes gh-51040 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index bde0650164d7..39d7e7a01663 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -425,7 +425,7 @@ bom { javadoc("elasticsearch-java", version -> "https://javadoc.io/doc/co.elastic.clients/elasticsearch-java/%s/index.html".formatted(version), "co.elastic.clients.elasticsearch", "co.elastic.clients.transport") } } - library("Flyway", "12.4.0") { + library("Flyway", "12.11.0") { group("org.flywaydb") { modules = [ "flyway-command-test-connection", From 1327dd2a7c83fa036180269a1d01004fbb420367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:21:14 +0200 Subject: [PATCH 746/930] Upgrade to Groovy 5.0.7 Closes gh-51041 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 39d7e7a01663..3a8b89f59cb7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -546,7 +546,7 @@ bom { releaseNotes("https://github.com/graphql-java/graphql-java/releases/tag/v{version}") } } - library("Groovy", "5.0.6") { + library("Groovy", "5.0.7") { prohibit { contains "-alpha-" because "we don't want alpha dependencies" From 74e17bc18379aa49ff80c9a18b3b62210c26fed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:21:19 +0200 Subject: [PATCH 747/930] Upgrade to Gson 2.14.0 Closes gh-51042 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3a8b89f59cb7..c219647cffd5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -588,7 +588,7 @@ bom { releaseNotes("https://github.com/grpc/grpc-kotlin/releases/tag/v{version}") } } - library("Gson", "2.13.2") { + library("Gson", "2.14.0") { group("com.google.code.gson") { modules = [ "gson" From f576d68879a66642877bb5219027c490d3255e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:21:23 +0200 Subject: [PATCH 748/930] Upgrade to Hibernate 7.4.5.Final Closes gh-51043 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c219647cffd5..94fec652a1c7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -641,7 +641,7 @@ bom { releaseNotes("https://github.com/hazelcast/hazelcast/releases/tag/v{version}") } } - library("Hibernate", "7.4.1.Final") { + library("Hibernate", "7.4.5.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From ea94b9f6e846dbaf5aa08f9b523b95ea119fb2df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:21:27 +0200 Subject: [PATCH 749/930] Upgrade to Hibernate Validator 9.1.2.Final Closes gh-51044 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 94fec652a1c7..72719fa62e8c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -676,7 +676,7 @@ bom { .formatted(version.major(), version.minor())) } } - library("Hibernate Validator", "9.1.0.Final") { + library("Hibernate Validator", "9.1.2.Final") { group("org.hibernate.validator") { modules = [ "hibernate-validator", From 93fae8fb8d8deefa6ad1bff24ba12672b6fba1df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:21:31 +0200 Subject: [PATCH 750/930] Upgrade to HikariCP 7.1.0 Closes gh-51045 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 72719fa62e8c..ccc9bbee7615 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -684,7 +684,7 @@ bom { ] } } - library("HikariCP", "7.0.2") { + library("HikariCP", "7.1.0") { group("com.zaxxer") { modules = [ "HikariCP" From 167e5c4fc3886412bf43f3b93fa00fc070d66930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:21:35 +0200 Subject: [PATCH 751/930] Upgrade to HttpClient5 5.6.2 Closes gh-51046 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ccc9bbee7615..44310527324e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -730,7 +730,7 @@ bom { ] } } - library("HttpClient5", "5.6.1") { + library("HttpClient5", "5.6.2") { prohibit { contains "-alpha" contains "-beta" From d03be7b9d301f04c87f5de0c2853e332380853ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:21:40 +0200 Subject: [PATCH 752/930] Upgrade to HttpCore5 5.4.3 Closes gh-51047 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 44310527324e..125afe151151 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -754,7 +754,7 @@ bom { ] } } - library("HttpCore5", "5.4.2") { + library("HttpCore5", "5.4.3") { group("org.apache.httpcomponents.core5") { modules = [ "httpcore5", From 6af5ce89ed14f674a8a973f89f8440215cb5f48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:21:44 +0200 Subject: [PATCH 753/930] Upgrade to Infinispan 16.2.1 Closes gh-51048 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 125afe151151..4378d18499dd 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -763,7 +763,7 @@ bom { ] } } - library("Infinispan", "16.1.4") { + library("Infinispan", "16.2.1") { group("org.infinispan") { bom("infinispan-bom") } From 2231dc3eb61832eb25469e398263bc744293e06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:21:52 +0200 Subject: [PATCH 754/930] Upgrade to Jackson Bom 3.1.5 Closes gh-51050 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index eea119077e7a..2d4c2fcc958d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ commonsCodecVersion=1.22.0 graalVersion=25 hamcrestVersion=3.0 jackson2Version=2.21.4 -jacksonVersion=3.1.4 +jacksonVersion=3.1.5 javaFormatVersion=0.0.47 junitJupiterVersion=6.0.3 kotlinVersion=2.3.21 From 626a65ac46968fa1bbb92e21a435261123b40112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:21:56 +0200 Subject: [PATCH 755/930] Upgrade to Jetty 12.1.11 Closes gh-51051 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4378d18499dd..3c869df603b4 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1132,7 +1132,7 @@ bom { ] } } - library("Jetty", "12.1.10") { + library("Jetty", "12.1.11") { prohibit { contains ".alpha" because "we don't want alpha dependencies" From 62c65fa8baa5ba059bcf484f182746a6507efb8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:22:00 +0200 Subject: [PATCH 756/930] Upgrade to jOOQ 3.21.6 Closes gh-51052 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3c869df603b4..ac5e8976cf98 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1156,7 +1156,7 @@ bom { ] } } - library("jOOQ", "3.21.5") { + library("jOOQ", "3.21.6") { group("org.jooq") { bom("jooq-bom") plugins = [ From 6c5840ace8b16d1a797c1e20eba11b79402cf9ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:22:05 +0200 Subject: [PATCH 757/930] Upgrade to JUnit Jupiter 6.1.2 Closes gh-51053 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2d4c2fcc958d..18650fbc067c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ hamcrestVersion=3.0 jackson2Version=2.21.4 jacksonVersion=3.1.5 javaFormatVersion=0.0.47 -junitJupiterVersion=6.0.3 +junitJupiterVersion=6.1.2 kotlinVersion=2.3.21 mavenVersion=3.9.13 mockitoVersion=5.23.0 From 526764e8de38f2d5168f1d7ac3383fc225c748c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:22:13 +0200 Subject: [PATCH 758/930] Upgrade to Log4j2 2.26.1 Closes gh-51055 --- .../boot/logging/log4j2/SpringBootTriggeringPolicy.java | 2 +- platform/spring-boot-dependencies/build.gradle | 2 +- .../testsupport/logging/ConfigureClasspathToPreferLog4j2.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java index e980052d36b7..fb4c31e5e656 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java @@ -133,7 +133,7 @@ private TriggeringPolicy createTimePolicy() { .parseInt(getSystemProperty(RollingPolicySystemProperty.TIME_INTERVAL, DEFAULT_TIME_INTERVAL)); boolean modulate = Boolean .parseBoolean(getSystemProperty(RollingPolicySystemProperty.TIME_MODULATE, Boolean.FALSE.toString())); - return TimeBasedTriggeringPolicy.newBuilder().withInterval(interval).withModulate(modulate).build(); + return TimeBasedTriggeringPolicy.newBuilder().setInterval(interval).setModulate(modulate).build(); } private TriggeringPolicy createCronPolicy() { diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ac5e8976cf98..b01d7270d1bd 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1369,7 +1369,7 @@ bom { releaseNotes("https://github.com/liquibase/liquibase/releases/tag/v{version}") } } - library("Log4j2", "2.25.4") { + library("Log4j2", "2.26.1") { prohibit { contains "-alpha" contains "-beta" diff --git a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/logging/ConfigureClasspathToPreferLog4j2.java b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/logging/ConfigureClasspathToPreferLog4j2.java index e56b739ba459..378a6c8e5d1b 100644 --- a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/logging/ConfigureClasspathToPreferLog4j2.java +++ b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/logging/ConfigureClasspathToPreferLog4j2.java @@ -35,8 +35,8 @@ @Target(ElementType.TYPE) @Documented @ClassPathExclusions("log4j-to-slf4j-*.jar") -@ClassPathOverrides({ "org.apache.logging.log4j:log4j-core:2.24.3", - "org.apache.logging.log4j:log4j-slf4j-impl:2.24.3" }) +@ClassPathOverrides({ "org.apache.logging.log4j:log4j-core:2.26.1", + "org.apache.logging.log4j:log4j-slf4j-impl:2.26.1" }) public @interface ConfigureClasspathToPreferLog4j2 { } From 530ee529848a7bb70633f6e0f7bf2829f2b1a67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:22:18 +0200 Subject: [PATCH 759/930] Upgrade to Logback 1.5.38 Closes gh-51056 --- .../SpringBootJoranConfiguratorTests.java | 3 +- .../logback/TestPropertyCondition.java | 34 +++++++++++++++++++ .../spring-boot-dependencies/build.gradle | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 core/spring-boot/src/test/java/org/springframework/boot/logging/logback/TestPropertyCondition.java diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java index d3a2b4141dfc..0b08bbe8ac2d 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java @@ -339,7 +339,8 @@ private interface Action { - + + diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/TestPropertyCondition.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/TestPropertyCondition.java new file mode 100644 index 000000000000..e9adc7bdc4c1 --- /dev/null +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/TestPropertyCondition.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 ch.qos.logback.core.boolex.PropertyConditionBase; + +/** + * Test condition used by {@link SpringBootJoranConfiguratorTests}. + * + * @author Stephane Nicoll + */ +@SuppressWarnings("unused") +public class TestPropertyCondition extends PropertyConditionBase { + + @Override + public boolean evaluate() { + return property("MINE").contains("true"); + } + +} diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b01d7270d1bd..513f09e00056 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1386,7 +1386,7 @@ bom { releaseNotes("https://github.com/apache/logging-log4j2/releases/tag/rel%2F{version}") } } - library("Logback", "1.5.34") { + library("Logback", "1.5.38") { group("ch.qos.logback") { modules = [ "logback-classic", From 7a355c002f8f3d1908b36ca69d76ace338e4d2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:22:22 +0200 Subject: [PATCH 760/930] Upgrade to MariaDB 3.5.9 Closes gh-51057 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 513f09e00056..100ad3a0bd2d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1409,7 +1409,7 @@ bom { javadoc("https://projectlombok.org/api") } } - library("MariaDB", "3.5.8") { + library("MariaDB", "3.5.9") { group("org.mariadb.jdbc") { modules = [ "mariadb-java-client" From a3fea8f4e96c68e5ba03d407ac84fe6060f502f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:22:26 +0200 Subject: [PATCH 761/930] Upgrade to Maven Dependency Plugin 3.11.0 Closes gh-51058 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 100ad3a0bd2d..bc988dad116b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1469,7 +1469,7 @@ bom { releaseNotes("https://github.com/apache/maven-compiler-plugin/releases/tag/maven-compiler-plugin-{version}") } } - library("Maven Dependency Plugin", "3.10.0") { + library("Maven Dependency Plugin", "3.11.0") { group("org.apache.maven.plugins") { plugins = [ "maven-dependency-plugin" From 5221508288f746c8183bacf3b565e11975507dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:22:30 +0200 Subject: [PATCH 762/930] Upgrade to Maven Help Plugin 3.5.2 Closes gh-51059 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index bc988dad116b..ed2fc7af76cd 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1513,7 +1513,7 @@ bom { releaseNotes("https://github.com/apache/maven-surefire/releases/tag/surefire-{version}") } } - library("Maven Help Plugin", "3.5.1") { + library("Maven Help Plugin", "3.5.2") { group("org.apache.maven.plugins") { plugins = [ "maven-help-plugin" From 87f96b13c141143a9790e7a942766da5e0358c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:22:34 +0200 Subject: [PATCH 763/930] Upgrade to Maven Invoker Plugin 3.10.1 Closes gh-51060 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ed2fc7af76cd..b0d8c09fc9ee 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1534,7 +1534,7 @@ bom { releaseNotes("https://github.com/apache/maven-install-plugin/releases/tag/maven-install-plugin-{version}") } } - library("Maven Invoker Plugin", "3.9.1") { + library("Maven Invoker Plugin", "3.10.1") { group("org.apache.maven.plugins") { plugins = [ "maven-invoker-plugin" From 751acf0be95c341bbee358cb50c46db1e0da6d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:22:39 +0200 Subject: [PATCH 764/930] Upgrade to Native Build Tools Plugin 1.1.5 Closes gh-51061 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 18650fbc067c..eb92a8525ec8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ junitJupiterVersion=6.1.2 kotlinVersion=2.3.21 mavenVersion=3.9.13 mockitoVersion=5.23.0 -nativeBuildToolsVersion=1.1.4 +nativeBuildToolsVersion=1.1.5 nullabilityPluginVersion=0.0.14 snakeYamlVersion=2.6 springGrpcVersion=1.1.0 From 77bbe240d8b69412d5f88c63cb5ddd10d9ac5954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:22:43 +0200 Subject: [PATCH 765/930] Upgrade to Netty 4.2.16.Final Closes gh-51062 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b0d8c09fc9ee..d5e714c44fc1 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1765,7 +1765,7 @@ bom { releaseNotes("https://github.com/neo4j/neo4j-java-driver/releases/tag/{version}") } } - library("Netty", "4.2.15.Final") { + library("Netty", "4.2.16.Final") { prohibit { contains ".Alpha" contains ".Beta" From 7a713c8c3def6d55fbf87bff1bca470ff4d7e3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:22:47 +0200 Subject: [PATCH 766/930] Upgrade to Protobuf Maven Plugin 5.1.7 Closes gh-51063 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d5e714c44fc1..ae08901239e7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1855,7 +1855,7 @@ bom { releaseNotes("https://github.com/oracle/oracle-r2dbc/releases/tag/{version}") } } - library("Protobuf Maven Plugin", "5.1.4") { + library("Protobuf Maven Plugin", "5.1.7") { group("io.github.ascopes") { plugins = [ "protobuf-maven-plugin" From d37e35091f80f4f9e9307e7cb67a46f228864a93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:22:51 +0200 Subject: [PATCH 767/930] Upgrade to Postgresql 42.7.13 Closes gh-51064 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ae08901239e7..f669dffc8934 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1876,7 +1876,7 @@ bom { javadoc("https://javadoc.io/doc/org.messaginghub/pooled-jms/{version}", "org.messaginghub.pooled.jms") } } - library("Postgresql", "42.7.11") { + library("Postgresql", "42.7.13") { group("org.postgresql") { modules = [ "postgresql" From bef5813be4fc7e274ecc097a6ea64f000adf1729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:22:55 +0200 Subject: [PATCH 768/930] Upgrade to Pulsar 4.2.3 Closes gh-51065 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index f669dffc8934..b657e0905fc3 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1915,7 +1915,7 @@ bom { releaseNotes("https://github.com/prometheus/client_java/releases/tag/parent-{version}") } } - library("Pulsar", "4.2.2") { + library("Pulsar", "4.2.3") { group("org.apache.pulsar") { bom("pulsar-bom") } From 84c38011b1602f14cf3910decb77a33d26d04181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:22:59 +0200 Subject: [PATCH 769/930] Upgrade to R2DBC MariaDB 1.4.1 Closes gh-51066 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b657e0905fc3..465aebd90a15 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1989,7 +1989,7 @@ bom { releaseNotes("https://github.com/r2dbc/r2dbc-h2/releases/tag/v{version}") } } - library("R2DBC MariaDB", "1.4.0") { + library("R2DBC MariaDB", "1.4.1") { group("org.mariadb") { modules = [ "r2dbc-mariadb" From e1e2df58f7a84ad3037c4a1eb549c1decf400a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:23:04 +0200 Subject: [PATCH 770/930] Upgrade to R2DBC MSSQL 1.0.5.RELEASE Closes gh-51067 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 465aebd90a15..5522bc7fe1dc 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1999,7 +1999,7 @@ bom { releaseNotes("https://github.com/mariadb-corporation/mariadb-connector-r2dbc/releases/tag/{version}") } } - library("R2DBC MSSQL", "1.0.4.RELEASE") { + library("R2DBC MSSQL", "1.0.5.RELEASE") { group ("io.r2dbc") { modules = [ "r2dbc-mssql" From ca1805e78cf923977a76eaababd4755c6073f199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:23:08 +0200 Subject: [PATCH 771/930] Upgrade to R2DBC Postgresql 1.1.2.RELEASE Closes gh-51068 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 5522bc7fe1dc..58a565e6a6f4 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2031,7 +2031,7 @@ bom { releaseNotes("https://github.com/r2dbc/r2dbc-pool/releases/tag/v{version}") } } - library("R2DBC Postgresql", "1.1.1.RELEASE") { + library("R2DBC Postgresql", "1.1.2.RELEASE") { considerSnapshots() group("org.postgresql") { modules = [ From 0adb9950ac7e30d5c1b73c9abb6db4fa982b41cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:23:16 +0200 Subject: [PATCH 772/930] Upgrade to RabbitMQ Stream Client 1.8.0 Closes gh-51070 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 58a565e6a6f4..7f1bde7dab4b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2090,7 +2090,7 @@ bom { releaseNotes("https://github.com/rabbitmq/rabbitmq-amqp-java-client/releases/tag/v{version}") } } - library("RabbitMQ Stream Client", "1.6.0") { + library("RabbitMQ Stream Client", "1.8.0") { group("com.rabbitmq") { modules = [ "stream-client" From 0913fea7532420842d369ffd10deded73b174b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:23:21 +0200 Subject: [PATCH 773/930] Upgrade to Selenium 4.46.0 Closes gh-51071 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7f1bde7dab4b..899ae7157409 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2481,7 +2481,7 @@ bom { ] } } - library("Selenium", "4.43.0") { + library("Selenium", "4.46.0") { group("org.seleniumhq.selenium") { bom("selenium-bom") } From 898fd202ad020dbd2d641399579fb3aceca49e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:23:25 +0200 Subject: [PATCH 774/930] Upgrade to Selenium HtmlUnit 4.46.0 Closes gh-51072 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 899ae7157409..e884021ecabc 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2491,7 +2491,7 @@ bom { releaseNotes("https://github.com/SeleniumHQ/selenium/releases/tag/selenium-{version}") } } - library("Selenium HtmlUnit", "4.43.0") { + library("Selenium HtmlUnit", "4.46.0") { group("org.seleniumhq.selenium") { modules = [ "htmlunit3-driver" From 961d698fb4219b3964b51d290c779fb5da7a83ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:23:29 +0200 Subject: [PATCH 775/930] Upgrade to Tomcat 11.0.24 Closes gh-51073 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index eb92a8525ec8..7bbd791dd7de 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,7 +24,7 @@ snakeYamlVersion=2.6 springGrpcVersion=1.1.0 springFrameworkVersion=7.0.8 springFramework60xVersion=6.0.23 -tomcatVersion=11.0.22 +tomcatVersion=11.0.24 protobufGradlePluginVersion=0.9.6 kotlin.stdlib.default.dependency=false From 32cb30fae65f45704c8ec0752fe7a761b8c58c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:23:33 +0200 Subject: [PATCH 776/930] Upgrade to UnboundID LDAPSDK 7.0.5 Closes gh-51074 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e884021ecabc..57890623df23 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2908,7 +2908,7 @@ bom { releaseNotes(version -> "https://tomcat.apache.org/tomcat-%s.%s-doc/changelog.html".formatted(version.major(), version.minor())) } } - library("UnboundID LDAPSDK", "7.0.4") { + library("UnboundID LDAPSDK", "7.0.5") { group("com.unboundid") { modules = [ "unboundid-ldapsdk" From a7958324e3dc37cbace66e26daafb20441d26526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:23:38 +0200 Subject: [PATCH 777/930] Upgrade to WebJars Locator Lite 1.1.4 Closes gh-51075 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 57890623df23..08a78d805b37 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2943,7 +2943,7 @@ bom { ] } } - library("WebJars Locator Lite", "1.1.3") { + library("WebJars Locator Lite", "1.1.4") { group("org.webjars") { modules = [ "webjars-locator-lite" From e2a68475dc5e8cc39eff862238f7b86bbba4d3c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 14:23:42 +0200 Subject: [PATCH 778/930] Upgrade to XmlUnit2 2.12.0 Closes gh-51076 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 08a78d805b37..dac7c424bf88 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2967,7 +2967,7 @@ bom { releaseNotes("https://github.com/mojohaus/xml-maven-plugin/releases/tag/{version}") } } - library("XmlUnit2", "2.11.0") { + library("XmlUnit2", "2.12.0") { group("org.xmlunit") { modules = [ "xmlunit-assertj", From 572a2c95928d20ea57bf8947ffb0b8f0d3ca2602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 15:34:35 +0200 Subject: [PATCH 779/930] Upgrade to HtmlUnit 5.3.0 Closes gh-51077 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index dac7c424bf88..7bf29d873286 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -706,7 +706,7 @@ bom { ] } } - library("HtmlUnit", "4.21.0") { + library("HtmlUnit", "5.3.0") { group("org.htmlunit") { modules = [ "htmlunit" From d22b8e44ad5f6ef8ce722e8b748a62b8dd0f9060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Jul 2026 16:55:54 +0200 Subject: [PATCH 780/930] Upgrade to Jackson 2 Bom 2.21.5 Closes gh-51078 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7bbd791dd7de..52b8a4c8c1cd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ checkstyleToolVersion=10.12.4 commonsCodecVersion=1.22.0 graalVersion=25 hamcrestVersion=3.0 -jackson2Version=2.21.4 +jackson2Version=2.21.5 jacksonVersion=3.1.5 javaFormatVersion=0.0.47 junitJupiterVersion=6.1.2 From 4293de5eee6aa60d5c31b0753e74076a373371ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Mon, 20 Jul 2026 15:25:54 -0600 Subject: [PATCH 781/930] Simplify Artemis AMQP 1.0 integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See gh-51081 Signed-off-by: Eddú Meléndez --- ...nectionDetailsFactoryIntegrationTests.java | 33 +++++-------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/ArtemisAmqpContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/ArtemisAmqpContainerConnectionDetailsFactoryIntegrationTests.java index d5439c03cdea..dec132ae0759 100644 --- a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/ArtemisAmqpContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/ArtemisAmqpContainerConnectionDetailsFactoryIntegrationTests.java @@ -20,7 +20,7 @@ import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; -import org.testcontainers.containers.Container.ExecResult; +import org.testcontainers.activemq.ArtemisContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -41,14 +41,18 @@ * Tests for {@link ArtemisAmqpContainerConnectionDetailsFactory}. * * @author Stephane Nicoll + * @author Eddú Meléndez */ @SpringJUnitConfig @Testcontainers(disabledWithoutDocker = true) class ArtemisAmqpContainerConnectionDetailsFactoryIntegrationTests { + private static final String QUEUE_NAME = UUID.randomUUID().toString(); + @Container @ServiceConnection - static final ArtemisLegacyContainer container = TestImage.container(ArtemisLegacyContainer.class); + static final ArtemisContainer container = TestImage.container(ArtemisLegacyContainer.class) + .withEnv("EXTRA_ARGS", "--queues %s:anycast".formatted(QUEUE_NAME)); @Autowired(required = false) private AmqpConnectionDetails connectionDetails; @@ -59,32 +63,11 @@ class ArtemisAmqpContainerConnectionDetailsFactoryIntegrationTests { @Test void connectionCanBeMadeToArtemisContainer() throws Exception { assertThat(this.connectionDetails).isNotNull(); - String address = createRandomQueue(); - this.amqpClient.to(address).body("test message").send(); - Object message = this.amqpClient.from(address).receiveAndConvert().get(4, TimeUnit.MINUTES); + this.amqpClient.to(QUEUE_NAME).body("test message").send(); + Object message = this.amqpClient.from(QUEUE_NAME).receiveAndConvert().get(4, TimeUnit.MINUTES); assertThat(message).isEqualTo("test message"); } - private String createRandomQueue() { - String name = UUID.randomUUID().toString(); - createQueue(name); - return name; - } - - private void createQueue(String name) { - try { - ExecResult execResult = container.execInContainer("/var/lib/artemis-instance/bin/artemis", "queue", - "create", "--name=" + name, "--auto-create-address", "--anycast", "--silent", - "--user=" + container.getUser(), "--password=" + container.getPassword()); - if (execResult.getExitCode() != 0) { - throw new IllegalStateException("Failed to create queue: " + execResult); - } - } - catch (Exception ex) { - throw new IllegalStateException("Failed to create queue " + name, ex); - } - } - @Configuration(proxyBeanMethods = false) @ImportAutoConfiguration(AmqpAutoConfiguration.class) static class TestConfiguration { From da318b2540a704516de8c438bda7a93135f73e38 Mon Sep 17 00:00:00 2001 From: Ahmed El amraouiyine Date: Sat, 20 Jun 2026 01:34:59 +0100 Subject: [PATCH 782/930] Honor gRPC overall health setting See gh-50799 Signed-off-by: Ahmed El amraouiyine --- .../AutoConfiguredHealthCheckedGrpcComponents.java | 8 +++++--- ...AutoConfiguredHealthCheckedGrpcComponentsTests.java | 9 +++++++++ .../health/GrpcServerHealthAutoConfigurationTests.java | 10 ++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java index bdee841ba665..4ded582191d3 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java @@ -51,7 +51,7 @@ */ class AutoConfiguredHealthCheckedGrpcComponents implements HealthCheckedGrpcComponents { - private final HealthCheckedGrpcComponent server; + private final @Nullable HealthCheckedGrpcComponent server; private final Map services; @@ -68,8 +68,10 @@ class AutoConfiguredHealthCheckedGrpcComponents implements HealthCheckedGrpcComp () -> StatusAggregator.of(properties.getStatus().getOrder())); StatusMapper statusMapper = getNonQualifiedBean(beanFactory, StatusMapper.class, () -> StatusMapper.of(properties.getStatus().getMapping())); - this.server = new AutoConfiguredHealthCheckedGrpcComponent(HealthContributorMembership.always(), - statusAggregator, statusMapper); + this.server = (properties.isIncludeOverallHealth()) + ? new AutoConfiguredHealthCheckedGrpcComponent(HealthContributorMembership.always(), statusAggregator, + statusMapper) + : null; this.services = createServices(properties.getService(), beanFactory, statusAggregator, statusMapper); } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentsTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentsTests.java index 8a47232a3ca2..c176d5802711 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentsTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponentsTests.java @@ -59,6 +59,15 @@ void getServerMatchesAllMembers() { }); } + @Test + void getServerWhenIncludeOverallHealthIsFalseReturnsNull() { + this.contextRunner.withPropertyValues("spring.grpc.server.health.include-overall-health=false") + .run((context) -> { + HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class); + assertThat(components.getServer()).isNull(); + }); + } + @Test void getServiceNamesReturnsServiceNames() { this.contextRunner diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java index 527c284f61ca..86913e45a6ad 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java @@ -302,6 +302,16 @@ void createsGrpcServerHealth() { }); } + @Test + void whenIncludeOverallHealthIsFalseDoesNotReportOverallHealth() { + this.contextRunner.withPropertyValues("spring.grpc.server.health.include-overall-health=false").run((context) -> { + GrpcServerHealth serverHealth = context.getBean(GrpcServerHealth.class); + Map result = new LinkedHashMap<>(); + serverHealth.update(result::put); + assertThat(result).isEmpty(); + }); + } + @Test void whenHasGrpcServerHealthBeanDoesNotCreateAdditional() { this.contextRunner.withUserConfiguration(GrpcServerHealthConfiguration.class).run((context) -> { From e2b4c93c5750b900c2f37522bcefb45558ac78bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 23 Jul 2026 14:16:14 +0200 Subject: [PATCH 783/930] Polish "Honor gRPC overall health setting" See gh-50799 --- .../AutoConfiguredHealthCheckedGrpcComponents.java | 6 ++---- .../GrpcServerHealthAutoConfigurationTests.java | 13 +++++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java index 4ded582191d3..cb088ecb0e93 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/AutoConfiguredHealthCheckedGrpcComponents.java @@ -68,10 +68,8 @@ class AutoConfiguredHealthCheckedGrpcComponents implements HealthCheckedGrpcComp () -> StatusAggregator.of(properties.getStatus().getOrder())); StatusMapper statusMapper = getNonQualifiedBean(beanFactory, StatusMapper.class, () -> StatusMapper.of(properties.getStatus().getMapping())); - this.server = (properties.isIncludeOverallHealth()) - ? new AutoConfiguredHealthCheckedGrpcComponent(HealthContributorMembership.always(), statusAggregator, - statusMapper) - : null; + this.server = (properties.isIncludeOverallHealth()) ? new AutoConfiguredHealthCheckedGrpcComponent( + HealthContributorMembership.always(), statusAggregator, statusMapper) : null; this.services = createServices(properties.getService(), beanFactory, statusAggregator, statusMapper); } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java index 86913e45a6ad..172b8a64a038 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java @@ -304,12 +304,13 @@ void createsGrpcServerHealth() { @Test void whenIncludeOverallHealthIsFalseDoesNotReportOverallHealth() { - this.contextRunner.withPropertyValues("spring.grpc.server.health.include-overall-health=false").run((context) -> { - GrpcServerHealth serverHealth = context.getBean(GrpcServerHealth.class); - Map result = new LinkedHashMap<>(); - serverHealth.update(result::put); - assertThat(result).isEmpty(); - }); + this.contextRunner.withPropertyValues("spring.grpc.server.health.include-overall-health=false") + .run((context) -> { + GrpcServerHealth serverHealth = context.getBean(GrpcServerHealth.class); + Map result = new LinkedHashMap<>(); + serverHealth.update(result::put); + assertThat(result).isEmpty(); + }); } @Test From 07f80348a576303eadda3e923593c89a818c8b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 24 Jul 2026 15:41:11 +0200 Subject: [PATCH 784/930] Polish "Remove APIs that were deprecated for removal in 4.2" See gh-50955 --- .../OperationMethodParameterTests.java | 17 +- .../BatchJdbcAutoConfiguration.java | 15 +- .../BatchJdbcAutoConfigurationTests.java | 28 -- .../autoconfigure/BatchAutoConfiguration.java | 15 +- .../BatchAutoConfigurationTests.java | 25 -- ...ClientHttpMessageConvertersCustomizer.java | 32 +-- ...ServerHttpMessageConvertersCustomizer.java | 33 +-- .../autoconfigure/HttpMessageConverters.java | 262 ------------------ ...ttpMessageConvertersAutoConfiguration.java | 10 +- .../HttpMessageConvertersTests.java | 192 ------------- .../WebServerGracefulShutdownLifecycle.java | 9 - 11 files changed, 26 insertions(+), 612 deletions(-) delete mode 100644 module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConverters.java delete mode 100644 module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersTests.java diff --git a/module/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java b/module/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java index f608b4b4bfff..fe4c898aa4f2 100644 --- a/module/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java +++ b/module/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java @@ -18,6 +18,7 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.endpoint.annotation.Selector; @@ -36,8 +37,6 @@ class OperationMethodParameterTests { private final Method example = findMethod("example", String.class, String.class); - private final Method exampleSpringNullable = findMethod("exampleSpringNullable", String.class, String.class); - private final Method exampleAnnotation = findMethod("exampleAnnotation", String.class); @Test @@ -64,14 +63,6 @@ void isMandatoryWhenNullableAnnotationShouldReturnFalse() { assertThat(parameter.isMandatory()).isFalse(); } - @Test - @Deprecated(since = "4.0.0") - void isMandatoryWhenSpringNullableAnnotationShouldReturnFalse() { - OperationMethodParameter parameter = new OperationMethodParameter("name", - this.exampleSpringNullable.getParameters()[1]); - assertThat(parameter.isMandatory()).isFalse(); - } - @Test void getAnnotationShouldReturnAnnotation() { OperationMethodParameter parameter = new OperationMethodParameter("name", @@ -87,11 +78,7 @@ private Method findMethod(String name, Class... parameters) { return method; } - void example(String one, @org.jspecify.annotations.Nullable String two) { - } - - @Deprecated(since = "4.0.0") - void exampleSpringNullable(String one, @org.springframework.lang.Nullable String two) { + void example(String one, @Nullable String two) { } void exampleAnnotation(@Selector(match = Match.ALL_REMAINING) String allRemaining) { diff --git a/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfiguration.java b/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfiguration.java index 65d914eea4e6..db76ac252fb3 100644 --- a/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfiguration.java +++ b/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfiguration.java @@ -25,7 +25,6 @@ import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration; import org.springframework.batch.core.configuration.support.JdbcDefaultBatchConfiguration; -import org.springframework.batch.core.converter.JobParametersConverter; import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.repository.ExecutionContextSerializer; import org.springframework.beans.factory.ObjectProvider; @@ -91,23 +90,19 @@ static class SpringBootBatchJdbcConfiguration extends JdbcDefaultBatchConfigurat private final @Nullable ExecutionContextSerializer executionContextSerializer; - private final @Nullable JobParametersConverter jobParametersConverter; - SpringBootBatchJdbcConfiguration(DataSource dataSource, @BatchDataSource ObjectProvider batchDataSource, PlatformTransactionManager transactionManager, @BatchTransactionManager ObjectProvider batchTransactionManager, @BatchTaskExecutor ObjectProvider batchTaskExecutor, BatchJdbcProperties properties, ObjectProvider batchConversionServiceCustomizers, - ObjectProvider executionContextSerializer, - ObjectProvider jobParametersConverter) { + ObjectProvider executionContextSerializer) { this.dataSource = batchDataSource.getIfAvailable(() -> dataSource); this.transactionManager = batchTransactionManager.getIfAvailable(() -> transactionManager); this.taskExecutor = batchTaskExecutor.getIfAvailable(); this.properties = properties; this.batchConversionServiceCustomizers = batchConversionServiceCustomizers.orderedStream().toList(); this.executionContextSerializer = executionContextSerializer.getIfAvailable(); - this.jobParametersConverter = jobParametersConverter.getIfAvailable(); } @Override @@ -152,14 +147,6 @@ protected ExecutionContextSerializer getExecutionContextSerializer() { : super.getExecutionContextSerializer(); } - @Override - @Deprecated(since = "4.0.0", forRemoval = true) - @SuppressWarnings("removal") - protected JobParametersConverter getJobParametersConverter() { - return (this.jobParametersConverter != null) ? this.jobParametersConverter - : super.getJobParametersConverter(); - } - @Override protected TaskExecutor getTaskExecutor() { return (this.taskExecutor != null) ? this.taskExecutor : super.getTaskExecutor(); diff --git a/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfigurationTests.java b/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfigurationTests.java index 479006a12ed1..bc61ad71644f 100644 --- a/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfigurationTests.java +++ b/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfigurationTests.java @@ -31,9 +31,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.converter.DefaultJobParametersConverter; -import org.springframework.batch.core.converter.JobParametersConverter; -import org.springframework.batch.core.converter.JsonJobParametersConverter; import org.springframework.batch.core.job.AbstractJob; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.job.JobExecution; @@ -514,31 +511,6 @@ void customJdbcPropertiesIsUsed() { } - @Test - @Deprecated(since = "4.0.0", forRemoval = true) - @SuppressWarnings("removal") - void customJobParametersConverterIsUsed() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withBean(JobParametersConverter.class, JsonJobParametersConverter::new) - .withPropertyValues("spring.datasource.generate-unique-name=true") - .run((context) -> { - assertThat(context).hasSingleBean(JsonJobParametersConverter.class); - assertThat(context.getBean(SpringBootBatchJdbcConfiguration.class).getJobParametersConverter()) - .isInstanceOf(JsonJobParametersConverter.class); - }); - } - - @Test - @Deprecated(since = "4.0.0", forRemoval = true) - @SuppressWarnings("removal") - void defaultJobParametersConverterIsUsed() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> { - assertThat(context).doesNotHaveBean(JobParametersConverter.class); - assertThat(context.getBean(SpringBootBatchJdbcConfiguration.class).getJobParametersConverter()) - .isInstanceOf(DefaultJobParametersConverter.class); - }); - } - private JobLauncherApplicationRunner createInstance(String... registeredJobNames) { JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(mock(JobOperator.class)); JobRegistry jobRegistry = mock(JobRegistry.class); diff --git a/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfiguration.java b/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfiguration.java index bf2dc30490cd..dcb3a75054cc 100644 --- a/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfiguration.java +++ b/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfiguration.java @@ -20,7 +20,6 @@ import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration; -import org.springframework.batch.core.converter.JobParametersConverter; import org.springframework.batch.core.launch.JobOperator; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -49,20 +48,8 @@ static class SpringBootBatchDefaultConfiguration extends DefaultBatchConfigurati private final @Nullable TaskExecutor taskExecutor; - private final @Nullable JobParametersConverter jobParametersConverter; - - SpringBootBatchDefaultConfiguration(@BatchTaskExecutor ObjectProvider batchTaskExecutor, - ObjectProvider jobParametersConverter) { + SpringBootBatchDefaultConfiguration(@BatchTaskExecutor ObjectProvider batchTaskExecutor) { this.taskExecutor = batchTaskExecutor.getIfAvailable(); - this.jobParametersConverter = jobParametersConverter.getIfAvailable(); - } - - @Override - @Deprecated(since = "4.0.0", forRemoval = true) - @SuppressWarnings("removal") - protected JobParametersConverter getJobParametersConverter() { - return (this.jobParametersConverter != null) ? this.jobParametersConverter - : super.getJobParametersConverter(); } @Override diff --git a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfigurationTests.java b/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfigurationTests.java index ce5264579a66..7537b733fe52 100644 --- a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfigurationTests.java +++ b/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfigurationTests.java @@ -25,9 +25,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.converter.DefaultJobParametersConverter; -import org.springframework.batch.core.converter.JobParametersConverter; -import org.springframework.batch.core.converter.JsonJobParametersConverter; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.repository.JobRepository; @@ -135,28 +132,6 @@ void whenTheUserDefinesAJobNameThatDoesNotExistWithRegisteredJobFailsFast() { .withMessage("No job found with name 'three'"); } - @Test - @Deprecated(since = "4.0.0", forRemoval = true) - @SuppressWarnings("removal") - void customJobParametersConverterIsUsed() { - this.contextRunner.withBean(JobParametersConverter.class, JsonJobParametersConverter::new).run((context) -> { - assertThat(context).hasSingleBean(JsonJobParametersConverter.class); - assertThat(context.getBean(SpringBootBatchDefaultConfiguration.class).getJobParametersConverter()) - .isInstanceOf(JsonJobParametersConverter.class); - }); - } - - @Test - @Deprecated(since = "4.0.0", forRemoval = true) - @SuppressWarnings("removal") - void defaultJobParametersConverterIsUsed() { - this.contextRunner.run((context) -> { - assertThat(context).doesNotHaveBean(JobParametersConverter.class); - assertThat(context.getBean(SpringBootBatchDefaultConfiguration.class).getJobParametersConverter()) - .isInstanceOf(DefaultJobParametersConverter.class); - }); - } - private JobLauncherApplicationRunner createInstance(String... registeredJobNames) { JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(mock(JobOperator.class)); JobRegistry jobRegistry = mock(JobRegistry.class); diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultClientHttpMessageConvertersCustomizer.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultClientHttpMessageConvertersCustomizer.java index 995863e5d1a2..ddd5e838e6bd 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultClientHttpMessageConvertersCustomizer.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultClientHttpMessageConvertersCustomizer.java @@ -18,41 +18,29 @@ import java.util.Collection; -import org.jspecify.annotations.Nullable; - import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; -@SuppressWarnings("deprecation") class DefaultClientHttpMessageConvertersCustomizer implements ClientHttpMessageConvertersCustomizer { - private final @Nullable HttpMessageConverters legacyConverters; - private final Collection> converters; - DefaultClientHttpMessageConvertersCustomizer(@Nullable HttpMessageConverters legacyConverters, - Collection> converters) { - this.legacyConverters = legacyConverters; + DefaultClientHttpMessageConvertersCustomizer(Collection> converters) { this.converters = converters; } @Override public void customize(ClientBuilder builder) { - if (this.legacyConverters != null) { - this.legacyConverters.forEach(builder::addCustomConverter); - } - else { - builder.registerDefaults(); - this.converters.forEach((converter) -> { - if (converter instanceof KotlinSerializationJsonHttpMessageConverter) { - builder.withKotlinSerializationJsonConverter(converter); - } - else { - builder.addCustomConverter(converter); - } - }); - } + builder.registerDefaults(); + this.converters.forEach((converter) -> { + if (converter instanceof KotlinSerializationJsonHttpMessageConverter) { + builder.withKotlinSerializationJsonConverter(converter); + } + else { + builder.addCustomConverter(converter); + } + }); } } diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultServerHttpMessageConvertersCustomizer.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultServerHttpMessageConvertersCustomizer.java index cab0bcd66405..004e9759d886 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultServerHttpMessageConvertersCustomizer.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultServerHttpMessageConvertersCustomizer.java @@ -18,42 +18,29 @@ import java.util.Collection; -import org.jspecify.annotations.Nullable; - import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; -@SuppressWarnings("deprecation") class DefaultServerHttpMessageConvertersCustomizer implements ServerHttpMessageConvertersCustomizer { - private final @Nullable HttpMessageConverters legacyConverters; - private final Collection> converters; - DefaultServerHttpMessageConvertersCustomizer(@Nullable HttpMessageConverters legacyConverters, - Collection> converters) { - - this.legacyConverters = legacyConverters; + DefaultServerHttpMessageConvertersCustomizer(Collection> converters) { this.converters = converters; } @Override public void customize(ServerBuilder builder) { - if (this.legacyConverters != null) { - this.legacyConverters.forEach(builder::addCustomConverter); - } - else { - builder.registerDefaults(); - this.converters.forEach((converter) -> { - if (converter instanceof KotlinSerializationJsonHttpMessageConverter) { - builder.withKotlinSerializationJsonConverter(converter); - } - else { - builder.addCustomConverter(converter); - } - }); - } + builder.registerDefaults(); + this.converters.forEach((converter) -> { + if (converter instanceof KotlinSerializationJsonHttpMessageConverter) { + builder.withKotlinSerializationJsonConverter(converter); + } + else { + builder.addCustomConverter(converter); + } + }); } } diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConverters.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConverters.java deleted file mode 100644 index 7a9bbd2f329a..000000000000 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConverters.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.http.converter.autoconfigure; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; -import org.springframework.http.converter.xml.AbstractXmlHttpMessageConverter; -import org.springframework.util.ClassUtils; -import org.springframework.util.CollectionUtils; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; - -/** - * Bean used to manage the {@link HttpMessageConverter}s used in a Spring Boot - * application. Provides a convenient way to add and merge additional - * {@link HttpMessageConverter}s to a web application. - *

    - * An instance of this bean can be registered with specific - * {@link #HttpMessageConverters(HttpMessageConverter...) additional converters} if - * needed, otherwise default converters will be used. - *

    - * NOTE: The default converters used are the same as standard Spring MVC (see - * {@link WebMvcConfigurationSupport}) with some slight re-ordering to put XML converters - * at the back of the list. - * - * @author Dave Syer - * @author Phillip Webb - * @author Andy Wilkinson - * @since 4.0.0 - * @see #HttpMessageConverters(HttpMessageConverter...) - * @see #HttpMessageConverters(Collection) - * @see #getConverters() - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link ClientHttpMessageConvertersCustomizer} and - * {@link ServerHttpMessageConvertersCustomizer}. - */ -@Deprecated(since = "4.0.0") -public class HttpMessageConverters implements Iterable> { - - private static final List> NON_REPLACING_CONVERTERS; - - static { - List> nonReplacingConverters = new ArrayList<>(); - addClassIfExists(nonReplacingConverters, - "org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter"); - NON_REPLACING_CONVERTERS = Collections.unmodifiableList(nonReplacingConverters); - } - - private static final MultiValueMap, Class> EQUIVALENT_CONVERTERS; - - static { - MultiValueMap, Class> equivalentConverters = new LinkedMultiValueMap<>(); - putIfExists(equivalentConverters, "org.springframework.http.converter.json.JacksonJsonHttpMessageConverter", - "org.springframework.http.converter.json.MappingJackson2HttpMessageConverter", - "org.springframework.http.converter.json.GsonHttpMessageConverter", - "org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter"); - putIfExists(equivalentConverters, "org.springframework.http.converter.json.MappingJackson2HttpMessageConverter", - "org.springframework.http.converter.json.GsonHttpMessageConverter", - "org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter"); - EQUIVALENT_CONVERTERS = CollectionUtils.unmodifiableMultiValueMap(equivalentConverters); - } - - private final List> converters; - - /** - * Create a new {@link HttpMessageConverters} instance with the specified additional - * converters. - * @param additionalConverters additional converters to be added. Items are added just - * before any default converter of the same type (or at the front of the list if no - * default converter is found). The {@link #postProcessConverters(List)} method can be - * used for further converter manipulation. - */ - public HttpMessageConverters(HttpMessageConverter... additionalConverters) { - this(Arrays.asList(additionalConverters)); - } - - /** - * Create a new {@link HttpMessageConverters} instance with the specified additional - * converters. - * @param additionalConverters additional converters to be added. Items are added just - * before any default converter of the same type (or at the front of the list if no - * default converter is found). The {@link #postProcessConverters(List)} method can be - * used for further converter manipulation. - */ - public HttpMessageConverters(Collection> additionalConverters) { - this(true, additionalConverters); - } - - /** - * Create a new {@link HttpMessageConverters} instance with the specified converters. - * @param addDefaultConverters if default converters should be added - * @param converters converters to be added. Items are added just before any default - * converter of the same type (or at the front of the list if no default converter is - * found). The {@link #postProcessConverters(List)} method can be used for further - * converter manipulation. - */ - public HttpMessageConverters(boolean addDefaultConverters, Collection> converters) { - List> combined = getCombinedConverters(converters, - addDefaultConverters ? getDefaultConverters() : Collections.emptyList()); - combined = postProcessConverters(combined); - this.converters = Collections.unmodifiableList(combined); - } - - private List> getCombinedConverters(Collection> converters, - List> defaultConverters) { - List> combined = new ArrayList<>(); - List> processing = new ArrayList<>(converters); - for (HttpMessageConverter defaultConverter : defaultConverters) { - Iterator> iterator = processing.iterator(); - while (iterator.hasNext()) { - HttpMessageConverter candidate = iterator.next(); - if (isReplacement(defaultConverter, candidate)) { - combined.add(candidate); - iterator.remove(); - } - } - combined.add(defaultConverter); - if (defaultConverter instanceof AllEncompassingFormHttpMessageConverter allEncompassingConverter) { - configurePartConverters(allEncompassingConverter, converters); - } - } - combined.addAll(0, processing); - return combined; - } - - private boolean isReplacement(HttpMessageConverter defaultConverter, HttpMessageConverter candidate) { - for (Class nonReplacingConverter : NON_REPLACING_CONVERTERS) { - if (nonReplacingConverter.isInstance(candidate)) { - return false; - } - } - Class converterClass = defaultConverter.getClass(); - if (ClassUtils.isAssignableValue(converterClass, candidate)) { - return true; - } - List> equivalentClasses = EQUIVALENT_CONVERTERS.get(converterClass); - return (equivalentClasses != null) && equivalentClasses.stream() - .anyMatch((equivalentClass) -> equivalentClass != null - && ClassUtils.isAssignableValue(equivalentClass, candidate)); - } - - private void configurePartConverters(AllEncompassingFormHttpMessageConverter formConverter, - Collection> converters) { - List> partConverters = formConverter.getPartConverters(); - List> combinedConverters = getCombinedConverters(converters, partConverters); - combinedConverters = postProcessPartConverters(combinedConverters); - formConverter.setPartConverters(combinedConverters); - } - - /** - * Method that can be used to post-process the {@link HttpMessageConverter} list - * before it is used. - * @param converters a mutable list of the converters that will be used. - * @return the final converts list to use - */ - protected List> postProcessConverters(List> converters) { - return converters; - } - - /** - * Method that can be used to post-process the {@link HttpMessageConverter} list - * before it is used to configure the part converters of - * {@link AllEncompassingFormHttpMessageConverter}. - * @param converters a mutable list of the converters that will be used. - * @return the final converts list to use - */ - protected List> postProcessPartConverters(List> converters) { - return converters; - } - - private List> getDefaultConverters() { - List> converters = new ArrayList<>(); - if (ClassUtils.isPresent("org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport", - null)) { - converters.addAll(new WebMvcConfigurationSupport() { - - public List> defaultMessageConverters() { - return super.getMessageConverters(); - } - - }.defaultMessageConverters()); - } - else { - converters.addAll(new RestTemplate().getMessageConverters()); - } - reorderXmlConvertersToEnd(converters); - return converters; - } - - @SuppressWarnings("removal") - private void reorderXmlConvertersToEnd(List> converters) { - List> xml = new ArrayList<>(); - for (Iterator> iterator = converters.iterator(); iterator.hasNext();) { - HttpMessageConverter converter = iterator.next(); - if ((converter instanceof AbstractXmlHttpMessageConverter) - || (converter instanceof org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter)) { - xml.add(converter); - iterator.remove(); - } - } - converters.addAll(xml); - } - - @Override - public Iterator> iterator() { - return getConverters().iterator(); - } - - /** - * Return an immutable list of the converters in the order that they will be - * registered. - * @return the converters - */ - public List> getConverters() { - return this.converters; - } - - private static void addClassIfExists(List> list, String className) { - try { - list.add(Class.forName(className)); - } - catch (ClassNotFoundException | NoClassDefFoundError ex) { - // Ignore - } - } - - private static void putIfExists(MultiValueMap, Class> map, String keyClassName, - String... valueClassNames) { - for (String valueClassName : valueClassNames) { - try { - map.add(Class.forName(keyClassName), Class.forName(valueClassName)); - } - catch (ClassNotFoundException | NoClassDefFoundError ex) { - // Ignore - } - } - } - -} diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java index 6b58a03eec7d..b91ac7c94574 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java @@ -69,22 +69,16 @@ public final class HttpMessageConvertersAutoConfiguration { @Bean @Order(0) - @SuppressWarnings("deprecation") ClientHttpMessageConvertersCustomizer clientConvertersCustomizer( - ObjectProvider legacyConverters, ObjectProvider> converters) { - return new DefaultClientHttpMessageConvertersCustomizer(legacyConverters.getIfAvailable(), - converters.orderedStream().toList()); + return new DefaultClientHttpMessageConvertersCustomizer(converters.orderedStream().toList()); } @Bean @Order(0) - @SuppressWarnings("deprecation") ServerHttpMessageConvertersCustomizer serverConvertersCustomizer( - ObjectProvider legacyConverters, ObjectProvider> converters) { - return new DefaultServerHttpMessageConvertersCustomizer(legacyConverters.getIfAvailable(), - converters.orderedStream().toList()); + return new DefaultServerHttpMessageConvertersCustomizer(converters.orderedStream().toList()); } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersTests.java b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersTests.java deleted file mode 100644 index 82b7d679552f..000000000000 --- a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersTests.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.http.converter.autoconfigure; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.stream.Stream; - -import org.jspecify.annotations.Nullable; -import org.junit.jupiter.api.Test; - -import org.springframework.http.converter.ByteArrayHttpMessageConverter; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.ResourceHttpMessageConverter; -import org.springframework.http.converter.ResourceRegionHttpMessageConverter; -import org.springframework.http.converter.StringHttpMessageConverter; -import org.springframework.http.converter.cbor.JacksonCborHttpMessageConverter; -import org.springframework.http.converter.json.GsonHttpMessageConverter; -import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; -import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; -import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; -import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link HttpMessageConverters}. - * - * @author Dave Syer - * @author Phillip Webb - */ -@SuppressWarnings("deprecation") -class HttpMessageConvertersTests { - - @Test - void containsDefaults() { - HttpMessageConverters converters = new HttpMessageConverters(); - List> converterClasses = new ArrayList<>(); - for (HttpMessageConverter converter : converters) { - converterClasses.add(converter.getClass()); - } - assertThat(converterClasses).containsExactly(ByteArrayHttpMessageConverter.class, - StringHttpMessageConverter.class, ResourceHttpMessageConverter.class, - ResourceRegionHttpMessageConverter.class, AllEncompassingFormHttpMessageConverter.class, - KotlinSerializationJsonHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class, - JacksonCborHttpMessageConverter.class, JacksonXmlHttpMessageConverter.class); - } - - @Test - void addBeforeExistingConverter() { - JacksonJsonHttpMessageConverter converter1 = new JacksonJsonHttpMessageConverter(); - JacksonJsonHttpMessageConverter converter2 = new JacksonJsonHttpMessageConverter(); - HttpMessageConverters converters = new HttpMessageConverters(converter1, converter2); - assertThat(converters.getConverters()).contains(converter1); - assertThat(converters.getConverters()).contains(converter2); - List httpConverters = new ArrayList<>(); - for (HttpMessageConverter candidate : converters) { - if (candidate instanceof JacksonJsonHttpMessageConverter jsonConverter) { - httpConverters.add(jsonConverter); - } - } - // The existing converter is still there, but with a lower priority - assertThat(httpConverters).hasSize(3); - assertThat(httpConverters.indexOf(converter1)).isZero(); - assertThat(httpConverters.indexOf(converter2)).isOne(); - assertThat(converters.getConverters().indexOf(converter1)).isNotZero(); - } - - @Test - void addBeforeExistingEquivalentConverter() { - GsonHttpMessageConverter converter1 = new GsonHttpMessageConverter(); - HttpMessageConverters converters = new HttpMessageConverters(converter1); - Stream> converterClasses = converters.getConverters().stream().map(HttpMessageConverter::getClass); - assertThat(converterClasses).containsSequence(KotlinSerializationJsonHttpMessageConverter.class, - GsonHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class); - } - - @Test - void addBeforeExistingAnotherEquivalentConverter() { - KotlinSerializationJsonHttpMessageConverter converter1 = new KotlinSerializationJsonHttpMessageConverter(); - HttpMessageConverters converters = new HttpMessageConverters(converter1); - Stream> converterClasses = converters.getConverters().stream().map(HttpMessageConverter::getClass); - assertThat(converterClasses).containsSequence(KotlinSerializationJsonHttpMessageConverter.class, - JacksonJsonHttpMessageConverter.class); - } - - @Test - void addBeforeExistingMultipleEquivalentConverters() { - GsonHttpMessageConverter converter1 = new GsonHttpMessageConverter(); - KotlinSerializationJsonHttpMessageConverter converter2 = new KotlinSerializationJsonHttpMessageConverter(); - HttpMessageConverters converters = new HttpMessageConverters(converter1, converter2); - Stream> converterClasses = converters.getConverters().stream().map(HttpMessageConverter::getClass); - assertThat(converterClasses).containsSequence(KotlinSerializationJsonHttpMessageConverter.class, - GsonHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class); - } - - @Test - void addNewConverters() { - HttpMessageConverter converter1 = mock(HttpMessageConverter.class); - HttpMessageConverter converter2 = mock(HttpMessageConverter.class); - HttpMessageConverters converters = new HttpMessageConverters(converter1, converter2); - assertThat(converters.getConverters().get(0)).isEqualTo(converter1); - assertThat(converters.getConverters().get(1)).isEqualTo(converter2); - } - - @Test - void convertersAreAddedToFormPartConverter() { - HttpMessageConverter converter1 = mock(HttpMessageConverter.class); - HttpMessageConverter converter2 = mock(HttpMessageConverter.class); - List> converters = new HttpMessageConverters(converter1, converter2).getConverters(); - List> partConverters = extractFormPartConverters(converters); - assertThat(partConverters.get(0)).isEqualTo(converter1); - assertThat(partConverters.get(1)).isEqualTo(converter2); - } - - @Test - void postProcessConverters() { - HttpMessageConverters converters = new HttpMessageConverters() { - - @Override - protected List> postProcessConverters(List> converters) { - converters.removeIf(JacksonXmlHttpMessageConverter.class::isInstance); - return converters; - } - - }; - List> converterClasses = new ArrayList<>(); - for (HttpMessageConverter converter : converters) { - converterClasses.add(converter.getClass()); - } - assertThat(converterClasses).containsExactly(ByteArrayHttpMessageConverter.class, - StringHttpMessageConverter.class, ResourceHttpMessageConverter.class, - ResourceRegionHttpMessageConverter.class, AllEncompassingFormHttpMessageConverter.class, - KotlinSerializationJsonHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class, - JacksonCborHttpMessageConverter.class); - } - - @Test - void postProcessPartConverters() { - HttpMessageConverters converters = new HttpMessageConverters() { - - @Override - protected List> postProcessPartConverters( - List> converters) { - converters.removeIf(JacksonXmlHttpMessageConverter.class::isInstance); - return converters; - } - - }; - List> converterClasses = new ArrayList<>(); - for (HttpMessageConverter converter : extractFormPartConverters(converters.getConverters())) { - converterClasses.add(converter.getClass()); - } - assertThat(converterClasses).containsExactly(ByteArrayHttpMessageConverter.class, - StringHttpMessageConverter.class, ResourceHttpMessageConverter.class, - KotlinSerializationJsonHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class, - JacksonCborHttpMessageConverter.class); - } - - private List> extractFormPartConverters(List> converters) { - AllEncompassingFormHttpMessageConverter formConverter = findFormConverter(converters); - assertThat(formConverter).isNotNull(); - return formConverter.getPartConverters(); - } - - private @Nullable AllEncompassingFormHttpMessageConverter findFormConverter( - Collection> converters) { - for (HttpMessageConverter converter : converters) { - if (converter instanceof AllEncompassingFormHttpMessageConverter allEncompassingConverter) { - return allEncompassingConverter; - } - } - return null; - } - -} diff --git a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/WebServerGracefulShutdownLifecycle.java b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/WebServerGracefulShutdownLifecycle.java index 5b7fe4ccc3c6..23361024a414 100644 --- a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/WebServerGracefulShutdownLifecycle.java +++ b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/WebServerGracefulShutdownLifecycle.java @@ -27,15 +27,6 @@ */ public final class WebServerGracefulShutdownLifecycle implements SmartLifecycle { - /** - * {@link SmartLifecycle#getPhase() SmartLifecycle phase} in which graceful shutdown - * of the web server is performed. - * @deprecated as of 4.0.0 in favor of - * {@link WebServerApplicationContext#GRACEFUL_SHUTDOWN_PHASE} - */ - @Deprecated(since = "4.0.0", forRemoval = true) - public static final int SMART_LIFECYCLE_PHASE = SmartLifecycle.DEFAULT_PHASE - 1024; - private final WebServer webServer; private volatile boolean running; From 3959c56bdd2a14b56101a6a9c871f7d492485964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 25 Jul 2026 07:51:06 +0200 Subject: [PATCH 785/930] Deprecate RestTemplate support With Spring Framework 7.1 deprecating RestTemplate for removal, this commit deprecates our supporting infrastructure as well, and steers users towards RestClient instead: * RestTemplateAutoConfiguration and RestTemplateObservationAutoConfiguration * RestTemplateBuilder, RestTemplateBuilderConfigurer, and RestTemplateBuilderClientHttpRequestInitializer * RestTemplateCustomizer, RestTemplateRequestCustomizer, and ObservationRestTemplateCustomizer * MockRestServiceServerAutoConfiguration, MockServerRestTemplateCustomizer, and RootUriRequestExpectationManager in the test support module The reference documentation no longer covers RestTemplate and now solely documents RestClient and WebClient. Code snippets and sample tests that only existed to illustrate RestTemplate usage have been removed accordingly. Closes gh-51118 --- .../modules/ROOT/pages/documentation.adoc | 2 +- .../antora/modules/ROOT/pages/redirect.adoc | 9 --- .../modules/how-to/pages/http-clients.adoc | 10 --- .../modules/how-to/pages/spring-mvc.adoc | 2 +- .../reference/pages/actuator/metrics.adoc | 7 +-- .../reference/pages/actuator/tracing.adoc | 4 +- .../reference/pages/io/rest-client.adoc | 63 +------------------ .../native-image/advanced-topics.adoc | 2 +- .../testing/spring-boot-applications.adoc | 8 +-- .../io/restclient/resttemplate/Details.java | 21 ------- .../io/restclient/resttemplate/MyService.java | 36 ----------- .../MyRestTemplateBuilderConfiguration.java | 36 ----------- .../MyRestTemplateCustomizer.java | 56 ----------------- .../resttemplate/ssl/MyService.java | 38 ----------- .../testresttemplate/MySpringBootTests.java | 1 + .../io/restclient/resttemplate/Details.kt | 20 ------ .../io/restclient/resttemplate/MyService.kt | 37 ----------- .../MyRestTemplateBuilderConfiguration.kt | 35 ----------- .../customization/MyRestTemplateCustomizer.kt | 51 --------------- .../restclient/resttemplate/ssl/MyService.kt | 38 ----------- .../testresttemplate/MySpringBootTests.kt | 2 + ...nmentUserDefinedTestRestTemplateTests.java | 2 + ...tTestUserDefinedTestRestTemplateTests.java | 2 + ...BootTestWebEnvironmentRandomPortTests.java | 1 + ...oundryHealthEndpointWebExtensionTests.java | 18 +++--- ...dFoundryInfoEndpointWebExtensionTests.java | 6 +- .../tests/DevToolsIntegrationTests.java | 1 + .../MockServerRestTemplateCustomizer.java | 4 ++ .../RootUriRequestExpectationManager.java | 3 + ...ockRestServiceServerAutoConfiguration.java | 2 + ...MockServerRestTemplateCustomizerTests.java | 3 + ...RootUriRequestExpectationManagerTests.java | 3 + .../AnotherExampleRestTemplateService.java | 3 + ...iceServerEnabledFalseIntegrationTests.java | 1 + ...thRestTemplateRootUriIntegrationTests.java | 3 + .../ExampleRestTemplateService.java | 3 + ...stServiceServerAutoConfigurationTests.java | 4 ++ ...ClientTestNoComponentIntegrationTests.java | 3 + ...AndRestClientTogetherIntegrationTests.java | 1 + ...lientTestRestTemplateIntegrationTests.java | 3 + ...TemplateTwoComponentsIntegrationTests.java | 3 + ...RestTemplateComponentIntegrationTests.java | 3 + ...entTestWithoutJacksonIntegrationTests.java | 6 +- .../boot/restclient/RestTemplateBuilder.java | 4 ++ ...teBuilderClientHttpRequestInitializer.java | 3 + .../restclient/RestTemplateCustomizer.java | 3 + .../RestTemplateRequestCustomizer.java | 2 + .../RestTemplateAutoConfiguration.java | 4 ++ .../RestTemplateBuilderConfigurer.java | 4 ++ ...tTemplateObservationAutoConfiguration.java | 4 ++ .../ObservationRestTemplateCustomizer.java | 4 ++ ...estClientWithRestTemplateBuilderTests.java | 3 + .../RestClientWithRestTemplateTests.java | 3 + ...lderClientHttpRequestInitializerTests.java | 3 + .../restclient/RestTemplateBuilderTests.java | 3 + .../RestTemplateAutoConfigurationTests.java | 3 + ...lateObservationAutoConfigurationTests.java | 3 + ...nAutoConfigurationWithoutMetricsTests.java | 3 + ...bservationRestTemplateCustomizerTests.java | 3 + .../boot/resttestclient/TestRestTemplate.java | 1 + ...TestRestTemplateTestAutoConfiguration.java | 1 + .../resttestclient/TestRestTemplateTests.java | 1 + .../CorsSampleActuatorApplicationTests.java | 1 + ...mpleActuatorExtensionApplicationTests.java | 1 + .../CorsSampleActuatorApplicationTests.java | 1 + .../tomcat/SampleTomcatApplicationTests.java | 1 + .../deployment/AbstractDeploymentTests.java | 1 + 67 files changed, 137 insertions(+), 479 deletions(-) delete mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/Details.java delete mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/MyService.java delete mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateBuilderConfiguration.java delete mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateCustomizer.java delete mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/ssl/MyService.java delete mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/Details.kt delete mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/MyService.kt delete mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateBuilderConfiguration.kt delete mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateCustomizer.kt delete mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/ssl/MyService.kt diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/documentation.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/documentation.adoc index 8406a8860c1e..59ac3c8f5695 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/documentation.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/documentation.adoc @@ -106,7 +106,7 @@ If your application needs IO capabilities, see one or more of the following sect * *Quartz:* xref:reference:io/quartz.adoc[Quartz Scheduling] * *Mail:* xref:reference:io/email.adoc[Sending Email] * *Validation:* xref:reference:io/validation.adoc[JSR-303 Validation] -* *REST Clients:* xref:reference:io/rest-client.adoc[Calling REST Services with RestTemplate and WebClient] +* *REST Clients:* xref:reference:io/rest-client.adoc[Calling REST Services with RestClient and WebClient] * *Webservices:* xref:reference:io/webservices.adoc[Auto-configuration for Spring Web Services] * *JTA:* xref:reference:io/jta.adoc[Distributed Transactions with JTA] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc index 393ca0ded329..650798f890d0 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc @@ -539,8 +539,6 @@ * xref:how-to:hotswapping.adoc#howto.hotswapping.reload-templates[#howto.hotswapping.reload-templates] * xref:how-to:hotswapping.adoc#howto.hotswapping[#howto-hotswapping] * xref:how-to:hotswapping.adoc#howto.hotswapping[#howto.hotswapping] -* xref:how-to:http-clients.adoc#howto.http-clients.rest-template-proxy-configuration[#howto-http-clients-proxy-configuration] -* xref:how-to:http-clients.adoc#howto.http-clients.rest-template-proxy-configuration[#howto.http-clients.rest-template-proxy-configuration] * xref:how-to:http-clients.adoc#howto.http-clients.webclient-reactor-netty-customization[#howto-webclient-reactor-netty-customization] * xref:how-to:http-clients.adoc#howto.http-clients.webclient-reactor-netty-customization[#howto.http-clients.webclient-reactor-netty-customization] * xref:how-to:http-clients.adoc#howto.http-clients[#howto-http-clients] @@ -1682,13 +1680,6 @@ * xref:reference:io/rest-client.adoc#io.rest-client.restclient.customization[#io.rest-client.restclient.customization] * xref:reference:io/rest-client.adoc#io.rest-client.restclient.ssl[#io.rest-client.restclient.ssl] * xref:reference:io/rest-client.adoc#io.rest-client.restclient[#io.rest-client.restclient] -* xref:reference:io/rest-client.adoc#io.rest-client.resttemplate.customization[#boot-features-resttemplate-customization] -* xref:reference:io/rest-client.adoc#io.rest-client.resttemplate.customization[#features.resttemplate.customization] -* xref:reference:io/rest-client.adoc#io.rest-client.resttemplate.customization[#io.rest-client.resttemplate.customization] -* xref:reference:io/rest-client.adoc#io.rest-client.resttemplate.ssl[#io.rest-client.resttemplate.ssl] -* xref:reference:io/rest-client.adoc#io.rest-client.resttemplate[#boot-features-resttemplate] -* xref:reference:io/rest-client.adoc#io.rest-client.resttemplate[#features.resttemplate] -* xref:reference:io/rest-client.adoc#io.rest-client.resttemplate[#io.rest-client.resttemplate] * xref:reference:io/rest-client.adoc#io.rest-client.webclient.customization[#boot-features-webclient-customization] * xref:reference:io/rest-client.adoc#io.rest-client.webclient.customization[#features.webclient.customization] * xref:reference:io/rest-client.adoc#io.rest-client.webclient.customization[#io.rest-client.webclient.customization] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/http-clients.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/http-clients.adoc index 583b08569ab0..0df0eee8ca5d 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/http-clients.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/http-clients.adoc @@ -6,16 +6,6 @@ This section answers questions related to using them. -[[howto.http-clients.rest-template-proxy-configuration]] -== Configure RestTemplate to Use a Proxy - -As described in xref:reference:io/rest-client.adoc#io.rest-client.resttemplate.customization[RestTemplate Customization], you can use a javadoc:org.springframework.boot.restclient.RestTemplateCustomizer[] with javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] to build a customized javadoc:org.springframework.web.client.RestTemplate[]. -This is the recommended approach for creating a javadoc:org.springframework.web.client.RestTemplate[] configured to use a proxy. - -The exact details of the proxy configuration depend on the underlying client request factory that is being used. - - - [[howto.http-clients.webclient-reactor-netty-customization]] == Configure the TcpClient used by a Reactor Netty-based WebClient diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc index 82a99e7cce93..f1b2811071e2 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc @@ -135,7 +135,7 @@ Spring uses javadoc:org.springframework.boot.http.converter.autoconfigure.HttpMe You can contribute additional converters by adding beans of the appropriate type in a Spring Boot context. If a bean you add is of a type that would have been included by default anyway (such as javadoc:org.springframework.http.converter.json.JacksonJsonHttpMessageConverter[] for JSON conversions), it replaces the default value. A convenience bean of type javadoc:org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters[] is provided and is always available if you use the default MVC configuration. -It has some useful methods to access the default and user-enhanced message converters (For example, it can be useful if you want to manually inject them into a custom javadoc:org.springframework.web.client.RestTemplate[]). +It has some useful methods to access the default and user-enhanced message converters (For example, it can be useful if you want to manually inject them into a custom javadoc:org.springframework.web.client.RestClient[]). As in normal MVC usage, any javadoc:org.springframework.web.servlet.config.annotation.WebMvcConfigurer[] beans that you provide can also contribute converters by overriding the `configureMessageConverters` method. However, unlike with normal MVC, you can supply only additional converters that you need (because Spring Boot uses the same mechanism to contribute its defaults). diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc index 9b20630a8c64..9dc34353b353 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc @@ -840,21 +840,20 @@ This metric is tagged with the following information: [[actuator.metrics.supported.http-clients]] === HTTP Client Metrics -Spring Boot Actuator manages the instrumentation of javadoc:org.springframework.web.client.RestTemplate[], javadoc:org.springframework.web.reactive.function.client.WebClient[] and javadoc:org.springframework.web.client.RestClient[]. +Spring Boot Actuator manages the instrumentation of javadoc:org.springframework.web.reactive.function.client.WebClient[] and javadoc:org.springframework.web.client.RestClient[]. For that, you have to inject the auto-configured builder and use it to create instances: -* javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] for javadoc:org.springframework.web.client.RestTemplate[] * javadoc:org.springframework.web.reactive.function.client.WebClient$Builder[] for javadoc:org.springframework.web.reactive.function.client.WebClient[] * javadoc:org.springframework.web.client.RestClient$Builder[] for javadoc:org.springframework.web.client.RestClient[] -You can also manually apply the customizers responsible for this instrumentation, namely javadoc:org.springframework.boot.restclient.observation.ObservationRestTemplateCustomizer[], javadoc:org.springframework.boot.webclient.observation.ObservationWebClientCustomizer[] and javadoc:org.springframework.boot.restclient.observation.ObservationRestClientCustomizer[]. +You can also manually apply the customizers responsible for this instrumentation, namely javadoc:org.springframework.boot.webclient.observation.ObservationWebClientCustomizer[] and javadoc:org.springframework.boot.restclient.observation.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 {url-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 javadoc:org.springframework.web.client.RestTemplate[] or javadoc:org.springframework.web.client.RestClient[], provide a javadoc:org.springframework.context.annotation.Bean[format=annotation] that implements javadoc:org.springframework.http.client.observation.ClientRequestObservationConvention[] from the `org.springframework.http.client.observation` package. +To customize the tags when using javadoc:org.springframework.web.client.RestClient[], provide a javadoc:org.springframework.context.annotation.Bean[format=annotation] that implements javadoc:org.springframework.http.client.observation.ClientRequestObservationConvention[] from the `org.springframework.http.client.observation` package. To customize the tags when using javadoc:org.springframework.web.reactive.function.client.WebClient[], provide a javadoc:org.springframework.context.annotation.Bean[format=annotation] that implements javadoc:org.springframework.web.reactive.function.client.ClientRequestObservationConvention[] from the `org.springframework.web.reactive.function.client` package. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc index 1d270ff91cf5..72c7cb0d6fac 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc @@ -97,9 +97,9 @@ Please read xref:reference:actuator/observability.adoc#actuator.observability.co [[actuator.micrometer-tracing.propagating-traces]] == Propagating Traces -To automatically propagate traces over the network, use the auto-configured xref:io/rest-client.adoc#io.rest-client.resttemplate[`RestTemplateBuilder`], xref:io/rest-client.adoc#io.rest-client.restclient[`RestClient.Builder`] or xref:io/rest-client.adoc#io.rest-client.webclient[`WebClient.Builder`] to construct the client. +To automatically propagate traces over the network, use the auto-configured xref:io/rest-client.adoc#io.rest-client.restclient[`RestClient.Builder`] or xref:io/rest-client.adoc#io.rest-client.webclient[`WebClient.Builder`] to construct the client. -WARNING: If you create the javadoc:org.springframework.web.client.RestTemplate[], the javadoc:org.springframework.web.client.RestClient[] or the javadoc:org.springframework.web.reactive.function.client.WebClient[] without using the auto-configured builders, automatic trace propagation won't work! +WARNING: If you create the javadoc:org.springframework.web.client.RestClient[] or the javadoc:org.springframework.web.reactive.function.client.WebClient[] without using the auto-configured builders, automatic trace propagation won't work! diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc index c5c5b20d222d..0f9af6bea5db 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc @@ -3,7 +3,7 @@ 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 javadoc:org.springframework.web.reactive.function.client.WebClient[]. -If you prefer imperative APIs then you can use javadoc:org.springframework.web.client.RestClient[] or javadoc:org.springframework.web.client.RestTemplate[]. +If you prefer imperative APIs then you can use javadoc:org.springframework.web.client.RestClient[]. @@ -159,67 +159,10 @@ include-code::settings/MyService[] -[[io.rest-client.resttemplate]] -== RestTemplate - -Spring Framework's javadoc:org.springframework.web.client.RestTemplate[] class predates javadoc:org.springframework.web.client.RestClient[] and is the classic way that many applications use to call remote REST services. -You might choose to use javadoc:org.springframework.web.client.RestTemplate[] when you have existing code that you don't want to migrate to javadoc:org.springframework.web.client.RestClient[], or because you're already familiar with the javadoc:org.springframework.web.client.RestTemplate[] API. - -Since javadoc:org.springframework.web.client.RestTemplate[] instances often need to be customized before being used, Spring Boot does not provide any single auto-configured javadoc:org.springframework.web.client.RestTemplate[] bean. -It does, however, auto-configure a javadoc:org.springframework.boot.restclient.RestTemplateBuilder[], which can be used to create javadoc:org.springframework.web.client.RestTemplate[] instances when needed. -The auto-configured javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] ensures that sensible javadoc:org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters[] and an appropriate javadoc:org.springframework.http.client.ClientHttpRequestFactory[] are applied to javadoc:org.springframework.web.client.RestTemplate[] instances. - -The following code shows a typical example: - -include-code::MyService[] - -javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] includes a number of useful methods that can be used to quickly configure a javadoc:org.springframework.web.client.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 javadoc:org.springframework.web.client.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 javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] and then call its methods as required. -Each method call returns a new javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] instance, so the customizations only affect this use of the builder. - -To make an application-wide, additive customization, use a javadoc:org.springframework.boot.restclient.RestTemplateCustomizer[] bean. -All such beans are automatically registered with the auto-configured javadoc:org.springframework.boot.restclient.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 javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] bean. -Doing so will replace the auto-configured builder. -If you want any javadoc:org.springframework.boot.restclient.RestTemplateCustomizer[] beans to be applied to your custom builder, as the auto-configuration would have done, configure it using a javadoc:org.springframework.boot.restclient.autoconfigure.RestTemplateBuilderConfigurer[]. -The following example exposes a javadoc:org.springframework.boot.restclient.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 javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] bean without using a configurer. -In addition to replacing the auto-configured builder, this also prevents any javadoc:org.springframework.boot.restclient.RestTemplateCustomizer[] beans from being used. - -TIP: You can also change the xref:io/rest-client.adoc#io.rest-client.clienthttprequestfactory.configuration[global HTTP client configuration]. - - - -[[io.rest-client.resttemplate.ssl]] -=== RestTemplate SSL Support - -If you need custom SSL configuration on the javadoc:org.springframework.web.client.RestTemplate[], you can apply an xref:features/ssl.adoc#features.ssl.bundles[SSL bundle] to the javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] as shown in this example: - -include-code::MyService[] - - - [[io.rest-client.clienthttprequestfactory]] -== HTTP Client Detection for RestClient and RestTemplate +== HTTP Client Detection for RestClient -Spring Boot will auto-detect which HTTP client to use with javadoc:org.springframework.web.client.RestClient[] and javadoc:org.springframework.web.client.RestTemplate[] depending on the libraries available on the application classpath. +Spring Boot will auto-detect which HTTP client to use with javadoc:org.springframework.web.client.RestClient[] depending on the libraries available on the application classpath. In order of preference, the following clients are supported: . Apache HttpClient diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/packaging/native-image/advanced-topics.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/packaging/native-image/advanced-topics.adoc index 9360231e7eb8..ff92a6052929 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/packaging/native-image/advanced-topics.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/packaging/native-image/advanced-topics.adoc @@ -168,7 +168,7 @@ You can then use javadoc:org.springframework.context.annotation.ImportRuntimeHin If you have classes which need binding (mostly needed when serializing or deserializing JSON), you can use {url-spring-framework-docs}/core/aot.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 javadoc:org.springframework.web.bind.annotation.RestController[format=annotation] method. -But when you work with javadoc:org.springframework.web.reactive.function.client.WebClient[], javadoc:org.springframework.web.client.RestClient[] or javadoc:org.springframework.web.client.RestTemplate[] directly, you might need to use javadoc:org.springframework.aot.hint.annotation.RegisterReflectionForBinding[format=annotation]. +But when you work with javadoc:org.springframework.web.reactive.function.client.WebClient[] or javadoc:org.springframework.web.client.RestClient[] directly, you might need to use javadoc:org.springframework.aot.hint.annotation.RegisterReflectionForBinding[format=annotation]. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc index f5307e6757f6..1eac09c2cde0 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc @@ -732,7 +732,7 @@ include-code::server/MyDataLdapTests[] == Auto-configured REST Clients You can use the javadoc:org.springframework.boot.restclient.test.autoconfigure.RestClientTest[format=annotation] annotation from the `spring-boot-restclient-test` module to test REST clients. -By default, it auto-configures Jackson, GSON, and Jsonb support, configures a javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] and a javadoc:org.springframework.web.client.RestClient$Builder[], and adds support for javadoc:org.springframework.test.web.client.MockRestServiceServer[]. +By default, it auto-configures Jackson, GSON, and Jsonb support, configures a javadoc:org.springframework.web.client.RestClient$Builder[], and adds support for javadoc:org.springframework.test.web.client.MockRestServiceServer[]. Regular javadoc:org.springframework.stereotype.Component[format=annotation] and javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation] beans are not scanned when the javadoc:org.springframework.boot.restclient.test.autoconfigure.RestClientTest[format=annotation] annotation is used. javadoc:org.springframework.boot.context.properties.EnableConfigurationProperties[format=annotation] can be used to include javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation] beans. @@ -740,11 +740,7 @@ TIP: A list of the auto-configuration settings that are enabled by javadoc:org.s The specific beans that you want to test should be specified by using the `value` or `components` attribute of javadoc:org.springframework.boot.restclient.test.autoconfigure.RestClientTest[format=annotation]. -When using a javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] in the beans under test and `RestTemplateBuilder.baseUri(String baseUri)` has been called when building the javadoc:org.springframework.web.client.RestTemplate[], then the base URI should be omitted from the javadoc:org.springframework.test.web.client.MockRestServiceServer[] expectations as shown in the following example: - -include-code::MyRestTemplateServiceTests[] - -When using a javadoc:org.springframework.web.client.RestClient$Builder[] in the beans under test, or when using a javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] without calling `baseUri(String baseUri)`, the full URI must be used in the javadoc:org.springframework.test.web.client.MockRestServiceServer[] expectations as shown in the following example: +When using a javadoc:org.springframework.web.client.RestClient$Builder[] in the beans under test, the full URI must be used in the javadoc:org.springframework.test.web.client.MockRestServiceServer[] expectations as shown in the following example: include-code::MyRestClientServiceTests[] diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/Details.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/Details.java deleted file mode 100644 index fb0ed4ae59a6..000000000000 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/Details.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttemplate; - -public class Details { - -} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/MyService.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/MyService.java deleted file mode 100644 index a537d11bf0b2..000000000000 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/MyService.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttemplate; - -import org.springframework.boot.restclient.RestTemplateBuilder; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -@Service -public class MyService { - - private final RestTemplate restTemplate; - - public MyService(RestTemplateBuilder restTemplateBuilder) { - this.restTemplate = restTemplateBuilder.build(); - } - - public Details someRestCall(String name) { - return this.restTemplate.getForObject("/{name}/details", Details.class, name); - } - -} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateBuilderConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateBuilderConfiguration.java deleted file mode 100644 index f9a6d113bbeb..000000000000 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateBuilderConfiguration.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttemplate.customization; - -import java.time.Duration; - -import org.springframework.boot.restclient.RestTemplateBuilder; -import org.springframework.boot.restclient.autoconfigure.RestTemplateBuilderConfigurer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration(proxyBeanMethods = false) -public class MyRestTemplateBuilderConfiguration { - - @Bean - public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer configurer) { - return configurer.configure(new RestTemplateBuilder()) - .connectTimeout(Duration.ofSeconds(5)) - .readTimeout(Duration.ofSeconds(2)); - } - -} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateCustomizer.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateCustomizer.java deleted file mode 100644 index 98e052a18636..000000000000 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateCustomizer.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttemplate.customization; - -import org.apache.hc.client5.http.classic.HttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; -import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner; -import org.apache.hc.client5.http.routing.HttpRoutePlanner; -import org.apache.hc.core5.http.HttpException; -import org.apache.hc.core5.http.HttpHost; -import org.apache.hc.core5.http.protocol.HttpContext; - -import org.springframework.boot.restclient.RestTemplateCustomizer; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.web.client.RestTemplate; - -public class MyRestTemplateCustomizer implements RestTemplateCustomizer { - - @Override - public void customize(RestTemplate restTemplate) { - HttpRoutePlanner routePlanner = new CustomRoutePlanner(new HttpHost("proxy.example.com")); - HttpClient httpClient = HttpClientBuilder.create().setRoutePlanner(routePlanner).build(); - restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)); - } - - static class CustomRoutePlanner extends DefaultProxyRoutePlanner { - - CustomRoutePlanner(HttpHost proxy) { - super(proxy); - } - - @Override - protected HttpHost determineProxy(HttpHost target, HttpContext context) throws HttpException { - if (target.getHostName().equals("192.168.0.5")) { - return null; - } - return super.determineProxy(target, context); - } - - } - -} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/ssl/MyService.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/ssl/MyService.java deleted file mode 100644 index d488da14a66c..000000000000 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/resttemplate/ssl/MyService.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttemplate.ssl; - -import org.springframework.boot.docs.io.restclient.resttemplate.Details; -import org.springframework.boot.restclient.RestTemplateBuilder; -import org.springframework.boot.ssl.SslBundles; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -@Service -public class MyService { - - private final RestTemplate restTemplate; - - public MyService(RestTemplateBuilder restTemplateBuilder, SslBundles sslBundles) { - this.restTemplate = restTemplateBuilder.sslBundle(sslBundles.getBundle("mybundle")).build(); - } - - public Details someRestCall(String name) { - return this.restTemplate.getForObject("/{name}/details", Details.class, name); - } - -} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.java index 4abeb71040d5..ccedfe30b180 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.java @@ -35,6 +35,7 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @AutoConfigureTestRestTemplate +@SuppressWarnings("removal") class MySpringBootTests { @Autowired diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/Details.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/Details.kt deleted file mode 100644 index 29278b49d758..000000000000 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/Details.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttemplate - -class Details - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/MyService.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/MyService.kt deleted file mode 100644 index 3778c5ed5c57..000000000000 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/MyService.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttemplate - -import org.springframework.boot.restclient.RestTemplateBuilder -import org.springframework.stereotype.Service -import org.springframework.web.client.RestTemplate - -@Service -class MyService(restTemplateBuilder: RestTemplateBuilder) { - - private val restTemplate: RestTemplate - - init { - restTemplate = restTemplateBuilder.build() - } - - fun someRestCall(name: String): Details { - return restTemplate.getForObject("/{name}/details", Details::class.java, name)!! - } - -} - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateBuilderConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateBuilderConfiguration.kt deleted file mode 100644 index 57bfd52ebd68..000000000000 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateBuilderConfiguration.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttemplate.customization - -import org.springframework.boot.restclient.autoconfigure.RestTemplateBuilderConfigurer -import org.springframework.boot.restclient.RestTemplateBuilder -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import java.time.Duration - -@Configuration(proxyBeanMethods = false) -class MyRestTemplateBuilderConfiguration { - - @Bean - fun restTemplateBuilder(configurer: RestTemplateBuilderConfigurer): RestTemplateBuilder { - return configurer.configure(RestTemplateBuilder()).connectTimeout(Duration.ofSeconds(5)) - .readTimeout(Duration.ofSeconds(2)) - } - -} - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateCustomizer.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateCustomizer.kt deleted file mode 100644 index 4bdc28fae721..000000000000 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/customization/MyRestTemplateCustomizer.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttemplate.customization - -import org.apache.hc.client5.http.classic.HttpClient -import org.apache.hc.client5.http.impl.classic.HttpClientBuilder -import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner -import org.apache.hc.client5.http.routing.HttpRoutePlanner -import org.apache.hc.core5.http.HttpException -import org.apache.hc.core5.http.HttpHost -import org.apache.hc.core5.http.protocol.HttpContext -import org.springframework.boot.restclient.RestTemplateCustomizer -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory -import org.springframework.web.client.RestTemplate - -class MyRestTemplateCustomizer : RestTemplateCustomizer { - - override fun customize(restTemplate: RestTemplate) { - val routePlanner: HttpRoutePlanner = CustomRoutePlanner(HttpHost("proxy.example.com")) - val httpClient: HttpClient = HttpClientBuilder.create().setRoutePlanner(routePlanner).build() - restTemplate.requestFactory = HttpComponentsClientHttpRequestFactory(httpClient) - } - - internal class CustomRoutePlanner(proxy: HttpHost?) : DefaultProxyRoutePlanner(proxy) { - - @Throws(HttpException::class) - public override fun determineProxy(target: HttpHost, context: HttpContext): HttpHost? { - if (target.hostName == "192.168.0.5") { - return null - } - return super.determineProxy(target, context) - } - - } - -} - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/ssl/MyService.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/ssl/MyService.kt deleted file mode 100644 index cbf24622815e..000000000000 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/resttemplate/ssl/MyService.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttemplate.ssl - -import org.springframework.boot.docs.io.restclient.resttemplate.Details -import org.springframework.boot.ssl.SslBundles -import org.springframework.boot.restclient.RestTemplateBuilder -import org.springframework.stereotype.Service -import org.springframework.web.client.RestTemplate - -@Service -class MyService(restTemplateBuilder: RestTemplateBuilder, sslBundles: SslBundles) { - - private val restTemplate: RestTemplate - - init { - restTemplate = restTemplateBuilder.sslBundle(sslBundles.getBundle("mybundle")).build() - } - - fun someRestCall(name: String): Details { - return restTemplate.getForObject("/{name}/details", Details::class.java, name)!! - } - -} - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.kt index ed212753beee..a873041114bf 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION") + package org.springframework.boot.docs.testing.utilities.testresttemplate import org.assertj.core.api.Assertions.assertThat diff --git a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestReactiveWebEnvironmentUserDefinedTestRestTemplateTests.java b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestReactiveWebEnvironmentUserDefinedTestRestTemplateTests.java index 4372bb33ee36..30b0ee966f10 100644 --- a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestReactiveWebEnvironmentUserDefinedTestRestTemplateTests.java +++ b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestReactiveWebEnvironmentUserDefinedTestRestTemplateTests.java @@ -34,10 +34,12 @@ * with a user-defined {@link RestTemplate} that is named {@code testRestTemplate}. * * @author Madhura Bhave + * @deprecated since 4.2.0 for removal in 4.4.0 */ @DirtiesContext @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.main.web-application-type=reactive", "value=123" }) +@Deprecated(since = "4.2.0", forRemoval = true) class SpringBootTestReactiveWebEnvironmentUserDefinedTestRestTemplateTests extends AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests { diff --git a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestUserDefinedTestRestTemplateTests.java b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestUserDefinedTestRestTemplateTests.java index 82c46ac7927f..337084689415 100644 --- a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestUserDefinedTestRestTemplateTests.java +++ b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestUserDefinedTestRestTemplateTests.java @@ -35,9 +35,11 @@ * * @author Phillip Webb * @author Andy Wilkinson + * @deprecated since 4.2.0 for removal in 4.4.0 */ @DirtiesContext @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "value=123" }) +@Deprecated(since = "4.2.0", forRemoval = true) class SpringBootTestUserDefinedTestRestTemplateTests extends AbstractSpringBootTestWebServerWebEnvironmentTests { @Test diff --git a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestWebEnvironmentRandomPortTests.java b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestWebEnvironmentRandomPortTests.java index a6cf4b62bc99..ceabe3645742 100644 --- a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestWebEnvironmentRandomPortTests.java +++ b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestWebEnvironmentRandomPortTests.java @@ -39,6 +39,7 @@ */ @DirtiesContext @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "value=123" }) +@SuppressWarnings("removal") class SpringBootTestWebEnvironmentRandomPortTests extends AbstractSpringBootTestWebServerWebEnvironmentTests { @Test diff --git a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryHealthEndpointWebExtensionTests.java b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryHealthEndpointWebExtensionTests.java index 4f5c1ce23686..f56569cc072b 100644 --- a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryHealthEndpointWebExtensionTests.java +++ b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryHealthEndpointWebExtensionTests.java @@ -36,7 +36,6 @@ import org.springframework.boot.health.contributor.HealthIndicator; import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; -import org.springframework.boot.restclient.autoconfigure.RestTemplateAutoConfiguration; import org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration; import org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration; import org.springframework.boot.servlet.autoconfigure.actuate.web.ServletManagementContextAutoConfiguration; @@ -55,15 +54,14 @@ class CloudFoundryHealthEndpointWebExtensionTests { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withPropertyValues("VCAP_APPLICATION={}") - .withConfiguration( - AutoConfigurations.of(SecurityAutoConfiguration.class, ServletWebSecurityAutoConfiguration.class, - WebMvcAutoConfiguration.class, JacksonAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, RestTemplateAutoConfiguration.class, - ManagementContextAutoConfiguration.class, ServletManagementContextAutoConfiguration.class, - EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, - HealthContributorAutoConfiguration.class, HealthContributorRegistryAutoConfiguration.class, - HealthEndpointAutoConfiguration.class, CloudFoundryActuatorAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(SecurityAutoConfiguration.class, + ServletWebSecurityAutoConfiguration.class, WebMvcAutoConfiguration.class, + JacksonAutoConfiguration.class, DispatcherServletAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class, + ManagementContextAutoConfiguration.class, ServletManagementContextAutoConfiguration.class, + EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, + HealthContributorAutoConfiguration.class, HealthContributorRegistryAutoConfiguration.class, + HealthEndpointAutoConfiguration.class, CloudFoundryActuatorAutoConfiguration.class)) .withUserConfiguration(TestHealthIndicator.class); @Test diff --git a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryInfoEndpointWebExtensionTests.java b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryInfoEndpointWebExtensionTests.java index 48c287a8cd74..3d15e89ee0e5 100644 --- a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryInfoEndpointWebExtensionTests.java +++ b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryInfoEndpointWebExtensionTests.java @@ -31,7 +31,6 @@ import org.springframework.boot.health.autoconfigure.actuate.endpoint.HealthEndpointAutoConfiguration; import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; -import org.springframework.boot.restclient.autoconfigure.RestTemplateAutoConfiguration; import org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration; import org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration; import org.springframework.boot.servlet.autoconfigure.actuate.web.ServletManagementContextAutoConfiguration; @@ -55,9 +54,8 @@ class CloudFoundryInfoEndpointWebExtensionTests { ServletWebSecurityAutoConfiguration.class, WebMvcAutoConfiguration.class, JacksonAutoConfiguration.class, DispatcherServletAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class, - RestTemplateAutoConfiguration.class, ManagementContextAutoConfiguration.class, - ServletManagementContextAutoConfiguration.class, EndpointAutoConfiguration.class, - WebEndpointAutoConfiguration.class, ProjectInfoAutoConfiguration.class, + ManagementContextAutoConfiguration.class, ServletManagementContextAutoConfiguration.class, + EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, ProjectInfoAutoConfiguration.class, InfoContributorAutoConfiguration.class, InfoEndpointAutoConfiguration.class, HealthEndpointAutoConfiguration.class, CloudFoundryActuatorAutoConfiguration.class)); diff --git a/module/spring-boot-devtools/src/intTest/java/org/springframework/boot/devtools/tests/DevToolsIntegrationTests.java b/module/spring-boot-devtools/src/intTest/java/org/springframework/boot/devtools/tests/DevToolsIntegrationTests.java index f6752a3780cc..5d897df5fbb8 100644 --- a/module/spring-boot-devtools/src/intTest/java/org/springframework/boot/devtools/tests/DevToolsIntegrationTests.java +++ b/module/spring-boot-devtools/src/intTest/java/org/springframework/boot/devtools/tests/DevToolsIntegrationTests.java @@ -37,6 +37,7 @@ * * @author Andy Wilkinson */ +@SuppressWarnings("removal") class DevToolsIntegrationTests extends AbstractDevToolsIntegrationTests { private final TestRestTemplate template = new TestRestTemplate(new RestTemplateBuilder() diff --git a/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/MockServerRestTemplateCustomizer.java b/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/MockServerRestTemplateCustomizer.java index 29c282e35d66..f4383c04106f 100644 --- a/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/MockServerRestTemplateCustomizer.java +++ b/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/MockServerRestTemplateCustomizer.java @@ -61,7 +61,11 @@ * @since 4.0.0 * @see #getServer() * @see #getServer(RestTemplate) + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of + * {@link MockServerRestClientCustomizer} */ +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") public class MockServerRestTemplateCustomizer implements RestTemplateCustomizer { private final Map expectationManagers = new ConcurrentHashMap<>(); diff --git a/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManager.java b/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManager.java index df22482633df..8e440a26f3b1 100644 --- a/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManager.java +++ b/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManager.java @@ -53,7 +53,10 @@ * @see DefaultUriBuilderFactory * @see #bindTo(RestTemplate) * @see #forRestTemplate(RestTemplate, RequestExpectationManager) + * @deprecated since 4.2.0 for removal in 4.4.0 with no replacement */ +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") public class RootUriRequestExpectationManager implements RequestExpectationManager { private final String rootUri; diff --git a/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfiguration.java b/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfiguration.java index b40ad6546bb8..7a1da8543fb2 100644 --- a/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfiguration.java +++ b/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfiguration.java @@ -51,10 +51,12 @@ @AutoConfiguration @ConditionalOnClass(MockServerRestTemplateCustomizer.class) @ConditionalOnBooleanProperty("spring.test.restclient.mockrestserviceserver.enabled") +@SuppressWarnings("removal") public final class MockRestServiceServerAutoConfiguration { @Bean @ConditionalOnMissingBean + @Deprecated(since = "4.2.0", forRemoval = true) MockServerRestTemplateCustomizer mockServerRestTemplateCustomizer() { return new MockServerRestTemplateCustomizer(); } diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/MockServerRestTemplateCustomizerTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/MockServerRestTemplateCustomizerTests.java index fe380c19fd26..80c9e1d8cc12 100644 --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/MockServerRestTemplateCustomizerTests.java +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/MockServerRestTemplateCustomizerTests.java @@ -40,7 +40,10 @@ * * @author Phillip Webb * @author Moritz Halbritter + * @deprecated since 4.2.0 for removal in 4.4.0 */ +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class MockServerRestTemplateCustomizerTests { private MockServerRestTemplateCustomizer customizer; diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManagerTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManagerTests.java index 7e912d342c15..be16e13fe8d7 100644 --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManagerTests.java +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/RootUriRequestExpectationManagerTests.java @@ -48,8 +48,11 @@ * Tests for {@link RootUriRequestExpectationManager}. * * @author Phillip Webb + * @deprecated since 4.2.0 for removal in 4.4.0 */ @ExtendWith(MockitoExtension.class) +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class RootUriRequestExpectationManagerTests { private final String uri = "https://example.com"; diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AnotherExampleRestTemplateService.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AnotherExampleRestTemplateService.java index 7402effc3fd2..5a3614cd5784 100644 --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AnotherExampleRestTemplateService.java +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AnotherExampleRestTemplateService.java @@ -26,8 +26,11 @@ * A second example web client used with {@link RestClientTest @RestClientTest} tests. * * @author Phillip Webb + * @deprecated since 4.2.0 for removal in 4.4.0 */ @Service +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") public class AnotherExampleRestTemplateService { private final RestTemplate restTemplate; diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureMockRestServiceServerEnabledFalseIntegrationTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureMockRestServiceServerEnabledFalseIntegrationTests.java index 27c5045203cc..bc2a06e6f514 100644 --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureMockRestServiceServerEnabledFalseIntegrationTests.java +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureMockRestServiceServerEnabledFalseIntegrationTests.java @@ -41,6 +41,7 @@ class AutoConfigureMockRestServiceServerEnabledFalseIntegrationTests { private ApplicationContext applicationContext; @Test + @SuppressWarnings("removal") void mockServerRestTemplateCustomizerShouldNotBeRegistered() { assertThatExceptionOfType(NoSuchBeanDefinitionException.class) .isThrownBy(() -> this.applicationContext.getBean(MockServerRestTemplateCustomizer.class)); diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureMockRestServiceServerWithRestTemplateRootUriIntegrationTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureMockRestServiceServerWithRestTemplateRootUriIntegrationTests.java index ec9de9d2fea1..90dda87efdc2 100644 --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureMockRestServiceServerWithRestTemplateRootUriIntegrationTests.java +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureMockRestServiceServerWithRestTemplateRootUriIntegrationTests.java @@ -40,9 +40,12 @@ * {@link RestTemplate} configured with a root URI. * * @author Andy Wilkinson + * @deprecated since 4.2.0 for removal in 4.4.0 */ @SpringBootTest @AutoConfigureMockRestServiceServer +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class AutoConfigureMockRestServiceServerWithRestTemplateRootUriIntegrationTests { @Autowired diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/ExampleRestTemplateService.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/ExampleRestTemplateService.java index 5acc9d7b83dd..75b505f92bfd 100644 --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/ExampleRestTemplateService.java +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/ExampleRestTemplateService.java @@ -27,8 +27,11 @@ * {@link RestClientTest @RestClientTest} tests. * * @author Phillip Webb + * @deprecated since 4.2.0 for removal in 4.4.0 */ @Service +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") public class ExampleRestTemplateService { private final RestTemplate restTemplate; diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfigurationTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfigurationTests.java index 87f4b52356f2..6e77660d8ea4 100644 --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfigurationTests.java +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/MockRestServiceServerAutoConfigurationTests.java @@ -46,6 +46,8 @@ void registersMockServerRestClientCustomizer() { } @Test + @Deprecated(since = "4.2.0", forRemoval = true) + @SuppressWarnings("removal") void registersMockServerRestTemplateCustomizer() { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(MockServerRestTemplateCustomizer.class)); } @@ -63,6 +65,8 @@ void backsOffWhenUserProvidesMockServerRestClientCustomizer() { } @Test + @Deprecated(since = "4.2.0", forRemoval = true) + @SuppressWarnings("removal") void backsOffWhenUserProvidesMockServerRestTemplateCustomizer() { this.contextRunner.withBean("userMockServerRestTemplateCustomizer", MockServerRestTemplateCustomizer.class) .run((context) -> assertThat(context).hasSingleBean(MockServerRestTemplateCustomizer.class) diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestNoComponentIntegrationTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestNoComponentIntegrationTests.java index 724c08b85d7a..dbe9b32f2fe5 100644 --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestNoComponentIntegrationTests.java +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestNoComponentIntegrationTests.java @@ -34,8 +34,11 @@ * Tests for {@link RestClientTest @RestClientTest} with no specific client. * * @author Phillip Webb + * @deprecated since 4.2.0 for removal in 4.4.0 */ @RestClientTest +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class RestClientTestNoComponentIntegrationTests { @Autowired diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestRestTemplateAndRestClientTogetherIntegrationTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestRestTemplateAndRestClientTogetherIntegrationTests.java index a54a73b1bfe9..e5444c0dd421 100644 --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestRestTemplateAndRestClientTogetherIntegrationTests.java +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestRestTemplateAndRestClientTogetherIntegrationTests.java @@ -36,6 +36,7 @@ * @author Scott Frederick */ @RestClientTest({ ExampleRestTemplateService.class, ExampleRestClientService.class }) +@SuppressWarnings("removal") class RestClientTestRestTemplateAndRestClientTogetherIntegrationTests { @Autowired diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestRestTemplateIntegrationTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestRestTemplateIntegrationTests.java index 1a178c2ab324..ed9535758a21 100644 --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestRestTemplateIntegrationTests.java +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestRestTemplateIntegrationTests.java @@ -31,8 +31,11 @@ * Tests for {@link RestClientTest @RestClientTest} gets reset after test methods. * * @author Phillip Webb + * @deprecated since 4.2.0 for removal in 4.4.0 */ @RestClientTest(ExampleRestTemplateService.class) +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class RestClientTestRestTemplateIntegrationTests { @Autowired diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestRestTemplateTwoComponentsIntegrationTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestRestTemplateTwoComponentsIntegrationTests.java index 90543c700541..9f6fcb1d1cea 100644 --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestRestTemplateTwoComponentsIntegrationTests.java +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestRestTemplateTwoComponentsIntegrationTests.java @@ -32,8 +32,11 @@ * Tests for {@link RestClientTest @RestClientTest} with two {@code RestTemplate} clients. * * @author Phillip Webb + * @deprecated since 4.2.0 for removal in 4.4.0 */ @RestClientTest({ ExampleRestTemplateService.class, AnotherExampleRestTemplateService.class }) +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class RestClientTestRestTemplateTwoComponentsIntegrationTests { @Autowired diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestWithRestTemplateComponentIntegrationTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestWithRestTemplateComponentIntegrationTests.java index 7d052a0ffe49..471fafb33998 100644 --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestWithRestTemplateComponentIntegrationTests.java +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestWithRestTemplateComponentIntegrationTests.java @@ -31,8 +31,11 @@ * {@code RestTemplate}. * * @author Phillip Webb + * @deprecated since 4.2.0 for removal in 4.4.0 */ @RestClientTest(ExampleRestTemplateService.class) +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class RestClientTestWithRestTemplateComponentIntegrationTests { @Autowired diff --git a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestWithoutJacksonIntegrationTests.java b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestWithoutJacksonIntegrationTests.java index b5190fe6caaa..47898f32ff52 100644 --- a/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestWithoutJacksonIntegrationTests.java +++ b/module/spring-boot-restclient-test/src/test/java/org/springframework/boot/restclient/test/autoconfigure/RestClientTestWithoutJacksonIntegrationTests.java @@ -34,21 +34,21 @@ * @author Andy Wilkinson */ @ClassPathExclusions("jackson-*.jar") -@RestClientTest(ExampleRestTemplateService.class) +@RestClientTest(ExampleRestClientService.class) class RestClientTestWithoutJacksonIntegrationTests { @Autowired private MockRestServiceServer server; @Autowired - private ExampleRestTemplateService client; + private ExampleRestClientService client; @Test void restClientTestCanBeUsedWhenJacksonIsNotOnTheClassPath() { ClassLoader classLoader = getClass().getClassLoader(); assertThat(ClassUtils.isPresent("com.fasterxml.jackson.databind.Module", classLoader)).isFalse(); assertThat(ClassUtils.isPresent("tools.jackson.databind.JacksonModule", classLoader)).isFalse(); - this.server.expect(requestTo("/test")).andRespond(withSuccess("hello", MediaType.TEXT_HTML)); + this.server.expect(requestTo("https://example.com/test")).andRespond(withSuccess("hello", MediaType.TEXT_HTML)); assertThat(this.client.test()).isEqualTo("hello"); } diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java index 27abdf5450a3..7f14fa850392 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java @@ -45,6 +45,7 @@ import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.UriTemplateHandler; @@ -71,7 +72,10 @@ * @author Scott Frederick * @author Yanming Zhou * @since 4.0.0 + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of {@link RestClient.Builder} */ +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") public class RestTemplateBuilder { private final HttpClientSettings clientSettings; diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilderClientHttpRequestInitializer.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilderClientHttpRequestInitializer.java index 1aad218d55ef..914ccc00cc35 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilderClientHttpRequestInitializer.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilderClientHttpRequestInitializer.java @@ -33,7 +33,10 @@ * * @author Dmytro Nosan * @author Ilya Lukyanovich + * @deprecated since 4.2.0 for removal in 4.4.0 with no replacement */ +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class RestTemplateBuilderClientHttpRequestInitializer implements ClientHttpRequestInitializer { private final @Nullable BasicAuthentication basicAuthentication; diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateCustomizer.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateCustomizer.java index e0d0c81f0563..f437b9cff3ef 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateCustomizer.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateCustomizer.java @@ -24,8 +24,11 @@ * @author Phillip Webb * @since 4.0.0 * @see RestTemplateBuilder + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of {@link RestClientCustomizer} */ @FunctionalInterface +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") public interface RestTemplateCustomizer { /** diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateRequestCustomizer.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateRequestCustomizer.java index 03ecdf684382..32e2388c8709 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateRequestCustomizer.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateRequestCustomizer.java @@ -30,8 +30,10 @@ * @since 4.0.0 * @see RestTemplateBuilder * @see ClientHttpRequestInitializer + * @deprecated since 4.2.0 for removal in 4.4.0 with no replacement */ @FunctionalInterface +@Deprecated(since = "4.2.0", forRemoval = true) public interface RestTemplateRequestCustomizer { /** diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfiguration.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfiguration.java index 9161d378ab5e..d6910f6fe106 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfiguration.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfiguration.java @@ -41,10 +41,14 @@ * @author Stephane Nicoll * @author Phillip Webb * @since 4.0.0 + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of + * {@link RestClientAutoConfiguration} */ @AutoConfiguration(after = HttpClientAutoConfiguration.class) @ConditionalOnClass({ RestTemplate.class, HttpMessageConverters.class }) @Conditional(NotReactiveWebApplicationCondition.class) +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") public final class RestTemplateAutoConfiguration { @Bean diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateBuilderConfigurer.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateBuilderConfigurer.java index dc54da613821..6c670fff9910 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateBuilderConfigurer.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateBuilderConfigurer.java @@ -41,7 +41,11 @@ * * @author Stephane Nicoll * @since 4.0.0 + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of + * {@link RestClientBuilderConfigurer} */ +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") public final class RestTemplateBuilderConfigurer { private @Nullable ClientHttpRequestFactoryBuilder requestFactoryBuilder; diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateObservationAutoConfiguration.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateObservationAutoConfiguration.java index fa2bff1946c1..00e0c5471616 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateObservationAutoConfiguration.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateObservationAutoConfiguration.java @@ -36,12 +36,16 @@ * * @author Brian Clozel * @since 4.0.0 + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of + * {@link RestClientObservationAutoConfiguration} */ @AutoConfiguration @ConditionalOnClass({ RestTemplate.class, ObservationRestTemplateCustomizer.class, ObservationRegistry.class, ObservationProperties.class }) @ConditionalOnBean({ ObservationRegistry.class, RestTemplateBuilder.class }) @EnableConfigurationProperties(ObservationProperties.class) +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") public final class RestTemplateObservationAutoConfiguration { @Bean diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/observation/ObservationRestTemplateCustomizer.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/observation/ObservationRestTemplateCustomizer.java index a7338539da6b..96b68a6078b6 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/observation/ObservationRestTemplateCustomizer.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/observation/ObservationRestTemplateCustomizer.java @@ -28,7 +28,11 @@ * * @author Brian Clozel * @since 4.0.0 + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of + * {@link ObservationRestClientCustomizer} */ +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") public class ObservationRestTemplateCustomizer implements RestTemplateCustomizer { private final ObservationRegistry observationRegistry; diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestClientWithRestTemplateBuilderTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestClientWithRestTemplateBuilderTests.java index 3a6dcb313d67..8544a3f2816a 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestClientWithRestTemplateBuilderTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestClientWithRestTemplateBuilderTests.java @@ -31,7 +31,10 @@ * Tests for building a {@link RestClient} from a {@link RestTemplateBuilder}. * * @author Scott Frederick + * @deprecated since 4.2.0 for removal in 4.4.0 */ +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class RestClientWithRestTemplateBuilderTests { @Test diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestClientWithRestTemplateTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestClientWithRestTemplateTests.java index 34a022c6a741..c5f7d2ee799a 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestClientWithRestTemplateTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestClientWithRestTemplateTests.java @@ -32,7 +32,10 @@ * Tests for building a {@link RestClient} from a {@link RestTemplate}. * * @author Scott Frederick + * @deprecated since 4.2.0 for removal in 4.4.0 */ +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class RestClientWithRestTemplateTests { @Test diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestTemplateBuilderClientHttpRequestInitializerTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestTemplateBuilderClientHttpRequestInitializerTests.java index 8a829be0f2a0..8930b979da80 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestTemplateBuilderClientHttpRequestInitializerTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestTemplateBuilderClientHttpRequestInitializerTests.java @@ -41,7 +41,10 @@ * @author Dmytro Nosan * @author Ilya Lukyanovich * @author Phillip Webb + * @deprecated since 4.2.0 for removal in 4.4.0 */ +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class RestTemplateBuilderClientHttpRequestInitializerTests { private final MockClientHttpRequest request = new MockClientHttpRequest(); diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestTemplateBuilderTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestTemplateBuilderTests.java index e23285d1a8aa..e453df73f476 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestTemplateBuilderTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestTemplateBuilderTests.java @@ -80,8 +80,11 @@ * @author Ilya Lukyanovich * @author Brian Clozel * @author Yanming Zhou + * @deprecated since 4.2.0 for removal in 4.4.0 */ @ExtendWith(MockitoExtension.class) +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class RestTemplateBuilderTests { private final RestTemplateBuilder builder = new RestTemplateBuilder(); diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfigurationTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfigurationTests.java index c35411ab6948..d200b52b6dae 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfigurationTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfigurationTests.java @@ -59,7 +59,10 @@ * * @author Stephane Nicoll * @author Phillip Webb + * @deprecated since 4.2.0 for removal in 4.4.0 */ +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class RestTemplateAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateObservationAutoConfigurationTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateObservationAutoConfigurationTests.java index 4997d616399b..717b2d307d6d 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateObservationAutoConfigurationTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateObservationAutoConfigurationTests.java @@ -49,8 +49,11 @@ * Tests for {@link RestTemplateObservationAutoConfiguration}. * * @author Brian Clozel + * @deprecated since 4.2.0 for removal in 4.4.0 */ @ExtendWith(OutputCaptureExtension.class) +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class RestTemplateObservationAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateObservationAutoConfigurationWithoutMetricsTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateObservationAutoConfigurationWithoutMetricsTests.java index cf651483f045..f04100cc464d 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateObservationAutoConfigurationWithoutMetricsTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateObservationAutoConfigurationWithoutMetricsTests.java @@ -41,9 +41,12 @@ * * @author Brian Clozel * @author Andy Wilkinson + * @deprecated since 4.2.0 for removal in 4.4.0 */ @ExtendWith(OutputCaptureExtension.class) @ClassPathExclusions("micrometer-core-*.jar") +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class RestTemplateObservationAutoConfigurationWithoutMetricsTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/observation/ObservationRestTemplateCustomizerTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/observation/ObservationRestTemplateCustomizerTests.java index 6cfe5bc834c3..2bc8f39871e1 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/observation/ObservationRestTemplateCustomizerTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/observation/ObservationRestTemplateCustomizerTests.java @@ -29,7 +29,10 @@ * Tests for {@link ObservationRestTemplateCustomizer}. * * @author Brian Clozel + * @deprecated since 4.2.0 for removal in 4.4.0 */ +@Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class ObservationRestTemplateCustomizerTests { private static final String TEST_METRIC_NAME = "http.test.metric.name"; diff --git a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java index 52ebd28f4096..c2cde6ede093 100644 --- a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java +++ b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java @@ -91,6 +91,7 @@ * @author Yanming Zhou * @since 4.0.0 */ +@SuppressWarnings("removal") public class TestRestTemplate { private final RestTemplateBuilder builder; diff --git a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfiguration.java b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfiguration.java index 453b45314f0f..706a3f35ba74 100644 --- a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfiguration.java +++ b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfiguration.java @@ -34,6 +34,7 @@ * @see AutoConfigureTestRestTemplate */ @AutoConfiguration +@SuppressWarnings("removal") final class TestRestTemplateTestAutoConfiguration { @Bean(name = "org.springframework.boot.resttestclient.TestRestTemplate") diff --git a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java index b621b1e018f1..052055cb2ebe 100644 --- a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java +++ b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java @@ -81,6 +81,7 @@ * @author Kristine Jetzke * @author Yanming Zhou */ +@SuppressWarnings("removal") class TestRestTemplateTests { @Test diff --git a/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/CorsSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/CorsSampleActuatorApplicationTests.java index c32cb39606ef..456f7e2c592b 100644 --- a/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/CorsSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/CorsSampleActuatorApplicationTests.java @@ -44,6 +44,7 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("cors") @AutoConfigureTestRestTemplate +@SuppressWarnings("removal") class CorsSampleActuatorApplicationTests { private TestRestTemplate testRestTemplate; diff --git a/smoke-test/spring-boot-smoke-test-actuator-extension/src/test/java/smoketest/actuator/extension/SampleActuatorExtensionApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator-extension/src/test/java/smoketest/actuator/extension/SampleActuatorExtensionApplicationTests.java index 9f640e6ea04e..d6db28f024cb 100644 --- a/smoke-test/spring-boot-smoke-test-actuator-extension/src/test/java/smoketest/actuator/extension/SampleActuatorExtensionApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator-extension/src/test/java/smoketest/actuator/extension/SampleActuatorExtensionApplicationTests.java @@ -35,6 +35,7 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.web.error.include-message=always" }) @AutoConfigureTestRestTemplate +@SuppressWarnings("removal") class SampleActuatorExtensionApplicationTests { @Autowired diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/CorsSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/CorsSampleActuatorApplicationTests.java index c2a2fe31e3ba..4092432ca45f 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/CorsSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/CorsSampleActuatorApplicationTests.java @@ -44,6 +44,7 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("cors") @AutoConfigureTestRestTemplate +@SuppressWarnings("removal") class CorsSampleActuatorApplicationTests { private TestRestTemplate testRestTemplate; diff --git a/smoke-test/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/SampleTomcatApplicationTests.java b/smoke-test/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/SampleTomcatApplicationTests.java index a3ea2a1947d9..303bc1269a6c 100644 --- a/smoke-test/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/SampleTomcatApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/SampleTomcatApplicationTests.java @@ -62,6 +62,7 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @ExtendWith(OutputCaptureExtension.class) @AutoConfigureTestRestTemplate +@SuppressWarnings("removal") class SampleTomcatApplicationTests { @Autowired diff --git a/system-test/spring-boot-deployment-system-tests/src/systemTest/java/org/springframework/boot/deployment/AbstractDeploymentTests.java b/system-test/spring-boot-deployment-system-tests/src/systemTest/java/org/springframework/boot/deployment/AbstractDeploymentTests.java index 4d6677f388e3..aaf99a989559 100644 --- a/system-test/spring-boot-deployment-system-tests/src/systemTest/java/org/springframework/boot/deployment/AbstractDeploymentTests.java +++ b/system-test/spring-boot-deployment-system-tests/src/systemTest/java/org/springframework/boot/deployment/AbstractDeploymentTests.java @@ -43,6 +43,7 @@ /** * Abstract class for deployment tests. */ +@SuppressWarnings("removal") abstract class AbstractDeploymentTests { protected static final int DEFAULT_PORT = 8080; From 5ebd999dd68c4c910f359f3ccf567d856cd29de6 Mon Sep 17 00:00:00 2001 From: wantaek Date: Fri, 24 Jul 2026 16:38:20 +0900 Subject: [PATCH 786/930] Fix stale type reference for OTLP logging transport metadata See gh-51119 Signed-off-by: wantaek --- .../META-INF/additional-spring-configuration-metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/spring-boot-opentelemetry/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-opentelemetry/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 6d64271a83ac..771da43d8545 100644 --- a/module/spring-boot-opentelemetry/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-opentelemetry/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -76,7 +76,7 @@ }, { "name": "management.otlp.logging.transport", - "type": "org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.Transport", + "type": "org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.Transport", "description": "Transport used to send the logs.", "defaultValue": "http", "deprecation": { From 2e650ac4a711b586f21addce66a00095513aa1b8 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 27 Jul 2026 16:07:11 +0200 Subject: [PATCH 787/930] Add testing utility for RestTestClient This commit adds a new `TrustAllTlsRequestFactory` request factory that can be used in tests for connecting a `RestTestClient` to a TLS endpoint without checking the certificates validity. See gh-46632 --- .../TrustAllTlsRequestFactory.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TrustAllTlsRequestFactory.java diff --git a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TrustAllTlsRequestFactory.java b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TrustAllTlsRequestFactory.java new file mode 100644 index 000000000000..eaa7bf667f4c --- /dev/null +++ b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TrustAllTlsRequestFactory.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.resttestclient; + +import java.net.http.HttpClient; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.JdkClientHttpRequestFactory; +import org.springframework.test.web.servlet.client.RestTestClient; + +/** + * Testing utility that trusts all TLS connections during tests. This class should not be + * in your production classpath as {@code "spring-boot-resttestclient"} is a test + * dependency in the first place. + *

    + * This can be configured on a test client when binding to a live server using + * {@link RestTestClient#bindToServer(ClientHttpRequestFactory)}. + * + * @author Brian Clozel + * @since 4.2.0 + */ +public final class TrustAllTlsRequestFactory { + + private TrustAllTlsRequestFactory() { + + } + + public static ClientHttpRequestFactory create() { + try { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[] { new TrustAllX509TrustManager() }, new SecureRandom()); + HttpClient httpClient = HttpClient.newBuilder().sslContext(sslContext).build(); + return new JdkClientHttpRequestFactory(httpClient); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to create trust-all SSL request factory", ex); + } + } + + private static final class TrustAllX509TrustManager implements X509TrustManager { + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + } + + } + +} From 2f9933e3cb7532eda9f948d94354fecf09122d2e Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 27 Jul 2026 16:09:12 +0200 Subject: [PATCH 788/930] Replace TestRestTemplate with RestTestClient in smoke tests Before deprecating `TestRestTemplate`, we must first stop using it in smoke tests and replace it with: * `RestTestClient` when the Spring MVC infrastructure is present * `RestClient` when the test does not have Spring MVC on classpath See gh-46632 --- ...ractSampleActuatorCustomSecurityTests.java | 184 ++++++++------- .../CorsSampleActuatorApplicationTests.java | 58 ++--- ...asePathSampleActuatorApplicationTests.java | 18 +- ...asePathSampleActuatorApplicationTests.java | 18 +- ...hCustomServletPathSampleActuatorTests.java | 16 +- ...ctuatorCustomSecurityApplicationTests.java | 21 +- ...mpleActuatorExtensionApplicationTests.java | 43 +--- .../SampleActuatorUiApplicationPortTests.java | 48 ++-- .../ui/SampleActuatorUiApplicationTests.java | 60 +++-- ...AndPathSampleActuatorApplicationTests.java | 96 +++++--- .../CorsSampleActuatorApplicationTests.java | 58 ++--- ...pertiesSampleActuatorApplicationTests.java | 45 ++-- ...gementAddressActuatorApplicationTests.java | 35 +-- ...HandlerSampleActuatorApplicationTests.java | 20 +- ...entPortSampleActuatorApplicationTests.java | 17 +- ...entPathSampleActuatorApplicationTests.java | 39 ++-- ...entPortSampleActuatorApplicationTests.java | 80 ++++--- ...gementPortWithLazyInitializationTests.java | 17 +- ...redPortSampleActuatorApplicationTests.java | 17 +- ...agementSampleActuatorApplicationTests.java | 35 ++- ...plicationIsolatedJsonMapperFalseTests.java | 22 +- ...pplicationIsolatedJsonMapperTrueTests.java | 20 +- .../SampleActuatorApplicationTests.java | 215 ++++++++++-------- ...letPathSampleActuatorApplicationTests.java | 53 +++-- ...hutdownSampleActuatorApplicationTests.java | 40 ++-- ...leDevToolsApplicationIntegrationTests.java | 29 +-- .../SampleHateoasApplicationTests.java | 53 ++--- .../AbstractJerseyApplicationTests.java | 25 +- .../AbstractJerseyManagementPortTests.java | 28 ++- ...ctuatorIsolatedObjectMapperFalseTests.java | 18 +- ...ActuatorIsolatedObjectMapperTrueTests.java | 15 +- ...ApplicationPathAndManagementPortTests.java | 15 +- ...entPortSampleActuatorApplicationTests.java | 10 +- .../jsp/SampleWebJspApplicationTests.java | 18 +- .../build.gradle | 2 +- .../ssl/SampleJettySslApplicationTests.java | 19 +- .../jetty/SampleJettyApplicationTests.java | 40 ++-- ...h2AuthorizationServerApplicationTests.java | 113 ++++----- .../SampleOAuth2ClientApplicationTests.java | 43 ++-- ...eOauth2ResourceServerApplicationTests.java | 30 +-- .../SamplePrometheusApplicationTests.java | 24 +- .../SampleQuartzApplicationWebTests.java | 92 ++++---- ...mpleSaml2RelyingPartyApplicationTests.java | 39 ++-- .../jersey/AbstractJerseySecureTests.java | 88 ++++--- ...mentPortAndPathJerseyApplicationTests.java | 5 +- ...tPortCustomApplicationPathJerseyTests.java | 5 +- .../build.gradle | 1 - .../SampleServletApplicationTests.java | 37 +-- .../SampleSessionRedisApplicationTests.java | 48 ++-- .../SampleSessionJdbcApplicationTests.java | 56 +++-- ...pleTestApplicationWebIntegrationTests.java | 13 +- .../testng/SampleTestNGApplicationTests.java | 16 +- .../jsp/SampleWebJspApplicationTests.java | 18 +- .../build.gradle | 2 +- ...leTomcatTwoConnectorsApplicationTests.java | 26 +-- .../build.gradle | 2 +- .../ssl/SampleTomcatSslApplicationTests.java | 40 ++-- ...igurationSampleTomcatApplicationTests.java | 16 +- .../tomcat/SampleTomcatApplicationTests.java | 81 ++++--- .../SampleTraditionalApplicationTests.java | 28 +-- .../SampleWebFreeMarkerApplicationTests.java | 56 ++--- .../SampleGroovyTemplateApplicationTests.java | 37 +-- .../jsp/SampleWebJspApplicationTests.java | 37 ++- .../SampleMethodSecurityApplicationTests.java | 108 +++++---- .../SampleWebMustacheApplicationTests.java | 82 +++---- ...SampleWebSecureCustomApplicationTests.java | 65 +++--- .../SampleWebSecureJdbcApplicationTests.java | 65 +++--- .../web/secure/AbstractErrorPageTests.java | 94 ++++---- ...AbstractUnauthenticatedErrorPageTests.java | 121 ++++++---- .../SampleWebSecureApplicationTests.java | 65 +++--- .../SampleWebStaticApplicationTests.java | 30 +-- .../SampleWebUiApplicationTests.java | 26 ++- ...ApplicationActuatorDifferentPortTests.java | 19 +- 73 files changed, 1646 insertions(+), 1529 deletions(-) diff --git a/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/AbstractSampleActuatorCustomSecurityTests.java b/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/AbstractSampleActuatorCustomSecurityTests.java index 6d66018bcb63..aa3a2fecf696 100644 --- a/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/AbstractSampleActuatorCustomSecurityTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/AbstractSampleActuatorCustomSecurityTests.java @@ -16,15 +16,12 @@ package smoketest.actuator.customsecurity; -import java.util.Map; - import org.junit.jupiter.api.Test; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.http.server.LocalTestWebServer; import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -43,156 +40,177 @@ abstract class AbstractSampleActuatorCustomSecurityTests { @Test void homeIsSecure() { - @SuppressWarnings("rawtypes") - ResponseEntity entity = restTemplate().getForEntity(getPath() + "/", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - assertThat(entity.getHeaders().headerNames()).doesNotContain("Set-Cookie"); + restTestClient().get() + .uri(getPath() + "/") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isUnauthorized() + .expectHeader() + .doesNotExist("Set-Cookie"); } @Test void testInsecureStaticResources() { - ResponseEntity entity = restTemplate().getForEntity(getPath() + "/css/bootstrap.min.css", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("body"); + restTestClient().get() + .uri(getPath() + "/css/bootstrap.min.css") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("body")); } @Test void actuatorInsecureEndpoint() { - ResponseEntity entity = restTemplate().getForEntity(getActuatorPath() + "/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"status\":\"UP\""); - entity = restTemplate().getForEntity(getActuatorPath() + "/health/diskSpace", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"status\":\"UP\""); + RestTestClient restTestClient = restTestClient(); + restTestClient.get() + .uri(getActuatorPath() + "/health") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"status\":\"UP\"")); + restTestClient.get() + .uri(getActuatorPath() + "/health/diskSpace") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"status\":\"UP\"")); } @Test void actuatorLinksWithAnonymous() { - ResponseEntity entity = restTemplate().getForEntity(getActuatorPath(), Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - entity = restTemplate().getForEntity(getActuatorPath() + "/", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + RestTestClient restTestClient = restTestClient(); + restTestClient.get().uri(getActuatorPath()).exchange().expectStatus().isUnauthorized(); + restTestClient.get().uri(getActuatorPath() + "/").exchange().expectStatus().isUnauthorized(); } @Test void actuatorLinksWithUnauthorizedUser() { - ResponseEntity entity = userRestTemplate().getForEntity(getActuatorPath(), Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - entity = userRestTemplate().getForEntity(getActuatorPath() + "/", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + RestTestClient restTestClient = userRestTestClient(); + restTestClient.get().uri(getActuatorPath()).exchange().expectStatus().isForbidden(); + restTestClient.get().uri(getActuatorPath() + "/").exchange().expectStatus().isForbidden(); } @Test void actuatorLinksWithAuthorizedUser() { - ResponseEntity entity = adminRestTemplate().getForEntity(getActuatorPath(), Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - adminRestTemplate().getForEntity(getActuatorPath() + "/", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + RestTestClient restTestClient = adminRestTestClient(); + restTestClient.get().uri(getActuatorPath()).accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk(); } @Test void actuatorSecureEndpointWithAnonymous() { - ResponseEntity entity = restTemplate().getForEntity(getActuatorPath() + "/env", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - entity = restTemplate().getForEntity(getActuatorPath() + "/env/management.endpoints.web.exposure.include", - Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + RestTestClient restTestClient = restTestClient(); + restTestClient.get().uri(getActuatorPath() + "/env").exchange().expectStatus().isUnauthorized(); + restTestClient.get() + .uri(getActuatorPath() + "/env/management.endpoints.web.exposure.include") + .exchange() + .expectStatus() + .isUnauthorized(); } @Test void actuatorSecureEndpointWithUnauthorizedUser() { - ResponseEntity entity = userRestTemplate().getForEntity(getActuatorPath() + "/env", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - entity = userRestTemplate().getForEntity(getActuatorPath() + "/env/management.endpoints.web.exposure.include", - Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + RestTestClient restTestClient = userRestTestClient(); + restTestClient.get().uri(getActuatorPath() + "/env").exchange().expectStatus().isForbidden(); + restTestClient.get() + .uri(getActuatorPath() + "/env/management.endpoints.web.exposure.include") + .exchange() + .expectStatus() + .isForbidden(); } @Test void actuatorSecureEndpointWithAuthorizedUser() { - ResponseEntity entity = adminRestTemplate().getForEntity(getActuatorPath() + "/env", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - entity = adminRestTemplate().getForEntity(getActuatorPath() + "/env/", Object.class); + RestTestClient restTestClient = adminRestTestClient(); + restTestClient.get().uri(getActuatorPath() + "/env").exchange().expectStatus().isOk(); // EndpointRequest matches the trailing slash but MVC doesn't - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - entity = adminRestTemplate().getForEntity(getActuatorPath() + "/env/management.endpoints.web.exposure.include", - Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + restTestClient.get().uri(getActuatorPath() + "/env/").exchange().expectStatus().isNotFound(); + restTestClient.get() + .uri(getActuatorPath() + "/env/management.endpoints.web.exposure.include") + .exchange() + .expectStatus() + .isOk(); } @Test void secureServletEndpointWithAnonymous() { - ResponseEntity entity = restTemplate().getForEntity(getActuatorPath() + "/se1", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - entity = restTemplate().getForEntity(getActuatorPath() + "/se1/list", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + RestTestClient restTestClient = restTestClient(); + restTestClient.get().uri(getActuatorPath() + "/se1").exchange().expectStatus().isUnauthorized(); + restTestClient.get().uri(getActuatorPath() + "/se1/list").exchange().expectStatus().isUnauthorized(); } @Test void secureServletEndpointWithUnauthorizedUser() { - ResponseEntity entity = userRestTemplate().getForEntity(getActuatorPath() + "/se1", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - entity = userRestTemplate().getForEntity(getActuatorPath() + "/se1/list", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + RestTestClient restTestClient = userRestTestClient(); + restTestClient.get().uri(getActuatorPath() + "/se1").exchange().expectStatus().isForbidden(); + restTestClient.get().uri(getActuatorPath() + "/se1/list").exchange().expectStatus().isForbidden(); } @Test void secureServletEndpointWithAuthorizedUser() { - ResponseEntity entity = adminRestTemplate().getForEntity(getActuatorPath() + "/se1", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - entity = adminRestTemplate().getForEntity(getActuatorPath() + "/se1/list", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + RestTestClient restTestClient = adminRestTestClient(); + restTestClient.get().uri(getActuatorPath() + "/se1").exchange().expectStatus().isOk(); + restTestClient.get().uri(getActuatorPath() + "/se1/list").exchange().expectStatus().isOk(); } @Test void actuatorCustomMvcSecureEndpointWithAnonymous() { - ResponseEntity entity = restTemplate().getForEntity(getActuatorPath() + "/example/echo?text={t}", - String.class, "test"); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + restTestClient().get() + .uri(getActuatorPath() + "/example/echo?text={t}", "test") + .exchange() + .expectStatus() + .isUnauthorized(); } @Test void actuatorCustomMvcSecureEndpointWithUnauthorizedUser() { - ResponseEntity entity = userRestTemplate().getForEntity(getActuatorPath() + "/example/echo?text={t}", - String.class, "test"); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + userRestTestClient().get() + .uri(getActuatorPath() + "/example/echo?text={t}", "test") + .exchange() + .expectStatus() + .isForbidden(); } @Test void actuatorCustomMvcSecureEndpointWithAuthorizedUser() { - ResponseEntity entity = adminRestTemplate().getForEntity(getActuatorPath() + "/example/echo?text={t}", - String.class, "test"); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("test"); - assertThat(entity.getHeaders().getFirst("echo")).isEqualTo("test"); + adminRestTestClient().get() + .uri(getActuatorPath() + "/example/echo?text={t}", "test") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .valueEquals("echo", "test") + .expectBody(String.class) + .isEqualTo("test"); } @Test void actuatorExcludedFromEndpointRequestMatcher() { - ResponseEntity entity = userRestTemplate().getForEntity(getActuatorPath() + "/mappings", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + userRestTestClient().get().uri(getActuatorPath() + "/mappings").exchange().expectStatus().isOk(); } - TestRestTemplate restTemplate() { - return configure(new TestRestTemplate()); + RestTestClient restTestClient() { + return configure(RestTestClient.bindToServer()).build(); } - TestRestTemplate adminRestTemplate() { - return configure(new TestRestTemplate("admin", "admin")); + RestTestClient adminRestTestClient() { + return configure( + RestTestClient.bindToServer().defaultHeaders((headers) -> headers.setBasicAuth("admin", "admin"))) + .build(); } - TestRestTemplate userRestTemplate() { - return configure(new TestRestTemplate("user", "password")); + RestTestClient userRestTestClient() { + return configure( + RestTestClient.bindToServer().defaultHeaders((headers) -> headers.setBasicAuth("user", "password"))) + .build(); } - TestRestTemplate beansRestTemplate() { - return configure(new TestRestTemplate("beans", "beans")); + RestTestClient beansRestTestClient() { + return configure( + RestTestClient.bindToServer().defaultHeaders((headers) -> headers.setBasicAuth("beans", "beans"))) + .build(); } - private TestRestTemplate configure(TestRestTemplate restTemplate) { + RestTestClient.Builder configure(RestTestClient.Builder builder) { LocalTestWebServer localTestWebServer = LocalTestWebServer.obtain(getApplicationContext()); - restTemplate.setUriTemplateHandler(localTestWebServer.uriBuilderFactory()); - return restTemplate; + return builder.baseUrl(localTestWebServer.uri()); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/CorsSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/CorsSampleActuatorApplicationTests.java index 456f7e2c592b..a860aa553884 100644 --- a/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/CorsSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/CorsSampleActuatorApplicationTests.java @@ -16,25 +16,13 @@ package smoketest.actuator.customsecurity; -import java.net.URI; -import java.util.Map; - -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.restclient.RestTemplateBuilder; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.http.server.LocalTestWebServer; -import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpStatus; -import org.springframework.http.RequestEntity; -import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; - -import static org.assertj.core.api.Assertions.assertThat; +import org.springframework.test.web.servlet.client.RestTestClient; /** * Integration test for cors preflight requests to management endpoints. @@ -43,47 +31,31 @@ */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("cors") -@AutoConfigureTestRestTemplate -@SuppressWarnings("removal") +@AutoConfigureRestTestClient class CorsSampleActuatorApplicationTests { - private TestRestTemplate testRestTemplate; - @Autowired - private ApplicationContext applicationContext; - - @BeforeEach - void setUp() { - RestTemplateBuilder builder = new RestTemplateBuilder(); - LocalTestWebServer localTestWebServer = LocalTestWebServer.obtain(this.applicationContext); - builder = builder.uriTemplateHandler(localTestWebServer.uriBuilderFactory()); - this.testRestTemplate = new TestRestTemplate(builder); - } + private RestTestClient restTestClient; @Test void endpointShouldReturnUnauthorized() { - ResponseEntity entity = this.testRestTemplate.getForEntity("/actuator/env", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + this.restTestClient.get().uri("/actuator/env").exchange().expectStatus().isUnauthorized(); } @Test - void preflightRequestToEndpointShouldReturnOk() throws Exception { - RequestEntity envRequest = RequestEntity.options(new URI("/actuator/env")) - .header("Origin", "http://localhost:8080") - .header("Access-Control-Request-Method", "GET") - .build(); - ResponseEntity exchange = this.testRestTemplate.exchange(envRequest, Map.class); - assertThat(exchange.getStatusCode()).isEqualTo(HttpStatus.OK); + void preflightRequestToEndpointShouldReturnOk() { + this.restTestClient.options().uri("/actuator/env").headers((headers) -> { + headers.set("Origin", "http://localhost:8080"); + headers.set("Access-Control-Request-Method", "GET"); + }).exchange().expectStatus().isOk(); } @Test - void preflightRequestWhenCorsConfigInvalidShouldReturnForbidden() throws Exception { - RequestEntity entity = RequestEntity.options(new URI("/actuator/env")) - .header("Origin", "http://localhost:9095") - .header("Access-Control-Request-Method", "GET") - .build(); - ResponseEntity exchange = this.testRestTemplate.exchange(entity, byte[].class); - assertThat(exchange.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + void preflightRequestWhenCorsConfigInvalidShouldReturnForbidden() { + this.restTestClient.options().uri("/actuator/env").headers((headers) -> { + headers.set("Origin", "http://localhost:9095"); + headers.set("Access-Control-Request-Method", "GET"); + }).exchange().expectStatus().isForbidden(); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementServerWithCustomBasePathAndWebEndpointsBasePathSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementServerWithCustomBasePathAndWebEndpointsBasePathSampleActuatorApplicationTests.java index bb118f9fe39b..3f6cad18a033 100644 --- a/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementServerWithCustomBasePathAndWebEndpointsBasePathSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementServerWithCustomBasePathAndWebEndpointsBasePathSampleActuatorApplicationTests.java @@ -19,14 +19,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalManagementPort; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -53,10 +51,16 @@ class ManagementServerWithCustomBasePathAndWebEndpointsBasePathSampleActuatorApp @Test void testMissing() { - ResponseEntity entity = new TestRestTemplate("admin", "admin") - .getForEntity(getActuatorPath() + "/missing", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - assertThat(entity.getBody()).contains("\"status\":404"); + RestTestClient.bindToServer() + .defaultHeaders((headers) -> headers.setBasicAuth("admin", "admin")) + .build() + .get() + .uri(getActuatorPath() + "/missing") + .exchange() + .expectStatus() + .isNotFound() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"status\":404")); } @Override diff --git a/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementServerWithCustomBasePathSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementServerWithCustomBasePathSampleActuatorApplicationTests.java index 20e1fdf28885..76cf1d4ae8ed 100644 --- a/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementServerWithCustomBasePathSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementServerWithCustomBasePathSampleActuatorApplicationTests.java @@ -19,14 +19,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalManagementPort; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -52,10 +50,16 @@ class ManagementServerWithCustomBasePathSampleActuatorApplicationTests @Test void testMissing() { - ResponseEntity entity = new TestRestTemplate("admin", "admin") - .getForEntity(getActuatorPath() + "/missing", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - assertThat(entity.getBody()).contains("\"status\":404"); + RestTestClient.bindToServer() + .defaultHeaders((headers) -> headers.setBasicAuth("admin", "admin")) + .build() + .get() + .uri(getActuatorPath() + "/missing") + .exchange() + .expectStatus() + .isNotFound() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"status\":404")); } @Override diff --git a/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementServerWithCustomServletPathSampleActuatorTests.java b/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementServerWithCustomServletPathSampleActuatorTests.java index 9a2192d55578..4f2e4736a092 100644 --- a/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementServerWithCustomServletPathSampleActuatorTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/ManagementServerWithCustomServletPathSampleActuatorTests.java @@ -19,15 +19,11 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalManagementPort; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; +import org.springframework.test.web.servlet.client.RestTestClient; /** * Integration tests for a separate management server with a custom dispatcher servlet @@ -50,9 +46,13 @@ class ManagementServerWithCustomServletPathSampleActuatorTests extends AbstractS @Test void actuatorPathOnMainPortShouldNotMatch() { - ResponseEntity entity = new TestRestTemplate() - .getForEntity("http://localhost:" + this.port + "/example/actuator/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + RestTestClient.bindToServer() + .build() + .get() + .uri("http://localhost:" + this.port + "/example/actuator/health") + .exchange() + .expectStatus() + .isUnauthorized(); } @Override diff --git a/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java index 63bf2a83cee8..5eab15313c45 100644 --- a/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java @@ -25,7 +25,6 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import static org.assertj.core.api.Assertions.assertThat; @@ -47,21 +46,21 @@ class SampleActuatorCustomSecurityApplicationTests extends AbstractSampleActuato private ApplicationContext applicationContext; @Test - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings("unchecked") void testInsecureApplicationPath() { - ResponseEntity entity = restTemplate().getForEntity(getPath() + "/foo", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - Map body = entity.getBody(); - assertThat(body).isNotNull(); - assertThat((String) body.get("message")).contains("Expected exception in controller"); + restTestClient().get() + .uri(getPath() + "/foo") + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .expectBody(Map.class) + .value((body) -> assertThat((String) body.get("message")).contains("Expected exception in controller")); } @Test void mvcMatchersCanBeUsedToSecureActuators() { - ResponseEntity entity = beansRestTemplate().getForEntity(getActuatorPath() + "/beans", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - entity = beansRestTemplate().getForEntity(getActuatorPath() + "/beans/", Object.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + beansRestTestClient().get().uri(getActuatorPath() + "/beans").exchange().expectStatus().isOk(); + beansRestTestClient().get().uri(getActuatorPath() + "/beans/").exchange().expectStatus().isForbidden(); } @Override diff --git a/smoke-test/spring-boot-smoke-test-actuator-extension/src/test/java/smoketest/actuator/extension/SampleActuatorExtensionApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator-extension/src/test/java/smoketest/actuator/extension/SampleActuatorExtensionApplicationTests.java index d6db28f024cb..2b82e3faeea1 100644 --- a/smoke-test/spring-boot-smoke-test-actuator-extension/src/test/java/smoketest/actuator/extension/SampleActuatorExtensionApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator-extension/src/test/java/smoketest/actuator/extension/SampleActuatorExtensionApplicationTests.java @@ -16,60 +16,37 @@ package smoketest.actuator.extension; -import java.util.Map; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.restclient.RestTemplateBuilder; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.http.server.LocalTestWebServer; -import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; +import org.springframework.test.web.servlet.client.RestTestClient; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.web.error.include-message=always" }) -@AutoConfigureTestRestTemplate -@SuppressWarnings("removal") +@AutoConfigureRestTestClient class SampleActuatorExtensionApplicationTests { @Autowired - private ApplicationContext applicationContext; - - @Autowired - private TestRestTemplate restTemplate; - - @Autowired - private RestTemplateBuilder restTemplateBuilder; + private RestTestClient restTestClient; @Test - @SuppressWarnings("rawtypes") void healthActuatorIsNotExposed() { - ResponseEntity entity = this.restTemplate.getForEntity("/actuator/health", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + this.restTestClient.get().uri("/actuator/health").exchange().expectStatus().isNotFound(); } @Test - @SuppressWarnings("rawtypes") void healthExtensionWithAuthHeaderIsDenied() { - ResponseEntity entity = this.restTemplate.getForEntity("/myextension/health", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + this.restTestClient.get().uri("/myextension/health").exchange().expectStatus().isUnauthorized(); } @Test - @SuppressWarnings("rawtypes") void healthExtensionWithAuthHeader() { - TestRestTemplate restTemplate = new TestRestTemplate( - this.restTemplateBuilder.defaultHeader("Authorization", "Bearer secret")); - LocalTestWebServer localTestWebServer = LocalTestWebServer.obtain(this.applicationContext); - restTemplate.setUriTemplateHandler(localTestWebServer.uriBuilderFactory()); - ResponseEntity entity = restTemplate.getForEntity("/myextension/health", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + RestTestClient restTestClient = this.restTestClient.mutate() + .defaultHeader("Authorization", "Bearer secret") + .build(); + restTestClient.get().uri("/myextension/health").exchange().expectStatus().isOk(); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator-ui/src/test/java/smoketest/actuator/ui/SampleActuatorUiApplicationPortTests.java b/smoke-test/spring-boot-smoke-test-actuator-ui/src/test/java/smoketest/actuator/ui/SampleActuatorUiApplicationPortTests.java index b21301132e36..996a1169c9d1 100644 --- a/smoke-test/spring-boot-smoke-test-actuator-ui/src/test/java/smoketest/actuator/ui/SampleActuatorUiApplicationPortTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator-ui/src/test/java/smoketest/actuator/ui/SampleActuatorUiApplicationPortTests.java @@ -16,19 +16,13 @@ package smoketest.actuator.ui; -import java.util.Map; - import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalManagementPort; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -38,7 +32,6 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "management.server.port:0" }) -@AutoConfigureTestRestTemplate class SampleActuatorUiApplicationPortTests { @LocalServerPort @@ -47,30 +40,41 @@ class SampleActuatorUiApplicationPortTests { @LocalManagementPort private int managementPort; - @Autowired - private TestRestTemplate testRestTemplate; - @Test void testHome() { - ResponseEntity entity = this.testRestTemplate.getForEntity("http://localhost:" + this.port, - String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.port) + .build() + .get() + .headers((headers) -> headers.setBasicAuth("user", getPassword())) + .exchange() + .expectStatus() + .isOk(); } @Test void testMetrics() { - @SuppressWarnings("rawtypes") - ResponseEntity entity = this.testRestTemplate - .getForEntity("http://localhost:" + this.managementPort + "/actuator/metrics", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.managementPort) + .build() + .get() + .uri("/actuator/metrics") + .exchange() + .expectStatus() + .isUnauthorized(); } @Test void testHealth() { - ResponseEntity entity = this.testRestTemplate.withBasicAuth("user", getPassword()) - .getForEntity("http://localhost:" + this.managementPort + "/actuator/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"status\":\"UP\""); + RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.managementPort) + .defaultHeaders((headers) -> headers.setBasicAuth("user", getPassword())) + .build() + .get() + .uri("/actuator/health") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"status\":\"UP\"")); } private String getPassword() { diff --git a/smoke-test/spring-boot-smoke-test-actuator-ui/src/test/java/smoketest/actuator/ui/SampleActuatorUiApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator-ui/src/test/java/smoketest/actuator/ui/SampleActuatorUiApplicationTests.java index 705474d3c5ab..8082b6492dc7 100644 --- a/smoke-test/spring-boot-smoke-test-actuator-ui/src/test/java/smoketest/actuator/ui/SampleActuatorUiApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator-ui/src/test/java/smoketest/actuator/ui/SampleActuatorUiApplicationTests.java @@ -16,22 +16,15 @@ package smoketest.actuator.ui; -import java.util.Arrays; -import java.util.Map; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -41,46 +34,51 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.web.error.include-message=always" }) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleActuatorUiApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test void testHome() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); - ResponseEntity entity = this.restTemplate.withBasicAuth("user", getPassword()) - .exchange("/", HttpMethod.GET, new HttpEntity<>(headers), String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("Hello"); + this.restTestClient.get() + .uri("/") + .accept(MediaType.TEXT_HTML) + .headers((headers) -> headers.setBasicAuth("user", getPassword())) + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("<title>Hello")); } @Test void testCss() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/css/bootstrap.min.css", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("body"); + this.restTestClient.get() + .uri("/css/bootstrap.min.css") + .headers((headers) -> headers.setBasicAuth("user", getPassword())) + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("body")); } @Test void testMetrics() { - @SuppressWarnings("rawtypes") - ResponseEntity<Map> entity = this.restTemplate.getForEntity("/actuator/metrics", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + this.restTestClient.get().uri("/actuator/metrics").exchange().expectStatus().isUnauthorized(); } @Test void testError() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); - ResponseEntity<String> entity = this.restTemplate.withBasicAuth("user", getPassword()) - .exchange("/error", HttpMethod.GET, new HttpEntity<>(headers), String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - assertThat(entity.getBody()).contains("<html>") - .contains("<body>") - .contains("Please contact the operator with the above information"); + this.restTestClient.get() + .uri("/error") + .accept(MediaType.TEXT_HTML) + .headers((headers) -> headers.setBasicAuth("user", getPassword())) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("<html>") + .contains("<body>") + .contains("Please contact the operator with the above information")); } private String getPassword() { diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/AbstractManagementPortAndPathSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/AbstractManagementPortAndPathSampleActuatorApplicationTests.java index 9dd7d56e2baf..2a5cd6b00c4e 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/AbstractManagementPortAndPathSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/AbstractManagementPortAndPathSampleActuatorApplicationTests.java @@ -21,12 +21,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.web.server.LocalManagementPort; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.core.env.Environment; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -47,75 +47,99 @@ abstract class AbstractManagementPortAndPathSampleActuatorApplicationTests { private Environment environment; @Test + @SuppressWarnings("unchecked") void testHome() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - new TestRestTemplate("user", "password").getForEntity("http://localhost:" + this.port, Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).containsEntry("message", "Hello Phil"); + appPortClient().get() + .uri("/") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> assertThat(body).containsEntry("message", "Hello Phil")); } @Test void testMetrics() { testHome(); // makes sure some requests have been made - ResponseEntity<Map<String, Object>> entity = asMapEntity(new TestRestTemplate() - .getForEntity("http://localhost:" + this.managementPort + "/admin/metrics", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + managementPortClient().get().uri("/admin/metrics").exchange().expectStatus().isUnauthorized(); } @Test void testHealth() { - ResponseEntity<String> entity = new TestRestTemplate().withBasicAuth("user", "password") - .getForEntity("http://localhost:" + this.managementPort + "/admin/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()) + managementPortClient().get() + .uri("/admin/health") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(String.class) .isEqualTo("{\"groups\":[\"comp\",\"live\",\"liveness\",\"readiness\",\"ready\"],\"status\":\"UP\"}"); } @Test void testGroupWithComposite() { - ResponseEntity<String> entity = new TestRestTemplate().withBasicAuth("user", "password") - .getForEntity("http://localhost:" + this.managementPort + "/admin/health/comp", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains( - "components\":{\"a\":{\"details\":{\"hello\":\"spring-a\"},\"status\":\"UP\"},\"c\":{\"details\":{\"hello\":\"spring-c\"},\"status\":\"UP\"}}"); + managementPortClient().get() + .uri("/admin/health/comp") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains( + "components\":{\"a\":{\"details\":{\"hello\":\"spring-a\"},\"status\":\"UP\"},\"c\":{\"details\":{\"hello\":\"spring-c\"},\"status\":\"UP\"}}")); } @Test void testEnvNotFound() { String unknownProperty = "test-does-not-exist"; assertThat(this.environment.containsProperty(unknownProperty)).isFalse(); - ResponseEntity<String> entity = new TestRestTemplate().withBasicAuth("user", "password") - .getForEntity("http://localhost:" + this.managementPort + "/admin/env/" + unknownProperty, String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + managementPortClient().get() + .uri("/admin/env/" + unknownProperty) + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isNotFound(); } @Test + @SuppressWarnings("unchecked") void testMissing() { - ResponseEntity<String> entity = new TestRestTemplate("user", "password") - .getForEntity("http://localhost:" + this.managementPort + "/admin/missing", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - assertThat(entity.getBody()).contains("\"status\":404"); + managementPortClient().get() + .uri("/admin/missing") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isNotFound() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"status\":404")); } @Test + @SuppressWarnings("unchecked") void testErrorPage() { - ResponseEntity<Map<String, Object>> entity = asMapEntity(new TestRestTemplate("user", "password") - .getForEntity("http://localhost:" + this.port + "/error", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - assertThat(entity.getBody()).containsEntry("status", 999); + appPortClient().get() + .uri("/error") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .expectBody(Map.class) + .value((body) -> assertThat(body).containsEntry("status", 999)); } @Test + @SuppressWarnings("unchecked") void testManagementErrorPage() { - ResponseEntity<Map<String, Object>> entity = asMapEntity(new TestRestTemplate("user", "password") - .getForEntity("http://localhost:" + this.managementPort + "/error", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).containsEntry("status", 999); + managementPortClient().get() + .uri("/error") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> assertThat(body).containsEntry("status", 999)); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - static <K, V> ResponseEntity<Map<K, V>> asMapEntity(ResponseEntity<Map> entity) { - return (ResponseEntity) entity; + RestTestClient appPortClient() { + return RestTestClient.bindToServer().baseUrl("http://localhost:" + this.port).build(); + } + + RestTestClient managementPortClient() { + return RestTestClient.bindToServer().baseUrl("http://localhost:" + this.managementPort).build(); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/CorsSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/CorsSampleActuatorApplicationTests.java index 4092432ca45f..5e0e964eebdf 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/CorsSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/CorsSampleActuatorApplicationTests.java @@ -16,25 +16,13 @@ package smoketest.actuator; -import java.net.URI; -import java.util.Map; - -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.restclient.RestTemplateBuilder; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.http.server.LocalTestWebServer; -import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpStatus; -import org.springframework.http.RequestEntity; -import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; - -import static org.assertj.core.api.Assertions.assertThat; +import org.springframework.test.web.servlet.client.RestTestClient; /** * Integration test for cors preflight requests to management endpoints. @@ -43,47 +31,31 @@ */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("cors") -@AutoConfigureTestRestTemplate -@SuppressWarnings("removal") +@AutoConfigureRestTestClient class CorsSampleActuatorApplicationTests { - private TestRestTemplate testRestTemplate; - @Autowired - private ApplicationContext applicationContext; - - @BeforeEach - void setUp() { - RestTemplateBuilder builder = new RestTemplateBuilder(); - LocalTestWebServer localTestWebServer = LocalTestWebServer.obtain(this.applicationContext); - builder = builder.uriTemplateHandler(localTestWebServer.uriBuilderFactory()); - this.testRestTemplate = new TestRestTemplate(builder); - } + private RestTestClient restTestClient; @Test void endpointShouldReturnUnauthorized() { - ResponseEntity<?> entity = this.testRestTemplate.getForEntity("/actuator/env", Map.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + this.restTestClient.get().uri("/actuator/env").exchange().expectStatus().isUnauthorized(); } @Test - void preflightRequestToEndpointShouldReturnOk() throws Exception { - RequestEntity<?> healthRequest = RequestEntity.options(new URI("/actuator/env")) - .header("Origin", "http://localhost:8080") - .header("Access-Control-Request-Method", "GET") - .build(); - ResponseEntity<?> exchange = this.testRestTemplate.exchange(healthRequest, Map.class); - assertThat(exchange.getStatusCode()).isEqualTo(HttpStatus.OK); + void preflightRequestToEndpointShouldReturnOk() { + this.restTestClient.options().uri("/actuator/env").headers((headers) -> { + headers.set("Origin", "http://localhost:8080"); + headers.set("Access-Control-Request-Method", "GET"); + }).exchange().expectStatus().isOk(); } @Test - void preflightRequestWhenCorsConfigInvalidShouldReturnForbidden() throws Exception { - RequestEntity<?> entity = RequestEntity.options(new URI("/actuator/env")) - .header("Origin", "http://localhost:9095") - .header("Access-Control-Request-Method", "GET") - .build(); - ResponseEntity<byte[]> exchange = this.testRestTemplate.exchange(entity, byte[].class); - assertThat(exchange.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + void preflightRequestWhenCorsConfigInvalidShouldReturnForbidden() { + this.restTestClient.options().uri("/actuator/env").headers((headers) -> { + headers.set("Origin", "http://localhost:9095"); + headers.set("Access-Control-Request-Method", "GET"); + }).exchange().expectStatus().isForbidden(); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/EndpointsPropertiesSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/EndpointsPropertiesSampleActuatorApplicationTests.java index 9e56bc3dfe97..6cbd15be8797 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/EndpointsPropertiesSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/EndpointsPropertiesSampleActuatorApplicationTests.java @@ -21,13 +21,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -38,34 +37,38 @@ */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @ActiveProfiles("endpoints") -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class EndpointsPropertiesSampleActuatorApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test + @SuppressWarnings("unchecked") void testCustomErrorPath() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.withBasicAuth("user", "password").getForEntity("/oops", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - Map<String, Object> body = entity.getBody(); - assertThat(body).containsEntry("error", "None"); - assertThat(body).containsEntry("status", 999); + this.restTestClient.get() + .uri("/oops") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .expectBody(Map.class) + .value((body) -> { + assertThat(body).containsEntry("error", "None"); + assertThat(body).containsEntry("status", 999); + }); } @Test void testCustomContextPath() { - ResponseEntity<String> entity = this.restTemplate.withBasicAuth("user", "password") - .getForEntity("/admin/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"status\":\"UP\""); - assertThat(entity.getBody()).contains("\"hello\":\"world\""); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - static <K, V> ResponseEntity<Map<K, V>> asMapEntity(ResponseEntity<Map> entity) { - return (ResponseEntity) entity; + this.restTestClient.get() + .uri("/admin/health") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"status\":\"UP\"").contains("\"hello\":\"world\"")); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementAddressActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementAddressActuatorApplicationTests.java index 7bb749e1333d..db3495ccb541 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementAddressActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementAddressActuatorApplicationTests.java @@ -16,17 +16,13 @@ package smoketest.actuator; -import java.util.Map; - import org.junit.jupiter.api.Test; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalManagementPort; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -47,22 +43,27 @@ class ManagementAddressActuatorApplicationTests { @Test void testHome() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - new TestRestTemplate().getForEntity("http://localhost:" + this.port, Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.port) + .build() + .get() + .uri("/") + .exchange() + .expectStatus() + .isUnauthorized(); } @Test void testHealth() { - ResponseEntity<String> entity = new TestRestTemplate().withBasicAuth("user", "password") - .getForEntity("http://localhost:" + this.managementPort + "/admin/actuator/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"status\":\"UP\""); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - static <K, V> ResponseEntity<Map<K, V>> asMapEntity(ResponseEntity<Map> entity) { - return (ResponseEntity) entity; + RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.managementPort) + .defaultHeaders((headers) -> headers.setBasicAuth("user", "password")) + .build() + .get() + .uri("/admin/actuator/health") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"status\":\"UP\"")); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementDifferentPortAndEndpointWithExceptionHandlerSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementDifferentPortAndEndpointWithExceptionHandlerSampleActuatorApplicationTests.java index c3c52cc205cc..d7b5ee610198 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementDifferentPortAndEndpointWithExceptionHandlerSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementDifferentPortAndEndpointWithExceptionHandlerSampleActuatorApplicationTests.java @@ -18,15 +18,12 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalManagementPort; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.bind.annotation.ExceptionHandler; -import static org.assertj.core.api.Assertions.assertThat; - /** * Integration tests for separate management and main service ports with Actuator's MVC * {@link org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint @@ -43,10 +40,17 @@ class ManagementDifferentPortAndEndpointWithExceptionHandlerSampleActuatorApplic @Test void testExceptionHandlerRestControllerEndpoint() { - ResponseEntity<String> entity = new TestRestTemplate("user", "password") - .getForEntity("http://localhost:" + this.managementPort + "/actuator/exception", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.EXPECTATION_FAILED); - assertThat(entity.getBody()).isEqualTo("this is a custom exception body"); + RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.managementPort) + .defaultHeaders((headers) -> headers.setBasicAuth("user", "password")) + .build() + .get() + .uri("/actuator/exception") + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.EXPECTATION_FAILED) + .expectBody(String.class) + .isEqualTo("this is a custom exception body"); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementDifferentPortSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementDifferentPortSampleActuatorApplicationTests.java index 3168a90d328d..01f0c0f09ea1 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementDifferentPortSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementDifferentPortSampleActuatorApplicationTests.java @@ -18,11 +18,9 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalManagementPort; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -41,10 +39,15 @@ class ManagementDifferentPortSampleActuatorApplicationTests { @Test void linksEndpointShouldBeAvailable() { - ResponseEntity<String> entity = new TestRestTemplate("user", "password") - .getForEntity("http://localhost:" + this.managementPort, String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"_links\""); + RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.managementPort) + .defaultHeaders((headers) -> headers.setBasicAuth("user", "password")) + .build() + .get() + .uri("/") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"_links\"")); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPathSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPathSampleActuatorApplicationTests.java index ce13d3bf200f..f5da731ee049 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPathSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPathSampleActuatorApplicationTests.java @@ -16,17 +16,14 @@ package smoketest.actuator; -import java.util.Map; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -37,30 +34,32 @@ */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "management.endpoints.web.base-path=/admin" }) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class ManagementPathSampleActuatorApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test void testHealth() { - ResponseEntity<String> entity = this.restTemplate.withBasicAuth("user", "password") - .getForEntity("/admin/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"status\":\"UP\""); + this.restTestClient.get() + .uri("/admin/health") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"status\":\"UP\"")); } @Test void testHomeIsSecure() { - ResponseEntity<Map<String, Object>> entity = asMapEntity(this.restTemplate.getForEntity("/", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - assertThat(entity.getHeaders().headerNames()).doesNotContain("Set-Cookie"); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - static <K, V> ResponseEntity<Map<K, V>> asMapEntity(ResponseEntity<Map> entity) { - return (ResponseEntity) entity; + this.restTestClient.get() + .uri("/") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isUnauthorized() + .expectHeader() + .doesNotExist("Set-Cookie"); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPortSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPortSampleActuatorApplicationTests.java index dd241cb70c79..8e57fdb9ce33 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPortSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPortSampleActuatorApplicationTests.java @@ -23,7 +23,6 @@ import smoketest.actuator.ManagementPortSampleActuatorApplicationTests.CustomErrorAttributes; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalManagementPort; @@ -32,9 +31,9 @@ import org.springframework.boot.webmvc.error.DefaultErrorAttributes; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.context.request.WebRequest; import static org.assertj.core.api.Assertions.assertThat; @@ -59,53 +58,78 @@ class ManagementPortSampleActuatorApplicationTests { private CustomErrorAttributes errorAttributes; @Test + @SuppressWarnings("unchecked") void testHome() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - new TestRestTemplate("user", "password").getForEntity("http://localhost:" + this.port, Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).containsEntry("message", "Hello Phil"); + RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.port) + .defaultHeaders((headers) -> headers.setBasicAuth("user", "password")) + .build() + .get() + .uri("/") + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> assertThat(body).containsEntry("message", "Hello Phil")); } @Test void testMetrics() { testHome(); // makes sure some requests have been made - ResponseEntity<Map<String, Object>> entity = asMapEntity(new TestRestTemplate() - .getForEntity("http://localhost:" + this.managementPort + "/actuator/metrics", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.managementPort) + .build() + .get() + .uri("/actuator/metrics") + .exchange() + .expectStatus() + .isUnauthorized(); } @Test void testHealth() { - ResponseEntity<String> entity = new TestRestTemplate().withBasicAuth("user", "password") - .getForEntity("http://localhost:" + this.managementPort + "/actuator/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"status\":\"UP\""); - assertThat(entity.getBody()).contains("\"example\""); - assertThat(entity.getBody()).contains("\"counter\":42"); + RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.managementPort) + .defaultHeaders((headers) -> headers.setBasicAuth("user", "password")) + .build() + .get() + .uri("/actuator/health") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"status\":\"UP\"") + .contains("\"example\"") + .contains("\"counter\":42")); } @Test + @SuppressWarnings("unchecked") void testErrorPage() { - ResponseEntity<Map<String, Object>> entity = asMapEntity(new TestRestTemplate("user", "password") - .getForEntity("http://localhost:" + this.managementPort + "/error", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).containsEntry("status", 999); + RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.managementPort) + .defaultHeaders((headers) -> headers.setBasicAuth("user", "password")) + .build() + .get() + .uri("/error") + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> assertThat(body).containsEntry("status", 999)); } @Test + @SuppressWarnings("unchecked") void securityContextIsAvailableToErrorHandling() { this.errorAttributes.securityContext = null; - ResponseEntity<Map<String, Object>> entity = asMapEntity(new TestRestTemplate("user", "password") - .getForEntity("http://localhost:" + this.managementPort + "/404", Map.class)); + RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.managementPort) + .defaultHeaders((headers) -> headers.setBasicAuth("user", "password")) + .build() + .get() + .uri("/404") + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.NOT_FOUND) + .expectBody(Map.class) + .value((body) -> assertThat(body).containsEntry("status", 404)); assertThat(this.errorAttributes.securityContext).isNotNull(); assertThat(this.errorAttributes.securityContext.getAuthentication()).isNotNull(); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - assertThat(entity.getBody()).containsEntry("status", 404); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - static <K, V> ResponseEntity<Map<K, V>> asMapEntity(ResponseEntity<Map> entity) { - return (ResponseEntity) entity; } static class CustomErrorAttributes extends DefaultErrorAttributes { diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPortWithLazyInitializationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPortWithLazyInitializationTests.java index 69e03630aee2..2f298c7ae367 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPortWithLazyInitializationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPortWithLazyInitializationTests.java @@ -18,11 +18,9 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalManagementPort; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -41,10 +39,15 @@ class ManagementPortWithLazyInitializationTests { @Test void testHealth() { - ResponseEntity<String> entity = new TestRestTemplate().withBasicAuth("user", "password") - .getForEntity("http://localhost:" + this.managementPort + "/actuator/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"status\":\"UP\""); + RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.managementPort) + .defaultHeaders((headers) -> headers.setBasicAuth("user", "password")) + .build() + .get() + .uri("/actuator/health") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"status\":\"UP\"")); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementRandomPortWithConfiguredPortSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementRandomPortWithConfiguredPortSampleActuatorApplicationTests.java index 8ce53f3fd59f..60563cb11366 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementRandomPortWithConfiguredPortSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementRandomPortWithConfiguredPortSampleActuatorApplicationTests.java @@ -18,12 +18,10 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalManagementPort; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -56,10 +54,15 @@ void randomManagementPortShouldBeApplied() { @Test void testHealth() { - ResponseEntity<String> entity = new TestRestTemplate().withBasicAuth("user", "password") - .getForEntity("http://localhost:" + this.managementPort + "/actuator/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"status\":\"UP\""); + RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.managementPort) + .defaultHeaders((headers) -> headers.setBasicAuth("user", "password")) + .build() + .get() + .uri("/actuator/health") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"status\":\"UP\"")); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/NoManagementSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/NoManagementSampleActuatorApplicationTests.java index 7f4d0bf6cf78..f4623b57ebfa 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/NoManagementSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/NoManagementSampleActuatorApplicationTests.java @@ -21,12 +21,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -36,31 +34,32 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "management.server.port=-1" }) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class NoManagementSampleActuatorApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test + @SuppressWarnings("unchecked") void testHome() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.withBasicAuth("user", "password").getForEntity("/", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).containsEntry("message", "Hello Phil"); + this.restTestClient.get() + .uri("/") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> assertThat(body).containsEntry("message", "Hello Phil")); } @Test void testMetricsNotAvailable() { testHome(); // makes sure some requests have been made - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.withBasicAuth("user", "password").getForEntity("/metrics", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - static <K, V> ResponseEntity<Map<K, V>> asMapEntity(ResponseEntity<Map> entity) { - return (ResponseEntity) entity; + this.restTestClient.get() + .uri("/metrics") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isNotFound(); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationIsolatedJsonMapperFalseTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationIsolatedJsonMapperFalseTests.java index caf10be6f74d..1bc7446b644d 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationIsolatedJsonMapperFalseTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationIsolatedJsonMapperFalseTests.java @@ -20,15 +20,11 @@ import tools.jackson.databind.json.JsonMapper; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.test.context.ContextConfiguration; - -import static org.assertj.core.api.Assertions.assertThat; +import org.springframework.test.web.servlet.client.RestTestClient; /** * Integration test for WebMVC actuator when not using an isolated {@link JsonMapper}. @@ -39,18 +35,20 @@ properties = { "management.endpoints.jackson.isolated-json-mapper=false", "spring.jackson.mapper.require-setters-for-getters=true" }) @ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleActuatorApplicationIsolatedJsonMapperFalseTests { @Autowired - private TestRestTemplate testRestTemplate; + private RestTestClient restTestClient; @Test void bodyIsEmptyDueToMainJsonMapperRequiringSettersForGetters() { - ResponseEntity<String> entity = this.testRestTemplate.withBasicAuth("user", "password") - .getForEntity("/actuator/startup", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("{}"); + this.restTestClient.get() + .uri("/actuator/startup") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(String.class) + .isEqualTo("{}"); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationIsolatedJsonMapperTrueTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationIsolatedJsonMapperTrueTests.java index 9a137e29f360..0c8eb9861bae 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationIsolatedJsonMapperTrueTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationIsolatedJsonMapperTrueTests.java @@ -20,13 +20,11 @@ import tools.jackson.databind.json.JsonMapper; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -39,18 +37,20 @@ properties = { "management.endpoints.jackson.isolated-json-mapper=true", "spring.jackson.mapper.require-setters-for-getters=true" }) @ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleActuatorApplicationIsolatedJsonMapperTrueTests { @Autowired - private TestRestTemplate testRestTemplate; + private RestTestClient restTestClient; @Test void bodyIsPresentAsOnlyMainObjectMapperRequiresSettersForGetters() { - ResponseEntity<String> entity = this.testRestTemplate.withBasicAuth("user", "password") - .getForEntity("/actuator/startup", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"timeline\":"); + this.restTestClient.get() + .uri("/actuator/startup") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"timeline\":")); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationTests.java index b830fdf540cb..63d8b5f4c405 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationTests.java @@ -16,7 +16,6 @@ package smoketest.actuator; -import java.util.Arrays; import java.util.List; import java.util.Map; @@ -24,17 +23,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.jdbc.autoconfigure.DataSourceProperties; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -46,160 +41,188 @@ * @author Stephane Nicoll */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleActuatorApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Autowired private ApplicationContext applicationContext; @Test void testHomeIsSecure() { - ResponseEntity<Map<String, Object>> entity = asMapEntity(this.restTemplate.getForEntity("/", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - assertThat(entity.getHeaders().headerNames()).doesNotContain("Set-Cookie"); + this.restTestClient.get() + .uri("/") + .exchange() + .expectStatus() + .isUnauthorized() + .expectHeader() + .doesNotExist("Set-Cookie"); } @Test void testMetricsIsSecure() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.getForEntity("/actuator/metrics", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - entity = asMapEntity(this.restTemplate.getForEntity("/actuator/metrics/", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - entity = asMapEntity(this.restTemplate.getForEntity("/actuator/metrics/foo", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - entity = asMapEntity(this.restTemplate.getForEntity("/actuator/metrics.json", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + this.restTestClient.get().uri("/actuator/metrics").exchange().expectStatus().isUnauthorized(); + this.restTestClient.get().uri("/actuator/metrics/").exchange().expectStatus().isUnauthorized(); + this.restTestClient.get().uri("/actuator/metrics/foo").exchange().expectStatus().isUnauthorized(); + this.restTestClient.get().uri("/actuator/metrics.json").exchange().expectStatus().isUnauthorized(); } @Test + @SuppressWarnings("unchecked") void testHome() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.withBasicAuth("user", "password").getForEntity("/", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).containsEntry("message", "Hello Phil"); + this.restTestClient.get() + .uri("/") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> assertThat(body).containsEntry("message", "Hello Phil")); } @Test @SuppressWarnings("unchecked") void testMetrics() { testHome(); // makes sure some requests have been made - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.withBasicAuth("user", "password").getForEntity("/actuator/metrics", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - Map<String, Object> body = entity.getBody(); - assertThat(body).isNotNull(); - assertThat(body).containsKey("names"); - List<String> names = (List<String>) body.get("names"); - assertThat(names).contains("jvm.buffer.count"); + this.restTestClient.get() + .uri("/actuator/metrics") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> { + assertThat(body).containsKey("names"); + List<String> names = (List<String>) body.get("names"); + assertThat(names).contains("jvm.buffer.count"); + }); } @Test + @SuppressWarnings("unchecked") void testEnv() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.withBasicAuth("user", "password").getForEntity("/actuator/env", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).containsKey("propertySources"); + this.restTestClient.get() + .uri("/actuator/env") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> assertThat(body).containsKey("propertySources")); } @Test void healthInsecureByDefault() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/actuator/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"status\":\"UP\""); - assertThat(entity.getBody()).doesNotContain("\"hello\":\"1\""); + this.restTestClient.get() + .uri("/actuator/health") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"status\":\"UP\"").doesNotContain("\"hello\":\"1\"")); } @Test void testErrorPage() { - ResponseEntity<String> entity = this.restTemplate.withBasicAuth("user", "password") - .getForEntity("/foo", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - String body = entity.getBody(); - assertThat(body).contains("\"error\":"); + this.restTestClient.get() + .uri("/foo") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"error\":")); } @Test void testHtmlErrorPage() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); - HttpEntity<?> request = new HttpEntity<Void>(headers); - ResponseEntity<String> entity = this.restTemplate.withBasicAuth("user", "password") - .exchange("/foo", HttpMethod.GET, request, String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - String body = entity.getBody(); - assertThat(body).as("Body was null").isNotNull(); - assertThat(body).contains("This application has no explicit mapping for /error"); + this.restTestClient.get() + .uri("/foo") + .accept(MediaType.TEXT_HTML) + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .expectBody(String.class) + .value((body) -> assertThat(body).as("Body was null") + .isNotNull() + .contains("This application has no explicit mapping for /error")); } @Test + @SuppressWarnings("unchecked") void testErrorPageDirectAccess() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.withBasicAuth("user", "password").getForEntity("/error", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - assertThat(entity.getBody()).containsEntry("error", "None"); - assertThat(entity.getBody()).containsEntry("status", 999); + this.restTestClient.get() + .uri("/error") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .expectBody(Map.class) + .value((body) -> { + assertThat(body).containsEntry("error", "None"); + assertThat(body).containsEntry("status", 999); + }); } @Test + @SuppressWarnings("unchecked") void testBeans() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.withBasicAuth("user", "password").getForEntity("/actuator/beans", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).containsOnlyKeys("contexts"); + this.restTestClient.get() + .uri("/actuator/beans") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> assertThat(body).containsOnlyKeys("contexts")); } @Test @SuppressWarnings("unchecked") void testConfigProps() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.withBasicAuth("user", "password").getForEntity("/actuator/configprops", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - Map<String, Object> body = entity.getBody(); - assertThat(body).isNotNull(); - Map<String, Object> contexts = (Map<String, Object>) body.get("contexts"); - assertThat(contexts).isNotNull(); - Map<String, Object> context = (Map<String, Object>) contexts.get(this.applicationContext.getId()); - assertThat(context).isNotNull(); - Map<String, Object> beans = (Map<String, Object>) context.get("beans"); - assertThat(beans).containsKey("spring.datasource-" + DataSourceProperties.class.getName()); + this.restTestClient.get() + .uri("/actuator/configprops") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> { + Map<String, Object> contexts = (Map<String, Object>) body.get("contexts"); + assertThat(contexts).isNotNull(); + Map<String, Object> context = (Map<String, Object>) contexts.get(this.applicationContext.getId()); + assertThat(context).isNotNull(); + Map<String, Object> beans = (Map<String, Object>) context.get("beans"); + assertThat(beans).containsKey("spring.datasource-" + DataSourceProperties.class.getName()); + }); } @Test + @SuppressWarnings("unchecked") void testLegacyDot() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.withBasicAuth("user", "password").getForEntity("/actuator/legacy", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains(entry("legacy", "legacy")); + this.restTestClient.get() + .uri("/actuator/legacy") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> assertThat(body).contains(entry("legacy", "legacy"))); } @Test + @SuppressWarnings("unchecked") void testLegacyHyphen() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.withBasicAuth("user", "password").getForEntity("/actuator/anotherlegacy", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains(entry("legacy", "legacy")); + this.restTestClient.get() + .uri("/actuator/anotherlegacy") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> assertThat(body).contains(entry("legacy", "legacy"))); } @Test @SuppressWarnings("unchecked") void testInfo() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.withBasicAuth("user", "password").getForEntity("/actuator/info", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).containsKey("build"); - Map<String, Object> body = entity.getBody(); - assertThat(body).isNotNull(); - Map<String, Object> example = (Map<String, Object>) body.get("example"); - assertThat(example).containsEntry("someKey", "someValue"); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - static <K, V> ResponseEntity<Map<K, V>> asMapEntity(ResponseEntity<Map> entity) { - return (ResponseEntity) entity; + this.restTestClient.get() + .uri("/actuator/info") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> { + assertThat(body).containsKey("build"); + Map<String, Object> example = (Map<String, Object>) body.get("example"); + assertThat(example).containsEntry("someKey", "someValue"); + }); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ServletPathSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ServletPathSampleActuatorApplicationTests.java index c6343815278d..9acc8f4a2b6d 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ServletPathSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ServletPathSampleActuatorApplicationTests.java @@ -21,12 +21,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -36,39 +36,48 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.mvc.servlet.path=/spring" }) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class ServletPathSampleActuatorApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test + @SuppressWarnings("unchecked") void testErrorPath() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.withBasicAuth("user", "password").getForEntity("/spring/error", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - assertThat(entity.getBody()).containsEntry("error", "None"); - assertThat(entity.getBody()).containsEntry("status", 999); + this.restTestClient.get() + .uri("/spring/error") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .expectBody(Map.class) + .value((body) -> { + assertThat(body).containsEntry("error", "None"); + assertThat(body).containsEntry("status", 999); + }); } @Test void testHealth() { - ResponseEntity<String> entity = this.restTemplate.withBasicAuth("user", "password") - .getForEntity("/spring/actuator/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"status\":\"UP\""); + this.restTestClient.get() + .uri("/spring/actuator/health") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("\"status\":\"UP\"")); } @Test void testHomeIsSecure() { - ResponseEntity<Map<String, Object>> entity = asMapEntity(this.restTemplate.getForEntity("/spring/", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - assertThat(entity.getHeaders().headerNames()).doesNotContain("Set-Cookie"); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - static <K, V> ResponseEntity<Map<K, V>> asMapEntity(ResponseEntity<Map> entity) { - return (ResponseEntity) entity; + this.restTestClient.get() + .uri("/spring/") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isUnauthorized() + .expectHeader() + .doesNotExist("Set-Cookie"); } } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ShutdownSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ShutdownSampleActuatorApplicationTests.java index 76849f91cbff..5e6bdf77446b 100644 --- a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ShutdownSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ShutdownSampleActuatorApplicationTests.java @@ -21,18 +21,16 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.web.SecurityFilterChain; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -43,35 +41,33 @@ */ @SpringBootTest(classes = { ShutdownSampleActuatorApplicationTests.SecurityConfiguration.class, SampleActuatorApplication.class }, webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class ShutdownSampleActuatorApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test + @SuppressWarnings("unchecked") void testHome() { - ResponseEntity<Map<String, Object>> entity = asMapEntity( - this.restTemplate.withBasicAuth("user", "password").getForEntity("/", Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - Map<String, Object> body = entity.getBody(); - assertThat(body).containsEntry("message", "Hello Phil"); + this.restTestClient.get() + .uri("/") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> assertThat(body).containsEntry("message", "Hello Phil")); } @Test @DirtiesContext + @SuppressWarnings("unchecked") void testShutdown() { - ResponseEntity<Map<String, Object>> entity = asMapEntity(this.restTemplate.withBasicAuth("user", "password") - .postForEntity("/actuator/shutdown", null, Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - Map<String, Object> body = entity.getBody(); - assertThat(body).isNotNull(); - assertThat(((String) body.get("message"))).contains("Shutting down"); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - static <K, V> ResponseEntity<Map<K, V>> asMapEntity(ResponseEntity<Map> entity) { - return (ResponseEntity) entity; + this.restTestClient.post() + .uri("/actuator/shutdown") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(Map.class) + .value((body) -> assertThat((String) body.get("message")).contains("Shutting down")); } @Configuration(proxyBeanMethods = false) diff --git a/smoke-test/spring-boot-smoke-test-devtools/src/test/java/smoketest/devtools/SampleDevToolsApplicationIntegrationTests.java b/smoke-test/spring-boot-smoke-test-devtools/src/test/java/smoketest/devtools/SampleDevToolsApplicationIntegrationTests.java index 641dcbc1809c..3399c708e3b2 100644 --- a/smoke-test/spring-boot-smoke-test-devtools/src/test/java/smoketest/devtools/SampleDevToolsApplicationIntegrationTests.java +++ b/smoke-test/spring-boot-smoke-test-devtools/src/test/java/smoketest/devtools/SampleDevToolsApplicationIntegrationTests.java @@ -19,12 +19,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -35,30 +33,33 @@ * @author Phillip Webb */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleDevToolsApplicationIntegrationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test void testStaticResource() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/css/application.css", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("color: green;"); + this.restTestClient.get() + .uri("/css/application.css") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("color: green;")); } @Test void testPublicResource() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/public.txt", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("public file"); + this.restTestClient.get() + .uri("/public.txt") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("public file")); } @Test void testClassResource() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/application.properties", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + this.restTestClient.get().uri("/application.properties").exchange().expectStatus().isNotFound(); } } diff --git a/smoke-test/spring-boot-smoke-test-hateoas/src/test/java/smoketest/hateoas/SampleHateoasApplicationTests.java b/smoke-test/spring-boot-smoke-test-hateoas/src/test/java/smoketest/hateoas/SampleHateoasApplicationTests.java index c101d801447d..5cfb91347550 100644 --- a/smoke-test/spring-boot-smoke-test-hateoas/src/test/java/smoketest/hateoas/SampleHateoasApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-hateoas/src/test/java/smoketest/hateoas/SampleHateoasApplicationTests.java @@ -16,61 +16,54 @@ package smoketest.hateoas; -import java.util.Arrays; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleHateoasApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test void hasHalLinksWhenAnythingIsAcceptable() { - HttpHeaders headers = new HttpHeaders(); - ResponseEntity<String> entity = this.restTemplate.exchange("/customers/1", HttpMethod.GET, - new HttpEntity<>(headers), String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).startsWith("{\"_links\":{\"self\":{\"href\""); - assertThat(entity.getBody()).endsWith(",\"id\":1,\"firstName\":\"Oliver\",\"lastName\":\"Gierke\"}"); + this.restTestClient.get().uri("/customers/1").exchangeSuccessfully().expectBody(String.class).value((body) -> { + assertThat(body).startsWith("{\"_links\":{\"self\":{\"href\""); + assertThat(body).endsWith(",\"id\":1,\"firstName\":\"Oliver\",\"lastName\":\"Gierke\"}"); + }); } @Test void hasHalLinksWhenJsonIsAcceptable() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); - ResponseEntity<String> entity = this.restTemplate.exchange("/customers/1", HttpMethod.GET, - new HttpEntity<>(headers), String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).startsWith("{\"_links\":{\"self\":{\"href\""); - assertThat(entity.getBody()).endsWith(",\"id\":1,\"firstName\":\"Oliver\",\"lastName\":\"Gierke\"}"); + this.restTestClient.get() + .uri("/customers/1") + .accept(MediaType.APPLICATION_JSON) + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> { + assertThat(body).startsWith("{\"_links\":{\"self\":{\"href\""); + assertThat(body).endsWith(",\"id\":1,\"firstName\":\"Oliver\",\"lastName\":\"Gierke\"}"); + }); } @Test void producesJsonWhenXmlIsPreferred() { - HttpHeaders headers = new HttpHeaders(); - headers.set(HttpHeaders.ACCEPT, "application/xml;q=0.9,application/json;q=0.8"); - HttpEntity<?> request = new HttpEntity<>(headers); - ResponseEntity<String> response = this.restTemplate.exchange("/customers/1", HttpMethod.GET, request, - String.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.parseMediaType("application/json")); + this.restTestClient.get() + .uri("/customers/1") + .header(HttpHeaders.ACCEPT, "application/xml;q=0.9,application/json;q=0.8") + .exchangeSuccessfully() + .expectHeader() + .contentType(MediaType.APPLICATION_JSON); } } diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyApplicationTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyApplicationTests.java index a881a351f58d..4cb5fa7158b5 100644 --- a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyApplicationTests.java @@ -19,46 +19,55 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.http.server.LocalTestWebServer; +import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; -@AutoConfigureTestRestTemplate @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "logging.level.root=debug") abstract class AbstractJerseyApplicationTests { + private final RestClient restClient = RestClient.create(); + @Autowired - private TestRestTemplate restTemplate; + private ApplicationContext applicationContext; @Test void contextLoads() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/hello", String.class); + ResponseEntity<String> entity = getForEntity("/hello"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } @Test void reverse() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/reverse?input=olleh", String.class); + ResponseEntity<String> entity = getForEntity("/reverse?input=olleh"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).isEqualTo("hello"); } @Test void validation() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/reverse", String.class); + ResponseEntity<String> entity = getForEntity("/reverse"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); } @Test void actuatorStatus() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/actuator/health", String.class); + ResponseEntity<String> entity = getForEntity("/actuator/health"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).isEqualTo("{\"status\":\"UP\",\"groups\":[\"liveness\",\"readiness\"]}"); } + private ResponseEntity<String> getForEntity(String path) { + String uri = LocalTestWebServer.obtain(this.applicationContext).uri(path); + return this.restClient.get().uri(uri).retrieve().onStatus(HttpStatusCode::isError, (request, response) -> { + }).toEntity(String.class); + } + } diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyManagementPortTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyManagementPortTests.java index 9b131217bff1..3b12e3b6e37e 100644 --- a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyManagementPortTests.java +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyManagementPortTests.java @@ -21,10 +21,7 @@ import org.junit.jupiter.api.Test; import smoketest.jersey.AbstractJerseyManagementPortTests.ResourceConfigConfiguration; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.jersey.autoconfigure.ResourceConfigCustomizer; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.web.server.LocalManagementPort; @@ -32,7 +29,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -42,49 +41,48 @@ * * @author Madhura Bhave */ -@AutoConfigureTestRestTemplate @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = "management.server.port=0") @Import(ResourceConfigConfiguration.class) class AbstractJerseyManagementPortTests { + private final RestClient restClient = RestClient.create(); + @LocalServerPort private int port; @LocalManagementPort private int managementPort; - @Autowired - private TestRestTemplate testRestTemplate; - @Test void resourceShouldBeAvailableOnMainPort() { - ResponseEntity<String> entity = this.testRestTemplate.getForEntity("http://localhost:" + this.port + "/test", - String.class); + ResponseEntity<String> entity = getForEntity("http://localhost:" + this.port + "/test"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).contains("test"); } @Test void resourceShouldNotBeAvailableOnManagementPort() { - ResponseEntity<String> entity = this.testRestTemplate - .getForEntity("http://localhost:" + this.managementPort + "/test", String.class); + ResponseEntity<String> entity = getForEntity("http://localhost:" + this.managementPort + "/test"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); } @Test void actuatorShouldBeAvailableOnManagementPort() { - ResponseEntity<String> entity = this.testRestTemplate - .getForEntity("http://localhost:" + this.managementPort + "/actuator/health", String.class); + ResponseEntity<String> entity = getForEntity("http://localhost:" + this.managementPort + "/actuator/health"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } @Test void actuatorShouldNotBeAvailableOnMainPort() { - ResponseEntity<String> entity = this.testRestTemplate - .getForEntity("http://localhost:" + this.port + "/actuator/health", String.class); + ResponseEntity<String> entity = getForEntity("http://localhost:" + this.port + "/actuator/health"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); } + private ResponseEntity<String> getForEntity(String uri) { + return this.restClient.get().uri(uri).retrieve().onStatus(HttpStatusCode::isError, (request, response) -> { + }).toEntity(String.class); + } + @TestConfiguration static class ResourceConfigConfiguration { diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperFalseTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperFalseTests.java index 284bda04a4af..d4ec7be37b57 100644 --- a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperFalseTests.java +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperFalseTests.java @@ -19,16 +19,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalManagementPort; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.test.context.ContextConfiguration; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -37,7 +36,6 @@ * * @author Phillip Webb */ -@AutoConfigureTestRestTemplate @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "management.endpoints.jackson2.isolated-object-mapper=false") @ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class) @@ -49,13 +47,15 @@ class JerseyActuatorIsolatedObjectMapperFalseTests { @LocalManagementPort private int managementPort; - @Autowired - private TestRestTemplate testRestTemplate; - @Test void resourceShouldBeAvailableOnMainPort() { - ResponseEntity<String> entity = this.testRestTemplate - .getForEntity("http://localhost:" + this.port + "/actuator/startup", String.class); + ResponseEntity<String> entity = RestClient.create() + .get() + .uri("http://localhost:" + this.port + "/actuator/startup") + .retrieve() + .onStatus(HttpStatusCode::isError, (request, response) -> { + }) + .toEntity(String.class); System.out.println(entity.getBody()); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); assertThat(entity.getBody()) diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperTrueTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperTrueTests.java index bf71e1a4dd40..03397d4dc3e9 100644 --- a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperTrueTests.java +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperTrueTests.java @@ -19,9 +19,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalManagementPort; @@ -29,6 +26,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.ContextConfiguration; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -37,7 +35,6 @@ * * @author Phillip Webb */ -@AutoConfigureTestRestTemplate @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "management.endpoints.jackson.isolated-json-mapper=true") @ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class) @@ -49,13 +46,13 @@ class JerseyActuatorIsolatedObjectMapperTrueTests { @LocalManagementPort private int managementPort; - @Autowired - private TestRestTemplate testRestTemplate; - @Test void resourceShouldBeAvailableOnMainPort() { - ResponseEntity<String> entity = this.testRestTemplate - .getForEntity("http://localhost:" + this.port + "/actuator/startup", String.class); + ResponseEntity<String> entity = RestClient.create() + .get() + .uri("http://localhost:" + this.port + "/actuator/startup") + .retrieve() + .toEntity(String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).contains("\"timeline\":"); } diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyApplicationPathAndManagementPortTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyApplicationPathAndManagementPortTests.java index c56376ec69d6..e00a08e649e4 100644 --- a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyApplicationPathAndManagementPortTests.java +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyApplicationPathAndManagementPortTests.java @@ -18,14 +18,12 @@ import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalManagementPort; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -35,7 +33,6 @@ * * @author Madhura Bhave */ -@AutoConfigureTestRestTemplate @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { "management.server.port=0", "spring.jersey.application-path=/app" }) class JerseyApplicationPathAndManagementPortTests { @@ -46,13 +43,13 @@ class JerseyApplicationPathAndManagementPortTests { @LocalManagementPort private int managementPort; - @Autowired - private TestRestTemplate testRestTemplate; - @Test void applicationPathShouldNotAffectActuators() { - ResponseEntity<String> entity = this.testRestTemplate - .getForEntity("http://localhost:" + this.managementPort + "/actuator/health", String.class); + ResponseEntity<String> entity = RestClient.create() + .get() + .uri("http://localhost:" + this.managementPort + "/actuator/health") + .retrieve() + .toEntity(String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).contains("\"status\":\"UP\""); } diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyDifferentPortSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyDifferentPortSampleActuatorApplicationTests.java index b27538dfb067..f1640da2605e 100644 --- a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyDifferentPortSampleActuatorApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyDifferentPortSampleActuatorApplicationTests.java @@ -18,11 +18,11 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalManagementPort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -41,8 +41,12 @@ class JerseyDifferentPortSampleActuatorApplicationTests { @Test void linksEndpointShouldBeAvailable() { - ResponseEntity<String> entity = new TestRestTemplate("user", getPassword()) - .getForEntity("http://localhost:" + this.managementPort + "/", String.class); + ResponseEntity<String> entity = RestClient.create() + .get() + .uri("http://localhost:" + this.managementPort + "/") + .headers((headers) -> headers.setBasicAuth("user", getPassword())) + .retrieve() + .toEntity(String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).contains("\"_links\""); } diff --git a/smoke-test/spring-boot-smoke-test-jetty-jsp/src/test/java/smoketest/jetty/jsp/SampleWebJspApplicationTests.java b/smoke-test/spring-boot-smoke-test-jetty-jsp/src/test/java/smoketest/jetty/jsp/SampleWebJspApplicationTests.java index eac7b3771673..9be5c6f29e20 100644 --- a/smoke-test/spring-boot-smoke-test-jetty-jsp/src/test/java/smoketest/jetty/jsp/SampleWebJspApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-jetty-jsp/src/test/java/smoketest/jetty/jsp/SampleWebJspApplicationTests.java @@ -19,12 +19,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -34,17 +32,19 @@ * @author Phillip Webb */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleWebJspApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test void testJspWithEl() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("/resources/text.txt"); + this.restTestClient.get() + .uri("/") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("/resources/text.txt")); } } diff --git a/smoke-test/spring-boot-smoke-test-jetty-ssl/build.gradle b/smoke-test/spring-boot-smoke-test-jetty-ssl/build.gradle index 5e816259fca2..a6e6864a6e2b 100644 --- a/smoke-test/spring-boot-smoke-test-jetty-ssl/build.gradle +++ b/smoke-test/spring-boot-smoke-test-jetty-ssl/build.gradle @@ -27,7 +27,7 @@ dependencies { } testImplementation(project(":starter:spring-boot-starter-webmvc-test")) - testImplementation(project(":module:spring-boot-restclient")) + testImplementation(project(":module:spring-boot-resttestclient")) testRuntimeOnly("org.apache.httpcomponents.client5:httpclient5") } diff --git a/smoke-test/spring-boot-smoke-test-jetty-ssl/src/test/java/smoketest/jetty/ssl/SampleJettySslApplicationTests.java b/smoke-test/spring-boot-smoke-test-jetty-ssl/src/test/java/smoketest/jetty/ssl/SampleJettySslApplicationTests.java index f2dc93b176df..2c22ad3a6ae8 100644 --- a/smoke-test/spring-boot-smoke-test-jetty-ssl/src/test/java/smoketest/jetty/ssl/SampleJettySslApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-jetty-ssl/src/test/java/smoketest/jetty/ssl/SampleJettySslApplicationTests.java @@ -19,14 +19,13 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.TrustAllTlsRequestFactory; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory; import org.springframework.boot.web.server.Ssl; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -36,11 +35,10 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "debug=true") -@AutoConfigureTestRestTemplate class SampleJettySslApplicationTests { - @Autowired - private TestRestTemplate restTemplate; + @LocalServerPort + private int port; @Autowired private AbstractConfigurableWebServerFactory webServerFactory; @@ -54,9 +52,10 @@ void testSsl() { @Test void testHome() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("Hello World"); + RestTestClient client = RestTestClient.bindToServer(TrustAllTlsRequestFactory.create()) + .baseUrl("https://localhost:" + this.port) + .build(); + client.get().uri("/").exchangeSuccessfully().expectBody(String.class).isEqualTo("Hello World"); } } diff --git a/smoke-test/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java b/smoke-test/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java index 12bcd600dd02..c21b21dac82a 100644 --- a/smoke-test/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java @@ -21,17 +21,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; +import org.springframework.test.web.servlet.client.RestTestClient; /** * Basic integration tests for demo application. @@ -43,46 +37,46 @@ * @author Moritz Halbritter */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleJettyApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Value("${server.max-http-request-header-size}") private int maxHttpRequestHeaderSize; @Test void testHome() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("Hello World"); + this.restTestClient.get().uri("/").exchangeSuccessfully().expectBody(String.class).isEqualTo("Hello World"); } @Test void testCompression() { // Jetty HttpClient sends Accept-Encoding: gzip by default - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("Hello World"); + this.restTestClient.get().uri("/").exchangeSuccessfully().expectBody(String.class).isEqualTo("Hello World"); // Jetty HttpClient decodes gzip responses automatically and removes the // Content-Encoding header. We have to assume that the response was gzipped. } @Test void testMaxHttpResponseHeaderSize() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/max-http-response-header", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + this.restTestClient.get() + .uri("/max-http-response-header") + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); } @Test void testMaxHttpRequestHeaderSize() { String headerValue = StringUtil.repeat('A', this.maxHttpRequestHeaderSize + 1); - HttpHeaders headers = new HttpHeaders(); - headers.add("x-max-request-header", headerValue); - HttpEntity<?> httpEntity = new HttpEntity<>(headers); - ResponseEntity<String> entity = this.restTemplate.exchange("/", HttpMethod.GET, httpEntity, String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE); + this.restTestClient.get() + .uri("/") + .headers((headers) -> headers.add("x-max-request-header", headerValue)) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE); } } diff --git a/smoke-test/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java b/smoke-test/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java index 871fe483d21f..243b55a24295 100644 --- a/smoke-test/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java @@ -16,38 +16,35 @@ package smoketest.oauth2.server; -import java.net.URI; -import java.util.List; import java.util.Map; import java.util.Objects; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpRedirects; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; 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.test.web.servlet.client.EntityExchangeResult; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleOAuth2AuthorizationServerApplicationTests { private static final ParameterizedTypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new ParameterizedTypeReference<>() { @@ -57,14 +54,24 @@ class SampleOAuth2AuthorizationServerApplicationTests { private int port; @Autowired - private TestRestTemplate restTemplate; + private RestTestClient rest; + + private RestTestClient nonFollowingRedirect() { + return RestTestClient + .bindToServer(ClientHttpRequestFactoryBuilder.detect() + .build(HttpClientSettings.defaults().withRedirects(HttpRedirects.DONT_FOLLOW))) + .baseUrl("http://localhost:" + this.port) + .build(); + } @Test void openidConfigurationShouldAllowAccess() { - ResponseEntity<Map<String, Object>> entity = this.restTemplate.exchange("/.well-known/openid-configuration", - HttpMethod.GET, null, MAP_TYPE_REFERENCE); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - Map<String, Object> body = entity.getBody(); + EntityExchangeResult<Map<String, Object>> result = this.rest.get() + .uri("/.well-known/openid-configuration") + .exchange() + .returnResult(MAP_TYPE_REFERENCE); + assertThat(result.getStatus()).isEqualTo(HttpStatus.OK); + Map<String, Object> body = result.getResponseBody(); assertThat(body).isNotNull(); OidcProviderConfiguration config = OidcProviderConfiguration.withClaims(body).build(); assertThat(config.getIssuer()).hasToString("https://provider.com"); @@ -82,10 +89,12 @@ void openidConfigurationShouldAllowAccess() { @Test void authServerMetadataShouldAllowAccess() { - ResponseEntity<Map<String, Object>> entity = this.restTemplate - .exchange("/.well-known/oauth-authorization-server", HttpMethod.GET, null, MAP_TYPE_REFERENCE); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - Map<String, Object> body = entity.getBody(); + EntityExchangeResult<Map<String, Object>> result = this.rest.get() + .uri("/.well-known/oauth-authorization-server") + .exchange() + .returnResult(MAP_TYPE_REFERENCE); + assertThat(result.getStatus()).isEqualTo(HttpStatus.OK); + Map<String, Object> body = result.getResponseBody(); assertThat(body).isNotNull(); OAuth2AuthorizationServerMetadata config = OAuth2AuthorizationServerMetadata.withClaims(body).build(); assertThat(config.getIssuer()).hasToString("https://provider.com"); @@ -101,26 +110,26 @@ void authServerMetadataShouldAllowAccess() { @Test void anonymousShouldRedirectToLogin() { - ResponseEntity<String> entity = this.restTemplate.withRedirects(HttpRedirects.DONT_FOLLOW) - .getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); - assertThat(entity.getHeaders().getLocation()).isEqualTo(URI.create("http://localhost:" + this.port + "/login")); + RestTestClient.ResponseSpec response = nonFollowingRedirect().get().uri("/").exchange(); + response.expectStatus().isFound(); + response.expectHeader().location("http://localhost:" + this.port + "/login"); } @Test void validTokenRequestShouldReturnTokenResponse() { - HttpHeaders headers = new HttpHeaders(); - headers.setBasicAuth("messaging-client", "secret"); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap<String, Object> 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<Object> request = new HttpEntity<>(body, headers); - ResponseEntity<Map<String, Object>> entity = this.restTemplate.exchange("/token", HttpMethod.POST, request, - MAP_TYPE_REFERENCE); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - Map<String, Object> tokenResponse = Objects.requireNonNull(entity.getBody()); + EntityExchangeResult<Map<String, Object>> result = this.rest.post() + .uri("/token") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .headers((headers) -> headers.setBasicAuth("messaging-client", "secret")) + .body(body) + .exchange() + .returnResult(MAP_TYPE_REFERENCE); + assertThat(result.getStatus()).isEqualTo(HttpStatus.OK); + Map<String, Object> tokenResponse = Objects.requireNonNull(result.getResponseBody()); assertThat(tokenResponse.get(OAuth2ParameterNames.ACCESS_TOKEN)).isNotNull(); assertThat(tokenResponse.get(OAuth2ParameterNames.EXPIRES_IN)).isNotNull(); assertThat(tokenResponse.get(OAuth2ParameterNames.SCOPE)).isEqualTo("message.read message.write"); @@ -130,47 +139,49 @@ void validTokenRequestShouldReturnTokenResponse() { @Test void anonymousTokenRequestShouldReturnUnauthorized() { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap<String, Object> 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<Object> request = new HttpEntity<>(body, headers); - ResponseEntity<Map<String, Object>> entity = this.restTemplate.exchange("/token", HttpMethod.POST, request, - MAP_TYPE_REFERENCE); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + EntityExchangeResult<Map<String, Object>> result = this.rest.post() + .uri("/token") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(body) + .exchange() + .returnResult(MAP_TYPE_REFERENCE); + assertThat(result.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test void anonymousTokenRequestWithAcceptHeaderAllShouldReturnUnauthorized() { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - headers.setAccept(List.of(MediaType.ALL)); MultiValueMap<String, Object> 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<Object> request = new HttpEntity<>(body, headers); - ResponseEntity<Map<String, Object>> entity = this.restTemplate.exchange("/token", HttpMethod.POST, request, - MAP_TYPE_REFERENCE); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + EntityExchangeResult<Map<String, Object>> result = this.rest.post() + .uri("/token") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .accept(MediaType.ALL) + .body(body) + .exchange() + .returnResult(MAP_TYPE_REFERENCE); + assertThat(result.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test void anonymousTokenRequestWithAcceptHeaderTextHtmlShouldRedirectToLogin() { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - headers.setAccept(List.of(MediaType.TEXT_HTML)); MultiValueMap<String, Object> 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<Object> request = new HttpEntity<>(body, headers); - ResponseEntity<Map<String, Object>> entity = this.restTemplate.withRedirects(HttpRedirects.DONT_FOLLOW) - .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")); + RestTestClient.ResponseSpec response = nonFollowingRedirect().post() + .uri("/token") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .accept(MediaType.TEXT_HTML) + .body(body) + .exchange(); + response.expectStatus().isFound(); + response.expectHeader().location("http://localhost:" + this.port + "/login"); } } diff --git a/smoke-test/spring-boot-smoke-test-oauth2-client/src/test/java/smoketest/oauth2/client/SampleOAuth2ClientApplicationTests.java b/smoke-test/spring-boot-smoke-test-oauth2-client/src/test/java/smoketest/oauth2/client/SampleOAuth2ClientApplicationTests.java index 64c268c62a95..63e68d23806b 100644 --- a/smoke-test/spring-boot-smoke-test-oauth2-client/src/test/java/smoketest/oauth2/client/SampleOAuth2ClientApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-oauth2-client/src/test/java/smoketest/oauth2/client/SampleOAuth2ClientApplicationTests.java @@ -16,49 +16,56 @@ package smoketest.oauth2.client; -import java.net.URI; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpRedirects; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { "APP-CLIENT-ID=my-client-id", "APP-CLIENT-SECRET=my-client-secret", "YAHOO-CLIENT-ID=my-yahoo-client-id", "YAHOO-CLIENT-SECRET=my-yahoo-client-secret" }) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleOAuth2ClientApplicationTests { @LocalServerPort private int port; @Autowired - private TestRestTemplate restTemplate; + private RestTestClient rest; + + private RestTestClient nonFollowingRedirect() { + return RestTestClient + .bindToServer(ClientHttpRequestFactoryBuilder.detect() + .build(HttpClientSettings.defaults().withRedirects(HttpRedirects.DONT_FOLLOW))) + .baseUrl("http://localhost:" + this.port) + .build(); + } @Test void everythingShouldRedirectToLogin() { - ResponseEntity<String> entity = this.restTemplate.withRedirects(HttpRedirects.DONT_FOLLOW) - .getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); - assertThat(entity.getHeaders().getLocation()).isEqualTo(URI.create("http://localhost:" + this.port + "/login")); + RestTestClient.ResponseSpec response = nonFollowingRedirect().get().uri("/").exchange(); + response.expectStatus().isFound(); + response.expectHeader().location("http://localhost:" + this.port + "/login"); } @Test void loginShouldHaveAllOAuth2ClientsToChooseFrom() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/login", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("/oauth2/authorization/yahoo"); - assertThat(entity.getBody()).contains("/oauth2/authorization/github-client-1"); - assertThat(entity.getBody()).contains("/oauth2/authorization/github-client-2"); - assertThat(entity.getBody()).contains("/oauth2/authorization/github-repos"); + this.rest.get() + .uri("/login") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("/oauth2/authorization/yahoo") + .contains("/oauth2/authorization/github-client-1") + .contains("/oauth2/authorization/github-client-2") + .contains("/oauth2/authorization/github-repos")); } } diff --git a/smoke-test/spring-boot-smoke-test-oauth2-resource-server/src/test/java/smoketest/oauth2/resource/SampleOauth2ResourceServerApplicationTests.java b/smoke-test/spring-boot-smoke-test-oauth2-resource-server/src/test/java/smoketest/oauth2/resource/SampleOauth2ResourceServerApplicationTests.java index bb4b93c88c55..eb56c2439d81 100644 --- a/smoke-test/spring-boot-smoke-test-oauth2-resource-server/src/test/java/smoketest/oauth2/resource/SampleOauth2ResourceServerApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-oauth2-resource-server/src/test/java/smoketest/oauth2/resource/SampleOauth2ResourceServerApplicationTests.java @@ -25,20 +25,14 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; +import org.springframework.test.web.servlet.client.RestTestClient; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleOauth2ResourceServerApplicationTests { private static final MockWebServer server = new MockWebServer(); @@ -50,7 +44,7 @@ class SampleOauth2ResourceServerApplicationTests { + "R44vmRqS5ncrF-1R0EGcPX49U6A"; @Autowired - private TestRestTemplate restTemplate; + private RestTestClient rest; @BeforeAll static void setup() throws Exception { @@ -68,19 +62,17 @@ static void shutdown() throws IOException { @Test void withValidBearerTokenShouldAllowAccess() { - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(VALID_TOKEN); - HttpEntity<?> request = new HttpEntity<Void>(headers); - ResponseEntity<String> entity = this.restTemplate.exchange("/", HttpMethod.GET, request, String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + this.rest.get() + .uri("/") + .headers((headers) -> headers.setBearerAuth(VALID_TOKEN)) + .exchange() + .expectStatus() + .isOk(); } @Test void withNoBearerTokenShouldNotAllowAccess() { - HttpHeaders headers = new HttpHeaders(); - HttpEntity<?> request = new HttpEntity<Void>(headers); - ResponseEntity<String> entity = this.restTemplate.exchange("/", HttpMethod.GET, request, String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + this.rest.get().uri("/").exchange().expectStatus().isUnauthorized(); } private static MockResponse mockResponse() { diff --git a/smoke-test/spring-boot-smoke-test-prometheus/src/test/java/smoketest/prometheus/SamplePrometheusApplicationTests.java b/smoke-test/spring-boot-smoke-test-prometheus/src/test/java/smoketest/prometheus/SamplePrometheusApplicationTests.java index ca615561db23..5b5f158ceded 100644 --- a/smoke-test/spring-boot-smoke-test-prometheus/src/test/java/smoketest/prometheus/SamplePrometheusApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-prometheus/src/test/java/smoketest/prometheus/SamplePrometheusApplicationTests.java @@ -20,15 +20,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.micrometer.metrics.test.autoconfigure.AutoConfigureMetrics; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpEntity; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -39,22 +37,26 @@ */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @AutoConfigureMetrics -@AutoConfigureTestRestTemplate class SamplePrometheusApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestClient.Builder restClientBuilder; + + @LocalServerPort + private int port; @Test void shouldExportExemplars() { + RestClient restClient = this.restClientBuilder.baseUrl("http://localhost:" + this.port).build(); for (int i = 0; i < 10; i++) { - ResponseEntity<String> response = this.restTemplate.getForEntity("/actuator", String.class); + ResponseEntity<Void> response = restClient.get().uri("/actuator").retrieve().toBodilessEntity(); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.ACCEPT, "application/openmetrics-text; version=1.0.0; charset=utf-8"); - ResponseEntity<String> metrics = this.restTemplate.exchange("/actuator/prometheus", HttpMethod.GET, - new HttpEntity<>(headers), String.class); + ResponseEntity<String> metrics = restClient.get() + .uri("/actuator/prometheus") + .header(HttpHeaders.ACCEPT, "application/openmetrics-text; version=1.0.0; charset=utf-8") + .retrieve() + .toEntity(String.class); assertThat(metrics.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(metrics.getBody()).containsSubsequence("http_client_requests_seconds_count", "span_id", "trace_id"); } diff --git a/smoke-test/spring-boot-smoke-test-quartz/src/test/java/smoketest/quartz/SampleQuartzApplicationWebTests.java b/smoke-test/spring-boot-smoke-test-quartz/src/test/java/smoketest/quartz/SampleQuartzApplicationWebTests.java index 956b28e92be1..7a3e960299cd 100644 --- a/smoke-test/spring-boot-smoke-test-quartz/src/test/java/smoketest/quartz/SampleQuartzApplicationWebTests.java +++ b/smoke-test/spring-boot-smoke-test-quartz/src/test/java/smoketest/quartz/SampleQuartzApplicationWebTests.java @@ -25,20 +25,18 @@ import org.assertj.core.api.InstanceOfAssertFactory; import org.assertj.core.api.MapAssert; import org.awaitility.Awaitility; -import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.test.web.servlet.client.RestTestClient.BodySpec; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -51,84 +49,88 @@ */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @ExtendWith(OutputCaptureExtension.class) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleQuartzApplicationWebTests { + private static final ParameterizedTypeReference<Map<String, Object>> MAP_TYPE = new ParameterizedTypeReference<>() { + }; + @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test void quartzGroupNames() { - Map<String, Object> content = getContent("/actuator/quartz"); - assertThat(content).containsOnlyKeys("jobs", "triggers"); + assertContent("/actuator/quartz").value((content) -> assertThat(content).containsOnlyKeys("jobs", "triggers")); } @Test void quartzJobGroups() { - Map<String, Object> content = getContent("/actuator/quartz/jobs"); - assertThat(content).containsOnlyKeys("groups"); - assertThat(content).extractingByKey("groups", nestedMap()).containsOnlyKeys("samples"); + assertContent("/actuator/quartz/jobs").value((content) -> { + assertThat(content).containsOnlyKeys("groups"); + assertThat(content).extractingByKey("groups", nestedMap()).containsOnlyKeys("samples"); + }); } @Test void quartzTriggerGroups() { - Map<String, Object> content = getContent("/actuator/quartz/triggers"); - assertThat(content).containsOnlyKeys("groups"); - assertThat(content).extractingByKey("groups", nestedMap()).containsOnlyKeys("DEFAULT", "samples"); + assertContent("/actuator/quartz/triggers").value((content) -> { + assertThat(content).containsOnlyKeys("groups"); + assertThat(content).extractingByKey("groups", nestedMap()).containsOnlyKeys("DEFAULT", "samples"); + }); } @Test void quartzJobDetail() { - Map<String, Object> content = getContent("/actuator/quartz/jobs/samples/helloJob"); - assertThat(content).containsEntry("name", "helloJob").containsEntry("group", "samples"); + assertContent("/actuator/quartz/jobs/samples/helloJob").value( + (content) -> assertThat(content).containsEntry("name", "helloJob").containsEntry("group", "samples")); } @Test void quartzJobDetailWhenNameDoesNotExistReturns404() { - ResponseEntity<String> response = this.restTemplate.getForEntity("/actuator/quartz/jobs/samples/does-not-exist", - String.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + this.restTestClient.get() + .uri("/actuator/quartz/jobs/samples/does-not-exist") + .exchange() + .expectStatus() + .isNotFound(); } @Test void quartzTriggerDetail() { - Map<String, Object> content = getContent("/actuator/quartz/triggers/samples/3am-weekdays"); - assertThat(content).contains(entry("group", "samples"), entry("name", "3am-weekdays"), entry("state", "NORMAL"), - entry("type", "cron")); + assertContent("/actuator/quartz/triggers/samples/3am-weekdays") + .value((content) -> assertThat(content).contains(entry("group", "samples"), entry("name", "3am-weekdays"), + entry("state", "NORMAL"), entry("type", "cron"))); } @Test void quartzTriggerDetailWhenNameDoesNotExistReturns404() { - ResponseEntity<String> response = this.restTemplate - .getForEntity("/actuator/quartz/triggers/samples/does-not-exist", String.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + this.restTestClient.get() + .uri("/actuator/quartz/triggers/samples/does-not-exist") + .exchange() + .expectStatus() + .isNotFound(); } @Test void quartzJobTriggeredManually(CapturedOutput output) { - ResponseEntity<Map<String, Object>> result = asMapEntity(this.restTemplate.postForEntity( - "/actuator/quartz/jobs/samples/onDemandJob", new HttpEntity<>(Map.of("state", "running")), Map.class)); - assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); - Map<String, Object> content = result.getBody(); - assertThat(content).contains(entry("group", "samples"), entry("name", "onDemandJob"), - entry("className", SampleJob.class.getName())); - assertThat(content).extractingByKey("triggerTime", InstanceOfAssertFactories.STRING) - .satisfies((triggerTime) -> assertThat(Instant.parse(triggerTime)).isCloseTo(Instant.now(), - within(10, ChronoUnit.SECONDS))); + this.restTestClient.post() + .uri("/actuator/quartz/jobs/samples/onDemandJob") + .body(Map.of("state", "running")) + .exchangeSuccessfully() + .expectBody(MAP_TYPE) + .value((content) -> { + assertThat(content).contains(entry("group", "samples"), entry("name", "onDemandJob"), + entry("className", SampleJob.class.getName())); + assertThat(content).extractingByKey("triggerTime", InstanceOfAssertFactories.STRING) + .satisfies((triggerTime) -> assertThat(Instant.parse(triggerTime)).isCloseTo(Instant.now(), + within(10, ChronoUnit.SECONDS))); + }); Awaitility.await() .atMost(Duration.ofSeconds(30)) .untilAsserted(() -> assertThat(output).contains("Hello On Demand Job")); } - private @Nullable Map<String, Object> getContent(String path) { - ResponseEntity<Map<String, Object>> entity = asMapEntity(this.restTemplate.getForEntity(path, Map.class)); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - return entity.getBody(); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private static <K, V> ResponseEntity<Map<K, V>> asMapEntity(ResponseEntity<Map> entity) { - return (ResponseEntity) entity; + private BodySpec<Map<String, Object>, ?> assertContent(String path) { + return this.restTestClient.get().uri(path).exchangeSuccessfully().expectBody(MAP_TYPE); } @SuppressWarnings("rawtypes") diff --git a/smoke-test/spring-boot-smoke-test-saml2-service-provider/src/test/java/smoketest/saml2/serviceprovider/SampleSaml2RelyingPartyApplicationTests.java b/smoke-test/spring-boot-smoke-test-saml2-service-provider/src/test/java/smoketest/saml2/serviceprovider/SampleSaml2RelyingPartyApplicationTests.java index 1123740a0247..e3e130bfbb9b 100644 --- a/smoke-test/spring-boot-smoke-test-saml2-service-provider/src/test/java/smoketest/saml2/serviceprovider/SampleSaml2RelyingPartyApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-saml2-service-provider/src/test/java/smoketest/saml2/serviceprovider/SampleSaml2RelyingPartyApplicationTests.java @@ -16,45 +16,52 @@ package smoketest.saml2.serviceprovider; -import java.net.URI; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpRedirects; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleSaml2RelyingPartyApplicationTests { @LocalServerPort private int port; @Autowired - private TestRestTemplate restTemplate; + private RestTestClient rest; + + private RestTestClient nonFollowingRedirect() { + return RestTestClient + .bindToServer(ClientHttpRequestFactoryBuilder.detect() + .build(HttpClientSettings.defaults().withRedirects(HttpRedirects.DONT_FOLLOW))) + .baseUrl("http://localhost:" + this.port) + .build(); + } @Test void everythingShouldRedirectToLogin() { - ResponseEntity<String> entity = this.restTemplate.withRedirects(HttpRedirects.DONT_FOLLOW) - .getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); - assertThat(entity.getHeaders().getLocation()).isEqualTo(URI.create("http://localhost:" + this.port + "/login")); + RestTestClient.ResponseSpec response = nonFollowingRedirect().get().uri("/").exchange(); + response.expectStatus().isFound(); + response.expectHeader().location("http://localhost:" + this.port + "/login"); } @Test void loginShouldHaveAllAssertingPartiesToChooseFrom() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/login", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("/saml2/authenticate?registrationId=simplesamlphp"); - assertThat(entity.getBody()).contains("/saml2/authenticate?registrationId=okta"); + this.rest.get() + .uri("/login") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("/saml2/authenticate?registrationId=simplesamlphp") + .contains("/saml2/authenticate?registrationId=okta")); } } diff --git a/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/AbstractJerseySecureTests.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/AbstractJerseySecureTests.java index 1c76932cdd75..a6aacc481150 100644 --- a/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/AbstractJerseySecureTests.java +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/AbstractJerseySecureTests.java @@ -18,11 +18,10 @@ import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -31,133 +30,126 @@ * * @author Madhura Bhave */ -@AutoConfigureTestRestTemplate abstract class AbstractJerseySecureTests { + private final RestClient restClient = RestClient.create(); + abstract String getPath(); abstract String getManagementPath(); - @Autowired - private TestRestTemplate testRestTemplate; - @Test void helloEndpointIsSecure() { - ResponseEntity<String> entity = restTemplate().getForEntity(getPath() + "/hello", String.class); + ResponseEntity<String> entity = getForEntity(restClient(), getPath() + "/hello"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test void actuatorInsecureEndpoint() { - ResponseEntity<String> entity = restTemplate().getForEntity(getManagementPath() + "/actuator/health", - String.class); + ResponseEntity<String> entity = getForEntity(restClient(), getManagementPath() + "/actuator/health"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).contains("\"status\":\"UP\""); - entity = restTemplate().getForEntity(getManagementPath() + "/actuator/health/diskSpace", String.class); + entity = getForEntity(restClient(), getManagementPath() + "/actuator/health/diskSpace"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).contains("\"status\":\"UP\""); } @Test void actuatorLinksWithAnonymous() { - ResponseEntity<String> entity = restTemplate().getForEntity(getManagementPath() + "/actuator", String.class); + ResponseEntity<String> entity = getForEntity(restClient(), getManagementPath() + "/actuator"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - entity = restTemplate().getForEntity(getManagementPath() + "/actuator/", String.class); + entity = getForEntity(restClient(), getManagementPath() + "/actuator/"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test void actuatorLinksWithUnauthorizedUser() { - ResponseEntity<String> entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator", - String.class); + ResponseEntity<String> entity = getForEntity(userRestClient(), getManagementPath() + "/actuator"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/", String.class); + entity = getForEntity(userRestClient(), getManagementPath() + "/actuator/"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); } @Test void actuatorLinksWithAuthorizedUser() { - ResponseEntity<String> entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator", - String.class); + ResponseEntity<String> entity = getForEntity(adminRestClient(), getManagementPath() + "/actuator"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - adminRestTemplate().getForEntity(getManagementPath() + "/actuator/", String.class); + getForEntity(adminRestClient(), getManagementPath() + "/actuator/"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } @Test void actuatorSecureEndpointWithAnonymous() { - ResponseEntity<String> entity = restTemplate().getForEntity(getManagementPath() + "/actuator/env", - String.class); + ResponseEntity<String> entity = getForEntity(restClient(), getManagementPath() + "/actuator/env"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - entity = restTemplate().getForEntity( - getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include", String.class); + entity = getForEntity(restClient(), + getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test void actuatorSecureEndpointWithUnauthorizedUser() { - ResponseEntity<String> entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/env", - String.class); + ResponseEntity<String> entity = getForEntity(userRestClient(), getManagementPath() + "/actuator/env"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - entity = userRestTemplate().getForEntity( - getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include", String.class); + entity = getForEntity(userRestClient(), + getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); } @Test void actuatorSecureEndpointWithAuthorizedUser() { - ResponseEntity<String> entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator/env", - String.class); + ResponseEntity<String> entity = getForEntity(adminRestClient(), getManagementPath() + "/actuator/env"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - entity = adminRestTemplate().getForEntity( - getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include", String.class); + entity = getForEntity(adminRestClient(), + getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } @Test void secureServletEndpointWithAnonymous() { - ResponseEntity<String> entity = restTemplate().getForEntity(getManagementPath() + "/actuator/se1", - String.class); + ResponseEntity<String> entity = getForEntity(restClient(), getManagementPath() + "/actuator/se1"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - entity = restTemplate().getForEntity(getManagementPath() + "/actuator/se1/list", String.class); + entity = getForEntity(restClient(), getManagementPath() + "/actuator/se1/list"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test void secureServletEndpointWithUnauthorizedUser() { - ResponseEntity<String> entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/se1", - String.class); + ResponseEntity<String> entity = getForEntity(userRestClient(), getManagementPath() + "/actuator/se1"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/se1/list", String.class); + entity = getForEntity(userRestClient(), getManagementPath() + "/actuator/se1/list"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); } @Test void secureServletEndpointWithAuthorizedUser() { - ResponseEntity<String> entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator/se1", - String.class); + ResponseEntity<String> entity = getForEntity(adminRestClient(), getManagementPath() + "/actuator/se1"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator/se1/list", String.class); + entity = getForEntity(adminRestClient(), getManagementPath() + "/actuator/se1/list"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } @Test void actuatorExcludedFromEndpointRequestMatcher() { - ResponseEntity<String> entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/mappings", - String.class); + ResponseEntity<String> entity = getForEntity(userRestClient(), getManagementPath() + "/actuator/mappings"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } - TestRestTemplate restTemplate() { - return this.testRestTemplate; + RestClient restClient() { + return this.restClient; + } + + RestClient adminRestClient() { + return RestClient.builder().defaultHeaders((headers) -> headers.setBasicAuth("admin", "admin")).build(); } - TestRestTemplate adminRestTemplate() { - return this.testRestTemplate.withBasicAuth("admin", "admin"); + RestClient userRestClient() { + return RestClient.builder().defaultHeaders((headers) -> headers.setBasicAuth("user", "password")).build(); } - TestRestTemplate userRestTemplate() { - return this.testRestTemplate.withBasicAuth("user", "password"); + static ResponseEntity<String> getForEntity(RestClient client, String uri) { + return client.get().uri(uri).retrieve().onStatus(HttpStatusCode::isError, (request, response) -> { + }).toEntity(String.class); } } diff --git a/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java index 1641ab8f8f24..448add3e6532 100644 --- a/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java @@ -18,7 +18,6 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalManagementPort; @@ -47,8 +46,8 @@ class ManagementPortAndPathJerseyApplicationTests extends AbstractJerseySecureTe @Test void testMissing() { - ResponseEntity<String> entity = new TestRestTemplate("admin", "admin") - .getForEntity("http://localhost:" + this.managementPort + "/management/actuator/missing", String.class); + ResponseEntity<String> entity = getForEntity(adminRestClient(), + "http://localhost:" + this.managementPort + "/management/actuator/missing"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); } diff --git a/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java index 84442ce90974..e1d6d58d0e7b 100644 --- a/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java @@ -18,7 +18,6 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalManagementPort; import org.springframework.boot.test.web.server.LocalServerPort; @@ -45,8 +44,8 @@ class ManagementPortCustomApplicationPathJerseyTests extends AbstractJerseySecur @Test void actuatorPathOnMainPortShouldNotMatch() { - ResponseEntity<String> entity = new TestRestTemplate() - .getForEntity("http://localhost:" + this.port + "/example/actuator/health", String.class); + ResponseEntity<String> entity = getForEntity(restClient(), + "http://localhost:" + this.port + "/example/actuator/health"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } diff --git a/smoke-test/spring-boot-smoke-test-servlet/build.gradle b/smoke-test/spring-boot-smoke-test-servlet/build.gradle index fc5cf715f793..694e486ba8cc 100644 --- a/smoke-test/spring-boot-smoke-test-servlet/build.gradle +++ b/smoke-test/spring-boot-smoke-test-servlet/build.gradle @@ -26,7 +26,6 @@ dependencies { implementation(project(":starter:spring-boot-starter-security")) testImplementation(project(":module:spring-boot-restclient")) - testImplementation(project(":module:spring-boot-resttestclient")) testImplementation(project(":starter:spring-boot-starter-test")) testRuntimeOnly(project(":starter:spring-boot-starter-tomcat")) diff --git a/smoke-test/spring-boot-smoke-test-servlet/src/test/java/smoketest/servlet/SampleServletApplicationTests.java b/smoke-test/spring-boot-smoke-test-servlet/src/test/java/smoketest/servlet/SampleServletApplicationTests.java index 4cb9d124a75f..7043e8c1909b 100644 --- a/smoke-test/spring-boot-smoke-test-servlet/src/test/java/smoketest/servlet/SampleServletApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-servlet/src/test/java/smoketest/servlet/SampleServletApplicationTests.java @@ -16,21 +16,16 @@ package smoketest.servlet; -import java.util.Collections; - import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -40,29 +35,35 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate class SampleServletApplicationTests { - @Autowired - private TestRestTemplate restTemplate; + @LocalServerPort + private int port; @Test void testHomeIsSecure() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - ResponseEntity<String> entity = this.restTemplate.exchange("/", HttpMethod.GET, new HttpEntity<>(headers), - String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + HttpStatusCode status = restClient().get() + .uri("/") + .accept(MediaType.APPLICATION_JSON) + .exchange((request, response) -> response.getStatusCode()); + assertThat(status).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test void testHome() { - ResponseEntity<String> entity = this.restTemplate.withBasicAuth("user", getPassword()) - .getForEntity("/", String.class); + ResponseEntity<String> entity = restClient().get() + .uri("/") + .headers((headers) -> headers.setBasicAuth("user", getPassword())) + .retrieve() + .toEntity(String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).isEqualTo("Hello World"); } + private RestClient restClient() { + return RestClient.create("http://localhost:" + this.port); + } + private String getPassword() { return "password"; } diff --git a/smoke-test/spring-boot-smoke-test-session-data-redis/src/dockerTest/java/smoketest/session/redis/SampleSessionRedisApplicationTests.java b/smoke-test/spring-boot-smoke-test-session-data-redis/src/dockerTest/java/smoketest/session/redis/SampleSessionRedisApplicationTests.java index 8357eea38cfb..7d3da8cac43f 100644 --- a/smoke-test/spring-boot-smoke-test-session-data-redis/src/dockerTest/java/smoketest/session/redis/SampleSessionRedisApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-session-data-redis/src/dockerTest/java/smoketest/session/redis/SampleSessionRedisApplicationTests.java @@ -16,8 +16,6 @@ package smoketest.session.redis; -import java.net.URI; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -27,19 +25,14 @@ import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.RequestEntity; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.EntityExchangeResult; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -55,7 +48,7 @@ */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Testcontainers(disabledWithoutDocker = true) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleSessionRedisApplicationTests { @Container @@ -63,42 +56,39 @@ class SampleSessionRedisApplicationTests { static RedisContainer redis = TestImage.container(RedisContainer.class); @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test @SuppressWarnings("unchecked") void sessionsEndpointShouldReturnUserSessions() { performLogin(); - ResponseEntity<Map<String, Object>> response = getSessions(); - assertThat(response).isNotNull(); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - Map<String, Object> body = response.getBody(); + Map<String, Object> body = getSessions().getResponseBody(); assertThat(body).isNotNull(); List<Map<String, Object>> sessions = (List<Map<String, Object>>) body.get("sessions"); assertThat(sessions).hasSize(1); } private void performLogin() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML)); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap<String, String> form = new LinkedMultiValueMap<>(); form.set("username", "user"); form.set("password", "password"); - this.restTemplate.exchange("/login", HttpMethod.POST, new HttpEntity<>(form, headers), String.class); + this.restTestClient.post() + .uri("/login") + .accept(MediaType.TEXT_HTML) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(form) + .exchange(); } - private RequestEntity<Object> getRequestEntity(URI uri) { - HttpHeaders headers = new HttpHeaders(); - headers.setBasicAuth("user", "password"); - return new RequestEntity<>(headers, HttpMethod.GET, uri); - } - - private ResponseEntity<Map<String, Object>> getSessions() { - RequestEntity<Object> request = getRequestEntity(URI.create("/actuator/sessions?username=user")); + private EntityExchangeResult<Map<String, Object>> getSessions() { ParameterizedTypeReference<Map<String, Object>> stringObjectMap = new ParameterizedTypeReference<>() { }; - return this.restTemplate.exchange(request, stringObjectMap); + return this.restTestClient.get() + .uri("/actuator/sessions?username=user") + .headers((headers) -> headers.setBasicAuth("user", "password")) + .exchangeSuccessfully() + .expectBody(stringObjectMap) + .returnResult(); } } diff --git a/smoke-test/spring-boot-smoke-test-session-jdbc/src/test/java/smoketest/session/SampleSessionJdbcApplicationTests.java b/smoke-test/spring-boot-smoke-test-session-jdbc/src/test/java/smoketest/session/SampleSessionJdbcApplicationTests.java index bf669174b174..989bc47f24ab 100644 --- a/smoke-test/spring-boot-smoke-test-session-jdbc/src/test/java/smoketest/session/SampleSessionJdbcApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-session-jdbc/src/test/java/smoketest/session/SampleSessionJdbcApplicationTests.java @@ -28,18 +28,16 @@ import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpRedirects; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.test.web.servlet.client.EntityExchangeResult; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; @@ -55,14 +53,14 @@ */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { "server.servlet.session.timeout:2", "debug=true" }) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleSessionJdbcApplicationTests { private static final HttpClientSettings DONT_FOLLOW_REDIRECTS = HttpClientSettings.defaults() .withRedirects(HttpRedirects.DONT_FOLLOW); @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @LocalServerPort @SuppressWarnings("NullAway.Init") @@ -70,14 +68,23 @@ class SampleSessionJdbcApplicationTests { private static final URI ROOT_URI = URI.create("/"); + /** + * A client that follows redirects, used to emulate a browser session. + */ + private RestTestClient browserClient() { + ClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder.detect() + .build(HttpClientSettings.defaults()); + return RestTestClient.bindToServer(requestFactory).baseUrl("http://localhost:" + this.port).build(); + } + @Test void sessionExpiry() throws Exception { String cookie = performLogin(); - String sessionId1 = performRequest(ROOT_URI, cookie).getBody(); - String sessionId2 = performRequest(ROOT_URI, cookie).getBody(); + String sessionId1 = performRequest(ROOT_URI, cookie).getResponseBody(); + String sessionId2 = performRequest(ROOT_URI, cookie).getResponseBody(); assertThat(sessionId1).isEqualTo(sessionId2); Thread.sleep(2100); - String loginPage = performRequest(ROOT_URI, cookie).getBody(); + String loginPage = performRequest(ROOT_URI, cookie).getResponseBody(); assertThat(loginPage).containsIgnoringCase("login"); } @@ -101,19 +108,20 @@ void sessionExpiry() throws Exception { @SuppressWarnings("unchecked") void sessionsEndpointShouldReturnUserSession() { performLogin(); - ResponseEntity<Map<String, Object>> response = getSessions(); - assertThat(response).isNotNull(); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - Map<String, Object> body = response.getBody(); + Map<String, Object> body = getSessions().getResponseBody(); assertThat(body).isNotNull(); List<Map<String, Object>> sessions = (List<Map<String, Object>>) body.get("sessions"); assertThat(sessions).hasSize(1); } - private ResponseEntity<String> performRequest(URI uri, @Nullable String cookie) { - HttpHeaders headers = getHeaders(cookie); - RequestEntity<Object> request = new RequestEntity<>(headers, HttpMethod.GET, uri); - return this.restTemplate.exchange(request, String.class); + private EntityExchangeResult<String> performRequest(URI uri, @Nullable String cookie) { + return browserClient().get() + .uri(uri) + .accept(MediaType.TEXT_HTML) + .headers((headers) -> headers.addAll(getHeaders(cookie))) + .exchangeSuccessfully() + .expectBody(String.class) + .returnResult(); } private HttpHeaders getHeaders(@Nullable String cookie) { @@ -131,13 +139,15 @@ private String getBasicAuth() { return "Basic " + Base64.getEncoder().encodeToString("user:password".getBytes()); } - private ResponseEntity<Map<String, Object>> getSessions() { - HttpHeaders headers = getHeaders(null); - RequestEntity<Object> request = new RequestEntity<>(headers, HttpMethod.GET, - URI.create("/actuator/sessions?username=user")); + private EntityExchangeResult<Map<String, Object>> getSessions() { ParameterizedTypeReference<Map<String, Object>> stringObjectMap = new ParameterizedTypeReference<>() { }; - return this.restTemplate.exchange(request, stringObjectMap); + return this.restTestClient.get() + .uri("/actuator/sessions?username=user") + .headers((headers) -> headers.addAll(getHeaders(null))) + .exchangeSuccessfully() + .expectBody(stringObjectMap) + .returnResult(); } } diff --git a/smoke-test/spring-boot-smoke-test-test/src/test/java/smoketest/test/SampleTestApplicationWebIntegrationTests.java b/smoke-test/spring-boot-smoke-test-test/src/test/java/smoketest/test/SampleTestApplicationWebIntegrationTests.java index bbe455b73e75..dcfe270afc12 100644 --- a/smoke-test/spring-boot-smoke-test-test/src/test/java/smoketest/test/SampleTestApplicationWebIntegrationTests.java +++ b/smoke-test/spring-boot-smoke-test-test/src/test/java/smoketest/test/SampleTestApplicationWebIntegrationTests.java @@ -24,14 +24,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpStatus; import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.client.RestTestClient; -import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; /** @@ -42,13 +40,13 @@ */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @AutoConfigureTestDatabase -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleTestApplicationWebIntegrationTests { private static final VehicleIdentificationNumber VIN = new VehicleIdentificationNumber("01234567890123456"); @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @MockitoBean private VehicleDetailsService vehicleDetailsService; @@ -60,8 +58,7 @@ void setup() { @Test void test() { - assertThat(this.restTemplate.getForEntity("/{username}/vehicle", String.class, "sframework").getStatusCode()) - .isEqualTo(HttpStatus.OK); + this.restTestClient.get().uri("/{username}/vehicle", "sframework").exchange().expectStatus().isOk(); } } diff --git a/smoke-test/spring-boot-smoke-test-testng/src/test/java/smoketest/testng/SampleTestNGApplicationTests.java b/smoke-test/spring-boot-smoke-test-testng/src/test/java/smoketest/testng/SampleTestNGApplicationTests.java index 43dd3da3bc5a..6468ca6a8211 100644 --- a/smoke-test/spring-boot-smoke-test-testng/src/test/java/smoketest/testng/SampleTestNGApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-testng/src/test/java/smoketest/testng/SampleTestNGApplicationTests.java @@ -19,15 +19,11 @@ import org.testng.annotations.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; - -import static org.assertj.core.api.Assertions.assertThat; +import org.springframework.test.web.servlet.client.RestTestClient; /** * Basic integration tests for demo application. @@ -35,17 +31,15 @@ * @author Phillip Webb */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient public class SampleTestNGApplicationTests extends AbstractTestNGSpringContextTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test public void testHome() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("Hello World"); + this.restTestClient.get().uri("/").exchangeSuccessfully().expectBody(String.class).isEqualTo("Hello World"); } } diff --git a/smoke-test/spring-boot-smoke-test-tomcat-jsp/src/test/java/smoketest/tomcat/jsp/SampleWebJspApplicationTests.java b/smoke-test/spring-boot-smoke-test-tomcat-jsp/src/test/java/smoketest/tomcat/jsp/SampleWebJspApplicationTests.java index d26086799d02..68b442685f7a 100644 --- a/smoke-test/spring-boot-smoke-test-tomcat-jsp/src/test/java/smoketest/tomcat/jsp/SampleWebJspApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-tomcat-jsp/src/test/java/smoketest/tomcat/jsp/SampleWebJspApplicationTests.java @@ -19,12 +19,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -34,17 +32,19 @@ * @author Phillip Webb */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleWebJspApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test void testJspWithEl() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("/resources/text.txt"); + this.restTestClient.get() + .uri("/") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("/resources/text.txt")); } } diff --git a/smoke-test/spring-boot-smoke-test-tomcat-multi-connectors/build.gradle b/smoke-test/spring-boot-smoke-test-tomcat-multi-connectors/build.gradle index 04b2df6f4ded..5df0de478273 100644 --- a/smoke-test/spring-boot-smoke-test-tomcat-multi-connectors/build.gradle +++ b/smoke-test/spring-boot-smoke-test-tomcat-multi-connectors/build.gradle @@ -24,7 +24,7 @@ dependencies { implementation(project(":starter:spring-boot-starter-webmvc")) testImplementation(project(":starter:spring-boot-starter-webmvc-test")) - testImplementation(project(":module:spring-boot-restclient")) + testImplementation(project(":module:spring-boot-resttestclient")) testImplementation("org.apache.httpcomponents.client5:httpclient5") } diff --git a/smoke-test/spring-boot-smoke-test-tomcat-multi-connectors/src/test/java/smoketest/tomcat/multiconnector/SampleTomcatTwoConnectorsApplicationTests.java b/smoke-test/spring-boot-smoke-test-tomcat-multi-connectors/src/test/java/smoketest/tomcat/multiconnector/SampleTomcatTwoConnectorsApplicationTests.java index 9f2f9bba732f..c76ea1fd632d 100644 --- a/smoke-test/spring-boot-smoke-test-tomcat-multi-connectors/src/test/java/smoketest/tomcat/multiconnector/SampleTomcatTwoConnectorsApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-tomcat-multi-connectors/src/test/java/smoketest/tomcat/multiconnector/SampleTomcatTwoConnectorsApplicationTests.java @@ -22,8 +22,7 @@ import smoketest.tomcat.multiconnector.SampleTomcatTwoConnectorsApplicationTests.Ports; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.TrustAllTlsRequestFactory; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.context.TestConfiguration; @@ -34,8 +33,7 @@ import org.springframework.boot.web.server.context.WebServerInitializedEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Import; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -47,7 +45,6 @@ */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @Import(Ports.class) -@AutoConfigureTestRestTemplate class SampleTomcatTwoConnectorsApplicationTests { @LocalServerPort @@ -56,9 +53,6 @@ class SampleTomcatTwoConnectorsApplicationTests { @Autowired private Ports ports; - @Autowired - private TestRestTemplate restTemplate; - @Autowired private AbstractConfigurableWebServerFactory webServerFactory; @@ -73,14 +67,14 @@ void testSsl() { void testHello() { assertThat(this.ports.getHttpsPort()).isEqualTo(this.port); assertThat(this.ports.getHttpPort()).isNotEqualTo(this.port); - ResponseEntity<String> entity = this.restTemplate - .getForEntity("http://localhost:" + this.ports.getHttpPort() + "/hello", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("hello"); - ResponseEntity<String> httpsEntity = this.restTemplate.getForEntity("https://localhost:" + this.port + "/hello", - String.class); - assertThat(httpsEntity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(httpsEntity.getBody()).isEqualTo("hello"); + RestTestClient httpClient = RestTestClient.bindToServer() + .baseUrl("http://localhost:" + this.ports.getHttpPort()) + .build(); + httpClient.get().uri("/hello").exchangeSuccessfully().expectBody(String.class).isEqualTo("hello"); + RestTestClient httpsClient = RestTestClient.bindToServer(TrustAllTlsRequestFactory.create()) + .baseUrl("https://localhost:" + this.port) + .build(); + httpsClient.get().uri("/hello").exchangeSuccessfully().expectBody(String.class).isEqualTo("hello"); } @TestConfiguration diff --git a/smoke-test/spring-boot-smoke-test-tomcat-ssl/build.gradle b/smoke-test/spring-boot-smoke-test-tomcat-ssl/build.gradle index fd86d66635a5..b08fc2ff4a4c 100644 --- a/smoke-test/spring-boot-smoke-test-tomcat-ssl/build.gradle +++ b/smoke-test/spring-boot-smoke-test-tomcat-ssl/build.gradle @@ -25,7 +25,7 @@ dependencies { implementation(project(":starter:spring-boot-starter-webmvc")) testImplementation(project(":starter:spring-boot-starter-webmvc-test")) - testImplementation(project(":module:spring-boot-restclient")) + testImplementation(project(":module:spring-boot-resttestclient")) testImplementation("org.apache.httpcomponents.client5:httpclient5") } diff --git a/smoke-test/spring-boot-smoke-test-tomcat-ssl/src/test/java/smoketest/tomcat/ssl/SampleTomcatSslApplicationTests.java b/smoke-test/spring-boot-smoke-test-tomcat-ssl/src/test/java/smoketest/tomcat/ssl/SampleTomcatSslApplicationTests.java index 1f451ef9c14b..8f5d57dd9012 100644 --- a/smoke-test/spring-boot-smoke-test-tomcat-ssl/src/test/java/smoketest/tomcat/ssl/SampleTomcatSslApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-tomcat-ssl/src/test/java/smoketest/tomcat/ssl/SampleTomcatSslApplicationTests.java @@ -19,24 +19,24 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.TrustAllTlsRequestFactory; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory; import org.springframework.boot.web.server.Ssl; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.test.json.JsonContent; +import org.springframework.test.web.servlet.client.EntityExchangeResult; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate class SampleTomcatSslApplicationTests { - @Autowired - private TestRestTemplate restTemplate; + @LocalServerPort + private int port; @Autowired private AbstractConfigurableWebServerFactory webServerFactory; @@ -50,16 +50,17 @@ void testSsl() { @Test void testHome() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("Hello, world"); + restTestClient().get().uri("/").exchangeSuccessfully().expectBody(String.class).isEqualTo("Hello, world"); } @Test void testSslInfo() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/actuator/info", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - String body = entity.getBody(); + EntityExchangeResult<String> result = restTestClient().get() + .uri("/actuator/info") + .exchange() + .returnResult(String.class); + assertThat(result.getStatus()).isEqualTo(HttpStatus.OK); + String body = result.getResponseBody(); assertThat(body).isNotNull(); JsonContent json = new JsonContent(body); assertThat(json).extractingPath("ssl.bundles[0].name").isEqualTo("ssldemo"); @@ -78,9 +79,12 @@ void testSslInfo() { @Test void testSslHealth() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/actuator/health", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE); - String body = entity.getBody(); + EntityExchangeResult<String> result = restTestClient().get() + .uri("/actuator/health") + .exchange() + .returnResult(String.class); + assertThat(result.getStatus()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE); + String body = result.getResponseBody(); assertThat(body).isNotNull(); JsonContent json = new JsonContent(body); assertThat(json).extractingPath("status").isEqualTo("OUT_OF_SERVICE"); @@ -98,4 +102,10 @@ void testSslHealth() { .startsWith("Not valid after "); } + private RestTestClient restTestClient() { + return RestTestClient.bindToServer(TrustAllTlsRequestFactory.create()) + .baseUrl("https://localhost:" + this.port) + .build(); + } + } diff --git a/smoke-test/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/NonAutoConfigurationSampleTomcatApplicationTests.java b/smoke-test/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/NonAutoConfigurationSampleTomcatApplicationTests.java index f7a91ea3b72e..9631cf054d6b 100644 --- a/smoke-test/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/NonAutoConfigurationSampleTomcatApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/NonAutoConfigurationSampleTomcatApplicationTests.java @@ -24,8 +24,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; @@ -34,10 +33,7 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; +import org.springframework.test.web.servlet.client.RestTestClient; /** * Basic integration tests for demo application. @@ -45,17 +41,15 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class NonAutoConfigurationSampleTomcatApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test void testHome() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("Hello World"); + this.restTestClient.get().uri("/").exchangeSuccessfully().expectBody(String.class).isEqualTo("Hello World"); } @Configuration(proxyBeanMethods = false) diff --git a/smoke-test/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/SampleTomcatApplicationTests.java b/smoke-test/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/SampleTomcatApplicationTests.java index 303bc1269a6c..3111904e24d5 100644 --- a/smoke-test/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/SampleTomcatApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-tomcat/src/test/java/smoketest/tomcat/SampleTomcatApplicationTests.java @@ -29,24 +29,20 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; -import org.springframework.boot.restclient.RestTemplateBuilder; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.tomcat.TomcatWebServer; import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext; import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.client.JdkClientHttpRequestFactory; +import org.springframework.test.web.servlet.client.EntityExchangeResult; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.util.StreamUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -61,12 +57,14 @@ */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @ExtendWith(OutputCaptureExtension.class) -@AutoConfigureTestRestTemplate -@SuppressWarnings("removal") +@AutoConfigureRestTestClient class SampleTomcatApplicationTests { + @LocalServerPort + private int port; + @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Autowired private ApplicationContext applicationContext; @@ -76,21 +74,29 @@ class SampleTomcatApplicationTests { @Test void testHome() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getHeaders().get(HttpHeaders.CONTENT_ENCODING)).isNull(); - assertThat(entity.getBody()).isEqualTo("Hello World"); + EntityExchangeResult<String> result = this.restTestClient.get().uri("/").exchange().returnResult(String.class); + assertThat(result.getStatus()).isEqualTo(HttpStatus.OK); + assertThat(result.getResponseHeaders().get(HttpHeaders.CONTENT_ENCODING)).isNull(); + assertThat(result.getResponseBody()).isEqualTo("Hello World"); } @Test void testCompression() throws IOException { - HttpHeaders requestHeaders = new HttpHeaders(); - requestHeaders.set("Accept-Encoding", "gzip"); - HttpEntity<?> requestEntity = new HttpEntity<>(requestHeaders); - ResponseEntity<byte[]> entity = this.restTemplate.exchange("/", HttpMethod.GET, requestEntity, byte[].class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getHeaders().get(HttpHeaders.CONTENT_ENCODING)).containsExactly("gzip"); - try (GZIPInputStream inflater = new GZIPInputStream(new ByteArrayInputStream(entity.getBody()))) { + // Use a client with automatic decompression disabled so that the raw gzipped + // response body and the Content-Encoding header can be verified. + JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(); + requestFactory.enableCompression(false); + RestTestClient nonDecompressingClient = RestTestClient.bindToServer(requestFactory) + .baseUrl("http://localhost:" + this.port) + .build(); + EntityExchangeResult<byte[]> result = nonDecompressingClient.get() + .uri("/") + .headers((headers) -> headers.set("Accept-Encoding", "gzip")) + .exchange() + .returnResult(byte[].class); + assertThat(result.getStatus()).isEqualTo(HttpStatus.OK); + assertThat(result.getResponseHeaders().get(HttpHeaders.CONTENT_ENCODING)).containsExactly("gzip"); + try (GZIPInputStream inflater = new GZIPInputStream(new ByteArrayInputStream(result.getResponseBody()))) { assertThat(StreamUtils.copyToString(inflater, StandardCharsets.UTF_8)).isEqualTo("Hello World"); } } @@ -107,8 +113,11 @@ void testTimeout() { @Test void testMaxHttpResponseHeaderSize(CapturedOutput output) { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/max-http-response-header", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + this.restTestClient.get() + .uri("/max-http-response-header") + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); assertThat(output).contains( "threw exception [Request processing failed: org.apache.coyote.http11.HeadersTooLargeException: An attempt was made to write more data to the response headers than there was room available in the buffer. Increase maxHttpHeaderSize on the connector or write less data into the response headers.]"); } @@ -116,23 +125,13 @@ void testMaxHttpResponseHeaderSize(CapturedOutput output) { @Test void testMaxHttpRequestHeaderSize(CapturedOutput output) { String headerValue = RandomStringUtil.getRandomBase64EncodedString(this.maxHttpRequestHeaderSize + 1); - HttpHeaders headers = new HttpHeaders(); - headers.add("x-max-request-header", headerValue); - HttpEntity<?> httpEntity = new HttpEntity<>(headers); - ResponseEntity<String> entity = this.restTemplate.exchange("/", HttpMethod.GET, httpEntity, String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + this.restTestClient.get() + .uri("/") + .headers((headers) -> headers.add("x-max-request-header", headerValue)) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.BAD_REQUEST); assertThat(output).contains("java.lang.IllegalArgumentException: Request header is too large"); } - @TestConfiguration - static class DisableCompressionConfiguration { - - @Bean - RestTemplateBuilder restTemplateBuilder() { - return new RestTemplateBuilder().requestFactoryBuilder(ClientHttpRequestFactoryBuilder.jdk() - .withCustomizer((factory) -> factory.enableCompression(false))); - } - - } - } diff --git a/smoke-test/spring-boot-smoke-test-traditional/src/test/java/smoketest/traditional/SampleTraditionalApplicationTests.java b/smoke-test/spring-boot-smoke-test-traditional/src/test/java/smoketest/traditional/SampleTraditionalApplicationTests.java index 43258806303e..9fd75e9456f5 100644 --- a/smoke-test/spring-boot-smoke-test-traditional/src/test/java/smoketest/traditional/SampleTraditionalApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-traditional/src/test/java/smoketest/traditional/SampleTraditionalApplicationTests.java @@ -19,12 +19,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -34,26 +32,28 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleTraditionalApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test void testHomeJsp() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - String body = entity.getBody(); - assertThat(body).contains("<html>").contains("<h1>Home</h1>"); + this.restTestClient.get() + .uri("/") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("<html>").contains("<h1>Home</h1>")); } @Test void testStaticPage() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/index.html", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - String body = entity.getBody(); - assertThat(body).contains("<html>").contains("<h1>Hello</h1>"); + this.restTestClient.get() + .uri("/index.html") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("<html>").contains("<h1>Hello</h1>")); } } diff --git a/smoke-test/spring-boot-smoke-test-web-freemarker/src/test/java/smoketest/freemarker/SampleWebFreeMarkerApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-freemarker/src/test/java/smoketest/freemarker/SampleWebFreeMarkerApplicationTests.java index 5a54887a79a9..4c792c09fa2f 100644 --- a/smoke-test/spring-boot-smoke-test-web-freemarker/src/test/java/smoketest/freemarker/SampleWebFreeMarkerApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-freemarker/src/test/java/smoketest/freemarker/SampleWebFreeMarkerApplicationTests.java @@ -16,21 +16,15 @@ package smoketest.freemarker; -import java.util.Arrays; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -41,43 +35,43 @@ * @author Andy Wilkinson */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleWebFreeMarkerApplicationTests { @Autowired - private TestRestTemplate testRestTemplate; + private RestTestClient restTestClient; @Test void testFreeMarkerTemplate() { - ResponseEntity<String> entity = this.testRestTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("Hello, Andy"); + this.restTestClient.get() + .uri("/") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("Hello, Andy")); } @Test void testFreeMarkerErrorTemplate() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); - HttpEntity<String> requestEntity = new HttpEntity<>(headers); - - ResponseEntity<String> responseEntity = this.testRestTemplate.exchange("/does-not-exist", HttpMethod.GET, - requestEntity, String.class); - - assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - assertThat(responseEntity.getBody()).contains("Something went wrong: 404 Not Found"); + this.restTestClient.get() + .uri("/does-not-exist") + .accept(MediaType.TEXT_HTML) + .exchange() + .expectStatus() + .isNotFound() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("Something went wrong: 404 Not Found")); } @Test void templateErrorPageForSpecificStatusCode() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); - HttpEntity<String> requestEntity = new HttpEntity<>(headers); - - ResponseEntity<String> responseEntity = this.testRestTemplate.exchange("/insufficient-storage", HttpMethod.GET, - requestEntity, String.class); - - assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.INSUFFICIENT_STORAGE); - assertThat(responseEntity.getBody()).contains("We are out of storage"); + this.restTestClient.get() + .uri("/insufficient-storage") + .accept(MediaType.TEXT_HTML) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.INSUFFICIENT_STORAGE) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("We are out of storage")); } } diff --git a/smoke-test/spring-boot-smoke-test-web-groovy-templates/src/test/java/smoketest/groovytemplates/SampleGroovyTemplateApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-groovy-templates/src/test/java/smoketest/groovytemplates/SampleGroovyTemplateApplicationTests.java index 8e96b6a433a8..141b49838557 100644 --- a/smoke-test/spring-boot-smoke-test-web-groovy-templates/src/test/java/smoketest/groovytemplates/SampleGroovyTemplateApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-groovy-templates/src/test/java/smoketest/groovytemplates/SampleGroovyTemplateApplicationTests.java @@ -16,18 +16,14 @@ package smoketest.groovytemplates; -import java.net.URI; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -39,21 +35,21 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.http.clients.redirects=dont-follow") -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleGroovyTemplateApplicationTests { @LocalServerPort private int port; @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test void testHome() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("<title>Messages"); - assertThat(entity.getBody()).doesNotContain("layout:fragment"); + this.restTestClient.get().uri("/").exchangeSuccessfully().expectBody(String.class).value((body) -> { + assertThat(body).contains("<title>Messages"); + assertThat(body).doesNotContain("layout:fragment"); + }); } @Test @@ -61,16 +57,21 @@ void testCreate() { MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); map.set("text", "FOO text"); map.set("summary", "FOO"); - URI location = this.restTemplate.postForLocation("/", map); - assertThat(location).isNotNull(); - assertThat(location.toString()).contains("localhost:" + this.port); + this.restTestClient.post() + .uri("/") + .body(map) + .exchange() + .expectHeader() + .value("Location", (location) -> assertThat(location).contains("localhost:" + this.port)); } @Test void testCss() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/css/bootstrap.min.css", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("body"); + this.restTestClient.get() + .uri("/css/bootstrap.min.css") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("body")); } } diff --git a/smoke-test/spring-boot-smoke-test-web-jsp/src/test/java/smoketest/jsp/SampleWebJspApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-jsp/src/test/java/smoketest/jsp/SampleWebJspApplicationTests.java index 00df3c960a78..57c2f81d34ed 100644 --- a/smoke-test/spring-boot-smoke-test-web-jsp/src/test/java/smoketest/jsp/SampleWebJspApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-jsp/src/test/java/smoketest/jsp/SampleWebJspApplicationTests.java @@ -16,22 +16,15 @@ package smoketest.jsp; -import java.net.URI; -import java.util.Arrays; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.RequestEntity; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -41,27 +34,31 @@ * @author Phillip Webb */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleWebJspApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test void testJspWithEl() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("/resources/text.txt"); + this.restTestClient.get() + .uri("/") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("/resources/text.txt")); } @Test void customErrorPage() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); - RequestEntity<Void> request = new RequestEntity<>(headers, HttpMethod.GET, URI.create("/foo")); - ResponseEntity<String> entity = this.restTemplate.exchange(request, String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - assertThat(entity.getBody()).contains("Something went wrong: 500 Internal Server Error"); + this.restTestClient.get() + .uri("/foo") + .accept(MediaType.TEXT_HTML) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("Something went wrong: 500 Internal Server Error")); } } diff --git a/smoke-test/spring-boot-smoke-test-web-method-security/src/test/java/smoketest/security/method/SampleMethodSecurityApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-method-security/src/test/java/smoketest/security/method/SampleMethodSecurityApplicationTests.java index 8c969ad4cec4..552182ae9b0c 100644 --- a/smoke-test/spring-boot-smoke-test-web-method-security/src/test/java/smoketest/security/method/SampleMethodSecurityApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-method-security/src/test/java/smoketest/security/method/SampleMethodSecurityApplicationTests.java @@ -22,18 +22,17 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.HttpRedirects; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.test.web.servlet.client.EntityExchangeResult; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -47,79 +46,96 @@ */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.http.clients.imperative.factory=simple") -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleMethodSecurityApplicationTests { @LocalServerPort private int port; @Autowired - private TestRestTemplate restTemplate; + private RestTestClient rest; + + private RestTestClient followingRedirects() { + return RestTestClient + .bindToServer(ClientHttpRequestFactoryBuilder.detect() + .build(HttpClientSettings.defaults().withRedirects(HttpRedirects.FOLLOW_WHEN_POSSIBLE))) + .baseUrl("http://localhost:" + this.port) + .build(); + } + + private RestTestClient nonFollowingRedirects() { + return RestTestClient + .bindToServer(ClientHttpRequestFactoryBuilder.detect() + .build(HttpClientSettings.defaults().withRedirects(HttpRedirects.DONT_FOLLOW))) + .baseUrl("http://localhost:" + this.port) + .build(); + } @Test void testHome() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML)); - ResponseEntity<String> entity = this.restTemplate.exchange("/", HttpMethod.GET, new HttpEntity<>(headers), - String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + followingRedirects().get().uri("/").accept(MediaType.TEXT_HTML).exchange().expectStatus().isOk(); } @Test void testLogin() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML)); MultiValueMap<String, String> form = new LinkedMultiValueMap<>(); form.set("username", "admin"); form.set("password", "admin"); - ResponseEntity<String> entity = this.restTemplate.exchange("/login", HttpMethod.POST, - new HttpEntity<>(form, headers), String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); - URI location = entity.getHeaders().getLocation(); + EntityExchangeResult<String> result = nonFollowingRedirects().post() + .uri("/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .accept(MediaType.TEXT_HTML) + .body(form) + .exchange() + .returnResult(String.class); + assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); + URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); assertThat(location.toString()).endsWith(this.port + "/"); } @Test void testDenied() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML)); MultiValueMap<String, String> form = new LinkedMultiValueMap<>(); form.set("username", "user"); form.set("password", "user"); - ResponseEntity<String> entity = this.restTemplate.exchange("/login", HttpMethod.POST, - new HttpEntity<>(form, headers), String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); - String cookie = entity.getHeaders().getFirst("Set-Cookie"); - headers.set("Cookie", cookie); - URI location = entity.getHeaders().getLocation(); + EntityExchangeResult<String> result = nonFollowingRedirects().post() + .uri("/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .accept(MediaType.TEXT_HTML) + .body(form) + .exchange() + .returnResult(String.class); + assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); + String cookie = result.getResponseHeaders().getFirst("Set-Cookie"); + URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); - ResponseEntity<String> page = this.restTemplate.exchange(location, HttpMethod.GET, new HttpEntity<>(headers), - String.class); - assertThat(page.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - assertThat(page.getBody()).contains("Access denied"); + EntityExchangeResult<String> page = this.rest.get().uri(location).headers((headers) -> { + headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML)); + headers.set("Cookie", cookie); + }).exchange().returnResult(String.class); + assertThat(page.getStatus()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(page.getResponseBody()).contains("Access denied"); } @Test void testManagementProtected() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - ResponseEntity<String> entity = this.restTemplate.exchange("/actuator/beans", HttpMethod.GET, - new HttpEntity<>(headers), String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + this.rest.get() + .uri("/actuator/beans") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isUnauthorized(); } @Test void testManagementAuthorizedAccess() { - BasicAuthenticationInterceptor basicAuthInterceptor = new BasicAuthenticationInterceptor("admin", "admin"); - this.restTemplate.getRestTemplate().getInterceptors().add(basicAuthInterceptor); - try { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/actuator/beans", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - } - finally { - this.restTemplate.getRestTemplate().getInterceptors().remove(basicAuthInterceptor); - } + this.rest.get() + .uri("/actuator/beans") + .headers((headers) -> headers.setBasicAuth("admin", "admin")) + .exchange() + .expectStatus() + .isOk(); } } diff --git a/smoke-test/spring-boot-smoke-test-web-mustache/src/test/java/smoketest/mustache/SampleWebMustacheApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-mustache/src/test/java/smoketest/mustache/SampleWebMustacheApplicationTests.java index 68cba59163bd..4ca963db0f98 100644 --- a/smoke-test/spring-boot-smoke-test-web-mustache/src/test/java/smoketest/mustache/SampleWebMustacheApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-mustache/src/test/java/smoketest/mustache/SampleWebMustacheApplicationTests.java @@ -16,21 +16,15 @@ package smoketest.mustache; -import java.util.Arrays; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -41,61 +35,67 @@ * @author Andy Wilkinson */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleWebMustacheApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test void testMustacheTemplate() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("Hello, Andy"); + this.restTestClient.get() + .uri("/") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("Hello, Andy")); } @Test void testMustacheErrorTemplate() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); - HttpEntity<String> requestEntity = new HttpEntity<>(headers); - ResponseEntity<String> responseEntity = this.restTemplate.exchange("/does-not-exist", HttpMethod.GET, - requestEntity, String.class); - assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - assertThat(responseEntity.getBody()).contains("Something went wrong: 404 Not Found"); + this.restTestClient.get() + .uri("/does-not-exist") + .accept(MediaType.TEXT_HTML) + .exchange() + .expectStatus() + .isNotFound() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("Something went wrong: 404 Not Found")); } @Test void test503HtmlResource() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); - HttpEntity<String> requestEntity = new HttpEntity<>(headers); - ResponseEntity<String> entity = this.restTemplate.exchange("/serviceUnavailable", HttpMethod.GET, requestEntity, - String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE); - assertThat(entity.getBody()).contains("I'm a 503"); + this.restTestClient.get() + .uri("/serviceUnavailable") + .accept(MediaType.TEXT_HTML) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.SERVICE_UNAVAILABLE) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("I'm a 503")); } @Test void test5xxHtmlResource() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); - HttpEntity<String> requestEntity = new HttpEntity<>(headers); - ResponseEntity<String> entity = this.restTemplate.exchange("/bang", HttpMethod.GET, requestEntity, - String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - assertThat(entity.getBody()).contains("I'm a 5xx"); + this.restTestClient.get() + .uri("/bang") + .accept(MediaType.TEXT_HTML) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("I'm a 5xx")); } @Test void test507Template() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); - HttpEntity<String> requestEntity = new HttpEntity<>(headers); - ResponseEntity<String> entity = this.restTemplate.exchange("/insufficientStorage", HttpMethod.GET, - requestEntity, String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INSUFFICIENT_STORAGE); - assertThat(entity.getBody()).contains("I'm a 507"); + this.restTestClient.get() + .uri("/insufficientStorage") + .accept(MediaType.TEXT_HTML) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.INSUFFICIENT_STORAGE) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("I'm a 507")); } } diff --git a/smoke-test/spring-boot-smoke-test-web-secure-custom/src/test/java/smoketest/web/secure/custom/SampleWebSecureCustomApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-secure-custom/src/test/java/smoketest/web/secure/custom/SampleWebSecureCustomApplicationTests.java index 77f7469b70a0..b16c34ab450e 100644 --- a/smoke-test/spring-boot-smoke-test-web-secure-custom/src/test/java/smoketest/web/secure/custom/SampleWebSecureCustomApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-secure-custom/src/test/java/smoketest/web/secure/custom/SampleWebSecureCustomApplicationTests.java @@ -17,23 +17,21 @@ package smoketest.web.secure.custom; import java.net.URI; -import java.util.Collections; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpRedirects; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.EntityExchangeResult; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -46,49 +44,60 @@ * @author Scott Frederick */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleWebSecureCustomApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient rest; @LocalServerPort private int port; + private RestTestClient nonFollowingRedirect() { + return RestTestClient + .bindToServer(ClientHttpRequestFactoryBuilder.detect() + .build(HttpClientSettings.defaults().withRedirects(HttpRedirects.DONT_FOLLOW))) + .baseUrl("http://localhost:" + this.port) + .build(); + } + @Test void testHome() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML)); - ResponseEntity<String> entity = this.restTemplate.withRedirects(HttpRedirects.DONT_FOLLOW) - .exchange("/", HttpMethod.GET, new HttpEntity<>(headers), String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); - URI location = entity.getHeaders().getLocation(); + EntityExchangeResult<String> result = nonFollowingRedirect().get() + .uri("/") + .accept(MediaType.TEXT_HTML) + .exchange() + .returnResult(String.class); + assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); + URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); assertThat(location.toString()).endsWith(this.port + "/login"); } @Test void testLoginPage() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML)); - ResponseEntity<String> entity = this.restTemplate.exchange("/login", HttpMethod.GET, new HttpEntity<>(headers), - String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("<title>Login"); + this.rest.get() + .uri("/login") + .accept(MediaType.TEXT_HTML) + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("Login")); } @Test void testLogin() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML)); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap form = new LinkedMultiValueMap<>(); form.set("username", "user"); form.set("password", "password"); - ResponseEntity entity = this.restTemplate.withRedirects(HttpRedirects.DONT_FOLLOW) - .exchange("/login", HttpMethod.POST, new HttpEntity<>(form, headers), String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); - URI location = entity.getHeaders().getLocation(); + EntityExchangeResult result = nonFollowingRedirect().post() + .uri("/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .accept(MediaType.TEXT_HTML) + .body(form) + .exchange() + .returnResult(String.class); + assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); + URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); assertThat(location.toString()).endsWith(this.port + "/"); } diff --git a/smoke-test/spring-boot-smoke-test-web-secure-jdbc/src/test/java/smoketest/web/secure/jdbc/SampleWebSecureJdbcApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-secure-jdbc/src/test/java/smoketest/web/secure/jdbc/SampleWebSecureJdbcApplicationTests.java index 8b46bff3e25a..82ba02cf1989 100644 --- a/smoke-test/spring-boot-smoke-test-web-secure-jdbc/src/test/java/smoketest/web/secure/jdbc/SampleWebSecureJdbcApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-secure-jdbc/src/test/java/smoketest/web/secure/jdbc/SampleWebSecureJdbcApplicationTests.java @@ -17,23 +17,21 @@ package smoketest.web.secure.jdbc; import java.net.URI; -import java.util.Collections; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpRedirects; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.EntityExchangeResult; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -46,49 +44,60 @@ * @author Scott Frederick */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleWebSecureJdbcApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient rest; @LocalServerPort private int port; + private RestTestClient nonFollowingRedirect() { + return RestTestClient + .bindToServer(ClientHttpRequestFactoryBuilder.detect() + .build(HttpClientSettings.defaults().withRedirects(HttpRedirects.DONT_FOLLOW))) + .baseUrl("http://localhost:" + this.port) + .build(); + } + @Test void testHome() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML)); - ResponseEntity entity = this.restTemplate.withRedirects(HttpRedirects.DONT_FOLLOW) - .exchange("/", HttpMethod.GET, new HttpEntity<>(headers), String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); - URI location = entity.getHeaders().getLocation(); + EntityExchangeResult result = nonFollowingRedirect().get() + .uri("/") + .accept(MediaType.TEXT_HTML) + .exchange() + .returnResult(String.class); + assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); + URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); assertThat(location.toString()).endsWith(this.port + "/login"); } @Test void testLoginPage() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML)); - ResponseEntity entity = this.restTemplate.exchange("/login", HttpMethod.GET, new HttpEntity<>(headers), - String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("Login"); + this.rest.get() + .uri("/login") + .accept(MediaType.TEXT_HTML) + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("Login")); } @Test void testLogin() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML)); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap form = new LinkedMultiValueMap<>(); form.set("username", "user"); form.set("password", "user"); - ResponseEntity entity = this.restTemplate.withRedirects(HttpRedirects.DONT_FOLLOW) - .exchange("/login", HttpMethod.POST, new HttpEntity<>(form, headers), String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); - URI location = entity.getHeaders().getLocation(); + EntityExchangeResult result = nonFollowingRedirect().post() + .uri("/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .accept(MediaType.TEXT_HTML) + .body(form) + .exchange() + .returnResult(String.class); + assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); + URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); assertThat(location.toString()).endsWith(this.port + "/"); } diff --git a/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/AbstractErrorPageTests.java b/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/AbstractErrorPageTests.java index 86477b1ca837..4e2bb1983d24 100644 --- a/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/AbstractErrorPageTests.java +++ b/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/AbstractErrorPageTests.java @@ -20,12 +20,9 @@ import tools.jackson.databind.JsonNode; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -37,11 +34,11 @@ * * @author Madhura Bhave */ -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient abstract class AbstractErrorPageTests { @Autowired - private TestRestTemplate testRestTemplate; + private RestTestClient client; private final String pathPrefix; @@ -51,58 +48,77 @@ protected AbstractErrorPageTests(String pathPrefix) { @Test void testBadCredentials() { - final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "wrongpassword") - .exchange(this.pathPrefix + "/test", HttpMethod.GET, null, JsonNode.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - JsonNode jsonResponse = response.getBody(); - assertThat(jsonResponse).isNull(); + this.client.get() + .uri(this.pathPrefix + "/test") + .headers((headers) -> headers.setBasicAuth("username", "wrongpassword")) + .exchange() + .expectStatus() + .isUnauthorized() + .expectBody(JsonNode.class) + .isEqualTo(null); } @Test void testNoCredentials() { - final ResponseEntity response = this.testRestTemplate.exchange(this.pathPrefix + "/test", - HttpMethod.GET, null, JsonNode.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - JsonNode jsonResponse = response.getBody(); - assertThat(jsonResponse).isNull(); + this.client.get() + .uri(this.pathPrefix + "/test") + .exchange() + .expectStatus() + .isUnauthorized() + .expectBody(JsonNode.class) + .isEqualTo(null); } @Test void testPublicNotFoundPageWithCorrectCredentials() { - final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "password") - .exchange(this.pathPrefix + "/public/notfound", HttpMethod.GET, null, JsonNode.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - JsonNode jsonResponse = response.getBody(); - assertThat(jsonResponse).isNotNull(); - assertThat(jsonResponse.get("error").asString()).isEqualTo("Not Found"); + this.client.get() + .uri(this.pathPrefix + "/public/notfound") + .headers((headers) -> headers.setBasicAuth("username", "password")) + .exchange() + .expectStatus() + .isNotFound() + .expectBody(JsonNode.class) + .value((body) -> { + assertThat(body).isNotNull(); + assertThat(body.get("error").asString()).isEqualTo("Not Found"); + }); } @Test void testPublicNotFoundPageWithBadCredentials() { - final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "wrong") - .exchange(this.pathPrefix + "/public/notfound", HttpMethod.GET, null, JsonNode.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - JsonNode jsonResponse = response.getBody(); - assertThat(jsonResponse).isNull(); + this.client.get() + .uri(this.pathPrefix + "/public/notfound") + .headers((headers) -> headers.setBasicAuth("username", "wrong")) + .exchange() + .expectStatus() + .isUnauthorized() + .expectBody(JsonNode.class) + .isEqualTo(null); } @Test void testCorrectCredentialsWithControllerException() { - final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "password") - .exchange(this.pathPrefix + "/fail", HttpMethod.GET, null, JsonNode.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - JsonNode jsonResponse = response.getBody(); - assertThat(jsonResponse).isNotNull(); - assertThat(jsonResponse.get("error").asString()).isEqualTo("Internal Server Error"); + this.client.get() + .uri(this.pathPrefix + "/fail") + .headers((headers) -> headers.setBasicAuth("username", "password")) + .exchange() + .expectStatus() + .is5xxServerError() + .expectBody(JsonNode.class) + .value((body) -> { + assertThat(body).isNotNull(); + assertThat(body.get("error").asString()).isEqualTo("Internal Server Error"); + }); } @Test void testCorrectCredentials() { - final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "password") - .exchange(this.pathPrefix + "/test", HttpMethod.GET, null, String.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - response.getBody(); - assertThat(response.getBody()).isEqualTo("test"); + this.client.get() + .uri(this.pathPrefix + "/test") + .headers((headers) -> headers.setBasicAuth("username", "password")) + .exchangeSuccessfully() + .expectBody(String.class) + .isEqualTo("test"); } @Configuration(proxyBeanMethods = false) diff --git a/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/AbstractUnauthenticatedErrorPageTests.java b/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/AbstractUnauthenticatedErrorPageTests.java index 2299e6f42353..7e1898a812e4 100644 --- a/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/AbstractUnauthenticatedErrorPageTests.java +++ b/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/AbstractUnauthenticatedErrorPageTests.java @@ -20,11 +20,8 @@ import tools.jackson.databind.JsonNode; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -34,11 +31,11 @@ * * @author Madhura Bhave */ -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient abstract class AbstractUnauthenticatedErrorPageTests { @Autowired - private TestRestTemplate testRestTemplate; + private RestTestClient rest; private final String pathPrefix; @@ -48,70 +45,100 @@ protected AbstractUnauthenticatedErrorPageTests(String pathPrefix) { @Test void testBadCredentials() { - final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "wrongpassword") - .exchange(this.pathPrefix + "/test", HttpMethod.GET, null, JsonNode.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - JsonNode jsonResponse = response.getBody(); - assertThat(jsonResponse).isNotNull(); - assertThat(jsonResponse.get("error").asString()).isEqualTo("Unauthorized"); + this.rest.get() + .uri(this.pathPrefix + "/test") + .headers((headers) -> headers.setBasicAuth("username", "wrongpassword")) + .exchange() + .expectStatus() + .isUnauthorized() + .expectBody(JsonNode.class) + .value((body) -> { + assertThat(body).isNotNull(); + assertThat(body.get("error").asString()).isEqualTo("Unauthorized"); + }); } @Test void testNoCredentials() { - final ResponseEntity response = this.testRestTemplate.exchange(this.pathPrefix + "/test", - HttpMethod.GET, null, JsonNode.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - JsonNode jsonResponse = response.getBody(); - assertThat(jsonResponse).isNotNull(); - assertThat(jsonResponse.get("error").asString()).isEqualTo("Unauthorized"); + this.rest.get() + .uri(this.pathPrefix + "/test") + .exchange() + .expectStatus() + .isUnauthorized() + .expectBody(JsonNode.class) + .value((body) -> { + assertThat(body).isNotNull(); + assertThat(body.get("error").asString()).isEqualTo("Unauthorized"); + }); } @Test void testPublicNotFoundPage() { - final ResponseEntity response = this.testRestTemplate.exchange(this.pathPrefix + "/public/notfound", - HttpMethod.GET, null, JsonNode.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - JsonNode jsonResponse = response.getBody(); - assertThat(jsonResponse).isNotNull(); - assertThat(jsonResponse.get("error").asString()).isEqualTo("Not Found"); + this.rest.get() + .uri(this.pathPrefix + "/public/notfound") + .exchange() + .expectStatus() + .isNotFound() + .expectBody(JsonNode.class) + .value((body) -> { + assertThat(body).isNotNull(); + assertThat(body.get("error").asString()).isEqualTo("Not Found"); + }); } @Test void testPublicNotFoundPageWithCorrectCredentials() { - final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "password") - .exchange(this.pathPrefix + "/public/notfound", HttpMethod.GET, null, JsonNode.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - JsonNode jsonResponse = response.getBody(); - assertThat(jsonResponse).isNotNull(); - assertThat(jsonResponse.get("error").asString()).isEqualTo("Not Found"); + this.rest.get() + .uri(this.pathPrefix + "/public/notfound") + .headers((headers) -> headers.setBasicAuth("username", "password")) + .exchange() + .expectStatus() + .isNotFound() + .expectBody(JsonNode.class) + .value((body) -> { + assertThat(body).isNotNull(); + assertThat(body.get("error").asString()).isEqualTo("Not Found"); + }); } @Test void testPublicNotFoundPageWithBadCredentials() { - final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "wrong") - .exchange(this.pathPrefix + "/public/notfound", HttpMethod.GET, null, JsonNode.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - JsonNode jsonResponse = response.getBody(); - assertThat(jsonResponse).isNotNull(); - assertThat(jsonResponse.get("error").asString()).isEqualTo("Unauthorized"); + this.rest.get() + .uri(this.pathPrefix + "/public/notfound") + .headers((headers) -> headers.setBasicAuth("username", "wrong")) + .exchange() + .expectStatus() + .isUnauthorized() + .expectBody(JsonNode.class) + .value((body) -> { + assertThat(body).isNotNull(); + assertThat(body.get("error").asString()).isEqualTo("Unauthorized"); + }); } @Test void testCorrectCredentialsWithControllerException() { - final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "password") - .exchange(this.pathPrefix + "/fail", HttpMethod.GET, null, JsonNode.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - JsonNode jsonResponse = response.getBody(); - assertThat(jsonResponse).isNotNull(); - assertThat(jsonResponse.get("error").asString()).isEqualTo("Internal Server Error"); + this.rest.get() + .uri(this.pathPrefix + "/fail") + .headers((headers) -> headers.setBasicAuth("username", "password")) + .exchange() + .expectStatus() + .is5xxServerError() + .expectBody(JsonNode.class) + .value((body) -> { + assertThat(body).isNotNull(); + assertThat(body.get("error").asString()).isEqualTo("Internal Server Error"); + }); } @Test void testCorrectCredentials() { - final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "password") - .exchange(this.pathPrefix + "/test", HttpMethod.GET, null, String.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isEqualTo("test"); + this.rest.get() + .uri(this.pathPrefix + "/test") + .headers((headers) -> headers.setBasicAuth("username", "password")) + .exchangeSuccessfully() + .expectBody(String.class) + .isEqualTo("test"); } } diff --git a/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/SampleWebSecureApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/SampleWebSecureApplicationTests.java index b0457604dabc..781d02fed73f 100644 --- a/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/SampleWebSecureApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/SampleWebSecureApplicationTests.java @@ -17,28 +17,26 @@ package smoketest.web.secure; import java.net.URI; -import java.util.Collections; import jakarta.servlet.DispatcherType; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpRedirects; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.annotation.Bean; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.test.web.servlet.client.EntityExchangeResult; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -53,49 +51,60 @@ */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = { SampleWebSecureApplicationTests.SecurityConfiguration.class, SampleWebSecureApplication.class }) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleWebSecureApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient rest; @LocalServerPort private int port; + private RestTestClient nonFollowingRedirect() { + return RestTestClient + .bindToServer(ClientHttpRequestFactoryBuilder.detect() + .build(HttpClientSettings.defaults().withRedirects(HttpRedirects.DONT_FOLLOW))) + .baseUrl("http://localhost:" + this.port) + .build(); + } + @Test void testHome() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML)); - ResponseEntity entity = this.restTemplate.withRedirects(HttpRedirects.DONT_FOLLOW) - .exchange("/home", HttpMethod.GET, new HttpEntity<>(headers), String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); - URI location = entity.getHeaders().getLocation(); + EntityExchangeResult result = nonFollowingRedirect().get() + .uri("/home") + .accept(MediaType.TEXT_HTML) + .exchange() + .returnResult(String.class); + assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); + URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); assertThat(location.toString()).endsWith(this.port + "/login"); } @Test void testLoginPage() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML)); - ResponseEntity entity = this.restTemplate.exchange("/login", HttpMethod.GET, new HttpEntity<>(headers), - String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("Login"); + this.rest.get() + .uri("/login") + .accept(MediaType.TEXT_HTML) + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("Login")); } @Test void testLogin() { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML)); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap form = new LinkedMultiValueMap<>(); form.set("username", "user"); form.set("password", "password"); - ResponseEntity entity = this.restTemplate.withRedirects(HttpRedirects.DONT_FOLLOW) - .exchange("/login", HttpMethod.POST, new HttpEntity<>(form, headers), String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); - URI location = entity.getHeaders().getLocation(); + EntityExchangeResult result = nonFollowingRedirect().post() + .uri("/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .accept(MediaType.TEXT_HTML) + .body(form) + .exchange() + .returnResult(String.class); + assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); + URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); assertThat(location.toString()).endsWith(this.port + "/"); } diff --git a/smoke-test/spring-boot-smoke-test-web-static/src/test/java/smoketest/web/staticcontent/SampleWebStaticApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-static/src/test/java/smoketest/web/staticcontent/SampleWebStaticApplicationTests.java index 76995f8cf234..13d68f7e822f 100644 --- a/smoke-test/spring-boot-smoke-test-web-static/src/test/java/smoketest/web/staticcontent/SampleWebStaticApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-static/src/test/java/smoketest/web/staticcontent/SampleWebStaticApplicationTests.java @@ -19,13 +19,11 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -35,26 +33,30 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleWebStaticApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @Test void testHome() { - ResponseEntity entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("Static"); + this.restTestClient.get() + .uri("/") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("<title>Static")); } @Test void testCss() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/webjars/bootstrap/3.0.3/css/bootstrap.min.css", - String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("body"); - assertThat(entity.getHeaders().getContentType()).isEqualTo(MediaType.valueOf("text/css")); + this.restTestClient.get() + .uri("/webjars/bootstrap/3.0.3/css/bootstrap.min.css") + .exchangeSuccessfully() + .expectHeader() + .contentType(MediaType.valueOf("text/css")) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("body")); } } diff --git a/smoke-test/spring-boot-smoke-test-web-thymeleaf/src/test/java/smoketest/web/thymeleaf/SampleWebUiApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-thymeleaf/src/test/java/smoketest/web/thymeleaf/SampleWebUiApplicationTests.java index 27e478b7f115..8c21af9d78aa 100644 --- a/smoke-test/spring-boot-smoke-test-web-thymeleaf/src/test/java/smoketest/web/thymeleaf/SampleWebUiApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-thymeleaf/src/test/java/smoketest/web/thymeleaf/SampleWebUiApplicationTests.java @@ -21,13 +21,11 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -39,21 +37,21 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.http.clients.redirects=dont-follow") -@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient class SampleWebUiApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private RestTestClient restTestClient; @LocalServerPort private int port; @Test void testHome() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("<title>Messages"); - assertThat(entity.getBody()).doesNotContain("layout:fragment"); + this.restTestClient.get().uri("/").exchangeSuccessfully().expectBody(String.class).value((body) -> { + assertThat(body).contains("<title>Messages"); + assertThat(body).doesNotContain("layout:fragment"); + }); } @Test @@ -61,7 +59,13 @@ void testCreate() { MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); map.set("text", "FOO text"); map.set("summary", "FOO"); - URI location = this.restTemplate.postForLocation("/", map); + URI location = this.restTestClient.post() + .uri("/") + .body(map) + .exchange() + .returnResult(Void.class) + .getResponseHeaders() + .getLocation(); assertThat(location).isNotNull(); assertThat(location.toString()).contains("localhost:" + this.port); } diff --git a/smoke-test/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/SampleWebFluxApplicationActuatorDifferentPortTests.java b/smoke-test/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/SampleWebFluxApplicationActuatorDifferentPortTests.java index bed7a5b4ad9d..c11c51b1acd3 100644 --- a/smoke-test/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/SampleWebFluxApplicationActuatorDifferentPortTests.java +++ b/smoke-test/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/SampleWebFluxApplicationActuatorDifferentPortTests.java @@ -18,11 +18,9 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalManagementPort; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.reactive.server.WebTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -41,10 +39,17 @@ class SampleWebFluxApplicationActuatorDifferentPortTests { @Test void linksEndpointShouldBeAvailable() { - ResponseEntity<String> entity = new TestRestTemplate("user", getPassword()) - .getForEntity("http://localhost:" + this.managementPort + "/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("\"_links\""); + WebTestClient.bindToServer() + .baseUrl("http://localhost:" + this.managementPort) + .build() + .get() + .uri("/") + .headers((headers) -> headers.setBasicAuth("user", getPassword())) + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .value((response) -> assertThat(response).contains("\"_links\"")); } private String getPassword() { From c7ab67a5c00d37ec2ec0b3161467dcc8f8a735bf Mon Sep 17 00:00:00 2001 From: Brian Clozel <brian.clozel@broadcom.com> Date: Mon, 27 Jul 2026 16:11:06 +0200 Subject: [PATCH 789/930] Replace TestRestTemplate with RestTestClient in system tests Before deprecating `TestRestTemplate`, we must first stop using it in system tests and replace it with: * `RestTestClient` when the Spring MVC infrastructure is present * `RestClient` when the test does not have Spring MVC on classpath See gh-46632 --- .../deployment/AbstractDeploymentTests.java | 56 +++++++++---------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/system-test/spring-boot-deployment-system-tests/src/systemTest/java/org/springframework/boot/deployment/AbstractDeploymentTests.java b/system-test/spring-boot-deployment-system-tests/src/systemTest/java/org/springframework/boot/deployment/AbstractDeploymentTests.java index aaf99a989559..0e6c43d9bf10 100644 --- a/system-test/spring-boot-deployment-system-tests/src/systemTest/java/org/springframework/boot/deployment/AbstractDeploymentTests.java +++ b/system-test/spring-boot-deployment-system-tests/src/systemTest/java/org/springframework/boot/deployment/AbstractDeploymentTests.java @@ -32,11 +32,8 @@ import org.testcontainers.images.builder.ImageFromDockerfile; import org.testcontainers.images.builder.dockerfile.DockerfileBuilder; -import org.springframework.boot.restclient.RestTemplateBuilder; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -50,37 +47,35 @@ abstract class AbstractDeploymentTests { @Test void home() { - getDeployedApplication().test((rest) -> { - ResponseEntity<String> response = rest.getForEntity("/", String.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isEqualTo("Hello World"); - }); + getDeployedApplication().test((client) -> client.get() + .uri("/") + .exchangeSuccessfully() + .expectBody(String.class) + .isEqualTo("Hello World")); } @Test void errorPage() { - getDeployedApplication().test((rest) -> { - ResponseEntity<String> response = rest.getForEntity("/does-not-exist", String.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - }); + getDeployedApplication() + .test((client) -> client.get().uri("/does-not-exist").exchange().expectStatus().isNotFound()); } @Test void health() { - getDeployedApplication().test((rest) -> { - ResponseEntity<String> response = rest.getForEntity("/actuator/health", String.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isEqualTo("{\"groups\":[\"liveness\",\"readiness\"],\"status\":\"UP\"}"); - }); + getDeployedApplication().test((client) -> client.get() + .uri("/actuator/health") + .exchangeSuccessfully() + .expectBody(String.class) + .isEqualTo("{\"groups\":[\"liveness\",\"readiness\"],\"status\":\"UP\"}")); } @Test void conditionalOnWarShouldBeTrue() { - getDeployedApplication().test((rest) -> { - ResponseEntity<String> response = rest.getForEntity("/actuator/war", String.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isEqualTo("{\"hello\":\"world\"}"); - }); + getDeployedApplication().test((client) -> client.get() + .uri("/actuator/war") + .exchangeSuccessfully() + .expectBody(String.class) + .isEqualTo("{\"hello\":\"world\"}")); } private DeployedApplication getDeployedApplication() { @@ -104,18 +99,19 @@ static final class DeployedApplication { this.port = port; } - private void test(Consumer<TestRestTemplate> consumer) { - TestRestTemplate rest = new TestRestTemplate(new RestTemplateBuilder() - .baseUri("http://" + this.container.getHost() + ":" + this.container.getMappedPort(this.port) - + "/spring-boot") - .requestFactory(() -> new HttpComponentsClientHttpRequestFactory(HttpClients.custom() + private void test(Consumer<RestTestClient> consumer) { + RestTestClient client = RestTestClient + .bindToServer(new HttpComponentsClientHttpRequestFactory(HttpClients.custom() .setRetryStrategy(new DefaultHttpRequestRetryStrategy(10, TimeValue.of(1, TimeUnit.SECONDS))) - .build()))); + .build())) + .baseUrl("http://" + this.container.getHost() + ":" + this.container.getMappedPort(this.port) + + "/spring-boot") + .build(); try { Awaitility.await().atMost(Duration.ofMinutes(10)).until(() -> { try { System.out.println(this.container.getLogs()); - consumer.accept(rest); + consumer.accept(client); return true; } catch (Throwable ex) { From 4daf90ada1d2bb3a0b3cd0892f8fde26cb198454 Mon Sep 17 00:00:00 2001 From: Brian Clozel <brian.clozel@broadcom.com> Date: Mon, 27 Jul 2026 16:11:46 +0200 Subject: [PATCH 790/930] Replace TestRestTemplate with RestTestClient in integration tests Before deprecating `TestRestTemplate`, we must first stop using it in integration tests and replace it with: * `RestTestClient` when the Spring MVC infrastructure is present * `RestClient` when the test does not have Spring MVC on classpath See gh-46632 --- ...bstractSpringBootTestEmbeddedReactiveWebEnvironmentTests.java | 1 + .../test/AbstractSpringBootTestWebServerWebEnvironmentTests.java | 1 + 2 files changed, 2 insertions(+) diff --git a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests.java b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests.java index 65e5a2e6e2c6..2a5315179e1a 100644 --- a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests.java +++ b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests.java @@ -49,6 +49,7 @@ */ @AutoConfigureWebTestClient @AutoConfigureTestRestTemplate +@SuppressWarnings("removal") abstract class AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests { @LocalServerPort diff --git a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestWebServerWebEnvironmentTests.java b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestWebServerWebEnvironmentTests.java index 943f4e89e811..44b790b26258 100644 --- a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestWebServerWebEnvironmentTests.java +++ b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestWebServerWebEnvironmentTests.java @@ -50,6 +50,7 @@ */ @AutoConfigureRestTestClient @AutoConfigureTestRestTemplate +@SuppressWarnings("removal") abstract class AbstractSpringBootTestWebServerWebEnvironmentTests { @LocalServerPort From c06abc12ac750db327823c8001eef094ebde4021 Mon Sep 17 00:00:00 2001 From: Brian Clozel <brian.clozel@broadcom.com> Date: Mon, 27 Jul 2026 16:13:10 +0200 Subject: [PATCH 791/930] Replace TestRestTemplate with RestTestClient in module tests Before deprecating `TestRestTemplate`, we must first stop using it in module tests and replace it with: * `RestTestClient` when the Spring MVC infrastructure is present * `RestClient` when the test does not have Spring MVC on classpath See gh-46632 --- .../tests/DevToolsIntegrationTests.java | 87 +++++----- ...ithLazyInitializationIntegrationTests.java | 16 +- ...toConfigurationCustomApplicationTests.java | 13 +- ...igurationCustomFilterContextPathTests.java | 13 +- ...utoConfigurationCustomFilterPathTests.java | 13 +- ...rationCustomObjectMapperProviderTests.java | 13 +- ...gurationCustomServletContextPathTests.java | 13 +- ...toConfigurationCustomServletPathTests.java | 13 +- ...toConfigurationDefaultFilterPathTests.java | 13 +- ...oConfigurationDefaultServletPathTests.java | 13 +- ...onfigurationObjectMapperProviderTests.java | 11 +- ...figurationWithoutApplicationPathTests.java | 13 +- ...oConfigurationServletIntegrationTests.java | 32 ++-- ...ConfigurationEarlyInitializationTests.java | 9 +- .../WelcomePageIntegrationTests.java | 27 ++- .../BasicErrorControllerIntegrationTests.java | 164 ++++++++++-------- .../RemappedErrorViewIntegrationTests.java | 28 +-- 17 files changed, 255 insertions(+), 236 deletions(-) diff --git a/module/spring-boot-devtools/src/intTest/java/org/springframework/boot/devtools/tests/DevToolsIntegrationTests.java b/module/spring-boot-devtools/src/intTest/java/org/springframework/boot/devtools/tests/DevToolsIntegrationTests.java index 5d897df5fbb8..6e0bd2da730f 100644 --- a/module/spring-boot-devtools/src/intTest/java/org/springframework/boot/devtools/tests/DevToolsIntegrationTests.java +++ b/module/spring-boot-devtools/src/intTest/java/org/springframework/boot/devtools/tests/DevToolsIntegrationTests.java @@ -25,10 +25,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.boot.restclient.RestTemplateBuilder; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.http.HttpStatus; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.test.web.servlet.client.RestTestClient.BodySpec; +import org.springframework.test.web.servlet.client.StatusAssertions; import static org.assertj.core.api.Assertions.assertThat; @@ -40,23 +40,23 @@ @SuppressWarnings("removal") class DevToolsIntegrationTests extends AbstractDevToolsIntegrationTests { - private final TestRestTemplate template = new TestRestTemplate(new RestTemplateBuilder() - .requestFactory(() -> new HttpComponentsClientHttpRequestFactory(HttpClients.custom() + private final RestTestClient client = RestTestClient + .bindToServer(new HttpComponentsClientHttpRequestFactory(HttpClients.custom() .setRetryStrategy(new DefaultHttpRequestRetryStrategy(10, TimeValue.of(1, TimeUnit.SECONDS))) - .build()))); + .build())) + .build(); @ParameterizedTest(name = "{0}") @MethodSource("parameters") void addARequestMappingToAnExistingController(ApplicationLauncher applicationLauncher) throws Exception { launchApplication(applicationLauncher, "--logging.level.org.springframework.boot=trace"); String urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); - assertThat(this.template.getForEntity(urlBase + "/two", String.class).getStatusCode()) - .isEqualTo(HttpStatus.NOT_FOUND); + expectResponseBody(urlBase + "/one").isEqualTo("one"); + expectResponseStatus(urlBase + "/two").isNotFound(); controller("com.example.ControllerOne").withRequestMapping("one").withRequestMapping("two").build(); urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); - assertThat(this.template.getForObject(urlBase + "/two", String.class)).isEqualTo("two"); + expectResponseBody(urlBase + "/one").isEqualTo("one"); + expectResponseBody(urlBase + "/two").isEqualTo("two"); } @ParameterizedTest(name = "{0}") @@ -64,11 +64,10 @@ void addARequestMappingToAnExistingController(ApplicationLauncher applicationLau void removeARequestMappingFromAnExistingController(ApplicationLauncher applicationLauncher) throws Exception { launchApplication(applicationLauncher); String urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); + expectResponseBody(urlBase + "/one").isEqualTo("one"); controller("com.example.ControllerOne").build(); urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForEntity(urlBase + "/one", String.class).getStatusCode()) - .isEqualTo(HttpStatus.NOT_FOUND); + expectResponseStatus(urlBase + "/one").isNotFound(); } @ParameterizedTest(name = "{0}") @@ -76,13 +75,12 @@ void removeARequestMappingFromAnExistingController(ApplicationLauncher applicati void createAController(ApplicationLauncher applicationLauncher) throws Exception { launchApplication(applicationLauncher); String urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); - assertThat(this.template.getForEntity(urlBase + "/two", String.class).getStatusCode()) - .isEqualTo(HttpStatus.NOT_FOUND); + expectResponseBody(urlBase + "/one").isEqualTo("one"); + expectResponseStatus(urlBase + "/two").isNotFound(); controller("com.example.ControllerTwo").withRequestMapping("two").build(); urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); - assertThat(this.template.getForObject(urlBase + "/two", String.class)).isEqualTo("two"); + expectResponseBody(urlBase + "/one").isEqualTo("one"); + expectResponseBody(urlBase + "/two").isEqualTo("two"); } @@ -91,16 +89,15 @@ void createAController(ApplicationLauncher applicationLauncher) throws Exception void createAControllerAndThenAddARequestMapping(ApplicationLauncher applicationLauncher) throws Exception { launchApplication(applicationLauncher); String urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); - assertThat(this.template.getForEntity(urlBase + "/two", String.class).getStatusCode()) - .isEqualTo(HttpStatus.NOT_FOUND); + expectResponseBody(urlBase + "/one").isEqualTo("one"); + expectResponseStatus(urlBase + "/two").isNotFound(); controller("com.example.ControllerTwo").withRequestMapping("two").build(); urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); - assertThat(this.template.getForObject(urlBase + "/two", String.class)).isEqualTo("two"); + expectResponseBody(urlBase + "/one").isEqualTo("one"); + expectResponseBody(urlBase + "/two").isEqualTo("two"); controller("com.example.ControllerTwo").withRequestMapping("two").withRequestMapping("three").build(); urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForObject(urlBase + "/three", String.class)).isEqualTo("three"); + expectResponseBody(urlBase + "/three").isEqualTo("three"); } @ParameterizedTest(name = "{0}") @@ -109,18 +106,17 @@ void createAControllerAndThenAddARequestMappingToAnExistingController(Applicatio throws Exception { launchApplication(applicationLauncher); String urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); - assertThat(this.template.getForEntity(urlBase + "/two", String.class).getStatusCode()) - .isEqualTo(HttpStatus.NOT_FOUND); + expectResponseBody(urlBase + "/one").isEqualTo("one"); + expectResponseStatus(urlBase + "/two").isNotFound(); controller("com.example.ControllerTwo").withRequestMapping("two").build(); urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); - assertThat(this.template.getForObject(urlBase + "/two", String.class)).isEqualTo("two"); + expectResponseBody(urlBase + "/one").isEqualTo("one"); + expectResponseBody(urlBase + "/two").isEqualTo("two"); controller("com.example.ControllerOne").withRequestMapping("one").withRequestMapping("three").build(); urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); - assertThat(this.template.getForObject(urlBase + "/two", String.class)).isEqualTo("two"); - assertThat(this.template.getForObject(urlBase + "/three", String.class)).isEqualTo("three"); + expectResponseBody(urlBase + "/one").isEqualTo("one"); + expectResponseBody(urlBase + "/two").isEqualTo("two"); + expectResponseBody(urlBase + "/three").isEqualTo("three"); } @ParameterizedTest(name = "{0}") @@ -128,12 +124,11 @@ void createAControllerAndThenAddARequestMappingToAnExistingController(Applicatio void deleteAController(ApplicationLauncher applicationLauncher) throws Exception { LaunchedApplication launchedApplication = launchApplication(applicationLauncher); String urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); + expectResponseBody(urlBase + "/one").isEqualTo("one"); assertThat(new File(launchedApplication.getClassesDirectory(), "com/example/ControllerOne.class").delete()) .isTrue(); urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForEntity(urlBase + "/one", String.class).getStatusCode()) - .isEqualTo(HttpStatus.NOT_FOUND); + expectResponseStatus(urlBase + "/one").isNotFound(); } @@ -142,18 +137,16 @@ void deleteAController(ApplicationLauncher applicationLauncher) throws Exception void createAControllerAndThenDeleteIt(ApplicationLauncher applicationLauncher) throws Exception { LaunchedApplication launchedApplication = launchApplication(applicationLauncher); String urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); - assertThat(this.template.getForEntity(urlBase + "/two", String.class).getStatusCode()) - .isEqualTo(HttpStatus.NOT_FOUND); + expectResponseBody(urlBase + "/one").isEqualTo("one"); + expectResponseStatus(urlBase + "/two").isNotFound(); controller("com.example.ControllerTwo").withRequestMapping("two").build(); urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); - assertThat(this.template.getForObject(urlBase + "/two", String.class)).isEqualTo("two"); + expectResponseBody(urlBase + "/one").isEqualTo("one"); + expectResponseBody(urlBase + "/two").isEqualTo("two"); assertThat(new File(launchedApplication.getClassesDirectory(), "com/example/ControllerTwo.class").delete()) .isTrue(); urlBase = "http://localhost:" + awaitServerPort(); - assertThat(this.template.getForEntity(urlBase + "/two", String.class).getStatusCode()) - .isEqualTo(HttpStatus.NOT_FOUND); + expectResponseStatus(urlBase + "/two").isNotFound(); } static Object[] parameters() { @@ -163,4 +156,12 @@ static Object[] parameters() { new Object[] { new JarFileRemoteApplicationLauncher(directories) } }; } + private BodySpec<String, ?> expectResponseBody(String url) { + return this.client.get().uri(url).exchangeSuccessfully().expectBody(String.class); + } + + private StatusAssertions expectResponseStatus(String url) { + return this.client.get().uri(url).exchange().expectStatus(); + } + } diff --git a/module/spring-boot-devtools/src/intTest/java/org/springframework/boot/devtools/tests/DevToolsWithLazyInitializationIntegrationTests.java b/module/spring-boot-devtools/src/intTest/java/org/springframework/boot/devtools/tests/DevToolsWithLazyInitializationIntegrationTests.java index e536c6c4f4b7..ae7200fc8f24 100644 --- a/module/spring-boot-devtools/src/intTest/java/org/springframework/boot/devtools/tests/DevToolsWithLazyInitializationIntegrationTests.java +++ b/module/spring-boot-devtools/src/intTest/java/org/springframework/boot/devtools/tests/DevToolsWithLazyInitializationIntegrationTests.java @@ -19,10 +19,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.http.HttpStatus; - -import static org.assertj.core.api.Assertions.assertThat; +import org.springframework.test.web.servlet.client.RestTestClient; /** * Integration tests for DevTools with lazy initialization enabled. @@ -36,15 +33,14 @@ class DevToolsWithLazyInitializationIntegrationTests extends AbstractDevToolsInt void addARequestMappingToAnExistingControllerWhenLazyInit(ApplicationLauncher applicationLauncher) throws Exception { launchApplication(applicationLauncher, "--spring.main.lazy-initialization=true"); - TestRestTemplate template = new TestRestTemplate(); + RestTestClient client = RestTestClient.bindToServer().build(); String urlBase = "http://localhost:" + awaitServerPort(); - assertThat(template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); - assertThat(template.getForEntity(urlBase + "/two", String.class).getStatusCode()) - .isEqualTo(HttpStatus.NOT_FOUND); + client.get().uri(urlBase + "/one").exchangeSuccessfully().expectBody(String.class).isEqualTo("one"); + client.get().uri(urlBase + "/two").exchange().expectStatus().isNotFound(); controller("com.example.ControllerOne").withRequestMapping("one").withRequestMapping("two").build(); urlBase = "http://localhost:" + awaitServerPort(); - assertThat(template.getForObject(urlBase + "/one", String.class)).isEqualTo("one"); - assertThat(template.getForObject(urlBase + "/two", String.class)).isEqualTo("two"); + client.get().uri(urlBase + "/one").exchangeSuccessfully().expectBody(String.class).isEqualTo("one"); + client.get().uri(urlBase + "/two").exchangeSuccessfully().expectBody(String.class).isEqualTo("two"); } static Object[] parameters() { diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomApplicationTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomApplicationTests.java index 0056e079e4fe..4e38ef1c8ea2 100644 --- a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomApplicationTests.java +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomApplicationTests.java @@ -25,17 +25,18 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.http.server.LocalTestWebServer; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -45,17 +46,17 @@ * @author Stephane Nicoll */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate @DirtiesContext class JerseyAutoConfigurationCustomApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private ApplicationContext applicationContext; @Test void contextLoads() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/test/hello", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + String uri = LocalTestWebServer.obtain(this.applicationContext).uri("/test/hello"); + ResponseEntity<String> response = RestClient.create().get().uri(uri).retrieve().toEntity(String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @ApplicationPath("/test") diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomFilterContextPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomFilterContextPathTests.java index 980fbb7a6812..564af5de496b 100644 --- a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomFilterContextPathTests.java +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomFilterContextPathTests.java @@ -32,16 +32,17 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.http.server.LocalTestWebServer; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -53,17 +54,17 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.jersey.type=filter", "server.servlet.context-path=/app", "server.servlet.register-default-servlet=true" }) -@AutoConfigureTestRestTemplate @DirtiesContext class JerseyAutoConfigurationCustomFilterContextPathTests { @Autowired - private TestRestTemplate restTemplate; + private ApplicationContext applicationContext; @Test void contextLoads() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/rest/hello", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + String uri = LocalTestWebServer.obtain(this.applicationContext).uri("/rest/hello"); + ResponseEntity<String> response = RestClient.create().get().uri(uri).retrieve().toEntity(String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @MinimalWebConfiguration diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomFilterPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomFilterPathTests.java index 7c80d224b033..10befd6ab6d7 100644 --- a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomFilterPathTests.java +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomFilterPathTests.java @@ -32,16 +32,17 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.http.server.LocalTestWebServer; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -52,17 +53,17 @@ */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.jersey.type=filter", "server.servlet.register-default-servlet=true" }) -@AutoConfigureTestRestTemplate @DirtiesContext class JerseyAutoConfigurationCustomFilterPathTests { @Autowired - private TestRestTemplate restTemplate; + private ApplicationContext applicationContext; @Test void contextLoads() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/rest/hello", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + String uri = LocalTestWebServer.obtain(this.applicationContext).uri("/rest/hello"); + ResponseEntity<String> response = RestClient.create().get().uri(uri).retrieve().toEntity(String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @MinimalWebConfiguration diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomObjectMapperProviderTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomObjectMapperProviderTests.java index d60816f68aa1..00d0616bf2fa 100644 --- a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomObjectMapperProviderTests.java +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomObjectMapperProviderTests.java @@ -31,16 +31,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.http.server.LocalTestWebServer; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -51,18 +52,18 @@ */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.jackson2.default-property-inclusion=non_null") -@AutoConfigureTestRestTemplate @DirtiesContext class JerseyAutoConfigurationCustomObjectMapperProviderTests { @Autowired - private TestRestTemplate restTemplate; + private ApplicationContext applicationContext; @Test void contextLoads() { - ResponseEntity<String> response = this.restTemplate.getForEntity("/rest/message", String.class); + String uri = LocalTestWebServer.obtain(this.applicationContext).uri("/rest/message"); + ResponseEntity<String> response = RestClient.create().get().uri(uri).retrieve().toEntity(String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat("{\"subject\":\"Jersey\"}").isEqualTo(response.getBody()); + assertThat(response.getBody()).isEqualTo("{\"subject\":\"Jersey\"}"); } @MinimalWebConfiguration diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomServletContextPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomServletContextPathTests.java index 7bd41efe1b10..b54c86f2f768 100644 --- a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomServletContextPathTests.java +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomServletContextPathTests.java @@ -32,16 +32,17 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.http.server.LocalTestWebServer; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -51,17 +52,17 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "server.servlet.contextPath=/app") -@AutoConfigureTestRestTemplate @DirtiesContext class JerseyAutoConfigurationCustomServletContextPathTests { @Autowired - private TestRestTemplate restTemplate; + private ApplicationContext applicationContext; @Test void contextLoads() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/rest/hello", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + String uri = LocalTestWebServer.obtain(this.applicationContext).uri("/rest/hello"); + ResponseEntity<String> response = RestClient.create().get().uri(uri).retrieve().toEntity(String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @MinimalWebConfiguration diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomServletPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomServletPathTests.java index e402cbbc644b..ca8859e28fc0 100644 --- a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomServletPathTests.java +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomServletPathTests.java @@ -32,16 +32,17 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.http.server.LocalTestWebServer; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -51,17 +52,17 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate @DirtiesContext class JerseyAutoConfigurationCustomServletPathTests { @Autowired - private TestRestTemplate restTemplate; + private ApplicationContext applicationContext; @Test void contextLoads() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/rest/hello", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + String uri = LocalTestWebServer.obtain(this.applicationContext).uri("/rest/hello"); + ResponseEntity<String> response = RestClient.create().get().uri(uri).retrieve().toEntity(String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @MinimalWebConfiguration diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationDefaultFilterPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationDefaultFilterPathTests.java index 6e4523a9cd9b..e9b2e401be72 100644 --- a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationDefaultFilterPathTests.java +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationDefaultFilterPathTests.java @@ -31,16 +31,17 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.http.server.LocalTestWebServer; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -51,17 +52,17 @@ */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.jersey.type=filter", "server.servlet.register-default-servlet=true" }) -@AutoConfigureTestRestTemplate @DirtiesContext class JerseyAutoConfigurationDefaultFilterPathTests { @Autowired - private TestRestTemplate restTemplate; + private ApplicationContext applicationContext; @Test void contextLoads() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/hello", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + String uri = LocalTestWebServer.obtain(this.applicationContext).uri("/hello"); + ResponseEntity<String> response = RestClient.create().get().uri(uri).retrieve().toEntity(String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @MinimalWebConfiguration diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationDefaultServletPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationDefaultServletPathTests.java index bbbf74455514..cdc7f302ae47 100644 --- a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationDefaultServletPathTests.java +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationDefaultServletPathTests.java @@ -31,16 +31,17 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.http.server.LocalTestWebServer; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -50,17 +51,17 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate @DirtiesContext class JerseyAutoConfigurationDefaultServletPathTests { @Autowired - private TestRestTemplate restTemplate; + private ApplicationContext applicationContext; @Test void contextLoads() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/hello", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + String uri = LocalTestWebServer.obtain(this.applicationContext).uri("/hello"); + ResponseEntity<String> response = RestClient.create().get().uri(uri).retrieve().toEntity(String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @MinimalWebConfiguration diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationObjectMapperProviderTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationObjectMapperProviderTests.java index d6f5fa89281e..55554f62506e 100644 --- a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationObjectMapperProviderTests.java +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationObjectMapperProviderTests.java @@ -32,16 +32,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.http.server.LocalTestWebServer; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -53,16 +54,16 @@ */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.jackson2.default-property-inclusion:non-null") -@AutoConfigureTestRestTemplate @DirtiesContext class JerseyAutoConfigurationObjectMapperProviderTests { @Autowired - private TestRestTemplate restTemplate; + private ApplicationContext applicationContext; @Test void responseIsSerializedUsingAutoConfiguredObjectMapper() { - ResponseEntity<String> response = this.restTemplate.getForEntity("/rest/message", String.class); + String uri = LocalTestWebServer.obtain(this.applicationContext).uri("/rest/message"); + ResponseEntity<String> response = RestClient.create().get().uri(uri).retrieve().toEntity(String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isEqualTo("{\"subject\":\"Jersey\"}"); } diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationWithoutApplicationPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationWithoutApplicationPathTests.java index 51d36468c232..b0adb20f3da9 100644 --- a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationWithoutApplicationPathTests.java +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationWithoutApplicationPathTests.java @@ -31,16 +31,17 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.http.server.LocalTestWebServer; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -50,17 +51,17 @@ * @author Eddú Meléndez */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.jersey.application-path=/api") -@AutoConfigureTestRestTemplate @DirtiesContext class JerseyAutoConfigurationWithoutApplicationPathTests { @Autowired - private TestRestTemplate restTemplate; + private ApplicationContext applicationContext; @Test void contextLoads() { - ResponseEntity<String> entity = this.restTemplate.getForEntity("/api/hello", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + String uri = LocalTestWebServer.obtain(this.applicationContext).uri("/api/hello"); + ResponseEntity<String> response = RestClient.create().get().uri(uri).retrieve().toEntity(String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @MinimalWebConfiguration diff --git a/module/spring-boot-mustache/src/test/java/org/springframework/boot/mustache/autoconfigure/MustacheAutoConfigurationServletIntegrationTests.java b/module/spring-boot-mustache/src/test/java/org/springframework/boot/mustache/autoconfigure/MustacheAutoConfigurationServletIntegrationTests.java index d0f8528d2d94..409a2cf19eaf 100644 --- a/module/spring-boot-mustache/src/test/java/org/springframework/boot/mustache/autoconfigure/MustacheAutoConfigurationServletIntegrationTests.java +++ b/module/spring-boot-mustache/src/test/java/org/springframework/boot/mustache/autoconfigure/MustacheAutoConfigurationServletIntegrationTests.java @@ -22,24 +22,22 @@ import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Template; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.mustache.servlet.view.MustacheView; import org.springframework.boot.mustache.servlet.view.MustacheViewResolver; -import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; -import org.springframework.boot.web.server.WebServer; -import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.stereotype.Controller; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.DispatcherServlet; @@ -54,19 +52,11 @@ */ @DirtiesContext @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@AutoConfigureRestTestClient class MustacheAutoConfigurationServletIntegrationTests { @Autowired - private ServletWebServerApplicationContext context; - - private int port; - - @BeforeEach - void init() { - WebServer webServer = this.context.getWebServer(); - assertThat(webServer).isNotNull(); - this.port = webServer.getPort(); - } + private RestTestClient restTestClient; @Test void shouldRenderTemplate() { @@ -79,14 +69,20 @@ void shouldRenderTemplate() { @Test void testHomePage() { - String body = new TestRestTemplate().getForObject("http://localhost:" + this.port, String.class); - assertThat(body).contains("Hello World"); + this.restTestClient.get() + .uri("/") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("Hello World")); } @Test void testPartialPage() { - String body = new TestRestTemplate().getForObject("http://localhost:" + this.port + "/partial", String.class); - assertThat(body).contains("Hello World"); + this.restTestClient.get() + .uri("/partial") + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("Hello World")); } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/web/servlet/SecurityFilterAutoConfigurationEarlyInitializationTests.java b/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/web/servlet/SecurityFilterAutoConfigurationEarlyInitializationTests.java index f3e1d0e69700..c3c24bebb6c8 100644 --- a/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/web/servlet/SecurityFilterAutoConfigurationEarlyInitializationTests.java +++ b/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/web/servlet/SecurityFilterAutoConfigurationEarlyInitializationTests.java @@ -32,7 +32,6 @@ import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration; import org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration; import org.springframework.boot.test.system.CapturedOutput; @@ -51,6 +50,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.stereotype.Component; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -82,7 +82,12 @@ void testSecurityFilterDoesNotCauseEarlyInitialization(CapturedOutput output) { int port = webServer.getPort(); Matcher password = PASSWORD_PATTERN.matcher(output); assertThat(password.find()).isTrue(); - new TestRestTemplate("user", password.group(1)).getForEntity("http://localhost:" + port, Object.class); + RestTestClient.bindToServer() + .defaultHeaders((headers) -> headers.setBasicAuth("user", password.group(1))) + .build() + .get() + .uri("http://localhost:" + port) + .exchange(); // If early initialization occurred a ConverterNotFoundException is thrown } } diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WelcomePageIntegrationTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WelcomePageIntegrationTests.java index 08333d30b6d5..c702de2071a1 100644 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WelcomePageIntegrationTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WelcomePageIntegrationTests.java @@ -16,14 +16,11 @@ package org.springframework.boot.webmvc.autoconfigure; -import java.net.URI; - import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.testsupport.classpath.resources.WithResource; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; @@ -32,8 +29,7 @@ import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.RequestEntity; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -53,7 +49,7 @@ class WelcomePageIntegrationTests { WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, TomcatServletWebServerAutoConfiguration.class, DispatcherServletAutoConfiguration.class)); - private final TestRestTemplate template = new TestRestTemplate(); + private final RestTestClient client = RestTestClient.bindToServer().build(); @Test void contentStrategyWithWelcomePage() { @@ -61,12 +57,12 @@ void contentStrategyWithWelcomePage() { WebServer webServer = ((WebServerApplicationContext) context.getSourceApplicationContext()).getWebServer(); assertThat(webServer).isNotNull(); int port = webServer.getPort(); - RequestEntity<?> entity = RequestEntity.get(new URI("http://localhost:" + port + "/")) + this.client.get() + .uri("http://localhost:" + port + "/") .header("Accept", MediaType.ALL.toString()) - .build(); - ResponseEntity<String> content = this.template.exchange(entity, String.class); - assertThat(content.getBody()).contains("custom welcome page"); - assertThat(content.getStatusCode()).isEqualTo(HttpStatus.OK); + .exchangeSuccessfully() + .expectBody(String.class) + .value((body) -> assertThat(body).contains("custom welcome page")); }); } @@ -76,11 +72,12 @@ void notAcceptableWelcomePage() { WebServer webServer = ((WebServerApplicationContext) context.getSourceApplicationContext()).getWebServer(); assertThat(webServer).isNotNull(); int port = webServer.getPort(); - RequestEntity<?> entity = RequestEntity.get(new URI("http://localhost:" + port + "/")) + this.client.get() + .uri("http://localhost:" + port + "/") .header("Accept", "spring/boot") - .build(); - ResponseEntity<String> content = this.template.exchange(entity, String.class); - assertThat(content.getStatusCode()).isEqualTo(HttpStatus.NOT_ACCEPTABLE); + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.NOT_ACCEPTABLE); }); } diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/error/BasicErrorControllerIntegrationTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/error/BasicErrorControllerIntegrationTests.java index 404162d6e213..a87dfa38e4c6 100755 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/error/BasicErrorControllerIntegrationTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/error/BasicErrorControllerIntegrationTests.java @@ -23,7 +23,6 @@ import java.lang.annotation.Target; import java.lang.reflect.Method; import java.lang.reflect.Parameter; -import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -43,7 +42,6 @@ import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebProperties; import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.error.ErrorAttributeOptions.Include; @@ -56,8 +54,8 @@ import org.springframework.core.MethodParameter; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.RequestEntity; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.validation.BindException; @@ -93,13 +91,11 @@ void closeContext() { } @Test - @SuppressWarnings({ "rawtypes", "unchecked" }) void testErrorForMachineClientDefault() { load(); - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("?trace=true"), Map.class); - assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", null, null, "/"); - assertThat(entity.getBody()).doesNotContainKey("exception"); - assertThat(entity.getBody()).doesNotContainKey("trace"); + Map<String, ?> response = getResponseMap("?trace=true"); + assertErrorAttributes(response, "500", "Internal Server Error", null, null, "/"); + assertThat(response).doesNotContainKey("exception").doesNotContainKey("trace"); } @Test @@ -138,44 +134,39 @@ void testErrorForMachineClientAlwaysParams() { } @Test - @SuppressWarnings("rawtypes") void testErrorForMachineClientAlwaysParamsWithoutMessage() { load("--spring.web.error.include-exception=true", "--spring.web.error.include-message=always"); - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/noMessage"), Map.class); - assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", IllegalStateException.class, + Map<String, ?> response = getResponseMap("/noMessage"); + assertErrorAttributes(response, "500", "Internal Server Error", IllegalStateException.class, "No message available", "/noMessage"); } - @SuppressWarnings({ "rawtypes", "unchecked" }) private void exceptionWithStackTraceAndMessage(String path) { - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl(path), Map.class); - assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", IllegalStateException.class, - "Expected!", "/"); - assertThat(entity.getBody()).containsKey("trace"); + Map<String, ?> response = getResponseMap(path); + assertErrorAttributes(response, "500", "Internal Server Error", IllegalStateException.class, "Expected!", "/"); + assertThat(response).containsKey("trace"); } - @SuppressWarnings({ "rawtypes", "unchecked" }) private void exceptionWithoutStackTraceAndMessage(String path) { - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl(path), Map.class); - assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", IllegalStateException.class, null, "/"); - assertThat(entity.getBody()).doesNotContainKey("trace"); + Map<String, ?> response = getResponseMap(path); + assertErrorAttributes(response, "500", "Internal Server Error", IllegalStateException.class, null, "/"); + assertThat(response).doesNotContainKey("trace"); } @Test - @SuppressWarnings("rawtypes") void testErrorForAnnotatedExceptionWithoutMessage() { load("--spring.web.error.include-exception=true"); - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotated"), Map.class); - assertErrorAttributes(entity.getBody(), "400", "Bad Request", TestConfiguration.Errors.ExpectedException.class, - null, "/annotated"); + Map<String, ?> response = getResponseMap("/annotated"); + assertErrorAttributes(response, "400", "Bad Request", TestConfiguration.Errors.ExpectedException.class, null, + "/annotated"); } @Test @SuppressWarnings("rawtypes") void testErrorForAnnotatedExceptionWithMessage() { load("--spring.web.error.include-exception=true", "--spring.web.error.include-message=always"); - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotated"), Map.class); - assertErrorAttributes(entity.getBody(), "400", "Bad Request", TestConfiguration.Errors.ExpectedException.class, + Map<String, ?> response = getResponseMap("/annotated"); + assertErrorAttributes(response, "400", "Bad Request", TestConfiguration.Errors.ExpectedException.class, "Expected!", "/annotated"); } @@ -183,8 +174,8 @@ void testErrorForAnnotatedExceptionWithMessage() { @SuppressWarnings("rawtypes") void testErrorForAnnotatedNoReasonExceptionWithoutMessage() { load("--spring.web.error.include-exception=true"); - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotatedNoReason"), Map.class); - assertErrorAttributes(entity.getBody(), "406", "Not Acceptable", + Map<String, ?> response = getResponseMap("/annotatedNoReason"); + assertErrorAttributes(response, "406", "Not Acceptable", TestConfiguration.Errors.NoReasonExpectedException.class, null, "/annotatedNoReason"); } @@ -192,8 +183,8 @@ void testErrorForAnnotatedNoReasonExceptionWithoutMessage() { @SuppressWarnings("rawtypes") void testErrorForAnnotatedNoReasonExceptionWithMessage() { load("--spring.web.error.include-exception=true", "--spring.web.error.include-message=always"); - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotatedNoReason"), Map.class); - assertErrorAttributes(entity.getBody(), "406", "Not Acceptable", + Map<String, ?> response = getResponseMap("/annotatedNoReason"); + assertErrorAttributes(response, "406", "Not Acceptable", TestConfiguration.Errors.NoReasonExpectedException.class, "Expected message", "/annotatedNoReason"); } @@ -201,8 +192,8 @@ void testErrorForAnnotatedNoReasonExceptionWithMessage() { @SuppressWarnings("rawtypes") void testErrorForAnnotatedNoMessageExceptionWithMessage() { load("--spring.web.error.include-exception=true", "--spring.web.error.include-message=always"); - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotatedNoMessage"), Map.class); - assertErrorAttributes(entity.getBody(), "406", "Not Acceptable", + Map<String, ?> response = getResponseMap("/annotatedNoMessage"); + assertErrorAttributes(response, "406", "Not Acceptable", TestConfiguration.Errors.NoReasonExpectedException.class, "No message available", "/annotatedNoMessage"); } @@ -267,87 +258,94 @@ void testBindingExceptionForMachineClientNeverMessage() { bindingExceptionWithoutMessage("?message=true"); } - @SuppressWarnings({ "rawtypes", "unchecked" }) private void bindingExceptionWithErrors(String param) { - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class); - assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null, - "/bind"); - assertThat(entity.getBody()).containsKey("errors"); + Map<String, ?> response = getResponseMap("/bind" + param); + assertErrorAttributes(response, "400", "Bad Request", MethodArgumentNotValidException.class, null, "/bind"); + assertThat(response).containsKey("errors"); } - @SuppressWarnings({ "rawtypes", "unchecked" }) private void bindingExceptionWithoutErrors(String param) { - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class); - assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null, - "/bind"); - assertThat(entity.getBody()).doesNotContainKey("errors"); + Map<String, ?> response = getResponseMap("/bind" + param); + assertErrorAttributes(response, "400", "Bad Request", MethodArgumentNotValidException.class, null, "/bind"); + assertThat(response).doesNotContainKey("errors"); } - @SuppressWarnings({ "rawtypes", "unchecked" }) private void bindingExceptionWithMessage(String param) { - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class); - assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, + Map<String, ?> response = getResponseMap("/bind" + param); + assertErrorAttributes(response, "400", "Bad Request", MethodArgumentNotValidException.class, "Validation failed for object='test'. Error count: 1", "/bind"); - assertThat(entity.getBody()).doesNotContainKey("errors"); + assertThat(response).doesNotContainKey("errors"); } - @SuppressWarnings({ "rawtypes", "unchecked" }) private void bindingExceptionWithoutMessage(String param) { - ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class); - assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null, - "/bind"); - assertThat(entity.getBody()).doesNotContainKey("errors"); + Map<String, ?> response = getResponseMap("/bind" + param); + assertErrorAttributes(response, "400", "Bad Request", MethodArgumentNotValidException.class, null, "/bind"); + assertThat(response).doesNotContainKey("errors"); } @Test - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings({ "unchecked" }) void testRequestBodyValidationForMachineClient() { load("--spring.web.error.include-exception=true"); - RequestEntity request = RequestEntity.post(URI.create(createUrl("/bodyValidation"))) - .accept(MediaType.APPLICATION_JSON) + Map<String, ?> response = createRestClient().post() + .uri("/bodyValidation") .contentType(MediaType.APPLICATION_JSON) - .body("{}"); - ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class); - assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null, + .accept(MediaType.APPLICATION_JSON) + .body("{}") + .exchange() + .expectBody(Map.class) + .returnResult() + .getResponseBody(); + assertErrorAttributes(response, "400", "Bad Request", MethodArgumentNotValidException.class, null, "/bodyValidation"); - assertThat(entity.getBody()).doesNotContainKey("errors"); + assertThat(response).doesNotContainKey("errors"); } @Test @SuppressWarnings({ "rawtypes", "unchecked" }) void testBindingExceptionForMachineClientDefault() { load(); - RequestEntity request = RequestEntity.get(URI.create(createUrl("/bind?trace=true,message=true"))) + Map<String, ?> response = createRestClient().get() + .uri("/bind?trace=true,message=true") .accept(MediaType.APPLICATION_JSON) - .build(); - ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class); - assertThat(entity.getBody()).doesNotContainKey("exception"); - assertThat(entity.getBody()).doesNotContainKey("trace"); - assertThat(entity.getBody()).doesNotContainKey("errors"); + .exchange() + .expectBody(Map.class) + .returnResult() + .getResponseBody(); + assertThat(response).doesNotContainKey("exception"); + assertThat(response).doesNotContainKey("trace"); + assertThat(response).doesNotContainKey("errors"); } @Test void testIncompatibleMediaType() { load(); - RequestEntity<?> request = RequestEntity.get(URI.create(createUrl("/incompatibleType"))) + createRestClient().get() + .uri("/incompatibleType") .accept(MediaType.TEXT_PLAIN) - .build(); - ResponseEntity<String> entity = new TestRestTemplate().exchange(request, String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(entity.getHeaders().getContentType()).isNull(); - assertThat(entity.getBody()).isNull(); + .exchange() + .expectStatus() + .isBadRequest() + .expectHeader() + .doesNotExist("Content-Type") + .expectBody() + .isEmpty(); } @Test @SuppressWarnings({ "rawtypes", "unchecked" }) void customErrorControllerWithoutStatusConfiguration() { load(CustomErrorControllerWithoutStatusConfiguration.class); - RequestEntity request = RequestEntity.post(URI.create(createUrl("/bodyValidation"))) + Map<String, ?> response = createRestClient().post() + .uri("/bodyValidation") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) - .body("{}"); - ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class); - assertThat(entity.getBody()).doesNotContainKey("status"); + .body("{}") + .exchange() + .expectBody(Map.class) + .returnResult() + .getResponseBody(); + assertThat(response).doesNotContainKey("status"); } private void assertErrorAttributes(@Nullable Map<?, ?> content, String status, String error, @@ -365,11 +363,23 @@ private void assertErrorAttributes(@Nullable Map<?, ?> content, String status, S assertThat(content.get("path")).as("Wrong path").isEqualTo(path); } - private String createUrl(String path) { + @SuppressWarnings("unchecked") + private Map<String, String> getResponseMap(String url) { + Map<String, String> responseBody = createRestClient().get() + .uri(url) + .exchange() + .expectBody(Map.class) + .returnResult() + .getResponseBody(); + Assert.notNull(responseBody, "Response body should not be null"); + return responseBody; + } + + RestTestClient createRestClient() { assertThat(this.context).isNotNull(); Integer port = this.context.getEnvironment().getProperty("local.server.port", Integer.class); assertThat(port).isNotNull(); - return "http://localhost:" + port + path; + return RestTestClient.bindToServer().baseUrl("http://localhost:" + port).build(); } private void load(String... arguments) { diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/error/RemappedErrorViewIntegrationTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/error/RemappedErrorViewIntegrationTests.java index 006e87cfcc52..5b97d97445e9 100644 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/error/RemappedErrorViewIntegrationTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/error/RemappedErrorViewIntegrationTests.java @@ -18,13 +18,13 @@ import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration; -import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; import org.springframework.boot.web.error.ErrorPage; import org.springframework.boot.web.error.ErrorPageRegistrar; @@ -35,6 +35,7 @@ import org.springframework.context.annotation.Import; import org.springframework.stereotype.Controller; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.bind.annotation.RequestMapping; import static org.assertj.core.api.Assertions.assertThat; @@ -45,26 +46,29 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.mvc.servlet.path:/spring/") +@AutoConfigureRestTestClient @DirtiesContext class RemappedErrorViewIntegrationTests { - @LocalServerPort - private int port; - - private final TestRestTemplate template = new TestRestTemplate(); + @Autowired + private RestTestClient client; @Test void directAccessToErrorPage() { - String content = this.template.getForObject("http://localhost:" + this.port + "/spring/error", String.class); - assertThat(content).contains("error"); - assertThat(content).contains("999"); + this.client.get() + .uri("/spring/error") + .exchange() + .expectBody(String.class) + .value((content) -> assertThat(content).contains("error").contains("999")); } @Test void forwardToErrorPage() { - String content = this.template.getForObject("http://localhost:" + this.port + "/spring/", String.class); - assertThat(content).contains("error"); - assertThat(content).contains("500"); + this.client.get() + .uri("/spring/") + .exchange() + .expectBody(String.class) + .value((content) -> assertThat(content).contains("error").contains("500")); } @Configuration(proxyBeanMethods = false) From c539367ea2e7cad05519627205615d00058d07bb Mon Sep 17 00:00:00 2001 From: Brian Clozel <brian.clozel@broadcom.com> Date: Mon, 27 Jul 2026 16:13:37 +0200 Subject: [PATCH 792/930] Deprecate `TestRestTemplate` Closes gh-46632 --- .../testing/spring-boot-applications.adoc | 9 ++-- .../pages/testing/test-utilities.adoc | 3 ++ .../springmvctests/MockTransportTests.java | 52 +++++++++++++++++++ .../MyRandomPortTestRestTemplateTests.java | 1 + .../utilities/testresttemplate/MyTests.java | 1 + .../springmvctests/MockTransportTests.kt | 45 ++++++++++++++++ .../MyRandomPortTestRestTemplateTests.kt | 2 + .../testresttemplate/MySpringBootTests.kt | 2 +- .../utilities/testresttemplate/MyTests.kt | 2 + .../boot/resttestclient/TestRestTemplate.java | 3 ++ ...TestRestTemplateTestAutoConfiguration.java | 7 ++- .../TestRestTemplateExtensions.kt | 2 + .../resttestclient/TestRestTemplateTests.java | 7 +-- ...estTemplateTestAutoConfigurationTests.java | 1 + 14 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/springmvctests/MockTransportTests.java create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/springbootapplications/springmvctests/MockTransportTests.kt diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc index 1eac09c2cde0..f5017c692987 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc @@ -340,7 +340,7 @@ include-code::MyJsonAssertJTests[tag=*] == Auto-configured Spring MVC Tests To test whether Spring MVC controllers are working as expected, use the javadoc:org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest[format=annotation] annotation from the `spring-boot-webmvc-test` module. -javadoc:org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest[format=annotation] auto-configures the Spring MVC infrastructure and limits scanned beans to javadoc:org.springframework.stereotype.Controller[format=annotation], javadoc:org.springframework.web.bind.annotation.ControllerAdvice[format=annotation], javadoc:org.springframework.boot.jackson.JacksonComponent[format=annotation], javadoc:org.springframework.boot.jackson2.JsonComponent[format=annotation] (deprecated), javadoc:org.springframework.core.convert.converter.Converter[], javadoc:org.springframework.core.convert.converter.GenericConverter[], javadoc:jakarta.servlet.Filter[], javadoc:org.springframework.web.servlet.HandlerInterceptor[], javadoc:org.springframework.web.servlet.config.annotation.WebMvcConfigurer[], javadoc:org.springframework.boot.webmvc.autoconfigure.WebMvcRegistrations[], and javadoc:org.springframework.web.method.support.HandlerMethodArgumentResolver[]. +javadoc:org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest[format=annotation] auto-configures the Spring MVC infrastructure and limits scanned beans to javadoc:org.springframework.stereotype.Controller[format=annotation], javadoc:org.springframework.web.bind.annotation.ControllerAdvice[format=annotation], javadoc:org.springframework.boot.jackson.JacksonComponent[format=annotation], javadoc:org.springframework.boot.jackson2.JsonComponent[format=annotation] (deprecated), javadoc:org.springframework.core.convert.converter.Converter[], javadoc:org.springframework.core.convert.converter.GenericConverter[], javadoc:jakarta.servlet.Filter[], javadoc:org.springframework.web.servlet.HandlerInterceptor[], javadoc:org.springframework.web.servlet.config.annotation.WebMvcConfigurer[], javadoc:org.springframework.boot.webmvc.autoconfigure.WebMvcRegistrations[], and javadoc:org.springframework.web.method.support.HandlerMethodArgumentResolver[]. Regular javadoc:org.springframework.stereotype.Component[format=annotation] and javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation] beans are not scanned when the javadoc:org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest[format=annotation] annotation is used. javadoc:org.springframework.boot.context.properties.EnableConfigurationProperties[format=annotation] can be used to include javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation] beans. @@ -353,14 +353,17 @@ Often, javadoc:org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest[for javadoc:org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest[format=annotation] also auto-configures javadoc:org.springframework.test.web.servlet.MockMvc[]. Mock MVC offers a powerful way to quickly test MVC controllers without needing to start a full HTTP server. If AssertJ is available, the AssertJ support provided by javadoc:org.springframework.test.web.servlet.assertj.MockMvcTester[] is auto-configured as well. + +include-code::MyControllerTests[] + If you'd like to use javadoc:org.springframework.test.web.servlet.client.RestTestClient[] in your tests, annotate your test class with javadoc:org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient[format=annotation]. A `RestTestClient` that uses the Mock MVC infrastructure will then be auto-configured. +include-code::MockTransportTests[] + TIP: You can also auto-configure javadoc:org.springframework.test.web.servlet.MockMvc[] and javadoc:org.springframework.test.web.servlet.assertj.MockMvcTester[] in a non-`@WebMvcTest` (such as javadoc:org.springframework.boot.test.context.SpringBootTest[format=annotation]) by annotating it with javadoc:org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc[format=annotation]. The following example uses javadoc:org.springframework.test.web.servlet.assertj.MockMvcTester[]: -include-code::MyControllerTests[] - TIP: If you need to configure elements of the auto-configuration (for example, when servlet filters should be applied) you can use attributes in the javadoc:org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc[format=annotation] annotation. If you use HtmlUnit and Selenium, auto-configuration also provides an HtmlUnit javadoc:org.springframework.web.reactive.function.client.WebClient[] bean and/or a Selenium javadoc:org.openqa.selenium.WebDriver[] bean. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc index 0143f15733bb..64ae3f407d43 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc @@ -42,6 +42,9 @@ include-code::MyOutputCaptureTests[] [[testing.utilities.test-rest-template]] == TestRestTemplate +WARNING: As of Spring Boot 4.2, `TestRestTemplate` is formally deprecated along `RestTemplate` itself in Spring Framework 7.1. +Test suites should xref:reference:testing/spring-boot-applications.adoc#testing.spring-boot-applications.spring-mvc-tests[migrate to `RestTestClient`], which can bind itself against a live server or a mock transport. + javadoc:org.springframework.boot.resttestclient.TestRestTemplate[] is a convenience alternative to Spring's javadoc:org.springframework.web.client.RestTemplate[] that is useful in integration tests. It's provided by the `spring-boot-resttestclient` module. A dependency on `spring-boot-restclient` is also required. diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/springmvctests/MockTransportTests.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/springmvctests/MockTransportTests.java new file mode 100644 index 000000000000..6893971ac542 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/springmvctests/MockTransportTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.testing.springbootapplications.springmvctests; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.restclient.test.autoconfigure.AutoConfigureRestClient; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.client.RestTestClient; + +import static org.mockito.BDDMockito.given; + +@WebMvcTest(UserVehicleController.class) +@AutoConfigureRestClient +class MockTransportTests { + + @Autowired + private RestTestClient restClient; + + @MockitoBean + private UserVehicleService userVehicleService; + + @Test + void testExample() { + // @formatter:off + given(this.userVehicleService.getVehicleDetails("sboot")) + .willReturn(new VehicleDetails("Honda", "Civic")); + this.restClient.get().uri("/sboot/vehicle").accept(MediaType.TEXT_PLAIN) + .exchangeSuccessfully() + .expectBody(String.class) + .isEqualTo("Honda Civic"); + // @formatter:on + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.java index bc63c8a99815..c745b3634706 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.java @@ -28,6 +28,7 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @AutoConfigureTestRestTemplate +@SuppressWarnings("removal") class MyRandomPortTestRestTemplateTests { @Test diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.java index 793b283fe7b2..ce5a75bb3eea 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.java @@ -24,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@SuppressWarnings("removal") class MyTests { private final TestRestTemplate template = new TestRestTemplate(); diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/springbootapplications/springmvctests/MockTransportTests.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/springbootapplications/springmvctests/MockTransportTests.kt new file mode 100644 index 000000000000..3510f835e026 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/springbootapplications/springmvctests/MockTransportTests.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.testing.springbootapplications.springmvctests + +import org.junit.jupiter.api.Test +import org.mockito.BDDMockito.given +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest +import org.springframework.http.MediaType +import org.springframework.test.context.bean.override.mockito.MockitoBean +import org.springframework.test.web.servlet.client.RestTestClient +import org.springframework.test.web.servlet.client.expectBody + +@WebMvcTest(UserVehicleController::class) +@AutoConfigureRestTestClient +class MockTransportTests(@Autowired val restClient: RestTestClient) { + + @MockitoBean + lateinit var userVehicleService: UserVehicleService + + @Test + fun testExample() { + given(userVehicleService.getVehicleDetails("sboot")) + .willReturn(VehicleDetails("Honda", "Civic")) + restClient.get().uri("/sboot/vehicle").accept(MediaType.TEXT_PLAIN) + .exchangeSuccessfully().expectBody<String>().isEqualTo("Honda Civic") + } + +} + diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.kt index 58f6d734b40c..998b965ba504 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION", "REMOVAL") + package org.springframework.boot.docs.testing.springbootapplications.withrunningserver import org.assertj.core.api.Assertions.assertThat diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.kt index a873041114bf..0047cc1237be 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -@file:Suppress("DEPRECATION") +@file:Suppress("DEPRECATION", "REMOVAL") package org.springframework.boot.docs.testing.utilities.testresttemplate diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.kt index f7a1a9e16688..cb9c135809f4 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION", "REMOVAL") + package org.springframework.boot.docs.testing.utilities.testresttemplate import org.assertj.core.api.Assertions.assertThat diff --git a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java index c2cde6ede093..f45e9da0c4cc 100644 --- a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java +++ b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java @@ -90,7 +90,10 @@ * @author Dmytro Nosan * @author Yanming Zhou * @since 4.0.0 + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link org.springframework.test.web.servlet.client.RestTestClient} */ +@Deprecated(since = "4.1.0", forRemoval = true) @SuppressWarnings("removal") public class TestRestTemplate { diff --git a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfiguration.java b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfiguration.java index 706a3f35ba74..3bae6d729a68 100644 --- a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfiguration.java +++ b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfiguration.java @@ -21,7 +21,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.TestRestTemplate.HttpClientOption; import org.springframework.boot.test.http.server.LocalTestWebServer; import org.springframework.boot.test.http.server.LocalTestWebServer.Scheme; import org.springframework.context.ApplicationContext; @@ -49,10 +48,10 @@ TestRestTemplate testRestTemplate(ObjectProvider<RestTemplateBuilder> builderPro return template; } - private HttpClientOption[] httpClientOptions(Scheme scheme) { + private TestRestTemplate.HttpClientOption[] httpClientOptions(Scheme scheme) { return switch (scheme) { - case HTTP -> new HttpClientOption[] {}; - case HTTPS -> new HttpClientOption[] { HttpClientOption.SSL }; + case HTTP -> new TestRestTemplate.HttpClientOption[] {}; + case HTTPS -> new TestRestTemplate.HttpClientOption[] { TestRestTemplate.HttpClientOption.SSL }; }; } diff --git a/module/spring-boot-resttestclient/src/main/kotlin/org/springframework/boot/resttestclient/TestRestTemplateExtensions.kt b/module/spring-boot-resttestclient/src/main/kotlin/org/springframework/boot/resttestclient/TestRestTemplateExtensions.kt index b4a059d944ed..a153539ecd43 100644 --- a/module/spring-boot-resttestclient/src/main/kotlin/org/springframework/boot/resttestclient/TestRestTemplateExtensions.kt +++ b/module/spring-boot-resttestclient/src/main/kotlin/org/springframework/boot/resttestclient/TestRestTemplateExtensions.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION", "REMOVAL") + package org.springframework.boot.resttestclient import org.springframework.core.ParameterizedTypeReference diff --git a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java index 052055cb2ebe..85f6e0cfe8b7 100644 --- a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java +++ b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java @@ -39,7 +39,6 @@ import org.springframework.boot.http.client.HttpCookieHandling; import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.restclient.RestTemplateBuilder; -import org.springframework.boot.resttestclient.TestRestTemplate.HttpClientOption; import org.springframework.boot.test.http.server.LocalTestWebServer; import org.springframework.boot.test.http.server.LocalTestWebServer.Scheme; import org.springframework.core.ParameterizedTypeReference; @@ -143,9 +142,8 @@ void authenticated() { } @Test - @SuppressWarnings("removal") void options() { - RequestConfig config = getRequestConfig(new TestRestTemplate(HttpClientOption.ENABLE_COOKIES)); + RequestConfig config = getRequestConfig(new TestRestTemplate(TestRestTemplate.HttpClientOption.ENABLE_COOKIES)); assertThat(config.getCookieSpec()).isEqualTo("strict"); } @@ -179,7 +177,6 @@ void jdkBuilderCanBeSpecifiedWithSpecificRedirects() { } @Test - @SuppressWarnings("removal") void httpComponentsAreBuiltConsideringSettingsInRestTemplateBuilder() { RestTemplateBuilder builder = new RestTemplateBuilder() .requestFactoryBuilder(ClientHttpRequestFactoryBuilder.httpComponents()); @@ -236,7 +233,7 @@ private RequestConfig getRequestConfig(TestRestTemplate template) { } private @Nullable RedirectStrategy getRedirectStrategy(@Nullable RestTemplateBuilder builder, - HttpClientOption... httpClientOptions) { + TestRestTemplate.HttpClientOption... httpClientOptions) { builder = (builder != null) ? builder : new RestTemplateBuilder(); TestRestTemplate template = new TestRestTemplate(builder, null, null, httpClientOptions); return getRedirectStrategy(template); diff --git a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfigurationTests.java b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfigurationTests.java index 1951b5c05519..d1fb3ca49d10 100644 --- a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfigurationTests.java +++ b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfigurationTests.java @@ -35,6 +35,7 @@ * * @author Stephane Nicoll */ +@SuppressWarnings("removal") class TestRestTemplateTestAutoConfigurationTests { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() From a36d078632acc1c225caba02a48ca55cac530a94 Mon Sep 17 00:00:00 2001 From: Tim Ysewyn <Tim.Ysewyn@me.com> Date: Tue, 30 Jun 2026 07:20:31 -0500 Subject: [PATCH 793/930] Add support for image-based build caches Added a new image cache option that uses the lifecycle's `-cache-image` argument for the creator, analyzer, restorer and exporter phases, so the build cache can be stored in an image and shared between hosts via an image registry. Exposed the option in the Maven (`<buildCache><image>`) and Gradle (`buildCache { image { } }`) plugins. Reject image caches configured for the launch cache, which the lifecycle does not support. See gh-50899 Signed-off-by: Tim Ysewyn <Tim.Ysewyn@me.com> --- .../boot-build-image-image-caches.gradle | 24 +++++++ .../boot-build-image-image-caches.gradle.kts | 22 ++++++ .../pages/packaging-oci-image.adoc | 18 +++++ .../boot/gradle/tasks/bundling/CacheSpec.java | 27 +++++++ .../docs/PackagingDocumentationTests.java | 7 ++ .../packaging-oci-image/image-caches-pom.xml | 22 ++++++ .../maven-plugin/pages/build-image.adoc | 7 ++ .../springframework/boot/maven/CacheInfo.java | 37 ++++++++++ .../boot/maven/ImageTests.java | 19 +++++ .../platform/build/BuildRequest.java | 1 + .../boot/buildpack/platform/build/Cache.java | 70 ++++++++++++++++++- .../buildpack/platform/build/Lifecycle.java | 21 +++++- .../boot/buildpack/platform/build/Phase.java | 4 ++ .../platform/build/BuildRequestTests.java | 8 +++ .../platform/build/LifecycleTests.java | 21 ++++++ .../buildpack/platform/build/PhaseTests.java | 11 +++ .../build/lifecycle-analyzer-image-cache.json | 33 +++++++++ .../build/lifecycle-creator-image-cache.json | 38 ++++++++++ .../build/lifecycle-exporter-image-cache.json | 34 +++++++++ .../build/lifecycle-restorer-image-cache.json | 27 +++++++ 20 files changed, 447 insertions(+), 4 deletions(-) create mode 100644 build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle create mode 100644 build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle.kts create mode 100644 build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml create mode 100644 buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-image-cache.json create mode 100644 buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-image-cache.json create mode 100644 buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-image-cache.json create mode 100644 buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-image-cache.json diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle new file mode 100644 index 000000000000..14ffe5e14446 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle @@ -0,0 +1,24 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version-spring-boot}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::caches[] +tasks.named("bootBuildImage") { + buildCache { + image { + name = "docker.io/library/${rootProject.name}:build" + } + } +} +// end::caches[] + +tasks.register("bootBuildImageCaches") { + doFirst { + bootBuildImage.buildCache.asCache().with { println "buildCache=$name" } + } +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle.kts b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle.kts new file mode 100644 index 000000000000..ea20581a168f --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle.kts @@ -0,0 +1,22 @@ +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{version-spring-boot}" +} + +// tag::caches[] +tasks.named<BootBuildImage>("bootBuildImage") { + buildCache { + image { + name.set("docker.io/library/${rootProject.name}:build") + } + } +} +// end::caches[] + +tasks.register("bootBuildImageCaches") { + doFirst { + println("buildCache=" + tasks.getByName<BootBuildImage>("bootBuildImage").buildCache.asCache()?.image?.name) + } +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc index 8e3b4bbfaded..9ffcbe13a296 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc @@ -573,6 +573,24 @@ include::example$packaging/boot-build-image-bind-caches.gradle.kts[tags=caches] ---- ====== +The build cache can also be stored in an image instead of a named volume, which allows the cache to be shared between hosts by pushing and pulling it from an image registry, as shown in the following example: + +[tabs] +====== +Groovy:: ++ +[source,groovy,indent=0,subs="verbatim,attributes"] +---- +include::example$packaging/boot-build-image-image-caches.gradle[tags=caches] +---- +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,attributes"] +---- +include::example$packaging/boot-build-image-image-caches.gradle.kts[tags=caches] +---- +====== + [[build-image.examples.docker]] diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java index b459b0cb205b..499f03fc754a 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java @@ -74,6 +74,19 @@ public void bind(Action<BindCacheSpec> action) { this.cache = Cache.bind(spec.getSource().get()); } + /** + * Configures an image cache using the given {@code action}. + * @param action the action + */ + public void image(Action<ImageCacheSpec> action) { + if (this.cache != null) { + throw new GradleException("Each image building cache can be configured only once"); + } + ImageCacheSpec spec = this.objectFactory.newInstance(ImageCacheSpec.class); + action.execute(spec); + this.cache = Cache.image(spec.getName().get()); + } + /** * Configuration for an image building cache stored in a Docker volume. */ @@ -102,4 +115,18 @@ public abstract static class BindCacheSpec { } + /** + * Configuration for an image building cache stored in an image. + */ + public abstract static class ImageCacheSpec { + + /** + * Returns the name of the cache image. + * @return the cache image name + */ + @Input + public abstract Property<String> getName(); + + } + } diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java index a9a3488d0efd..6de73280aea5 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java @@ -324,6 +324,13 @@ void bootBuildImageWithBindCaches() { .containsPattern("launchCache=/tmp/cache-gradle-[\\d]+.launch"); } + @TestTemplate + void bootBuildImageWithImageCaches() { + BuildResult result = this.gradleBuild.script(Examples.DIR + "packaging/boot-build-image-image-caches") + .build("bootBuildImageCaches"); + assertThat(result.getOutput()).containsPattern("buildCache=docker.io/library/gradle-[\\d]+:build"); + } + 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/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml new file mode 100644 index 000000000000..ac8f689957dd --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- tag::caches[] --> +<project> + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <configuration> + <image> + <buildCache> + <image> + <name>docker.io/library/${project.artifactId}:build</name> + </image> + </buildCache> + </image> + </configuration> + </plugin> + </plugins> + </build> +</project> +<!-- end::caches[] --> \ No newline at end of file diff --git a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc index e099dcef4ff3..0c492ca6b557 100644 --- a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc +++ b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc @@ -494,6 +494,13 @@ The caches and the build workspace can be configured to use bind mounts instead include::example$packaging-oci-image/bind-caches-pom.xml[tags=caches] ---- +The build cache can also be stored in an image instead of a named volume, which allows the cache to be shared between hosts by pushing and pulling it from an image registry, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes"] +---- +include::example$packaging-oci-image/image-caches-pom.xml[tags=caches] +---- + [[build-image.examples.docker]] diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java index 98d23db744bd..0a0df4d5c920 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java @@ -52,6 +52,13 @@ public void setBind(BindCacheInfo info) { this.cache = Cache.bind(source); } + public void setImage(ImageCacheInfo info) { + Assert.state(this.cache == null, "Each image building cache can be configured only once"); + String name = info.getName(); + Assert.state(name != null, "'name' must not be null"); + this.cache = Cache.image(name); + } + @Nullable Cache asCache() { return this.cache; } @@ -68,6 +75,12 @@ static CacheInfo fromBind(BindCacheInfo cacheInfo) { return new CacheInfo(Cache.bind(source)); } + static CacheInfo fromImage(ImageCacheInfo cacheInfo) { + String name = cacheInfo.getName(); + Assert.state(name != null, "'name' must not be null"); + return new CacheInfo(Cache.image(name)); + } + /** * Encapsulates configuration of an image building cache stored in a volume. */ @@ -116,4 +129,28 @@ void setSource(@Nullable String source) { } + /** + * Encapsulates configuration of an image building cache stored in an image. + */ + public static class ImageCacheInfo { + + private @Nullable String name; + + public ImageCacheInfo() { + } + + ImageCacheInfo(String name) { + this.name = name; + } + + public @Nullable String getName() { + return this.name; + } + + void setName(@Nullable String name) { + this.name = name; + } + + } + } diff --git a/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index a6a2b4d55bac..e4c4dfb348b9 100644 --- a/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -37,9 +37,11 @@ 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.ImageCacheInfo; import org.springframework.boot.maven.CacheInfo.VolumeCacheInfo; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.entry; import static org.mockito.Mockito.mock; @@ -245,6 +247,23 @@ void getBuildRequestWhenHasLaunchCacheBindUsesCache() { assertThat(request.getLaunchCache()).isEqualTo(Cache.bind("launch-cache-dir")); } + @Test + void getBuildRequestWhenHasBuildCacheImageUsesCache() { + Image image = new Image(); + image.buildCache = CacheInfo.fromImage(new ImageCacheInfo("build-cache-image")); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getBuildCache()).isEqualTo(Cache.image("build-cache-image")); + } + + @Test + void getBuildRequestWhenHasLaunchCacheImageThrowsException() { + Image image = new Image(); + image.launchCache = CacheInfo.fromImage(new ImageCacheInfo("launch-cache-image")); + assertThatIllegalArgumentException() + .isThrownBy(() -> image.getBuildRequest(createArtifact(), mockApplicationContent())) + .withMessage("Launch cache must not be an image cache"); + } + @Test void getBuildRequestWhenHasCreatedDateUsesCreatedDate() { Image image = new Image(); diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index bdbf888926c2..d5f83e2f4ca2 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -423,6 +423,7 @@ public BuildRequest withBuildCache(Cache buildCache) { */ public BuildRequest withLaunchCache(Cache launchCache) { Assert.notNull(launchCache, "'launchCache' must not be null"); + Assert.isNull(launchCache.getImage(), "Launch cache must not be an image cache"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, 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, diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java index a3e4f98a815b..749ed1280747 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java @@ -45,7 +45,12 @@ public enum Format { /** * A cache stored as a bind mount. */ - BIND("bind mount"); + BIND("bind mount"), + + /** + * A cache stored as an image. + */ + IMAGE("image"); private final String description; @@ -81,6 +86,14 @@ public String getDescription() { return (this.format.equals(Format.BIND)) ? (Bind) this : null; } + /** + * Return the details of the cache if it is an image cache. + * @return the cache, or {@code null} if it is not an image cache + */ + public @Nullable Image getImage() { + return (this.format.equals(Format.IMAGE)) ? (Image) this : null; + } + /** * Create a new {@code Cache} that uses a volume with the provided name. * @param name the cache volume name @@ -111,6 +124,16 @@ public static Cache bind(String source) { return new Bind(source); } + /** + * Create a new {@code Cache} that uses an image with the provided name. + * @param name the cache image name + * @return a new cache instance + */ + public static Cache image(String name) { + Assert.notNull(name, "'name' must not be null"); + return new Image(name); + } + @Override public boolean equals(@Nullable Object obj) { if (this == obj) { @@ -222,4 +245,49 @@ public String toString() { } + /** + * Details of a cache stored in an image. + */ + public static class Image extends Cache { + + private final String name; + + Image(String name) { + super(Format.IMAGE); + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + if (!super.equals(obj)) { + return false; + } + Image other = (Image) obj; + return Objects.equals(this.name, other.name); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.name); + return result; + } + + @Override + public String toString() { + return this.format.getDescription() + " '" + this.name + "'"; + } + + } + } diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java index b5467db65173..1c1598524fce 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java @@ -195,7 +195,7 @@ private Phase createPhase() { Assert.state(runImage != null, "'runImage' must not be null"); phase.withRunImage(runImage); phase.withLayers(Directory.LAYERS, Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS)); - phase.withBuildCache(Directory.CACHE, Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + configureBuildCache(phase); phase.withLaunchCache(Directory.LAUNCH_CACHE, Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); configureDaemonAccess(phase); @@ -215,6 +215,10 @@ private Phase createPhase() { private Phase analyzePhase() { Phase phase = new Phase("analyzer", isVerboseLogging()); configureDaemonAccess(phase); + Cache.Image buildCacheImage = this.buildCache.getImage(); + if (buildCacheImage != null) { + phase.withBuildCache(buildCacheImage.getName()); + } phase.withLaunchCache(Directory.LAUNCH_CACHE, Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); phase.withLayers(Directory.LAYERS, Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS)); @@ -239,7 +243,7 @@ private Phase detectPhase() { private Phase restorePhase() { Phase phase = new Phase("restorer", isVerboseLogging()); configureDaemonAccess(phase); - phase.withBuildCache(Directory.CACHE, Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + configureBuildCache(phase); phase.withLayers(Directory.LAYERS, Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS)); configureOptions(phase); return phase; @@ -260,7 +264,7 @@ private Phase exportPhase() { configureDaemonAccess(phase); phase.withApp(this.applicationDirectory, Binding.from(getCacheBindingSource(this.application), this.applicationDirectory)); - phase.withBuildCache(Directory.CACHE, Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + configureBuildCache(phase); phase.withLaunchCache(Directory.LAUNCH_CACHE, Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); phase.withLayers(Directory.LAYERS, Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS)); @@ -343,6 +347,17 @@ private void configureDaemonAccess(Phase phase) { } } + private void configureBuildCache(Phase phase) { + Cache.Image image = this.buildCache.getImage(); + if (image != null) { + phase.withBuildCache(image.getName()); + } + else { + phase.withBuildCache(Directory.CACHE, + Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + } + } + private void configureCreatedDate(Phase phase) { if (this.request.getCreatedDate() != null) { phase.withEnv(SOURCE_DATE_EPOCH_KEY, Long.toString(this.request.getCreatedDate().getEpochSecond())); diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java index d037d33b072e..985e6992b875 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java @@ -75,6 +75,10 @@ void withBuildCache(String path, Binding binding) { withBinding(binding); } + void withBuildCache(String image) { + withArgs("-cache-image", image); + } + /** * Update this phase with Docker daemon access. */ diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index 7bbb13d3e72a..dab8563813ae 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -349,6 +349,14 @@ void withLaunchBindCacheAddsCache() throws IOException { assertThat(withCache.getLaunchCache()).isEqualTo(Cache.bind("/tmp/launch-cache")); } + @Test + void withLaunchImageCacheThrowsException() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + assertThatIllegalArgumentException() + .isThrownBy(() -> request.withLaunchCache(Cache.image("launch-cache-image"))) + .withMessage("Launch cache must not be an image cache"); + } + @Test @SuppressWarnings("NullAway") // Test null check void withLaunchVolumeCacheWhenCacheIsNullThrowsException() throws IOException { diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java index 69c8b8f477b3..eac76947b9fb 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -291,6 +291,27 @@ void executeWithCacheBindMountsExecutesPhases(boolean trustBuilder) throws Excep assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); } + @ParameterizedTest + @BooleanValueSource + void executeWithImageBuildCacheExecutesPhases(boolean trustBuilder) throws Exception { + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + BuildRequest request = getTestRequest(trustBuilder).withBuildCache(Cache.image("my-cache-image")); + createLifecycle(request).execute(); + if (trustBuilder) { + assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-image-cache.json")); + } + else { + assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer-image-cache.json")); + assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json")); + assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer-image-cache.json")); + assertPhaseWasRun("builder", withExpectedConfig("lifecycle-builder.json")); + assertPhaseWasRun("exporter", withExpectedConfig("lifecycle-exporter-image-cache.json")); + } + assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + } + @ParameterizedTest @BooleanValueSource void executeWithCreatedDateExecutesPhases(boolean trustBuilder) throws Exception { diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java index 42d06b41f148..0771fed79281 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java @@ -102,6 +102,17 @@ void applyWhenWithArgsUpdatesConfigurationWithArguments() { then(update).shouldHaveNoMoreInteractions(); } + @Test + void applyWhenWithImageBuildCacheUpdatesConfigurationWithCacheImage() { + Phase phase = new Phase("test", false); + phase.withBuildCache("my-cache-image"); + Update update = mock(Update.class); + phase.apply(update); + then(update).should().withCommand("/cnb/lifecycle/test", "-cache-image", "my-cache-image"); + then(update).should().withLabel("author", "spring-boot"); + then(update).shouldHaveNoMoreInteractions(); + } + @Test void applyWhenWithBindsUpdatesConfigurationWithBinds() { Phase phase = new Phase("test", false); diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-image-cache.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-image-cache.json new file mode 100644 index 000000000000..e18ed997905c --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-image-cache.json @@ -0,0 +1,33 @@ +{ + "User": "root", + "Image": "pack.local/ephemeral-builder", + "Cmd": [ + "/cnb/lifecycle/analyzer", + "-daemon", + "-cache-image", + "my-cache-image", + "-launch-cache", + "/launch-cache", + "-layers", + "/layers", + "-run-image", + "docker.io/cloudfoundry/run:latest", + "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-cache-b35197ac41ea.launch:/launch-cache", + "pack-layers-aaaaaaaaaa:/layers" + ], + "SecurityOpt" : [ + "label=disable" + ] + } +} diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-image-cache.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-image-cache.json new file mode 100644 index 000000000000..babbd120c8ef --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-image-cache.json @@ -0,0 +1,38 @@ +{ + "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-image", + "my-cache-image", + "-launch-cache", + "/launch-cache", + "-daemon", + "docker.io/library/my-application:latest" + ], + "Env": [ + "CNB_PLATFORM_API=0.8" + ], + "Labels": { + "author": "spring-boot" + }, + "HostConfig": { + "Binds": [ + "pack-app-aaaaaaaaaa:/workspace", + "pack-layers-aaaaaaaaaa:/layers", + "pack-cache-b35197ac41ea.launch:/launch-cache", + "/var/run/docker.sock:/var/run/docker.sock" + ], + "SecurityOpt" : [ + "label=disable" + ] + } +} diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-image-cache.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-image-cache.json new file mode 100644 index 000000000000..34a4f76b2cad --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-image-cache.json @@ -0,0 +1,34 @@ +{ + "User": "root", + "Image": "pack.local/ephemeral-builder", + "Cmd": [ + "/cnb/lifecycle/exporter", + "-daemon", + "-app", + "/workspace", + "-cache-image", + "my-cache-image", + "-launch-cache", + "/launch-cache", + "-layers", + "/layers", + "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-app-aaaaaaaaaa:/workspace", + "pack-cache-b35197ac41ea.launch:/launch-cache", + "pack-layers-aaaaaaaaaa:/layers" + ], + "SecurityOpt" : [ + "label=disable" + ] + } +} diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-image-cache.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-image-cache.json new file mode 100644 index 000000000000..a4e74529206a --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-image-cache.json @@ -0,0 +1,27 @@ +{ + "User": "root", + "Image": "pack.local/ephemeral-builder", + "Cmd": [ + "/cnb/lifecycle/restorer", + "-daemon", + "-cache-image", + "my-cache-image", + "-layers", + "/layers" + ], + "Env": [ + "CNB_PLATFORM_API=0.8" + ], + "Labels": { + "author": "spring-boot" + }, + "HostConfig": { + "Binds": [ + "/var/run/docker.sock:/var/run/docker.sock", + "pack-layers-aaaaaaaaaa:/layers" + ], + "SecurityOpt" : [ + "label=disable" + ] + } +} From 5bd3e6435cb7c7f798faf2cb555ecb32f1f982e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 27 Jul 2026 16:54:45 +0200 Subject: [PATCH 794/930] Polish "Add support for image-based build caches" See gh-50899 --- .../gradle/tasks/bundling/BootBuildImage.java | 27 ++- .../boot/gradle/tasks/bundling/CacheSpec.java | 31 +-- .../gradle/tasks/bundling/LocalCacheSpec.java | 107 +++++++++ .../tasks/bundling/BootBuildImageTests.java | 25 ++ .../packaging-oci-image/image-caches-pom.xml | 2 +- .../springframework/boot/maven/CacheInfo.java | 50 +--- .../org/springframework/boot/maven/Image.java | 9 +- .../boot/maven/LocalCacheInfo.java | 121 ++++++++++ .../boot/maven/ImageTests.java | 22 +- .../platform/build/BuildRequest.java | 17 +- .../boot/buildpack/platform/build/Cache.java | 227 ++---------------- .../buildpack/platform/build/ImageCache.java | 66 +++++ .../buildpack/platform/build/Lifecycle.java | 41 ++-- .../buildpack/platform/build/LocalCache.java | 117 +++++++++ .../platform/build/BuildRequestTests.java | 16 +- 15 files changed, 519 insertions(+), 359 deletions(-) create mode 100644 build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LocalCacheSpec.java create mode 100644 build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LocalCacheInfo.java create mode 100644 buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageCache.java create mode 100644 buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/LocalCache.java diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 078180fafae4..09939ea281a7 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -47,6 +47,7 @@ import org.springframework.boot.buildpack.platform.build.BuildpackReference; import org.springframework.boot.buildpack.platform.build.Cache; import org.springframework.boot.buildpack.platform.build.Creator; +import org.springframework.boot.buildpack.platform.build.LocalCache; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; import org.springframework.boot.buildpack.platform.docker.type.Binding; @@ -75,11 +76,11 @@ public abstract class BootBuildImage extends DefaultTask { private final String projectName; - private final CacheSpec buildWorkspace; + private final LocalCacheSpec buildWorkspace; private final CacheSpec buildCache; - private final CacheSpec launchCache; + private final LocalCacheSpec launchCache; private final DockerSpec docker; @@ -100,9 +101,9 @@ public BootBuildImage() { getCleanCache().convention(false); getVerboseLogging().convention(false); getPublish().convention(false); - this.buildWorkspace = getProject().getObjects().newInstance(CacheSpec.class); + this.buildWorkspace = getProject().getObjects().newInstance(LocalCacheSpec.class); this.buildCache = getProject().getObjects().newInstance(CacheSpec.class); - this.launchCache = getProject().getObjects().newInstance(CacheSpec.class); + this.launchCache = getProject().getObjects().newInstance(LocalCacheSpec.class); this.docker = getProject().getObjects().newInstance(DockerSpec.class); this.pullPolicy = getProject().getObjects().property(PullPolicy.class); getSecurityOptions().convention((Iterable<? extends String>) null); @@ -280,17 +281,17 @@ public void setPullPolicy(String pullPolicy) { */ @Nested @Optional - public CacheSpec getBuildWorkspace() { + public LocalCacheSpec getBuildWorkspace() { return this.buildWorkspace; } /** - * Customizes the {@link CacheSpec} for the build temporary workspace using the given - * {@code action}. + * Customizes the {@link LocalCacheSpec} for the build temporary workspace using the + * given {@code action}. * @param action the action * @since 3.2.0 */ - public void buildWorkspace(Action<CacheSpec> action) { + public void buildWorkspace(Action<LocalCacheSpec> action) { action.execute(this.buildWorkspace); } @@ -319,16 +320,16 @@ public void buildCache(Action<CacheSpec> action) { */ @Nested @Optional - public CacheSpec getLaunchCache() { + public LocalCacheSpec getLaunchCache() { return this.launchCache; } /** - * Customizes the {@link CacheSpec} for the launch cache using the given + * Customizes the {@link LocalCacheSpec} for the launch cache using the given * {@code action}. * @param action the action */ - public void launchCache(Action<CacheSpec> action) { + public void launchCache(Action<LocalCacheSpec> action) { action.execute(this.launchCache); } @@ -495,7 +496,7 @@ private BuildRequest customizeTags(BuildRequest request) { } private BuildRequest customizeCaches(BuildRequest request) { - Cache buildWorkspaceCache = this.buildWorkspace.asCache(); + LocalCache buildWorkspaceCache = this.buildWorkspace.asCache(); if (buildWorkspaceCache != null) { request = request.withBuildWorkspace(buildWorkspaceCache); } @@ -503,7 +504,7 @@ private BuildRequest customizeCaches(BuildRequest request) { if (buildCache != null) { request = request.withBuildCache(buildCache); } - Cache launchCache = this.launchCache.asCache(); + LocalCache launchCache = this.launchCache.asCache(); if (launchCache != null) { request = request.withLaunchCache(launchCache); } diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java index 499f03fc754a..303b1b63556c 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java @@ -26,6 +26,8 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.gradle.tasks.bundling.LocalCacheSpec.BindCacheSpec; +import org.springframework.boot.gradle.tasks.bundling.LocalCacheSpec.VolumeCacheSpec; /** * Configuration for an image building cache. @@ -77,6 +79,7 @@ public void bind(Action<BindCacheSpec> action) { /** * Configures an image cache using the given {@code action}. * @param action the action + * @since 4.2.0 */ public void image(Action<ImageCacheSpec> action) { if (this.cache != null) { @@ -87,34 +90,6 @@ public void image(Action<ImageCacheSpec> action) { this.cache = Cache.image(spec.getName().get()); } - /** - * Configuration for an image building cache stored in a Docker volume. - */ - public abstract static class VolumeCacheSpec { - - /** - * Returns the name of the cache. - * @return the cache name - */ - @Input - public abstract Property<String> getName(); - - } - - /** - * 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<String> getSource(); - - } - /** * Configuration for an image building cache stored in an image. */ diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LocalCacheSpec.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LocalCacheSpec.java new file mode 100644 index 000000000000..0283f77546e7 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LocalCacheSpec.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.tasks.bundling; + +import javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.GradleException; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.buildpack.platform.build.LocalCache; + +/** + * Configuration for a local image building cache stored in a volume or bind mount. + * + * @author Scott Frederick + * @author Stephane Nicoll + * @since 4.2.0 + */ +public class LocalCacheSpec { + + private final ObjectFactory objectFactory; + + private @Nullable LocalCache cache; + + @Inject + public LocalCacheSpec(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + } + + public @Nullable LocalCache asCache() { + return this.cache; + } + + /** + * Configures a volume cache using the given {@code action}. + * @param action the action + */ + public void volume(Action<VolumeCacheSpec> action) { + if (this.cache != null) { + throw new GradleException("Each image building cache can be configured only once"); + } + VolumeCacheSpec spec = this.objectFactory.newInstance(VolumeCacheSpec.class); + action.execute(spec); + this.cache = Cache.volume(spec.getName().get()); + } + + /** + * Configures a bind cache using the given {@code action}. + * @param action the action + */ + public void bind(Action<BindCacheSpec> 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. + */ + public abstract static class VolumeCacheSpec { + + /** + * Returns the name of the cache. + * @return the cache name + */ + @Input + public abstract Property<String> getName(); + + } + + /** + * 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<String> getSource(); + + } + +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java index 9a917d023825..ce72a74808e4 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -29,6 +29,7 @@ import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.BuildpackReference; +import org.springframework.boot.buildpack.platform.build.Cache; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.ImagePlatform; import org.springframework.boot.buildpack.platform.docker.type.Binding; @@ -357,4 +358,28 @@ void whenImagePlatformIsConfiguredThenRequestHasImagePlatform() { assertThat(this.buildImage.createRequest().getImagePlatform()).isEqualTo(ImagePlatform.of("linux/arm64/v1")); } + @Test + void whenBuildWorkspaceIsConfiguredThenRequestHasBuildWorkspace() { + this.buildImage.buildWorkspace((cache) -> cache.volume((volume) -> volume.getName().set("workspace-vol"))); + assertThat(this.buildImage.createRequest().getBuildWorkspace()).isEqualTo(Cache.volume("workspace-vol")); + } + + @Test + void whenBuildCacheIsConfiguredWithVolumeThenRequestHasBuildCache() { + this.buildImage.buildCache((cache) -> cache.volume((volume) -> volume.getName().set("build-vol"))); + assertThat(this.buildImage.createRequest().getBuildCache()).isEqualTo(Cache.volume("build-vol")); + } + + @Test + void whenBuildCacheIsConfiguredWithImageThenRequestHasBuildCache() { + this.buildImage.buildCache((cache) -> cache.image((image) -> image.getName().set("build-cache-image"))); + assertThat(this.buildImage.createRequest().getBuildCache()).isEqualTo(Cache.image("build-cache-image")); + } + + @Test + void whenLaunchCacheIsConfiguredThenRequestHasLaunchCache() { + this.buildImage.launchCache((cache) -> cache.bind((bind) -> bind.getSource().set("/tmp/launch"))); + assertThat(this.buildImage.createRequest().getLaunchCache()).isEqualTo(Cache.bind("/tmp/launch")); + } + } diff --git a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml index ac8f689957dd..72e273228a21 100644 --- a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml +++ b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml @@ -19,4 +19,4 @@ </plugins> </build> </project> -<!-- end::caches[] --> \ No newline at end of file +<!-- end::caches[] --> diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java index 0a0df4d5c920..32a05ea6d44a 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java @@ -19,6 +19,8 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.maven.LocalCacheInfo.BindCacheInfo; +import org.springframework.boot.maven.LocalCacheInfo.VolumeCacheInfo; import org.springframework.util.Assert; /** @@ -81,54 +83,6 @@ static CacheInfo fromImage(ImageCacheInfo cacheInfo) { return new CacheInfo(Cache.image(name)); } - /** - * Encapsulates configuration of an image building cache stored in a volume. - */ - public static class VolumeCacheInfo { - - private @Nullable String name; - - public VolumeCacheInfo() { - } - - VolumeCacheInfo(String name) { - this.name = name; - } - - public @Nullable String getName() { - return this.name; - } - - void setName(@Nullable String name) { - this.name = name; - } - - } - - /** - * Encapsulates configuration of an image building cache stored in a bind mount. - */ - public static class BindCacheInfo { - - private @Nullable String source; - - public BindCacheInfo() { - } - - BindCacheInfo(String name) { - this.source = name; - } - - public @Nullable String getSource() { - return this.source; - } - - void setSource(@Nullable String source) { - this.source = source; - } - - } - /** * Encapsulates configuration of an image building cache stored in an image. */ diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java index 42e7f7ca4cdb..3b359cc4a892 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java @@ -26,6 +26,7 @@ import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.BuildpackReference; import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.buildpack.platform.build.LocalCache; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.type.Binding; import org.springframework.boot.buildpack.platform.docker.type.ImageName; @@ -74,11 +75,11 @@ public class Image { @Nullable List<String> tags; - @Nullable CacheInfo buildWorkspace; + @Nullable LocalCacheInfo buildWorkspace; @Nullable CacheInfo buildCache; - @Nullable CacheInfo launchCache; + @Nullable LocalCacheInfo launchCache; @Nullable String createdDate; @@ -284,7 +285,7 @@ private BuildRequest customize(BuildRequest request) { request = request.withTags(this.tags.stream().map(ImageReference::of).toList()); } if (this.buildWorkspace != null) { - Cache cache = this.buildWorkspace.asCache(); + LocalCache cache = this.buildWorkspace.asCache(); Assert.state(cache != null, "'cache' must not be null"); request = request.withBuildWorkspace(cache); } @@ -294,7 +295,7 @@ private BuildRequest customize(BuildRequest request) { request = request.withBuildCache(cache); } if (this.launchCache != null) { - Cache cache = this.launchCache.asCache(); + LocalCache cache = this.launchCache.asCache(); Assert.state(cache != null, "'cache' must not be null"); request = request.withLaunchCache(cache); } diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LocalCacheInfo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LocalCacheInfo.java new file mode 100644 index 000000000000..8d088f246753 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LocalCacheInfo.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.buildpack.platform.build.LocalCache; +import org.springframework.util.Assert; + +/** + * Encapsulates configuration of a local image building cache. + * + * @author Scott Frederick + * @author Stephane Nicoll + * @since 4.2.0 + */ +public class LocalCacheInfo { + + private @Nullable LocalCache cache; + + public LocalCacheInfo() { + } + + LocalCacheInfo(LocalCache cache) { + this.cache = cache; + } + + public void setVolume(VolumeCacheInfo info) { + Assert.state(this.cache == null, "Each image building cache can be configured only once"); + String name = info.getName(); + Assert.state(name != null, "'name' must not be null"); + this.cache = Cache.volume(name); + } + + public void setBind(BindCacheInfo info) { + Assert.state(this.cache == null, "Each image building cache can be configured only once"); + String source = info.getSource(); + Assert.state(source != null, "'source' must not be null"); + this.cache = Cache.bind(source); + } + + @Nullable LocalCache asCache() { + return this.cache; + } + + static LocalCacheInfo fromVolume(VolumeCacheInfo cacheInfo) { + String name = cacheInfo.getName(); + Assert.state(name != null, "'name' must not be null"); + return new LocalCacheInfo(Cache.volume(name)); + } + + static LocalCacheInfo fromBind(BindCacheInfo cacheInfo) { + String source = cacheInfo.getSource(); + Assert.state(source != null, "'source' must not be null"); + return new LocalCacheInfo(Cache.bind(source)); + } + + /** + * Encapsulates configuration of an image building cache stored in a volume. + */ + public static class VolumeCacheInfo { + + private @Nullable String name; + + public VolumeCacheInfo() { + } + + VolumeCacheInfo(String name) { + this.name = name; + } + + public @Nullable String getName() { + return this.name; + } + + void setName(@Nullable String name) { + this.name = name; + } + + } + + /** + * Encapsulates configuration of an image building cache stored in a bind mount. + */ + public static class BindCacheInfo { + + private @Nullable String source; + + public BindCacheInfo() { + } + + BindCacheInfo(String name) { + this.source = name; + } + + public @Nullable String getSource() { + return this.source; + } + + void setSource(@Nullable String source) { + this.source = source; + } + + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index e4c4dfb348b9..2e0c7b7a1802 100644 --- a/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -36,12 +36,11 @@ 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.ImageCacheInfo; -import org.springframework.boot.maven.CacheInfo.VolumeCacheInfo; +import org.springframework.boot.maven.LocalCacheInfo.BindCacheInfo; +import org.springframework.boot.maven.LocalCacheInfo.VolumeCacheInfo; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.entry; import static org.mockito.Mockito.mock; @@ -202,7 +201,7 @@ void getBuildRequestWhenHasTagsUsesTags() { @Test void getBuildRequestWhenHasBuildWorkspaceVolumeUsesWorkspace() { Image image = new Image(); - image.buildWorkspace = CacheInfo.fromVolume(new VolumeCacheInfo("build-work-vol")); + image.buildWorkspace = LocalCacheInfo.fromVolume(new VolumeCacheInfo("build-work-vol")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getBuildWorkspace()).isEqualTo(Cache.volume("build-work-vol")); } @@ -218,7 +217,7 @@ void getBuildRequestWhenHasBuildCacheVolumeUsesCache() { @Test void getBuildRequestWhenHasLaunchCacheVolumeUsesCache() { Image image = new Image(); - image.launchCache = CacheInfo.fromVolume(new VolumeCacheInfo("launch-cache-vol")); + image.launchCache = LocalCacheInfo.fromVolume(new VolumeCacheInfo("launch-cache-vol")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getLaunchCache()).isEqualTo(Cache.volume("launch-cache-vol")); } @@ -226,7 +225,7 @@ void getBuildRequestWhenHasLaunchCacheVolumeUsesCache() { @Test void getBuildRequestWhenHasBuildWorkspaceBindUsesWorkspace() { Image image = new Image(); - image.buildWorkspace = CacheInfo.fromBind(new BindCacheInfo("build-work-dir")); + image.buildWorkspace = LocalCacheInfo.fromBind(new BindCacheInfo("build-work-dir")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getBuildWorkspace()).isEqualTo(Cache.bind("build-work-dir")); } @@ -242,7 +241,7 @@ void getBuildRequestWhenHasBuildCacheBindUsesCache() { @Test void getBuildRequestWhenHasLaunchCacheBindUsesCache() { Image image = new Image(); - image.launchCache = CacheInfo.fromBind(new BindCacheInfo("launch-cache-dir")); + image.launchCache = LocalCacheInfo.fromBind(new BindCacheInfo("launch-cache-dir")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getLaunchCache()).isEqualTo(Cache.bind("launch-cache-dir")); } @@ -255,15 +254,6 @@ void getBuildRequestWhenHasBuildCacheImageUsesCache() { assertThat(request.getBuildCache()).isEqualTo(Cache.image("build-cache-image")); } - @Test - void getBuildRequestWhenHasLaunchCacheImageThrowsException() { - Image image = new Image(); - image.launchCache = CacheInfo.fromImage(new ImageCacheInfo("launch-cache-image")); - assertThatIllegalArgumentException() - .isThrownBy(() -> image.getBuildRequest(createArtifact(), mockApplicationContent())) - .withMessage("Launch cache must not be an image cache"); - } - @Test void getBuildRequestWhenHasCreatedDateUsesCreatedDate() { Image image = new Image(); diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index d5f83e2f4ca2..f3f996311da0 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -95,11 +95,11 @@ public class BuildRequest { private final List<ImageReference> tags; - private final @Nullable Cache buildWorkspace; + private final @Nullable LocalCache buildWorkspace; private final @Nullable Cache buildCache; - private final @Nullable Cache launchCache; + private final @Nullable LocalCache launchCache; private final @Nullable Instant createdDate; @@ -140,8 +140,8 @@ public class BuildRequest { @Nullable Boolean trustBuilder, @Nullable ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache, boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks, List<Binding> bindings, @Nullable String network, - List<ImageReference> tags, @Nullable Cache buildWorkspace, @Nullable Cache buildCache, - @Nullable Cache launchCache, @Nullable Instant createdDate, @Nullable String applicationDirectory, + List<ImageReference> tags, @Nullable LocalCache buildWorkspace, @Nullable Cache buildCache, + @Nullable LocalCache launchCache, @Nullable Instant createdDate, @Nullable String applicationDirectory, @Nullable List<String> securityOptions, @Nullable ImagePlatform platform) { this.name = name; this.applicationContent = applicationContent; @@ -395,7 +395,7 @@ public BuildRequest withTags(List<ImageReference> tags) { * @return an updated build request * @since 3.2.0 */ - public BuildRequest withBuildWorkspace(Cache buildWorkspace) { + public BuildRequest withBuildWorkspace(LocalCache buildWorkspace) { Assert.notNull(buildWorkspace, "'buildWorkspace' must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, @@ -421,9 +421,8 @@ public BuildRequest withBuildCache(Cache buildCache) { * @param launchCache the cache * @return an updated build request */ - public BuildRequest withLaunchCache(Cache launchCache) { + public BuildRequest withLaunchCache(LocalCache launchCache) { Assert.notNull(launchCache, "'launchCache' must not be null"); - Assert.isNull(launchCache.getImage(), "Launch cache must not be an image cache"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, 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, @@ -633,7 +632,7 @@ public List<ImageReference> getTags() { * @return the build workspace or {@code null} * @since 3.2.0 */ - public @Nullable Cache getBuildWorkspace() { + public @Nullable LocalCache getBuildWorkspace() { return this.buildWorkspace; } @@ -649,7 +648,7 @@ public List<ImageReference> getTags() { * Return the custom launch cache that should be used by the lifecycle. * @return the launch cache */ - public @Nullable Cache getLaunchCache() { + public @Nullable LocalCache getLaunchCache() { return this.launchCache; } diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java index 749ed1280747..ce4c20032f5d 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java @@ -16,82 +16,46 @@ package org.springframework.boot.buildpack.platform.build; -import java.util.Objects; - import org.jspecify.annotations.Nullable; +import org.springframework.boot.buildpack.platform.build.LocalCache.Bind; +import org.springframework.boot.buildpack.platform.build.LocalCache.Volume; import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * Details of a cache for use by the CNB builder. * * @author Scott Frederick + * @author Tim Ysewyn + * @author Stephane Nicoll * @since 2.6.0 */ -public class Cache { - - /** - * The format of the cache. - */ - public enum Format { - - /** - * A cache stored as a volume in the Docker daemon. - */ - VOLUME("volume"), - - /** - * A cache stored as a bind mount. - */ - BIND("bind mount"), - - /** - * A cache stored as an image. - */ - IMAGE("image"); - - private final String description; - - Format(String description) { - this.description = description; - } - - public String getDescription() { - return this.description; - } - - } - - protected final Format format; - - Cache(Format format) { - this.format = format; - } +public sealed interface Cache permits LocalCache, ImageCache { /** * Return the details of the cache if it is a volume cache. * @return the cache, or {@code null} if it is not a volume cache */ - public @Nullable Volume getVolume() { - return (this.format.equals(Format.VOLUME)) ? (Volume) this : null; + default LocalCache.@Nullable Volume getVolume() { + return (this instanceof LocalCache.Volume volume) ? volume : 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 @Nullable Bind getBind() { - return (this.format.equals(Format.BIND)) ? (Bind) this : null; + default LocalCache.@Nullable Bind getBind() { + return (this instanceof LocalCache.Bind bind) ? bind : null; } /** * Return the details of the cache if it is an image cache. * @return the cache, or {@code null} if it is not an image cache + * @since 4.2.0 */ - public @Nullable Image getImage() { - return (this.format.equals(Format.IMAGE)) ? (Image) this : null; + default @Nullable ImageCache getImage() { + return (this instanceof ImageCache image) ? image : null; } /** @@ -99,7 +63,7 @@ public String getDescription() { * @param name the cache volume name * @return a new cache instance */ - public static Cache volume(String name) { + static LocalCache volume(String name) { Assert.notNull(name, "'name' must not be null"); return new Volume(VolumeName.of(name)); } @@ -109,7 +73,7 @@ public static Cache volume(String name) { * @param name the cache volume name * @return a new cache instance */ - public static Cache volume(VolumeName name) { + static LocalCache volume(VolumeName name) { Assert.notNull(name, "'name' must not be null"); return new Volume(name); } @@ -119,7 +83,7 @@ public static Cache volume(VolumeName name) { * @param source the cache bind mount source * @return a new cache instance */ - public static Cache bind(String source) { + static LocalCache bind(String source) { Assert.notNull(source, "'source' must not be null"); return new Bind(source); } @@ -128,166 +92,11 @@ public static Cache bind(String source) { * Create a new {@code Cache} that uses an image with the provided name. * @param name the cache image name * @return a new cache instance + * @since 4.2.0 */ - public static Cache image(String name) { + static ImageCache image(String name) { Assert.notNull(name, "'name' must not be null"); - return new Image(name); - } - - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - Cache other = (Cache) obj; - return Objects.equals(this.format, other.format); - } - - @Override - public int hashCode() { - return ObjectUtils.nullSafeHashCode(this.format); - } - - /** - * Details of a cache stored in a Docker volume. - */ - public static class Volume extends Cache { - - private final VolumeName name; - - Volume(VolumeName name) { - super(Format.VOLUME); - this.name = name; - } - - public String getName() { - return this.name.toString(); - } - - public VolumeName getVolumeName() { - return this.name; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - if (!super.equals(obj)) { - return false; - } - Volume other = (Volume) obj; - return Objects.equals(this.name, other.name); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + ObjectUtils.nullSafeHashCode(this.name); - 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(@Nullable 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 + "'"; - } - - } - - /** - * Details of a cache stored in an image. - */ - public static class Image extends Cache { - - private final String name; - - Image(String name) { - super(Format.IMAGE); - this.name = name; - } - - public String getName() { - return this.name; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - if (!super.equals(obj)) { - return false; - } - Image other = (Image) obj; - return Objects.equals(this.name, other.name); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + ObjectUtils.nullSafeHashCode(this.name); - return result; - } - - @Override - public String toString() { - return this.format.getDescription() + " '" + this.name + "'"; - } - + return new ImageCache(name); } } diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageCache.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageCache.java new file mode 100644 index 000000000000..98f8a3a4eede --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageCache.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.build; + +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.ObjectUtils; + +/** + * Details of a cache stored in an image. + * + * @author Tim Ysewyn + * @author Stephane Nicoll + * @since 4.2.0 + */ +public final class ImageCache implements Cache { + + private final String name; + + ImageCache(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ImageCache other = (ImageCache) obj; + return Objects.equals(this.name, other.name); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.name); + } + + @Override + public String toString() { + return "image '" + this.name + "'"; + } + +} diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java index 1c1598524fce..a4b1dc841f07 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java @@ -27,7 +27,6 @@ import com.sun.jna.Platform; import org.jspecify.annotations.Nullable; -import org.springframework.boot.buildpack.platform.build.Cache.Bind; import org.springframework.boot.buildpack.platform.docker.ApiVersion; import org.springframework.boot.buildpack.platform.docker.DockerApi; import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent; @@ -78,13 +77,13 @@ class Lifecycle implements Closeable { private final ApiVersion platformVersion; - private final Cache layers; + private final LocalCache layers; - private final Cache application; + private final LocalCache application; private final Cache buildCache; - private final Cache launchCache; + private final LocalCache launchCache; private final String applicationDirectory; @@ -130,7 +129,7 @@ private Cache getBuildCache(BuildRequest request) { return createVolumeCache(request, "build"); } - private Cache getLaunchCache(BuildRequest request) { + private LocalCache getLaunchCache(BuildRequest request) { if (request.getLaunchCache() != null) { return request.getLaunchCache(); } @@ -215,9 +214,8 @@ private Phase createPhase() { private Phase analyzePhase() { Phase phase = new Phase("analyzer", isVerboseLogging()); configureDaemonAccess(phase); - Cache.Image buildCacheImage = this.buildCache.getImage(); - if (buildCacheImage != null) { - phase.withBuildCache(buildCacheImage.getName()); + if (this.buildCache instanceof ImageCache imageBuildCache) { + phase.withBuildCache(imageBuildCache.getName()); } phase.withLaunchCache(Directory.LAUNCH_CACHE, Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); @@ -277,44 +275,43 @@ private Phase exportPhase() { return phase; } - private Cache getLayersBindingSource(BuildRequest request) { + private LocalCache getLayersBindingSource(BuildRequest request) { if (request.getBuildWorkspace() != null) { return getBuildWorkspaceBindingSource(request.getBuildWorkspace(), "layers"); } return createVolumeCache("pack-layers-"); } - private Cache getApplicationBindingSource(BuildRequest request) { + private LocalCache getApplicationBindingSource(BuildRequest request) { if (request.getBuildWorkspace() != null) { return getBuildWorkspaceBindingSource(request.getBuildWorkspace(), "app"); } return createVolumeCache("pack-app-"); } - private Cache getBuildWorkspaceBindingSource(Cache buildWorkspace, String suffix) { + private LocalCache getBuildWorkspaceBindingSource(LocalCache buildWorkspace, String suffix) { if (buildWorkspace.getVolume() != null) { return Cache.volume(buildWorkspace.getVolume().getName() + "-" + suffix); } - - Bind bind = buildWorkspace.getBind(); + LocalCache.Bind bind = buildWorkspace.getBind(); Assert.state(bind != null, "'bind' must not be null"); return Cache.bind(bind.getSource() + "-" + suffix); } - private String getCacheBindingSource(Cache cache) { + private String getCacheBindingSource(LocalCache cache) { if (cache.getVolume() != null) { return cache.getVolume().getName(); } - Bind bind = cache.getBind(); + LocalCache.Bind bind = cache.getBind(); Assert.state(bind != null, "'bind' must not be null"); return bind.getSource(); } - private Cache createVolumeCache(String prefix) { + private LocalCache createVolumeCache(String prefix) { return Cache.volume(createRandomVolumeName(prefix)); } - private Cache createVolumeCache(BuildRequest request, String suffix) { + private LocalCache createVolumeCache(BuildRequest request, String suffix) { return Cache.volume( VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6)); } @@ -348,13 +345,11 @@ private void configureDaemonAccess(Phase phase) { } private void configureBuildCache(Phase phase) { - Cache.Image image = this.buildCache.getImage(); - if (image != null) { + if (this.buildCache instanceof ImageCache image) { phase.withBuildCache(image.getName()); } - else { - phase.withBuildCache(Directory.CACHE, - Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + else if (this.buildCache instanceof LocalCache localCache) { + phase.withBuildCache(Directory.CACHE, Binding.from(getCacheBindingSource(localCache), Directory.CACHE)); } } @@ -436,7 +431,7 @@ private void deleteVolume(VolumeName name) throws IOException { this.docker.volume().delete(name, true); } - private void deleteBind(Cache.Bind bind) { + private void deleteBind(LocalCache.Bind bind) { try { FileSystemUtils.deleteRecursively(Path.of(bind.getSource())); } diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/LocalCache.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/LocalCache.java new file mode 100644 index 000000000000..4a46d4c9c95a --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/LocalCache.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.build; + +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.buildpack.platform.docker.type.VolumeName; +import org.springframework.util.ObjectUtils; + +/** + * Details of a local cache stored as a volume or bind mount. + * + * @author Scott Frederick + * @author Stephane Nicoll + * @since 4.2.0 + */ +public sealed interface LocalCache extends Cache permits LocalCache.Volume, LocalCache.Bind { + + /** + * Details of a cache stored in a Docker volume. + */ + final class Volume implements LocalCache { + + private final VolumeName name; + + Volume(VolumeName name) { + this.name = name; + } + + public String getName() { + return this.name.toString(); + } + + public VolumeName getVolumeName() { + return this.name; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Volume other = (Volume) obj; + return Objects.equals(this.name, other.name); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.name); + } + + @Override + public String toString() { + return "volume '" + this.name + "'"; + } + + } + + /** + * Details of a cache stored in a bind mount. + */ + final class Bind implements LocalCache { + + private final String source; + + Bind(String source) { + this.source = source; + } + + public String getSource() { + return this.source; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Bind other = (Bind) obj; + return Objects.equals(this.source, other.source); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.source); + } + + @Override + public String toString() { + return "bind mount '" + this.source + "'"; + } + + } + +} diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index dab8563813ae..19ca097fc3dd 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -325,6 +325,14 @@ void withBuildBindCacheAddsCache() throws IOException { assertThat(withCache.getBuildCache()).isEqualTo(Cache.bind("/tmp/build-cache")); } + @Test + void withBuildImageCacheAddsCache() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withCache = request.withBuildCache(Cache.image("build-cache-image")); + assertThat(request.getBuildCache()).isNull(); + assertThat(withCache.getBuildCache()).isEqualTo(Cache.image("build-cache-image")); + } + @Test @SuppressWarnings("NullAway") // Test null check void withBuildVolumeCacheWhenCacheIsNullThrowsException() throws IOException { @@ -349,14 +357,6 @@ void withLaunchBindCacheAddsCache() throws IOException { assertThat(withCache.getLaunchCache()).isEqualTo(Cache.bind("/tmp/launch-cache")); } - @Test - void withLaunchImageCacheThrowsException() throws IOException { - BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); - assertThatIllegalArgumentException() - .isThrownBy(() -> request.withLaunchCache(Cache.image("launch-cache-image"))) - .withMessage("Launch cache must not be an image cache"); - } - @Test @SuppressWarnings("NullAway") // Test null check void withLaunchVolumeCacheWhenCacheIsNullThrowsException() throws IOException { From 12eeed09fee1a26d80e0a3bf4fea4a4ea102c10e Mon Sep 17 00:00:00 2001 From: Alessio Attilio <309061723+alessio-attilio@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:45:14 +0200 Subject: [PATCH 795/930] Use GracefulHandler for graceful shutdown of Jetty See gh-51128 Signed-off-by: Alessio Attilio <309061723+alessio-attilio@users.noreply.github.com> --- .../boot/jetty/JettyWebServer.java | 20 +++++++++---------- .../JettyReactiveWebServerFactory.java | 8 ++++---- .../servlet/JettyServletWebServerFactory.java | 8 ++++---- ...activeWebServerAutoConfigurationTests.java | 4 ++-- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyWebServer.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyWebServer.java index 8e8ff7d63b93..d0c99560bca5 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyWebServer.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyWebServer.java @@ -31,7 +31,7 @@ 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.StatisticsHandler; +import org.eclipse.jetty.server.handler.GracefulHandler; import org.jspecify.annotations.Nullable; import org.springframework.boot.jetty.reactive.JettyReactiveWebServerFactory; @@ -94,23 +94,23 @@ public JettyWebServer(Server server, boolean autoStart) { } private @Nullable GracefulShutdown createGracefulShutdown(Server server) { - StatisticsHandler statisticsHandler = findStatisticsHandler(server); - if (statisticsHandler == null) { + GracefulHandler gracefulHandler = findGracefulHandler(server); + if (gracefulHandler == null) { return null; } - return new GracefulShutdown(server, statisticsHandler::getRequestsActive); + return new GracefulShutdown(server, () -> (int) (gracefulHandler.getCurrentRequestCount() + gracefulHandler.getCurrentStreamWrapperCount())); } - private @Nullable StatisticsHandler findStatisticsHandler(Server server) { - return findStatisticsHandler(server.getHandler()); + private @Nullable GracefulHandler findGracefulHandler(Server server) { + return findGracefulHandler(server.getHandler()); } - private @Nullable StatisticsHandler findStatisticsHandler(Handler handler) { - if (handler instanceof StatisticsHandler statisticsHandler) { - return statisticsHandler; + private @Nullable GracefulHandler findGracefulHandler(Handler handler) { + if (handler instanceof GracefulHandler gracefulHandler) { + return gracefulHandler; } if (handler instanceof Handler.Wrapper handlerWrapper) { - return findStatisticsHandler(handlerWrapper.getHandler()); + return findGracefulHandler(handlerWrapper.getHandler()); } return null; } diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/reactive/JettyReactiveWebServerFactory.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/reactive/JettyReactiveWebServerFactory.java index 1164fc1cb28f..82ee17f02c30 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/reactive/JettyReactiveWebServerFactory.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/reactive/JettyReactiveWebServerFactory.java @@ -24,7 +24,7 @@ import org.eclipse.jetty.ee11.servlet.ServletHolder; import org.eclipse.jetty.server.NetworkConnectionLimit; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.StatisticsHandler; +import org.eclipse.jetty.server.handler.GracefulHandler; import org.jspecify.annotations.Nullable; import org.springframework.boot.jetty.ConfigurableJettyWebServerFactory; @@ -121,9 +121,9 @@ protected Server createJettyServer(ServletHttpHandlerAdapter servlet) { new ForwardHeadersCustomizer().customize(server); } if (getShutdown() == Shutdown.GRACEFUL) { - StatisticsHandler statisticsHandler = new StatisticsHandler(); - statisticsHandler.setHandler(server.getHandler()); - server.setHandler(statisticsHandler); + GracefulHandler gracefulHandler = new GracefulHandler(); + gracefulHandler.setHandler(server.getHandler()); + server.setHandler(gracefulHandler); } return server; } diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java index aeadd1de85d5..0dd9605395e0 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java @@ -58,7 +58,7 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.StatisticsHandler; +import org.eclipse.jetty.server.handler.GracefulHandler; import org.eclipse.jetty.session.DefaultSessionCache; import org.eclipse.jetty.session.FileSessionDataStore; import org.eclipse.jetty.session.SessionConfig; @@ -180,9 +180,9 @@ public WebServer getWebServer(ServletContextInitializer... initializers) { new ForwardHeadersCustomizer().customize(server); } if (getShutdown() == Shutdown.GRACEFUL) { - StatisticsHandler statisticsHandler = new StatisticsHandler(); - statisticsHandler.setHandler(server.getHandler()); - server.setHandler(statisticsHandler); + GracefulHandler gracefulHandler = new GracefulHandler(); + gracefulHandler.setHandler(server.getHandler()); + server.setHandler(gracefulHandler); } return getJettyWebServer(server); } diff --git a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/reactive/JettyReactiveWebServerAutoConfigurationTests.java b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/reactive/JettyReactiveWebServerAutoConfigurationTests.java index abfb38c3abe2..4da788ef9706 100644 --- a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/reactive/JettyReactiveWebServerAutoConfigurationTests.java +++ b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/reactive/JettyReactiveWebServerAutoConfigurationTests.java @@ -19,7 +19,7 @@ import jakarta.websocket.server.ServerContainer; import org.eclipse.jetty.ee11.servlet.ServletContextHandler; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.StatisticsHandler; +import org.eclipse.jetty.server.handler.GracefulHandler; import org.junit.jupiter.api.Test; import org.springframework.boot.jetty.JettyServerCustomizer; @@ -77,7 +77,7 @@ void webSocketServerContainerIsAvailableFromServletContext() { WebServer webServer = ((ReactiveWebServerApplicationContext) context.getSourceApplicationContext()) .getWebServer(); assertThat(webServer).isNotNull(); - ServletContextHandler servletContextHandler = (ServletContextHandler) ((StatisticsHandler) ((JettyWebServer) webServer) + ServletContextHandler servletContextHandler = (ServletContextHandler) ((GracefulHandler) ((JettyWebServer) webServer) .getServer() .getHandler()).getHandler(); Object serverContainer = servletContextHandler.getContext() From 30f358ab9b6a71611f36a22cea853c21537c5281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 27 Jul 2026 19:51:11 +0200 Subject: [PATCH 796/930] Polish "Use GracefulHandler for graceful shutdown of Jetty" See gh-51128 --- .../java/org/springframework/boot/jetty/JettyWebServer.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyWebServer.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyWebServer.java index d0c99560bca5..4a86aff1a115 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyWebServer.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyWebServer.java @@ -98,7 +98,10 @@ public JettyWebServer(Server server, boolean autoStart) { if (gracefulHandler == null) { return null; } - return new GracefulShutdown(server, () -> (int) (gracefulHandler.getCurrentRequestCount() + gracefulHandler.getCurrentStreamWrapperCount())); + return new GracefulShutdown(server, + () -> (int) Math.min( + gracefulHandler.getCurrentRequestCount() + gracefulHandler.getCurrentStreamWrapperCount(), + Integer.MAX_VALUE)); } private @Nullable GracefulHandler findGracefulHandler(Server server) { From 297ee8694540bb21243dedabb74c18000716058e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 27 Jul 2026 21:12:35 +0200 Subject: [PATCH 797/930] Polish "Deprecate `TestRestTemplate`" See gh-46632 --- .../antora/modules/ROOT/pages/redirect.adoc | 2 - .../reference/pages/features/kotlin.adoc | 3 - .../testing/spring-boot-applications.adoc | 8 --- .../pages/testing/test-utilities.adoc | 37 ----------- .../MyRandomPortTestRestTemplateTests.java | 40 ------------ .../testresttemplate/MySpringBootTests.java | 61 ------------------- .../MySpringBootTestsConfiguration.java | 46 -------------- .../utilities/testresttemplate/MyTests.java | 40 ------------ .../MyRandomPortTestRestTemplateTests.kt | 40 ------------ .../testresttemplate/MySpringBootTests.kt | 57 ----------------- .../MySpringBootTestsConfiguration.kt | 50 --------------- .../utilities/testresttemplate/MyTests.kt | 38 ------------ .../MySpringBootTestsTests.java | 26 -------- ...stEmbeddedReactiveWebEnvironmentTests.java | 1 + ...gBootTestWebServerWebEnvironmentTests.java | 1 + module/spring-boot-jersey/build.gradle | 1 - .../AutoConfigureRestClient.java | 3 +- .../boot/resttestclient/TestRestTemplate.java | 4 +- .../AutoConfigureTestRestTemplate.java | 3 + ...TestRestTemplateTestAutoConfiguration.java | 3 + .../TestRestTemplateExtensions.kt | 3 + .../resttestclient/TestRestTemplateTests.java | 2 + ...estTemplateTestAutoConfigurationTests.java | 2 + 23 files changed, 18 insertions(+), 453 deletions(-) delete mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.java delete mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.java delete mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTestsConfiguration.java delete mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.java delete mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.kt delete mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.kt delete mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTestsConfiguration.kt delete mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.kt delete mode 100644 documentation/spring-boot-docs/src/test/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTestsTests.java diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc index 650798f890d0..2fb7cd7c77ba 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc @@ -1939,8 +1939,6 @@ * xref:reference:testing/test-utilities.adoc#testing.utilities.output-capture[#features.testing.utilities.output-capture] * xref:reference:testing/test-utilities.adoc#testing.utilities.test-property-values[#boot-features-test-property-values] * xref:reference:testing/test-utilities.adoc#testing.utilities.test-property-values[#features.testing.utilities.test-property-values] -* xref:reference:testing/test-utilities.adoc#testing.utilities.test-rest-template[#boot-features-rest-templates-test-utility] -* xref:reference:testing/test-utilities.adoc#testing.utilities.test-rest-template[#features.testing.utilities.test-rest-template] * xref:reference:testing/test-utilities.adoc#testing.utilities[#boot-features-test-utilities] * xref:reference:testing/test-utilities.adoc#testing.utilities[#features.testing.utilities] * xref:reference:testing/testcontainers.adoc#testing.testcontainers.dynamic-properties[#features.testing.testcontainers.dynamic-properties] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/kotlin.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/kotlin.adoc index bb32a7e5d39c..d7215bff6761 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/kotlin.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/kotlin.adoc @@ -85,9 +85,6 @@ runApplication<MyApplication>(*args) { Kotlin {url-kotlin-docs}/extensions.html[extensions] provide the ability to extend existing classes with additional functionality. The Spring Boot Kotlin API makes use of these extensions to add new Kotlin specific conveniences to existing APIs. -javadoc:org.springframework.boot.resttestclient.TestRestTemplate[] extensions, similar to those provided by Spring Framework for javadoc:org.springframework.web.client.RestOperations[] in Spring Framework, are provided. -Among other things, the extensions make it possible to take advantage of Kotlin reified type parameters. - [[features.kotlin.dependency-management]] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc index f5017c692987..387c96326f5a 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc @@ -199,14 +199,6 @@ TIP: javadoc:org.springframework.test.web.reactive.server.WebTestClient[] can al NOTE: For certain xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.autoconfigured-tests[sliced tests] you may need to use the javadoc:org.springframework.boot.web.server.test.AutoConfigureWebServer[format=annotation] annotation to auto-configure an embedded web server. -The `spring-boot-resttestclient` module also provides a javadoc:org.springframework.boot.resttestclient.TestRestTemplate[] facility: - -include-code::MyRandomPortTestRestTemplateTests[] - -To use `TestRestTemplate` a dependency on `spring-boot-restclient` is also required. -Take care when adding this dependency as it will enable auto-configuration for `RestClient.Builder`. -If your main code uses `RestClient.Builder`, declare the `spring-boot-restclient` dependency so that it is on your application's main classpath and not only on its test classpath. - [[testing.spring-boot-applications.customizing-rest-test-client]] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc index 64ae3f407d43..e94323cea6a1 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc @@ -36,40 +36,3 @@ javadoc:org.springframework.boot.test.system.OutputCaptureExtension[] is a JUnit To use it, add `@ExtendWith(OutputCaptureExtension.class)` and inject javadoc:org.springframework.boot.test.system.CapturedOutput[] as an argument to your test class constructor or test method as follows: include-code::MyOutputCaptureTests[] - - - -[[testing.utilities.test-rest-template]] -== TestRestTemplate - -WARNING: As of Spring Boot 4.2, `TestRestTemplate` is formally deprecated along `RestTemplate` itself in Spring Framework 7.1. -Test suites should xref:reference:testing/spring-boot-applications.adoc#testing.spring-boot-applications.spring-mvc-tests[migrate to `RestTestClient`], which can bind itself against a live server or a mock transport. - -javadoc:org.springframework.boot.resttestclient.TestRestTemplate[] is a convenience alternative to Spring's javadoc:org.springframework.web.client.RestTemplate[] that is useful in integration tests. -It's provided by the `spring-boot-resttestclient` module. -A dependency on `spring-boot-restclient` is also required. -Take care when adding this dependency as it will enable auto-configuration for `RestClient.Builder`. -If your main code uses `RestClient.Builder`, declare the `spring-boot-restclient` dependency so that it is on your application's main classpath and not only on its test classpath. - -You can get a vanilla template or one that sends Basic HTTP authentication (with a username and password). -In either case, the template is fault tolerant. -This means that it behaves in a test-friendly way by not throwing exceptions on 4xx and 5xx errors. -Instead, such errors can be detected through the returned javadoc:org.springframework.http.ResponseEntity[] and its status code. - -If you need fluent API for assertions, consider using javadoc:org.springframework.test.web.servlet.client.RestTestClient[] that works with xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-mock-environment[mock environments] and xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-running-server[end-to-end tests]. - -If you are using Spring WebFlux, consider the javadoc:org.springframework.test.web.reactive.server.WebTestClient[] that provides a similar API and works with xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-mock-environment[mock environments], xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.spring-webflux-tests[WebFlux integration tests], and xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-running-server[end-to-end tests]. - -It is recommended, but not mandatory, to use the Apache HTTP Client (version 5.1 or better). -If you have that on your classpath, the javadoc:org.springframework.boot.resttestclient.TestRestTemplate[] responds by configuring the client appropriately. - -javadoc:org.springframework.boot.resttestclient.TestRestTemplate[] can be instantiated directly in your integration tests, as shown in the following example: - -include-code::MyTests[] - -Alternatively, if you use the javadoc:org.springframework.boot.test.context.SpringBootTest[format=annotation] annotation with `WebEnvironment.RANDOM_PORT` or `WebEnvironment.DEFINED_PORT`, you can inject a fully configured javadoc:org.springframework.boot.resttestclient.TestRestTemplate[] by annotating the test class with javadoc:org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate[format=annotation]. -If necessary, additional customizations can be applied through the javadoc:org.springframework.boot.restclient.RestTemplateBuilder[] bean. - -Any URLs that do not specify a host and port automatically connect to the embedded server, as shown in the following example: - -include-code::MySpringBootTests[] diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.java deleted file mode 100644 index c745b3634706..000000000000 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.testing.springbootapplications.withrunningserver; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate -@SuppressWarnings("removal") -class MyRandomPortTestRestTemplateTests { - - @Test - void exampleTest(@Autowired TestRestTemplate restTemplate) { - String body = restTemplate.getForObject("/", String.class); - assertThat(body).isEqualTo("Hello World"); - } - -} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.java deleted file mode 100644 index ccedfe30b180..000000000000 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.testing.utilities.testresttemplate; - -import java.time.Duration; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.restclient.RestTemplateBuilder; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate -@SuppressWarnings("removal") -class MySpringBootTests { - - @Autowired - private TestRestTemplate template; - - @Test - void testRequest() { - ResponseEntity<String> response = this.template.getForEntity("/example", String.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - // Other assertions to verify the response - } - - @TestConfiguration(proxyBeanMethods = false) - static class RestTemplateBuilderConfiguration { - - @Bean - RestTemplateBuilder restTemplateBuilder() { - return new RestTemplateBuilder().connectTimeout(Duration.ofSeconds(1)).readTimeout(Duration.ofSeconds(1)); - } - - } - -} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTestsConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTestsConfiguration.java deleted file mode 100644 index 10f4ad060ac0..000000000000 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTestsConfiguration.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.testing.utilities.testresttemplate; - -import java.net.URI; - -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration; -import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; -import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; -import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@SpringBootConfiguration(proxyBeanMethods = false) -@ImportAutoConfiguration({ TomcatServletWebServerAutoConfiguration.class, DispatcherServletAutoConfiguration.class, - JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class }) -public class MySpringBootTestsConfiguration { - - @RestController - private static final class ExampleController { - - @RequestMapping("/example") - ResponseEntity<String> example() { - return ResponseEntity.ok().location(URI.create("https://other.example.com/example")).body("test"); - } - - } - -} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.java deleted file mode 100644 index ce5a75bb3eea..000000000000 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.testing.utilities.testresttemplate; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; - -@SuppressWarnings("removal") -class MyTests { - - private final TestRestTemplate template = new TestRestTemplate(); - - @Test - void testRequest() { - ResponseEntity<String> response = this.template.getForEntity("https://myhost.example.com/example", - String.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - // Other assertions to verify the response - } - -} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.kt deleted file mode 100644 index 998b965ba504..000000000000 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:Suppress("DEPRECATION", "REMOVAL") - -package org.springframework.boot.docs.testing.springbootapplications.withrunningserver - -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.context.SpringBootTest -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment -import org.springframework.boot.resttestclient.TestRestTemplate -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate - -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate -class MyRandomPortTestRestTemplateTests { - - @Test - fun exampleTest(@Autowired restTemplate: TestRestTemplate) { - val body = restTemplate.getForObject("/", String::class.java) - assertThat(body).isEqualTo("Hello World") - } - -} - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.kt deleted file mode 100644 index 0047cc1237be..000000000000 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTests.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:Suppress("DEPRECATION", "REMOVAL") - -package org.springframework.boot.docs.testing.utilities.testresttemplate - -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.context.SpringBootTest -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.boot.restclient.RestTemplateBuilder -import org.springframework.boot.resttestclient.TestRestTemplate -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate -import org.springframework.context.annotation.Bean -import org.springframework.http.HttpStatus -import java.time.Duration - -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate -class MySpringBootTests(@Autowired val template: TestRestTemplate) { - - @Test - fun testRequest() { - val response = template.getForEntity("/example", String::class.java) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - // Other assertions to verify the response - } - - @TestConfiguration(proxyBeanMethods = false) - internal class RestTemplateBuilderConfiguration { - - @Bean - fun restTemplateBuilder(): RestTemplateBuilder { - return RestTemplateBuilder().connectTimeout(Duration.ofSeconds(1)) - .readTimeout(Duration.ofSeconds(1)) - } - - } - -} - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTestsConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTestsConfiguration.kt deleted file mode 100644 index 1878d5fbc285..000000000000 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTestsConfiguration.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.testing.utilities.testresttemplate - -import org.springframework.boot.SpringBootConfiguration -import org.springframework.boot.autoconfigure.ImportAutoConfiguration -import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration -import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration -import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration -import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController -import java.net.URI - -@SpringBootConfiguration(proxyBeanMethods = false) -@ImportAutoConfiguration( - TomcatServletWebServerAutoConfiguration::class, - DispatcherServletAutoConfiguration::class, - JacksonAutoConfiguration::class, - HttpMessageConvertersAutoConfiguration::class -) -class MySpringBootTestsConfiguration { - - @RestController - private class ExampleController { - - @RequestMapping("/example") - fun example(): ResponseEntity<String> { - return ResponseEntity.ok().location(URI.create("https://other.example.com/example")).body("test") - } - - } - -} - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.kt deleted file mode 100644 index cb9c135809f4..000000000000 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/testing/utilities/testresttemplate/MyTests.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:Suppress("DEPRECATION", "REMOVAL") - -package org.springframework.boot.docs.testing.utilities.testresttemplate - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.springframework.boot.resttestclient.TestRestTemplate -import org.springframework.http.HttpStatus - -class MyTests { - - private val template = TestRestTemplate() - - @Test - fun testRequest() { - val response = template.getForEntity("https://myhost.example.com/example", String::class.java) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - // Other assertions to verify the response - } - -} - diff --git a/documentation/spring-boot-docs/src/test/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTestsTests.java b/documentation/spring-boot-docs/src/test/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTestsTests.java deleted file mode 100644 index 08b28061ab69..000000000000 --- a/documentation/spring-boot-docs/src/test/java/org/springframework/boot/docs/testing/utilities/testresttemplate/MySpringBootTestsTests.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.testing.utilities.testresttemplate; - -/** - * Tests for {@link MySpringBootTests}. - * - * @author Stephane Nicoll - */ -class MySpringBootTestsTests extends MySpringBootTests { - -} diff --git a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests.java b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests.java index 2a5315179e1a..38afafd5e088 100644 --- a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests.java +++ b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests.java @@ -91,6 +91,7 @@ void injectWebTestClient() { } @Test + @Deprecated(since = "4.2.0", forRemoval = true) void injectTestRestTemplate() { String body = this.restTemplate.getForObject("/", String.class); assertThat(body).isEqualTo("Hello World"); diff --git a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestWebServerWebEnvironmentTests.java b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestWebServerWebEnvironmentTests.java index 44b790b26258..2b421948bae2 100644 --- a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestWebServerWebEnvironmentTests.java +++ b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/AbstractSpringBootTestWebServerWebEnvironmentTests.java @@ -75,6 +75,7 @@ WebApplicationContext getContext() { return this.context; } + @Deprecated(since = "4.2.0", forRemoval = true) TestRestTemplate getRestTemplate() { return this.restTemplate; } diff --git a/module/spring-boot-jersey/build.gradle b/module/spring-boot-jersey/build.gradle index 05b9982cbacf..ae79d007b86e 100644 --- a/module/spring-boot-jersey/build.gradle +++ b/module/spring-boot-jersey/build.gradle @@ -52,7 +52,6 @@ dependencies { testImplementation(project(":core:spring-boot-test")) testImplementation(project(":module:spring-boot-restclient")) - testImplementation(project(":module:spring-boot-resttestclient")) testImplementation(project(":module:spring-boot-tomcat")) testImplementation(project(":test-support:spring-boot-test-support")) testImplementation(testFixtures(project(":module:spring-boot-actuator-autoconfigure"))) diff --git a/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureRestClient.java b/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureRestClient.java index dd943f08ba30..3c7ca91ab9f3 100644 --- a/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureRestClient.java +++ b/module/spring-boot-restclient-test/src/main/java/org/springframework/boot/restclient/test/autoconfigure/AutoConfigureRestClient.java @@ -24,13 +24,12 @@ import java.lang.annotation.Target; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.boot.test.autoconfigure.json.AutoConfigureJson; import org.springframework.web.client.RestClient.Builder; /** * Annotation that can be applied to a test class to enable auto-configuration of a - * {@link Builder RestClient.Builder} and a {@link RestTemplateBuilder}. + * {@link Builder RestClient.Builder}. * * @author Andy Wilkinson * @since 4.0.0 diff --git a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java index f45e9da0c4cc..d219fc053515 100644 --- a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java +++ b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/TestRestTemplate.java @@ -90,10 +90,10 @@ * @author Dmytro Nosan * @author Yanming Zhou * @since 4.0.0 - * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of * {@link org.springframework.test.web.servlet.client.RestTestClient} */ -@Deprecated(since = "4.1.0", forRemoval = true) +@Deprecated(since = "4.2.0", forRemoval = true) @SuppressWarnings("removal") public class TestRestTemplate { diff --git a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/AutoConfigureTestRestTemplate.java b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/AutoConfigureTestRestTemplate.java index 6459cd96eedf..e11b45579504 100644 --- a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/AutoConfigureTestRestTemplate.java +++ b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/AutoConfigureTestRestTemplate.java @@ -32,12 +32,15 @@ * * @author Andy Wilkinson * @since 4.0.0 + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of + * {@link AutoConfigureRestTestClient} */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @ImportAutoConfiguration +@Deprecated(since = "4.2.0", forRemoval = true) public @interface AutoConfigureTestRestTemplate { } diff --git a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfiguration.java b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfiguration.java index 3bae6d729a68..07f5d237ec75 100644 --- a/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfiguration.java +++ b/module/spring-boot-resttestclient/src/main/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfiguration.java @@ -31,8 +31,11 @@ * * @author Andy Wilkinson * @see AutoConfigureTestRestTemplate + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of + * {@link RestTestClientTestAutoConfiguration} */ @AutoConfiguration +@Deprecated(since = "4.2.0", forRemoval = true) @SuppressWarnings("removal") final class TestRestTemplateTestAutoConfiguration { diff --git a/module/spring-boot-resttestclient/src/main/kotlin/org/springframework/boot/resttestclient/TestRestTemplateExtensions.kt b/module/spring-boot-resttestclient/src/main/kotlin/org/springframework/boot/resttestclient/TestRestTemplateExtensions.kt index a153539ecd43..662dd1e5cbba 100644 --- a/module/spring-boot-resttestclient/src/main/kotlin/org/springframework/boot/resttestclient/TestRestTemplateExtensions.kt +++ b/module/spring-boot-resttestclient/src/main/kotlin/org/springframework/boot/resttestclient/TestRestTemplateExtensions.kt @@ -34,7 +34,10 @@ import java.net.URI * * @author Sebastien Deleuze * @since 4.0.0 + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of + * {@link org.springframework.test.web.servlet.client.RestTestClient} */ +@Deprecated(message = "in favor of RestTestClient", level = DeprecationLevel.ERROR) @Throws(RestClientException::class) inline fun <reified T : Any> TestRestTemplate.getForObject(url: String, vararg uriVariables: Any): T? = getForObject(url, T::class.java, *uriVariables) diff --git a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java index 85f6e0cfe8b7..def29c1ab0ed 100644 --- a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java +++ b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/TestRestTemplateTests.java @@ -79,7 +79,9 @@ * @author Andy Wilkinson * @author Kristine Jetzke * @author Yanming Zhou + * @deprecated since 4.2.0 for removal in 4.4.0 */ +@Deprecated(since = "4.2.0", forRemoval = true) @SuppressWarnings("removal") class TestRestTemplateTests { diff --git a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfigurationTests.java b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfigurationTests.java index d1fb3ca49d10..9e593dc5d044 100644 --- a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfigurationTests.java +++ b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/autoconfigure/TestRestTemplateTestAutoConfigurationTests.java @@ -34,7 +34,9 @@ * Tests for {@link TestRestTemplateTestAutoConfiguration}. * * @author Stephane Nicoll + * @deprecated since 4.2.0 for removal in 4.4.0 */ +@Deprecated(since = "4.2.0", forRemoval = true) @SuppressWarnings("removal") class TestRestTemplateTestAutoConfigurationTests { From e7741bf1522ee61298b28d9214cbc38a0a168956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Fri, 24 Jul 2026 16:22:59 +0200 Subject: [PATCH 798/930] Start building against Spring Framework 7.1.0-M1 snapshots See gh-51123 --- gradle.properties | 2 +- ...eactiveWebEnvironmentUserDefinedTestRestTemplateTests.java | 1 + .../test/SpringBootTestUserDefinedTestRestTemplateTests.java | 1 + .../actuate/endpoint/ProducibleOperationArgumentResolver.java | 4 ++-- .../MessageConverterBackgroundPreinitializer.java | 4 ++-- .../reactive/ReactiveWebServerConfiguration.java | 1 + .../autoconfigure/servlet/ServletWebServerConfiguration.java | 1 + .../AbstractReactiveWebServerAutoConfigurationTests.java | 1 + .../AbstractServletWebServerAutoConfigurationTests.java | 1 + 9 files changed, 11 insertions(+), 5 deletions(-) diff --git a/gradle.properties b/gradle.properties index 52b8a4c8c1cd..6368c4350413 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,7 +22,7 @@ nativeBuildToolsVersion=1.1.5 nullabilityPluginVersion=0.0.14 snakeYamlVersion=2.6 springGrpcVersion=1.1.0 -springFrameworkVersion=7.0.8 +springFrameworkVersion=7.1.0-SNAPSHOT springFramework60xVersion=6.0.23 tomcatVersion=11.0.24 protobufGradlePluginVersion=0.9.6 diff --git a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestReactiveWebEnvironmentUserDefinedTestRestTemplateTests.java b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestReactiveWebEnvironmentUserDefinedTestRestTemplateTests.java index 30b0ee966f10..1202f9b183c0 100644 --- a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestReactiveWebEnvironmentUserDefinedTestRestTemplateTests.java +++ b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestReactiveWebEnvironmentUserDefinedTestRestTemplateTests.java @@ -40,6 +40,7 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.main.web-application-type=reactive", "value=123" }) @Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class SpringBootTestReactiveWebEnvironmentUserDefinedTestRestTemplateTests extends AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests { diff --git a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestUserDefinedTestRestTemplateTests.java b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestUserDefinedTestRestTemplateTests.java index 337084689415..b6259816a8a5 100644 --- a/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestUserDefinedTestRestTemplateTests.java +++ b/integration-test/spring-boot-test-integration-tests/src/test/java/org/springframework/boot/web/server/test/SpringBootTestUserDefinedTestRestTemplateTests.java @@ -40,6 +40,7 @@ @DirtiesContext @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "value=123" }) @Deprecated(since = "4.2.0", forRemoval = true) +@SuppressWarnings("removal") class SpringBootTestUserDefinedTestRestTemplateTests extends AbstractSpringBootTestWebServerWebEnvironmentTests { @Test diff --git a/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ProducibleOperationArgumentResolver.java b/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ProducibleOperationArgumentResolver.java index 39ddca4f61cf..996512949b8e 100644 --- a/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ProducibleOperationArgumentResolver.java +++ b/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ProducibleOperationArgumentResolver.java @@ -66,8 +66,8 @@ public boolean canResolve(Class<?> type) { } Enum<? extends Producible<?>> result = null; for (String accept : accepts) { - for (String mimeType : MimeTypeUtils.tokenize(accept)) { - result = mostRecent(result, forMimeType(values, MimeTypeUtils.parseMimeType(mimeType))); + for (MimeType mimeType : MimeTypeUtils.parseMimeTypes(accept)) { + result = mostRecent(result, forMimeType(values, mimeType)); } } return result; diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/MessageConverterBackgroundPreinitializer.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/MessageConverterBackgroundPreinitializer.java index c0b83033b200..c32cd8db4c7b 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/MessageConverterBackgroundPreinitializer.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/MessageConverterBackgroundPreinitializer.java @@ -18,7 +18,7 @@ import org.springframework.boot.autoconfigure.preinitialize.BackgroundPreinitializer; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; +import org.springframework.http.converter.multipart.MultipartHttpMessageConverter; /** * {@link BackgroundPreinitializer} Spring's {@link HttpMessageConverter} implementations. @@ -29,7 +29,7 @@ final class MessageConverterBackgroundPreinitializer implements BackgroundPreini @Override public void preinitialize() throws Exception { - new AllEncompassingFormHttpMessageConverter(); + new MultipartHttpMessageConverter(); } } diff --git a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/reactive/ReactiveWebServerConfiguration.java b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/reactive/ReactiveWebServerConfiguration.java index 14938716e969..5cdfe79f60b4 100644 --- a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/reactive/ReactiveWebServerConfiguration.java +++ b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/reactive/ReactiveWebServerConfiguration.java @@ -60,6 +60,7 @@ ReactiveWebServerFactoryCustomizer reactiveWebServerFactoryCustomizer(ServerProp @Bean @ConditionalOnMissingBean @ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework") + @SuppressWarnings("removal") // gh-51030 ForwardedHeaderTransformer forwardedHeaderTransformer() { return new ForwardedHeaderTransformer(); } diff --git a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/servlet/ServletWebServerConfiguration.java b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/servlet/ServletWebServerConfiguration.java index 088f134ace40..240c366b9abe 100644 --- a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/servlet/ServletWebServerConfiguration.java +++ b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/servlet/ServletWebServerConfiguration.java @@ -72,6 +72,7 @@ ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProper @Bean @ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework") @ConditionalOnMissingFilterBean(ForwardedHeaderFilter.class) + @SuppressWarnings("removal") // gh-51030 FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter( ObjectProvider<ForwardedHeaderFilterCustomizer> customizerProvider) { ForwardedHeaderFilter filter = new ForwardedHeaderFilter(); diff --git a/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/reactive/AbstractReactiveWebServerAutoConfigurationTests.java b/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/reactive/AbstractReactiveWebServerAutoConfigurationTests.java index 57f7e316ce79..ef009c0c0443 100644 --- a/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/reactive/AbstractReactiveWebServerAutoConfigurationTests.java +++ b/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/reactive/AbstractReactiveWebServerAutoConfigurationTests.java @@ -178,6 +178,7 @@ MockReactiveWebServerFactory mockReactiveWebServerFactory() { static class ForwardedHeaderTransformerConfiguration { @Bean + @SuppressWarnings("removal") // gh-51030 ForwardedHeaderTransformer testForwardedHeaderTransformer() { return new ForwardedHeaderTransformer(); } diff --git a/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/servlet/AbstractServletWebServerAutoConfigurationTests.java b/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/servlet/AbstractServletWebServerAutoConfigurationTests.java index 27f4b97aad67..83698582c0e6 100644 --- a/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/servlet/AbstractServletWebServerAutoConfigurationTests.java +++ b/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/servlet/AbstractServletWebServerAutoConfigurationTests.java @@ -212,6 +212,7 @@ public Object postProcessAfterInitialization(Object bean, String beanName) { static class ForwardedHeaderFilterConfiguration { @Bean + @SuppressWarnings("removal") // gh-51030 FilterRegistrationBean<ForwardedHeaderFilter> testForwardedHeaderFilter() { ForwardedHeaderFilter filter = new ForwardedHeaderFilter(); return new FilterRegistrationBean<>(filter); From cd791e7246611ece07f8fba911c28c945da60011 Mon Sep 17 00:00:00 2001 From: Chris <udo2000@freenet.de> Date: Mon, 29 Jun 2026 00:37:17 +0200 Subject: [PATCH 799/930] Support timeout option in RetryPolicySettings See gh-50891 Signed-off-by: Chris <udo2000@freenet.de> --- .../boot/retry/RetryPolicySettings.java | 31 +++++++++++++++++++ .../boot/retry/RetryPolicySettingsTests.java | 5 +++ 2 files changed, 36 insertions(+) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java b/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java index 6b406e2850c1..1eb3a96c3568 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java @@ -56,6 +56,11 @@ public final class RetryPolicySettings { */ public static final Duration DEFAULT_MAX_DELAY = Duration.ofMillis(RetryPolicy.Builder.DEFAULT_MAX_DELAY); + /** + * Default maximum elapsed time (infinite). + */ + public static final Duration DEFAULT_TIMEOUT = RetryPolicy.withDefaults().getTimeout(); + private List<Class<? extends Throwable>> exceptionIncludes = new ArrayList<>(); private List<Class<? extends Throwable>> exceptionExcludes = new ArrayList<>(); @@ -72,6 +77,8 @@ public final class RetryPolicySettings { private Duration maxDelay = DEFAULT_MAX_DELAY; + private Duration timeout = DEFAULT_TIMEOUT; + private @Nullable Function<Builder, RetryPolicy> factory; /** @@ -89,6 +96,7 @@ public RetryPolicy createRetryPolicy() { map.from(this::getJitter).to(builder::jitter); map.from(this::getMultiplier).to(builder::multiplier); map.from(this::getMaxDelay).to(builder::maxDelay); + map.from(this::getTimeout).to(builder::timeout); return (this.factory != null) ? this.factory.apply(builder) : builder.build(); } @@ -250,6 +258,29 @@ public void setMaxDelay(Duration maxDelay) { this.maxDelay = maxDelay; } + /** + * Return the timeout for the maximum amount of elapsed time allowed for the initial + * invocation and any subsequent retry attempts, including delays. + * @return the maximum duration the request can take + */ + public Duration getTimeout() { + return this.timeout; + } + + /** + * Specify a timeout for the maximum amount of elapsed time allowed for the initial + * invocation and any subsequent retry attempts, including delays. + * <p> + * The default is {@link Duration#ZERO}, which signals that no timeout should be + * applied. + * @param timeout the timeout, typically in milliseconds or seconds; must be greater + * than or equal to zero + * @see #DEFAULT_TIMEOUT + */ + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + /** * Set the factory to use to create the {@link RetryPolicy}, or {@code null} to use * the default. The function takes a {@link Builder RetryPolicy.Builder} initialized diff --git a/core/spring-boot/src/test/java/org/springframework/boot/retry/RetryPolicySettingsTests.java b/core/spring-boot/src/test/java/org/springframework/boot/retry/RetryPolicySettingsTests.java index cd42a3734ba2..7254f7e31917 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/retry/RetryPolicySettingsTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/retry/RetryPolicySettingsTests.java @@ -98,6 +98,11 @@ void getDefaultExceptionPredicate() { assertThat(new RetryPolicySettings().getExceptionPredicate()).isNull(); } + @Test + void getDefaultTimeout() { + assertThat(new RetryPolicySettings().getTimeout()).isEqualTo(RetryPolicy.withDefaults().getTimeout()); + } + @Test void createRetryPolicyWithExceptionPredicate() { IllegalArgumentException exception = new IllegalArgumentException("test"); From b77f7fbc9353fc593315b5fb0c13b92801f8353a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 28 Jul 2026 06:44:52 +0200 Subject: [PATCH 800/930] Polish "Support timeout option in RetryPolicySettings" See gh-50891 --- .../boot/retry/RetryPolicySettings.java | 65 ++++++++++--------- .../boot/retry/RetryPolicySettingsTests.java | 8 +-- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java b/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java index 1eb3a96c3568..80f6d7320bb4 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java @@ -41,6 +41,12 @@ public final class RetryPolicySettings { */ public static final long DEFAULT_MAX_RETRIES = RetryPolicy.Builder.DEFAULT_MAX_RETRIES; + /** + * Default maximum elapsed time (infinite). + * @since 4.2.0 + */ + public static final Duration DEFAULT_TIMEOUT = RetryPolicy.Builder.DEFAULT_TIMEOUT; + /** * Default initial delay. */ @@ -56,11 +62,6 @@ public final class RetryPolicySettings { */ public static final Duration DEFAULT_MAX_DELAY = Duration.ofMillis(RetryPolicy.Builder.DEFAULT_MAX_DELAY); - /** - * Default maximum elapsed time (infinite). - */ - public static final Duration DEFAULT_TIMEOUT = RetryPolicy.withDefaults().getTimeout(); - private List<Class<? extends Throwable>> exceptionIncludes = new ArrayList<>(); private List<Class<? extends Throwable>> exceptionExcludes = new ArrayList<>(); @@ -69,6 +70,8 @@ public final class RetryPolicySettings { private Long maxRetries = DEFAULT_MAX_RETRIES; + private Duration timeout = DEFAULT_TIMEOUT; + private Duration delay = DEFAULT_DELAY; private @Nullable Duration jitter; @@ -77,8 +80,6 @@ public final class RetryPolicySettings { private Duration maxDelay = DEFAULT_MAX_DELAY; - private Duration timeout = DEFAULT_TIMEOUT; - private @Nullable Function<Builder, RetryPolicy> factory; /** @@ -92,11 +93,11 @@ public RetryPolicy createRetryPolicy() { map.from(this::getExceptionExcludes).to(builder::excludes); map.from(this::getExceptionPredicate).to(builder::predicate); map.from(this::getMaxRetries).to(builder::maxRetries); + map.from(this::getTimeout).to(builder::timeout); map.from(this::getDelay).to(builder::delay); map.from(this::getJitter).to(builder::jitter); map.from(this::getMultiplier).to(builder::multiplier); map.from(this::getMaxDelay).to(builder::maxDelay); - map.from(this::getTimeout).to(builder::timeout); return (this.factory != null) ? this.factory.apply(builder) : builder.build(); } @@ -176,6 +177,31 @@ public void setMaxRetries(Long maxRetries) { this.maxRetries = maxRetries; } + /** + * Return the timeout for the maximum amount of elapsed time allowed for the initial + * invocation and any subsequent retry attempts, including delays. + * @return the maximum duration the request can take + * @since 4.2.0 + */ + public Duration getTimeout() { + return this.timeout; + } + + /** + * Specify a timeout for the maximum amount of elapsed time allowed for the initial + * invocation and any subsequent retry attempts, including delays. + * <p> + * The default is {@link Duration#ZERO}, which signals that no timeout should be + * applied. + * @param timeout the timeout, typically in milliseconds or seconds; must be greater + * than or equal to zero + * @since 4.2.0 + * @see #DEFAULT_TIMEOUT + */ + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + /** * Return the base delay after the initial invocation. * @return the base delay @@ -258,29 +284,6 @@ public void setMaxDelay(Duration maxDelay) { this.maxDelay = maxDelay; } - /** - * Return the timeout for the maximum amount of elapsed time allowed for the initial - * invocation and any subsequent retry attempts, including delays. - * @return the maximum duration the request can take - */ - public Duration getTimeout() { - return this.timeout; - } - - /** - * Specify a timeout for the maximum amount of elapsed time allowed for the initial - * invocation and any subsequent retry attempts, including delays. - * <p> - * The default is {@link Duration#ZERO}, which signals that no timeout should be - * applied. - * @param timeout the timeout, typically in milliseconds or seconds; must be greater - * than or equal to zero - * @see #DEFAULT_TIMEOUT - */ - public void setTimeout(Duration timeout) { - this.timeout = timeout; - } - /** * Set the factory to use to create the {@link RetryPolicy}, or {@code null} to use * the default. The function takes a {@link Builder RetryPolicy.Builder} initialized diff --git a/core/spring-boot/src/test/java/org/springframework/boot/retry/RetryPolicySettingsTests.java b/core/spring-boot/src/test/java/org/springframework/boot/retry/RetryPolicySettingsTests.java index 7254f7e31917..5ee433295368 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/retry/RetryPolicySettingsTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/retry/RetryPolicySettingsTests.java @@ -98,11 +98,6 @@ void getDefaultExceptionPredicate() { assertThat(new RetryPolicySettings().getExceptionPredicate()).isNull(); } - @Test - void getDefaultTimeout() { - assertThat(new RetryPolicySettings().getTimeout()).isEqualTo(RetryPolicy.withDefaults().getTimeout()); - } - @Test void createRetryPolicyWithExceptionPredicate() { IllegalArgumentException exception = new IllegalArgumentException("test"); @@ -120,6 +115,7 @@ void createRetryPolicyWithExceptionPredicate() { void createRetryPolicyWithDefaultsMatchesBackOffDefaults() { RetryPolicy defaultRetryPolicy = RetryPolicy.builder().build(); RetryPolicy retryPolicy = new RetryPolicySettings().createRetryPolicy(); + assertThat(retryPolicy.getTimeout()).isEqualByComparingTo(defaultRetryPolicy.getTimeout()); assertThat(retryPolicy.getBackOff()).isInstanceOf(ExponentialBackOff.class); ExponentialBackOff defaultBackOff = (ExponentialBackOff) defaultRetryPolicy.getBackOff(); ExponentialBackOff backOff = (ExponentialBackOff) retryPolicy.getBackOff(); @@ -134,11 +130,13 @@ void createRetryPolicyWithDefaultsMatchesBackOffDefaults() { void createRetryPolicyWithCustomAttributes() { RetryPolicySettings settings = new RetryPolicySettings(); settings.setMaxRetries(10L); + settings.setTimeout(Duration.ofHours(1)); settings.setDelay(Duration.ofSeconds(2)); settings.setJitter(Duration.ofMillis(500)); settings.setMultiplier(2.0); settings.setMaxDelay(Duration.ofSeconds(20)); RetryPolicy retryPolicy = settings.createRetryPolicy(); + assertThat(retryPolicy.getTimeout()).isEqualTo(Duration.ofHours(1)); assertThat(retryPolicy.getBackOff()).isInstanceOfSatisfying(ExponentialBackOff.class, (backOff) -> { assertThat(backOff.getMaxAttempts()).isEqualTo(10); assertThat(backOff.getInitialInterval()).isEqualTo(2000); From c66a5ef94fe381025e6585dda895be33a35fb50c Mon Sep 17 00:00:00 2001 From: LordKay-sudo <lkandiro@gmail.com> Date: Tue, 28 Jul 2026 14:27:11 +0200 Subject: [PATCH 801/930] Disable Micrometer global registry in tests by default Default management.metrics.use-global-registry to false in tests when unset, mirroring the Reactor global-resources customizer, so Micrometer does not pin cached application contexts. See gh-51142 Signed-off-by: LordKay-sudo <lkandiro@gmail.com> --- ...MeterRegistryContextCustomizerFactory.java | 77 ++++++++++++++++ .../boot/test/metrics/package-info.java | 23 +++++ .../main/resources/META-INF/spring.factories | 3 +- ...RegistryContextCustomizerFactoryTests.java | 90 +++++++++++++++++++ 4 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java create mode 100644 core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java create mode 100644 core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java new file mode 100644 index 000000000000..dbc0ac206d26 --- /dev/null +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.metrics; + +import java.util.List; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +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.util.ClassUtils; + +/** + * {@link ContextCustomizerFactory} to disable the use of Micrometer's + * {@link io.micrometer.core.instrument.Metrics#globalRegistry global registry} in tests, + * preventing {@link io.micrometer.core.instrument.MeterRegistry meter registries} from + * pinning application contexts when many test contexts are cached. + * + * @author Lordwill Kandiro + */ +class DisableGlobalMeterRegistryContextCustomizerFactory implements ContextCustomizerFactory { + + private static final String METRICS_CLASS = "io.micrometer.core.instrument.Metrics"; + + private static final String USE_GLOBAL_REGISTRY_PROPERTY = "management.metrics.use-global-registry"; + + @Override + public @Nullable ContextCustomizer createContextCustomizer(Class<?> testClass, + List<ContextConfigurationAttributes> configAttributes) { + if (ClassUtils.isPresent(METRICS_CLASS, testClass.getClassLoader())) { + return new DisableGlobalMeterRegistryContextCustomizer(); + } + return null; + } + + static final class DisableGlobalMeterRegistryContextCustomizer implements ContextCustomizer { + + @Override + public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { + ConfigurableEnvironment environment = context.getEnvironment(); + if (environment.getProperty(USE_GLOBAL_REGISTRY_PROPERTY) == null) { + TestPropertyValues.of(USE_GLOBAL_REGISTRY_PROPERTY + "=false").applyTo(environment); + } + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof DisableGlobalMeterRegistryContextCustomizer); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + } + +} diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java new file mode 100644 index 000000000000..4d2792dd73c3 --- /dev/null +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 support for metrics testing. + */ +@NullMarked +package org.springframework.boot.test.metrics; + +import org.jspecify.annotations.NullMarked; diff --git a/core/spring-boot-test/src/main/resources/META-INF/spring.factories b/core/spring-boot-test/src/main/resources/META-INF/spring.factories index 8a9b2c7d1412..69d1c39b3217 100644 --- a/core/spring-boot-test/src/main/resources/META-INF/spring.factories +++ b/core/spring-boot-test/src/main/resources/META-INF/spring.factories @@ -5,7 +5,8 @@ org.springframework.boot.test.context.PropertyMappingContextCustomizerFactory,\ org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizerFactory,\ org.springframework.boot.test.context.filter.annotation.TypeExcludeFiltersContextCustomizerFactory,\ org.springframework.boot.test.http.client.DisableReactorResourceFactoryGlobalResourcesContextCustomizerFactory,\ -org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory +org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory,\ +org.springframework.boot.test.metrics.DisableGlobalMeterRegistryContextCustomizerFactory # Application Context Initializers org.springframework.context.ApplicationContextInitializer=\ diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java new file mode 100644 index 000000000000..92697014b1c0 --- /dev/null +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.metrics; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.MergedContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DisableGlobalMeterRegistryContextCustomizerFactory}. + * + * @author Lordwill Kandiro + */ +class DisableGlobalMeterRegistryContextCustomizerFactoryTests { + + private final DisableGlobalMeterRegistryContextCustomizerFactory factory = new DisableGlobalMeterRegistryContextCustomizerFactory(); + + @Test + void createContextCustomizerReturnsNullWhenMetricsIsNotPresent() { + assertThat(this.factory.createContextCustomizer(getClass(), Collections.emptyList())).isNull(); + } + + @Test + void disablesGlobalMeterRegistryByDefault() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.setEnvironment(new StandardEnvironment()); + new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer() + .customizeContext(context, mock(MergedContextConfiguration.class)); + assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")) + .isEqualTo("false"); + } + } + + @Test + void doesNotOverrideExplicitTrue() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + StandardEnvironment environment = new StandardEnvironment(); + TestPropertyValues.of("management.metrics.use-global-registry=true").applyTo(environment); + context.setEnvironment(environment); + new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer() + .customizeContext(context, mock(MergedContextConfiguration.class)); + assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")) + .isEqualTo("true"); + } + } + + @Test + void doesNotOverrideExplicitFalse() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + StandardEnvironment environment = new StandardEnvironment(); + TestPropertyValues.of("management.metrics.use-global-registry=false").applyTo(environment); + context.setEnvironment(environment); + new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer() + .customizeContext(context, mock(MergedContextConfiguration.class)); + assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")) + .isEqualTo("false"); + } + } + + @Test + void equalsAndHashCode() { + ContextCustomizer customizer1 = new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer(); + ContextCustomizer customizer2 = new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer(); + assertThat(customizer1).isEqualTo(customizer2).hasSameHashCodeAs(customizer2); + } + +} From be35ff4149e87b4ccd42d0a22d9b150e307dc125 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter <moritz.halbritter@broadcom.com> Date: Wed, 29 Jul 2026 09:25:07 +0200 Subject: [PATCH 802/930] Polish "Disable Micrometer global registry in tests by default" See gh-51142 --- ...MeterRegistryContextCustomizerFactory.java | 77 ---------------- .../boot/test/metrics/package-info.java | 23 ----- .../main/resources/META-INF/spring.factories | 3 +- ...RegistryContextCustomizerFactoryTests.java | 90 ------------------ .../reference/pages/actuator/metrics.adoc | 3 + .../testing/spring-boot-applications.adoc | 3 + .../autoconfigure/AutoConfigureMetrics.java | 11 +++ .../MetricsContextCustomizerFactory.java | 32 +++++-- .../GlobalMeterRegistryIntegrationTests.java | 92 +++++++++++++++++++ .../MetricsContextCustomizerFactoryTests.java | 90 ++++++++++++++++++ 10 files changed, 224 insertions(+), 200 deletions(-) delete mode 100644 core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java delete mode 100644 core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java delete mode 100644 core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java create mode 100644 module/spring-boot-micrometer-metrics-test/src/test/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/GlobalMeterRegistryIntegrationTests.java diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java deleted file mode 100644 index dbc0ac206d26..000000000000 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.metrics; - -import java.util.List; - -import org.jspecify.annotations.Nullable; - -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.env.ConfigurableEnvironment; -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.util.ClassUtils; - -/** - * {@link ContextCustomizerFactory} to disable the use of Micrometer's - * {@link io.micrometer.core.instrument.Metrics#globalRegistry global registry} in tests, - * preventing {@link io.micrometer.core.instrument.MeterRegistry meter registries} from - * pinning application contexts when many test contexts are cached. - * - * @author Lordwill Kandiro - */ -class DisableGlobalMeterRegistryContextCustomizerFactory implements ContextCustomizerFactory { - - private static final String METRICS_CLASS = "io.micrometer.core.instrument.Metrics"; - - private static final String USE_GLOBAL_REGISTRY_PROPERTY = "management.metrics.use-global-registry"; - - @Override - public @Nullable ContextCustomizer createContextCustomizer(Class<?> testClass, - List<ContextConfigurationAttributes> configAttributes) { - if (ClassUtils.isPresent(METRICS_CLASS, testClass.getClassLoader())) { - return new DisableGlobalMeterRegistryContextCustomizer(); - } - return null; - } - - static final class DisableGlobalMeterRegistryContextCustomizer implements ContextCustomizer { - - @Override - public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { - ConfigurableEnvironment environment = context.getEnvironment(); - if (environment.getProperty(USE_GLOBAL_REGISTRY_PROPERTY) == null) { - TestPropertyValues.of(USE_GLOBAL_REGISTRY_PROPERTY + "=false").applyTo(environment); - } - } - - @Override - public boolean equals(Object obj) { - return (obj instanceof DisableGlobalMeterRegistryContextCustomizer); - } - - @Override - public int hashCode() { - return getClass().hashCode(); - } - - } - -} diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java deleted file mode 100644 index 4d2792dd73c3..000000000000 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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 support for metrics testing. - */ -@NullMarked -package org.springframework.boot.test.metrics; - -import org.jspecify.annotations.NullMarked; diff --git a/core/spring-boot-test/src/main/resources/META-INF/spring.factories b/core/spring-boot-test/src/main/resources/META-INF/spring.factories index 69d1c39b3217..8a9b2c7d1412 100644 --- a/core/spring-boot-test/src/main/resources/META-INF/spring.factories +++ b/core/spring-boot-test/src/main/resources/META-INF/spring.factories @@ -5,8 +5,7 @@ org.springframework.boot.test.context.PropertyMappingContextCustomizerFactory,\ org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizerFactory,\ org.springframework.boot.test.context.filter.annotation.TypeExcludeFiltersContextCustomizerFactory,\ org.springframework.boot.test.http.client.DisableReactorResourceFactoryGlobalResourcesContextCustomizerFactory,\ -org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory,\ -org.springframework.boot.test.metrics.DisableGlobalMeterRegistryContextCustomizerFactory +org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory # Application Context Initializers org.springframework.context.ApplicationContextInitializer=\ diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java deleted file mode 100644 index 92697014b1c0..000000000000 --- a/core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.metrics; - -import java.util.Collections; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.test.context.ContextCustomizer; -import org.springframework.test.context.MergedContextConfiguration; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link DisableGlobalMeterRegistryContextCustomizerFactory}. - * - * @author Lordwill Kandiro - */ -class DisableGlobalMeterRegistryContextCustomizerFactoryTests { - - private final DisableGlobalMeterRegistryContextCustomizerFactory factory = new DisableGlobalMeterRegistryContextCustomizerFactory(); - - @Test - void createContextCustomizerReturnsNullWhenMetricsIsNotPresent() { - assertThat(this.factory.createContextCustomizer(getClass(), Collections.emptyList())).isNull(); - } - - @Test - void disablesGlobalMeterRegistryByDefault() { - try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { - context.setEnvironment(new StandardEnvironment()); - new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer() - .customizeContext(context, mock(MergedContextConfiguration.class)); - assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")) - .isEqualTo("false"); - } - } - - @Test - void doesNotOverrideExplicitTrue() { - try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { - StandardEnvironment environment = new StandardEnvironment(); - TestPropertyValues.of("management.metrics.use-global-registry=true").applyTo(environment); - context.setEnvironment(environment); - new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer() - .customizeContext(context, mock(MergedContextConfiguration.class)); - assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")) - .isEqualTo("true"); - } - } - - @Test - void doesNotOverrideExplicitFalse() { - try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { - StandardEnvironment environment = new StandardEnvironment(); - TestPropertyValues.of("management.metrics.use-global-registry=false").applyTo(environment); - context.setEnvironment(environment); - new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer() - .customizeContext(context, mock(MergedContextConfiguration.class)); - assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")) - .isEqualTo("false"); - } - } - - @Test - void equalsAndHashCode() { - ContextCustomizer customizer1 = new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer(); - ContextCustomizer customizer2 = new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer(); - assertThat(customizer1).isEqualTo(customizer2).hasSameHashCodeAs(customizer2); - } - -} diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc index 9dc34353b353..e34188a17df8 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc @@ -64,6 +64,9 @@ management: use-global-registry: false ---- +NOTE: In tests that use `spring-boot-micrometer-metrics-test`, the global registry is disabled by default. +If you want to use the global registry in such a test, annotate it with javadoc:org.springframework.boot.micrometer.metrics.test.autoconfigure.AutoConfigureMetrics[format=annotation] using `useGlobalRegistry = true`, or set the configprop:management.metrics.use-global-registry[] property explicitly to `true`. + You can register any number of javadoc:org.springframework.boot.micrometer.metrics.autoconfigure.MeterRegistryCustomizer[] beans to further configure the registry, such as applying common tags, before any meters are registered with the registry: include-code::commontags/MyMeterRegistryConfiguration[] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc index 387c96326f5a..e4e4e19ddf45 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc @@ -244,6 +244,9 @@ If you need to export metrics to a different backend as part of an integration t If you annotate xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.autoconfigured-tests[a sliced test] with javadoc:org.springframework.boot.micrometer.metrics.test.autoconfigure.AutoConfigureMetrics[format=annotation], it auto-configures an in-memory javadoc:io.micrometer.core.instrument.MeterRegistry[]. Data exporting in sliced tests is not supported with the javadoc:org.springframework.boot.micrometer.metrics.test.autoconfigure.AutoConfigureMetrics[format=annotation] annotation. +Meter registries are not added to the xref:actuator/metrics.adoc#actuator.metrics.getting-started[global registry] by default in tests. +If you need the global registry in a test, annotate it with javadoc:org.springframework.boot.micrometer.metrics.test.autoconfigure.AutoConfigureMetrics[format=annotation] using `useGlobalRegistry = true`, or set the configprop:management.metrics.use-global-registry[] property explicitly to `true`. + [[testing.spring-boot-applications.tracing]] diff --git a/module/spring-boot-micrometer-metrics-test/src/main/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/AutoConfigureMetrics.java b/module/spring-boot-micrometer-metrics-test/src/main/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/AutoConfigureMetrics.java index 3ac4b5be589b..588704e122c5 100644 --- a/module/spring-boot-micrometer-metrics-test/src/main/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/AutoConfigureMetrics.java +++ b/module/spring-boot-micrometer-metrics-test/src/main/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/AutoConfigureMetrics.java @@ -36,6 +36,7 @@ * an {@link ObservationRegistry} are added to the application context. * * @author Moritz Halbritter + * @author Lordwill Kandiro * @since 4.0.0 */ @Target(ElementType.TYPE) @@ -51,4 +52,14 @@ */ boolean export() default true; + /** + * Whether {@link MeterRegistry meter registries} should be added to Micrometer's + * {@link io.micrometer.core.instrument.Metrics#globalRegistry global registry} in the + * test. If {@code management.metrics.use-global-registry} has been set explicitly, + * its value is used and this attribute has no effect. + * @return whether the global registry should be used in the test + * @since 4.2.0 + */ + boolean useGlobalRegistry() default false; + } diff --git a/module/spring-boot-micrometer-metrics-test/src/main/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/MetricsContextCustomizerFactory.java b/module/spring-boot-micrometer-metrics-test/src/main/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/MetricsContextCustomizerFactory.java index af9efc3ceb71..f08cfdd414b4 100644 --- a/module/spring-boot-micrometer-metrics-test/src/main/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/MetricsContextCustomizerFactory.java +++ b/module/spring-boot-micrometer-metrics-test/src/main/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/MetricsContextCustomizerFactory.java @@ -31,43 +31,52 @@ import org.springframework.test.context.TestContextAnnotationUtils; /** - * {@link ContextCustomizerFactory} that globally disables metrics export in tests. The - * behaviour can be controlled with {@link AutoConfigureMetrics} on the test class or via - * the {@value #AUTO_CONFIGURE_PROPERTY} property. + * {@link ContextCustomizerFactory} that globally disables metrics export and the use of + * Micrometer's {@link io.micrometer.core.instrument.Metrics#globalRegistry global + * registry} in tests. The behaviour can be controlled with {@link AutoConfigureMetrics} + * on the test class, via the {@value #AUTO_CONFIGURE_PROPERTY} property and via the + * {@value #USE_GLOBAL_REGISTRY_PROPERTY} property. * * @author Chris Bono * @author Moritz Halbritter * @author Andy Wilkinson + * @author Lordwill Kandiro */ class MetricsContextCustomizerFactory implements ContextCustomizerFactory { static final String AUTO_CONFIGURE_PROPERTY = "spring.test.metrics.export"; + static final String USE_GLOBAL_REGISTRY_PROPERTY = "management.metrics.use-global-registry"; + @Override public ContextCustomizer createContextCustomizer(Class<?> testClass, List<ContextConfigurationAttributes> configAttributes) { AutoConfigureMetrics annotation = TestContextAnnotationUtils.findMergedAnnotation(testClass, AutoConfigureMetrics.class); - return new DisableMetricsExportContextCustomizer(annotation); + return new MetricsContextCustomizer(annotation); } - private static class DisableMetricsExportContextCustomizer implements ContextCustomizer { + private static class MetricsContextCustomizer implements ContextCustomizer { private final @Nullable AutoConfigureMetrics annotation; - DisableMetricsExportContextCustomizer(@Nullable AutoConfigureMetrics annotation) { + MetricsContextCustomizer(@Nullable AutoConfigureMetrics annotation) { this.annotation = annotation; } @Override public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedContextConfiguration) { - if (areMetricsDisabled(context.getEnvironment())) { + Environment environment = context.getEnvironment(); + if (areMetricsDisabled(environment)) { TestPropertyValues .of("management.defaults.metrics.export.enabled=false", "management.simple.metrics.export.enabled=true") .applyTo(context); } + if (isGlobalRegistryDisabled(environment)) { + TestPropertyValues.of(USE_GLOBAL_REGISTRY_PROPERTY + "=false").applyTo(context); + } } private boolean areMetricsDisabled(Environment environment) { @@ -77,6 +86,13 @@ private boolean areMetricsDisabled(Environment environment) { return !environment.getProperty(AUTO_CONFIGURE_PROPERTY, Boolean.class, false); } + private boolean isGlobalRegistryDisabled(Environment environment) { + if (this.annotation != null && this.annotation.useGlobalRegistry()) { + return false; + } + return !environment.containsProperty(USE_GLOBAL_REGISTRY_PROPERTY); + } + @Override public boolean equals(@Nullable Object o) { if (this == o) { @@ -85,7 +101,7 @@ public boolean equals(@Nullable Object o) { if (o == null || getClass() != o.getClass()) { return false; } - DisableMetricsExportContextCustomizer that = (DisableMetricsExportContextCustomizer) o; + MetricsContextCustomizer that = (MetricsContextCustomizer) o; return Objects.equals(this.annotation, that.annotation); } diff --git a/module/spring-boot-micrometer-metrics-test/src/test/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/GlobalMeterRegistryIntegrationTests.java b/module/spring-boot-micrometer-metrics-test/src/test/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/GlobalMeterRegistryIntegrationTests.java new file mode 100644 index 000000000000..485c50cfbd70 --- /dev/null +++ b/module/spring-boot-micrometer-metrics-test/src/test/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/GlobalMeterRegistryIntegrationTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.micrometer.metrics.test.autoconfigure; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MetricsContextCustomizerFactory} in a real Spring test + * context, verifying that auto-configured {@link MeterRegistry meter registries} are kept + * out of {@link Metrics#globalRegistry} by default and can be opted back in with + * {@link AutoConfigureMetrics @AutoConfigureMetrics} or the + * {@code management.metrics.use-global-registry} property. + * + * @author Moritz Halbritter + * @author Lordwill Kandiro + */ +class GlobalMeterRegistryIntegrationTests { + + @SpringJUnitConfig + @Import({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) + @DirtiesContext + static class WhenNotAnnotatedTests { + + @Autowired + private MeterRegistry meterRegistry; + + @Test + void meterRegistryIsNotAddedToTheGlobalRegistry() { + assertThat(Metrics.globalRegistry.getRegistries()).doesNotContain(this.meterRegistry); + } + + } + + @SpringJUnitConfig + @Import({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) + @AutoConfigureMetrics(useGlobalRegistry = true) + @DirtiesContext + static class WhenAnnotatedWithTrueUseGlobalRegistryAttributeTests { + + @Autowired + private MeterRegistry meterRegistry; + + @Test + void meterRegistryIsAddedToTheGlobalRegistry() { + assertThat(Metrics.globalRegistry.getRegistries()).contains(this.meterRegistry); + } + + } + + @SpringJUnitConfig + @Import({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) + @TestPropertySource(properties = "management.metrics.use-global-registry=true") + @DirtiesContext + static class WhenPropertyIsExplicitlyEnabledTests { + + @Autowired + private MeterRegistry meterRegistry; + + @Test + void meterRegistryIsAddedToTheGlobalRegistry() { + assertThat(Metrics.globalRegistry.getRegistries()).contains(this.meterRegistry); + } + + } + +} diff --git a/module/spring-boot-micrometer-metrics-test/src/test/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/MetricsContextCustomizerFactoryTests.java b/module/spring-boot-micrometer-metrics-test/src/test/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/MetricsContextCustomizerFactoryTests.java index 150d1019a7da..d367d0cee820 100644 --- a/module/spring-boot-micrometer-metrics-test/src/test/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/MetricsContextCustomizerFactoryTests.java +++ b/module/spring-boot-micrometer-metrics-test/src/test/java/org/springframework/boot/micrometer/metrics/test/autoconfigure/MetricsContextCustomizerFactoryTests.java @@ -74,6 +74,71 @@ void whenAnnotatedWithTrueExportAttributeMetricsExportIsEnabled() { assertThatMetricsAreEnabled(context); } + @Test + void whenNotAnnotatedGlobalRegistryIsDisabled() { + ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class); + ConfigurableApplicationContext context = new GenericApplicationContext(); + applyCustomizerToContext(customizer, context); + assertThatGlobalRegistryIsDisabled(context); + } + + @Test + void whenAnnotatedWithDefaultAttributeGlobalRegistryIsDisabled() { + ContextCustomizer customizer = createContextCustomizer(MetricsExportDefault.class); + ConfigurableApplicationContext context = new GenericApplicationContext(); + applyCustomizerToContext(customizer, context); + assertThatGlobalRegistryIsDisabled(context); + } + + @Test + void whenAnnotatedWithFalseUseGlobalRegistryAttributeGlobalRegistryIsDisabled() { + ContextCustomizer customizer = createContextCustomizer(UseGlobalRegistryDisabled.class); + ConfigurableApplicationContext context = new GenericApplicationContext(); + applyCustomizerToContext(customizer, context); + assertThatGlobalRegistryIsDisabled(context); + } + + @Test + void whenAnnotatedWithTrueUseGlobalRegistryAttributeGlobalRegistryIsEnabled() { + ContextCustomizer customizer = createContextCustomizer(UseGlobalRegistryEnabled.class); + ConfigurableApplicationContext context = new GenericApplicationContext(); + applyCustomizerToContext(customizer, context); + assertThatGlobalRegistryIsNotConfigured(context); + } + + @Test + void globalRegistryPropertySetToTrueIsNotOverridden() { + ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class); + ConfigurableApplicationContext context = new GenericApplicationContext(); + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("management.metrics.use-global-registry", "true"); + context.setEnvironment(environment); + applyCustomizerToContext(customizer, context); + assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")).isEqualTo("true"); + } + + @Test + void globalRegistryPropertySetToFalseIsNotOverridden() { + ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class); + ConfigurableApplicationContext context = new GenericApplicationContext(); + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("management.metrics.use-global-registry", "false"); + context.setEnvironment(environment); + applyCustomizerToContext(customizer, context); + assertThatGlobalRegistryIsDisabled(context); + } + + @Test + void annotationDoesNotTakePrecedenceOverGlobalRegistryProperty() { + ContextCustomizer customizer = createContextCustomizer(UseGlobalRegistryDisabled.class); + ConfigurableApplicationContext context = new GenericApplicationContext(); + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("management.metrics.use-global-registry", "true"); + context.setEnvironment(environment); + applyCustomizerToContext(customizer, context); + assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")).isEqualTo("true"); + } + @Test void notEquals() { ContextCustomizer customizer1 = createContextCustomizer(MetricsExportEnabled.class); @@ -81,6 +146,13 @@ void notEquals() { assertThat(customizer1).isNotEqualTo(customizer2); } + @Test + void notEqualsWhenUseGlobalRegistryDiffers() { + ContextCustomizer customizer1 = createContextCustomizer(UseGlobalRegistryEnabled.class); + ContextCustomizer customizer2 = createContextCustomizer(UseGlobalRegistryDisabled.class); + assertThat(customizer1).isNotEqualTo(customizer2); + } + @Test void equals() { ContextCustomizer customizer1 = createContextCustomizer(MetricsExportEnabled.class); @@ -155,6 +227,14 @@ private void assertThatMetricsAreEnabled(ConfigurableApplicationContext context) assertThat(context.getEnvironment().getProperty("management.simple.metrics.export.enabled")).isNull(); } + private void assertThatGlobalRegistryIsDisabled(ConfigurableApplicationContext context) { + assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")).isEqualTo("false"); + } + + private void assertThatGlobalRegistryIsNotConfigured(ConfigurableApplicationContext context) { + assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")).isNull(); + } + static class NoAnnotation { } @@ -174,4 +254,14 @@ static class MetricsExportEnabled { } + @AutoConfigureMetrics(useGlobalRegistry = false) + static class UseGlobalRegistryDisabled { + + } + + @AutoConfigureMetrics(useGlobalRegistry = true) + static class UseGlobalRegistryEnabled { + + } + } From b9fb384f8dac2b88736e196cb6c35fea3ee64429 Mon Sep 17 00:00:00 2001 From: Brian Clozel <brian.clozel@broadcom.com> Date: Wed, 29 Jul 2026 14:45:12 +0200 Subject: [PATCH 803/930] Update Forwarded headers support for Framework Prior to this commit, Spring Boot would auto-configure the `ForwardedHeaderFilter`/`ForwardedHeaderTransformer` when the "NATIVE" strategy is chosen. Spring Framework now requires an explicit choice between the supported HTTP header variants as of spring-projects/spring-framework#37072. This commit adapts to this new behavior with the following: * the "FRAMEWORK" strategy now only applies to Spring MVC and Spring WebFlux applications, since "NATIVE" strategies are now a good choice for most deployments. * the format of HTTP headers is now configured with `spring.mvc.forwarded-headers.header-format` and `spring.webflux.forwarded-headers.header-format`, with additional options. The default header format is now "X-Forwarded-*" for both NATIVE and FRAMEWORK strategies. The reference documentation also reflects those changes. Closes gh-51030 --- .../modules/how-to/pages/webserver.adoc | 39 +++++++--- ...mcatServletWebServerAutoConfiguration.java | 9 --- ...ervletWebServerAutoConfigurationTests.java | 27 ------- .../ReactiveWebServerConfiguration.java | 11 --- .../ForwardedHeaderFilterCustomizer.java | 3 + .../ServletWebServerConfiguration.java | 20 ----- ...activeWebServerAutoConfigurationTests.java | 34 --------- ...ervletWebServerAutoConfigurationTests.java | 36 --------- .../WebFluxAutoConfiguration.java | 15 ++++ .../autoconfigure/WebFluxProperties.java | 56 ++++++++++++++ .../WebFluxAutoConfigurationTests.java | 52 +++++++++++++ .../ForwardedHeaderFilterCustomizer.java | 36 +++++++++ .../WebMvcAutoConfiguration.java | 47 ++++++++++++ .../autoconfigure/WebMvcProperties.java | 56 ++++++++++++++ .../WebMvcAutoConfigurationTests.java | 75 +++++++++++++++++++ 15 files changed, 370 insertions(+), 146 deletions(-) create mode 100644 module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/ForwardedHeaderFilterCustomizer.java diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc index 17359b005c48..e61358d17c28 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc @@ -498,18 +498,36 @@ For more details, see the Jetty documentation. If your application is running behind a proxy, a load-balancer or in the cloud, the request information (like the host, port, scheme...) might change along the way. Your application may be running on `10.10.10.10:8080`, but HTTP clients should only see `example.org`. -https://tools.ietf.org/html/rfc7239[RFC7239 "Forwarded Headers"] defines the `Forwarded` HTTP header; proxies can use this header to provide information about the original request. -You can configure your application to read those headers and automatically use that information when creating links and sending them to clients in HTTP 302 responses, JSON documents or HTML pages. -There are also non-standard headers, like `X-Forwarded-Host`, `X-Forwarded-Port`, `X-Forwarded-Proto`, `X-Forwarded-Ssl`, and `X-Forwarded-Prefix`. +There are two sets of HTTP headers that intermediaries can use to provide information about the original request: -If the proxy adds the commonly used `X-Forwarded-For` and `X-Forwarded-Proto` headers, setting `server.forward-headers-strategy` to `NATIVE` is enough to support those. -With this option, the Web servers themselves natively support this feature; you can check their specific documentation to learn about specific behavior. +1. the well-known `"X-Forwarded-*"` headers (like `"X-Forwarded-Host"`, `"X-Forwarded-Port"`, `"X-Forwarded-Proto"`, `"X-Forwarded-For"`) +2. the `"Forwarded"` header, as defined by https://tools.ietf.org/html/rfc7239[RFC7239 "Forwarded Headers"] -If this is not enough, Spring Framework provides a {url-spring-framework-docs}/web/webmvc/filters.html#filters-forwarded-headers[ForwardedHeaderFilter] for the servlet stack and a {url-spring-framework-docs}/web/webflux/reactive-spring.html#webflux-forwarded-headers[ForwardedHeaderTransformer] for the reactive stack. -You can use them in your application by setting configprop:server.forward-headers-strategy[] to `FRAMEWORK`. +As a first step, application developers need to look up which set of headers is supported by their proxy, load-balancer or cloud platform. +While RFC7239 is a standard, its adoption is quite low in the industry and the well-known `"X-Forwarded-*"` headers are probably the ones you will need. +You can then configure your application to read those headers and automatically use that information when creating links and sending them to clients in HTTP 302 responses, JSON documents or HTML pages. -TIP: If you are using Tomcat and terminating SSL at the proxy, configprop:server.tomcat.redirect-context-root[] should be set to `false`. -This allows the `X-Forwarded-Proto` header to be honored before any redirects are performed. +If your web server of choice supports the set of HTTP headers you need, setting `server.forward-headers-strategy` to `NATIVE` is a good choice: + +|=== +| Server | Support | Also see + +| Tomcat +| `"X-Forwarded-*"` +| xref:how-to:webserver.adoc#howto.webserver.use-behind-a-proxy-server.tomcat[] + +| Jetty +| `"X-Forwarded-*"` +| + +| Reactor Netty +| `"X-Forwarded-*"` +| + +|=== + +If this is not enough, Spring Framework provides a {url-spring-framework-docs}/web/webmvc/filters.html#filters-forwarded-headers[ForwardedHeaderFilter] for Spring MVC apps and a {url-spring-framework-docs}/web/webflux/reactive-spring.html#webflux-forwarded-headers[ForwardedHeaderTransformer] for WebFlux apps. +You can use them in your application by setting configprop:server.forward-headers-strategy[] to `FRAMEWORK` and select the appropriate variant with configprop:spring.mvc.forwarded-headers.header-format[] or configprop:spring.webflux.forwarded-headers.header-format[]. NOTE: If your application runs javadoc:org.springframework.boot.cloud.CloudPlatform#enum-constant-summary[in a supported Cloud Platform], the configprop:server.forward-headers-strategy[] property defaults to `NATIVE`. In all other instances, it defaults to `NONE`. @@ -544,6 +562,9 @@ server: NOTE: You can trust all proxies by setting the `internal-proxies` to empty (but do not do so in production). +TIP: If you are using Tomcat and terminating SSL at the proxy, configprop:server.tomcat.redirect-context-root[] should be set to `false`. +This allows the `X-Forwarded-Proto` header to be honored before any redirects are performed. + You can take complete control of the configuration of Tomcat's javadoc:org.apache.catalina.valves.RemoteIpValve[] by switching the automatic one off (to do so, set `server.forward-headers-strategy=NONE`) and adding a new valve instance using a javadoc:org.springframework.boot.web.server.WebServerFactoryCustomizer[] bean. diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerAutoConfiguration.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerAutoConfiguration.java index 1b8ad7aef19d..31db9b912695 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerAutoConfiguration.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerAutoConfiguration.java @@ -25,7 +25,6 @@ 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.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.condition.SearchStrategy; @@ -36,8 +35,6 @@ import org.springframework.boot.tomcat.autoconfigure.TomcatServerProperties; import org.springframework.boot.tomcat.autoconfigure.TomcatWebServerConfiguration; import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory; -import org.springframework.boot.web.server.autoconfigure.ServerProperties; -import org.springframework.boot.web.server.autoconfigure.servlet.ForwardedHeaderFilterCustomizer; import org.springframework.boot.web.server.autoconfigure.servlet.ServletWebServerConfiguration; import org.springframework.boot.web.server.servlet.ServletWebServerFactory; import org.springframework.context.annotation.Bean; @@ -82,10 +79,4 @@ TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer( return new TomcatServletWebServerFactoryCustomizer(tomcatProperties); } - @Bean - @ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework") - ForwardedHeaderFilterCustomizer tomcatForwardedHeaderFilterCustomizer(ServerProperties serverProperties) { - return (filter) -> filter.setRelativeRedirects(this.tomcatProperties.isUseRelativeRedirects()); - } - } diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerAutoConfigurationTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerAutoConfigurationTests.java index d705934002c9..3a5784d0a36d 100644 --- a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerAutoConfigurationTests.java +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerAutoConfigurationTests.java @@ -16,7 +16,6 @@ package org.springframework.boot.tomcat.autoconfigure.servlet; -import jakarta.servlet.Filter; import org.apache.catalina.Context; import org.apache.catalina.connector.Connector; import org.junit.jupiter.api.Test; @@ -27,10 +26,8 @@ import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.server.autoconfigure.servlet.AbstractServletWebServerAutoConfigurationTests; -import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.web.filter.ForwardedHeaderFilter; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -120,30 +117,6 @@ void tomcatProtocolHandlerCustomizerRegisteredAsBeanAndViaFactoryIsOnlyCalledOnc }); } - @Test - void whenUsingFrameworkForwardHeadersStrategyAndRelativeRedirectsAreEnabledThenFilterIsConfiguredToUseRelativeRedirects() { - this.serverRunner - .withPropertyValues("server.forward-headers-strategy=framework", - "server.tomcat.use-relative-redirects=true", "server.port=0") - .run((context) -> { - Filter filter = context.getBean(FilterRegistrationBean.class).getFilter(); - assertThat(filter).isInstanceOf(ForwardedHeaderFilter.class); - assertThat(filter).extracting("relativeRedirects").isEqualTo(true); - }); - } - - @Test - void whenUsingFrameworkForwardHeadersStrategyAndNotUsingRelativeRedirectsThenFilterIsNotConfiguredToUseRelativeRedirects() { - this.serverRunner - .withPropertyValues("server.forward-headers-strategy=framework", - "server.tomcat.use-relative-redirects=false", "server.port=0") - .run((context) -> { - Filter filter = context.getBean(FilterRegistrationBean.class).getFilter(); - assertThat(filter).isInstanceOf(ForwardedHeaderFilter.class); - assertThat(filter).extracting("relativeRedirects").isEqualTo(false); - }); - } - @Configuration(proxyBeanMethods = false) static class TomcatConnectorCustomizerConfiguration { diff --git a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/reactive/ReactiveWebServerConfiguration.java b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/reactive/ReactiveWebServerConfiguration.java index 5cdfe79f60b4..ee6e822cda09 100644 --- a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/reactive/ReactiveWebServerConfiguration.java +++ b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/reactive/ReactiveWebServerConfiguration.java @@ -25,8 +25,6 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; -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.ssl.SslBundles; import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor; @@ -37,7 +35,6 @@ import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; import org.springframework.util.ObjectUtils; -import org.springframework.web.server.adapter.ForwardedHeaderTransformer; /** * {@link Configuration Configuration} for a reactive web server. @@ -57,14 +54,6 @@ ReactiveWebServerFactoryCustomizer reactiveWebServerFactoryCustomizer(ServerProp return new ReactiveWebServerFactoryCustomizer(serverProperties, sslBundles.getIfAvailable()); } - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework") - @SuppressWarnings("removal") // gh-51030 - ForwardedHeaderTransformer forwardedHeaderTransformer() { - return new ForwardedHeaderTransformer(); - } - /** * Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via * {@link ImportBeanDefinitionRegistrar} for early registration. diff --git a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/servlet/ForwardedHeaderFilterCustomizer.java b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/servlet/ForwardedHeaderFilterCustomizer.java index b8044a9e3e88..2570a4fbacf3 100644 --- a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/servlet/ForwardedHeaderFilterCustomizer.java +++ b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/servlet/ForwardedHeaderFilterCustomizer.java @@ -23,7 +23,10 @@ * * @author Andy Wilkinson * @since 4.0.0 + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of + * {@code org.springframework.boot.webmvc.autoconfigure.ForwardedHeaderFilterCustomizer}. */ +@Deprecated(since = "4.2.0", forRemoval = true) public interface ForwardedHeaderFilterCustomizer { /** diff --git a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/servlet/ServletWebServerConfiguration.java b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/servlet/ServletWebServerConfiguration.java index 240c366b9abe..434aa542cf7a 100644 --- a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/servlet/ServletWebServerConfiguration.java +++ b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/autoconfigure/servlet/ServletWebServerConfiguration.java @@ -16,7 +16,6 @@ package org.springframework.boot.web.server.autoconfigure.servlet; -import jakarta.servlet.DispatcherType; import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; @@ -26,8 +25,6 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingFilterBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.web.error.ErrorPageRegistrarBeanPostProcessor; @@ -35,15 +32,12 @@ import org.springframework.boot.web.server.autoconfigure.ServerProperties; import org.springframework.boot.web.server.servlet.CookieSameSiteSupplier; import org.springframework.boot.web.server.servlet.WebListenerRegistrar; -import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; -import org.springframework.core.Ordered; import org.springframework.core.type.AnnotationMetadata; import org.springframework.util.ObjectUtils; -import org.springframework.web.filter.ForwardedHeaderFilter; /** * {@link Configuration Configuration} for a servlet web server. @@ -69,20 +63,6 @@ ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProper cookieSameSiteSuppliers.orderedStream().toList(), sslBundles.getIfAvailable()); } - @Bean - @ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework") - @ConditionalOnMissingFilterBean(ForwardedHeaderFilter.class) - @SuppressWarnings("removal") // gh-51030 - FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter( - ObjectProvider<ForwardedHeaderFilterCustomizer> customizerProvider) { - ForwardedHeaderFilter filter = new ForwardedHeaderFilter(); - customizerProvider.ifAvailable((customizer) -> customizer.customize(filter)); - FilterRegistrationBean<ForwardedHeaderFilter> registration = new FilterRegistrationBean<>(filter); - registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR); - registration.setOrder(Ordered.HIGHEST_PRECEDENCE); - return registration; - } - /** * Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via * {@link ImportBeanDefinitionRegistrar} for early registration. diff --git a/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/reactive/AbstractReactiveWebServerAutoConfigurationTests.java b/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/reactive/AbstractReactiveWebServerAutoConfigurationTests.java index ef009c0c0443..ce0fe8a06285 100644 --- a/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/reactive/AbstractReactiveWebServerAutoConfigurationTests.java +++ b/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/reactive/AbstractReactiveWebServerAutoConfigurationTests.java @@ -32,7 +32,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.web.server.adapter.ForwardedHeaderTransformer; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -112,28 +111,6 @@ void webServerFailsWithInvalidSslBundle() { }); } - @Test - void forwardedHeaderTransformerShouldBeConfigured() { - this.mockServerRunner.withUserConfiguration(HttpHandlerConfiguration.class) - .withPropertyValues("server.forward-headers-strategy=framework", "server.port=0") - .run((context) -> assertThat(context).hasSingleBean(ForwardedHeaderTransformer.class)); - } - - @Test - void forwardedHeaderTransformerWhenStrategyNotFilterShouldNotBeConfigured() { - this.mockServerRunner.withUserConfiguration(HttpHandlerConfiguration.class) - .withPropertyValues("server.forward-headers-strategy=native", "server.port=0") - .run((context) -> assertThat(context).doesNotHaveBean(ForwardedHeaderTransformer.class)); - } - - @Test - void forwardedHeaderTransformerWhenAlreadyRegisteredShouldBackOff() { - this.mockServerRunner - .withUserConfiguration(ForwardedHeaderTransformerConfiguration.class, HttpHandlerConfiguration.class) - .withPropertyValues("server.forward-headers-strategy=framework", "server.port=0") - .run((context) -> assertThat(context).hasSingleBean(ForwardedHeaderTransformer.class)); - } - @Configuration(proxyBeanMethods = false) static class HttpHandlerConfiguration { @@ -174,15 +151,4 @@ MockReactiveWebServerFactory mockReactiveWebServerFactory() { } - @Configuration(proxyBeanMethods = false) - static class ForwardedHeaderTransformerConfiguration { - - @Bean - @SuppressWarnings("removal") // gh-51030 - ForwardedHeaderTransformer testForwardedHeaderTransformer() { - return new ForwardedHeaderTransformer(); - } - - } - } diff --git a/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/servlet/AbstractServletWebServerAutoConfigurationTests.java b/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/servlet/AbstractServletWebServerAutoConfigurationTests.java index 83698582c0e6..b7a3211825f6 100644 --- a/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/servlet/AbstractServletWebServerAutoConfigurationTests.java +++ b/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/autoconfigure/servlet/AbstractServletWebServerAutoConfigurationTests.java @@ -54,7 +54,6 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient.RequestHeadersSpec; -import org.springframework.web.filter.ForwardedHeaderFilter; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -115,29 +114,6 @@ void initParametersAreConfiguredOnTheServletContext() { }); } - @Test - void forwardedHeaderFilterShouldBeConfigured() { - this.mockServerRunner.withPropertyValues("server.forward-headers-strategy=framework").run((context) -> { - assertThat(context).hasSingleBean(FilterRegistrationBean.class); - Filter filter = context.getBean(FilterRegistrationBean.class).getFilter(); - assertThat(filter).isInstanceOf(ForwardedHeaderFilter.class); - assertThat(filter).extracting("relativeRedirects").isEqualTo(false); - }); - } - - @Test - void forwardedHeaderFilterWhenStrategyNotFilterShouldNotBeConfigured() { - this.mockServerRunner.withPropertyValues("server.forward-headers-strategy=native") - .run((context) -> assertThat(context).doesNotHaveBean(FilterRegistrationBean.class)); - } - - @Test - void forwardedHeaderFilterWhenFilterAlreadyRegisteredShouldBackOff() { - this.mockServerRunner.withUserConfiguration(ForwardedHeaderFilterConfiguration.class) - .withPropertyValues("server.forward-headers-strategy=framework") - .run((context) -> assertThat(context).hasSingleBean(FilterRegistrationBean.class)); - } - @Test void cookieSameSiteSuppliersAreApplied() { this.mockServerRunner.withUserConfiguration(CookieSameSiteSupplierConfiguration.class).run((context) -> { @@ -208,18 +184,6 @@ public Object postProcessAfterInitialization(Object bean, String beanName) { } - @Configuration(proxyBeanMethods = false) - static class ForwardedHeaderFilterConfiguration { - - @Bean - @SuppressWarnings("removal") // gh-51030 - FilterRegistrationBean<ForwardedHeaderFilter> testForwardedHeaderFilter() { - ForwardedHeaderFilter filter = new ForwardedHeaderFilter(); - return new FilterRegistrationBean<>(filter); - } - - } - @Configuration(proxyBeanMethods = false) static class CookieSameSiteSupplierConfiguration { diff --git a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java index 1546af244b03..1d3d3c387cbd 100644 --- a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java +++ b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java @@ -36,6 +36,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; @@ -57,6 +58,8 @@ import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Apiversion; import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Apiversion.Use; import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Format; +import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Forwardedheaders; +import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.HeaderFormat; import org.springframework.boot.webflux.filter.OrderedHiddenHttpMethodFilter; import org.springframework.context.ApplicationContext; import org.springframework.context.EmbeddedValueResolverAware; @@ -100,6 +103,7 @@ import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.WebSession; +import org.springframework.web.server.adapter.ForwardedHeaderTransformer; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; import org.springframework.web.server.i18n.FixedLocaleContextResolver; @@ -413,6 +417,17 @@ WebSessionManager webSessionManager(ObjectProvider<WebSessionIdResolver> webSess return webSessionManager; } + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework") + ForwardedHeaderTransformer forwardedHeaderTransformer() { + Forwardedheaders properties = this.webFluxProperties.getForwardedHeaders(); + ForwardedHeaderTransformer transformer = new ForwardedHeaderTransformer( + properties.getHeaderFormat() == HeaderFormat.STANDARD); + transformer.setUseForwardedPrefix(properties.isUseForwardedPrefix()); + return transformer; + } + @Override @ConditionalOnMissingBean(name = "webFluxApiVersionStrategy") public @Nullable ApiVersionStrategy webFluxApiVersionStrategy() { diff --git a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxProperties.java b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxProperties.java index aadd36b79ac4..66cec905654b 100644 --- a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxProperties.java +++ b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxProperties.java @@ -48,6 +48,8 @@ public class WebFluxProperties { private final Apiversion apiversion = new Apiversion(); + private final Forwardedheaders forwardedHeaders = new Forwardedheaders(); + /** * Path pattern used for static resources. */ @@ -99,6 +101,10 @@ public Apiversion getApiversion() { return this.apiversion; } + public Forwardedheaders getForwardedHeaders() { + return this.forwardedHeaders; + } + public String getStaticPathPattern() { return this.staticPathPattern; } @@ -308,4 +314,54 @@ public void setMediaTypeParameter(Map<MediaType, String> mediaTypeParameter) { } + /** + * Forwarded headers. + */ + public static class Forwardedheaders { + + /** + * Format of the forwarded headers to support. + */ + private HeaderFormat headerFormat = HeaderFormat.X_FORWARDED; + + /** + * Whether to use the "X-Forwarded-Prefix" header to determine the context path. + */ + private boolean useForwardedPrefix; + + public HeaderFormat getHeaderFormat() { + return this.headerFormat; + } + + public void setHeaderFormat(HeaderFormat headerFormat) { + this.headerFormat = headerFormat; + } + + public boolean isUseForwardedPrefix() { + return this.useForwardedPrefix; + } + + public void setUseForwardedPrefix(boolean useForwardedPrefix) { + this.useForwardedPrefix = useForwardedPrefix; + } + + } + + /** + * Formats of forwarded headers supported by {@link Forwardedheaders}. + */ + public enum HeaderFormat { + + /** + * Use the standard "Forwarded" header, as defined by RFC 7239. + */ + STANDARD, + + /** + * Use the non-standard "X-Forwarded-*" headers. + */ + X_FORWARDED + + } + } diff --git a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java index e4c2ad3ff1aa..1577037d558c 100644 --- a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java +++ b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java @@ -123,6 +123,7 @@ import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; +import org.springframework.web.server.adapter.ForwardedHeaderTransformer; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; import org.springframework.web.server.i18n.FixedLocaleContextResolver; @@ -474,6 +475,47 @@ void hiddenHttpMethodFilterCanBeEnabled() { .run((context) -> assertThat(context).hasSingleBean(OrderedHiddenHttpMethodFilter.class)); } + @Test + void forwardedHeaderTransformerIsNotConfiguredByDefault() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ForwardedHeaderTransformer.class)); + } + + @Test + void forwardedHeaderTransformerIsNotConfiguredWhenStrategyIsNotFramework() { + this.contextRunner.withPropertyValues("server.forward-headers-strategy=native") + .run((context) -> assertThat(context).doesNotHaveBean(ForwardedHeaderTransformer.class)); + } + + @Test + void forwardedHeaderTransformerIsConfiguredWhenFrameworkStrategyIsUsed() { + this.contextRunner.withPropertyValues("server.forward-headers-strategy=framework").run((context) -> { + assertThat(context).hasSingleBean(ForwardedHeaderTransformer.class); + ForwardedHeaderTransformer transformer = context.getBean(ForwardedHeaderTransformer.class); + assertThat(transformer).extracting("useStandardHeader").isEqualTo(false); + assertThat(transformer).extracting("useForwardedPrefix").isEqualTo(false); + }); + } + + @Test + void forwardedHeaderTransformerAppliesConfiguredProperties() { + this.contextRunner + .withPropertyValues("server.forward-headers-strategy=framework", + "spring.webflux.forwarded-headers.header-format=standard", + "spring.webflux.forwarded-headers.use-forwarded-prefix=true") + .run((context) -> { + ForwardedHeaderTransformer transformer = context.getBean(ForwardedHeaderTransformer.class); + assertThat(transformer).extracting("useStandardHeader").isEqualTo(true); + assertThat(transformer).extracting("useForwardedPrefix").isEqualTo(true); + }); + } + + @Test + void forwardedHeaderTransformerBacksOffWhenBeanAlreadyRegistered() { + this.contextRunner.withUserConfiguration(ForwardedHeaderTransformerConfiguration.class) + .withPropertyValues("server.forward-headers-strategy=framework") + .run((context) -> assertThat(context).hasSingleBean(ForwardedHeaderTransformer.class)); + } + @Test void customRequestMappingHandlerMapping() { this.contextRunner.withUserConfiguration(CustomRequestMappingHandlerMapping.class).run((context) -> { @@ -1144,6 +1186,16 @@ HiddenHttpMethodFilter customHiddenHttpMethodFilter() { } + @Configuration(proxyBeanMethods = false) + static class ForwardedHeaderTransformerConfiguration { + + @Bean + ForwardedHeaderTransformer testForwardedHeaderTransformer() { + return new ForwardedHeaderTransformer(false); + } + + } + @Configuration(proxyBeanMethods = false) static class CustomRequestMappingHandlerAdapter { diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/ForwardedHeaderFilterCustomizer.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/ForwardedHeaderFilterCustomizer.java new file mode 100644 index 000000000000..ca75129d19f5 --- /dev/null +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/ForwardedHeaderFilterCustomizer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.webmvc.autoconfigure; + +import org.springframework.web.filter.ForwardedHeaderFilter; + +/** + * Customizer for the auto-configured {@link ForwardedHeaderFilter}. + * + * @author Brian Clozel + * @since 4.2.0 + */ +@FunctionalInterface +public interface ForwardedHeaderFilterCustomizer { + + /** + * Customize the given {@link ForwardedHeaderFilter}. + * @param filter the filter to customize + */ + void customize(ForwardedHeaderFilter filter); + +} diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java index fbcb80ae2597..e42c021263e1 100644 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java @@ -22,6 +22,7 @@ import java.util.Map; import java.util.function.Consumer; +import jakarta.servlet.DispatcherType; import jakarta.servlet.Servlet; import jakarta.servlet.ServletContext; import org.apache.commons.logging.Log; @@ -43,6 +44,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingFilterBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; @@ -62,10 +64,13 @@ import org.springframework.boot.servlet.filter.OrderedHiddenHttpMethodFilter; import org.springframework.boot.servlet.filter.OrderedRequestContextFilter; import org.springframework.boot.validation.autoconfigure.ValidatorAdapter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Apiversion; import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Apiversion.Use; import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Format; +import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Forwardedheaders; +import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.HeaderFormat; import org.springframework.context.ApplicationContext; import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.ResourceLoaderAware; @@ -101,6 +106,7 @@ import org.springframework.web.context.request.RequestContextListener; import org.springframework.web.context.support.ServletContextResource; import org.springframework.web.filter.FormContentFilter; +import org.springframework.web.filter.ForwardedHeaderFilter; import org.springframework.web.filter.HiddenHttpMethodFilter; import org.springframework.web.filter.RequestContextFilter; import org.springframework.web.servlet.DispatcherServlet; @@ -405,6 +411,47 @@ static RequestContextFilter requestContextFilter() { return new OrderedRequestContextFilter(); } + @Bean + @ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework") + @ConditionalOnMissingFilterBean(ForwardedHeaderFilter.class) + FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter( + ObjectProvider<ForwardedHeaderFilterCustomizer> customizerProvider) { + Forwardedheaders properties = this.mvcProperties.getForwardedHeaders(); + ForwardedHeaderFilter filter = new ForwardedHeaderFilter( + properties.getHeaderFormat() == HeaderFormat.STANDARD); + filter.setUseForwardedPrefix(properties.isUseForwardedPrefix()); + customizerProvider.ifAvailable((customizer) -> customizer.customize(filter)); + FilterRegistrationBean<ForwardedHeaderFilter> registration = new FilterRegistrationBean<>(filter); + registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR); + registration.setOrder(Ordered.HIGHEST_PRECEDENCE); + return registration; + } + + } + + /** + * Adapts a deprecated {@code spring-boot-web-server} + * {@link org.springframework.boot.web.server.autoconfigure.servlet.ForwardedHeaderFilterCustomizer} + * bean to {@link ForwardedHeaderFilterCustomizer}. Defined as a separate nested + * config, guarded by {@link ConditionalOnClass}, so that referencing the deprecated + * type does not prevent reflection on other configuration classes when + * {@code spring-boot-web-server} is not on the classpath (as is the case for + * traditional WAR deployments). + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(org.springframework.boot.web.server.autoconfigure.servlet.ForwardedHeaderFilterCustomizer.class) + @SuppressWarnings("removal") + static class DeprecatedForwardedHeaderFilterCustomizerConfiguration { + + @Bean + @ConditionalOnBean(org.springframework.boot.web.server.autoconfigure.servlet.ForwardedHeaderFilterCustomizer.class) + @ConditionalOnMissingBean + @SuppressWarnings("removal") + ForwardedHeaderFilterCustomizer deprecatedForwardedHeaderFilterCustomizerAdapter( + org.springframework.boot.web.server.autoconfigure.servlet.ForwardedHeaderFilterCustomizer customizer) { + return customizer::customize; + } + } /** diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcProperties.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcProperties.java index aa7486556b09..1cf2583849db 100644 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcProperties.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcProperties.java @@ -104,6 +104,8 @@ public class WebMvcProperties { private final Apiversion apiversion = new Apiversion(); + private final Forwardedheaders forwardedHeaders = new Forwardedheaders(); + public DefaultMessageCodesResolver.@Nullable Format getMessageCodesResolverFormat() { return this.messageCodesResolverFormat; } @@ -201,6 +203,10 @@ public Apiversion getApiversion() { return this.apiversion; } + public Forwardedheaders getForwardedHeaders() { + return this.forwardedHeaders; + } + public static class Async { /** @@ -593,4 +599,54 @@ public void setMediaTypeParameter(Map<MediaType, String> mediaTypeParameter) { } + /** + * Forwarded headers. + */ + public static class Forwardedheaders { + + /** + * Format of the forwarded headers to support. + */ + private HeaderFormat headerFormat = HeaderFormat.X_FORWARDED; + + /** + * Whether to use the "X-Forwarded-Prefix" header to determine the context path. + */ + private boolean useForwardedPrefix; + + public HeaderFormat getHeaderFormat() { + return this.headerFormat; + } + + public void setHeaderFormat(HeaderFormat headerFormat) { + this.headerFormat = headerFormat; + } + + public boolean isUseForwardedPrefix() { + return this.useForwardedPrefix; + } + + public void setUseForwardedPrefix(boolean useForwardedPrefix) { + this.useForwardedPrefix = useForwardedPrefix; + } + + } + + /** + * Formats of forwarded headers supported by {@link Forwardedheaders}. + */ + public enum HeaderFormat { + + /** + * Use the standard "Forwarded" header, as defined by RFC 7239. + */ + STANDARD, + + /** + * Use the non-standard "X-Forwarded-*" headers. + */ + X_FORWARDED + + } + } diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java index 4d9661126a16..dfc8cd5b1c87 100644 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java @@ -33,6 +33,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import jakarta.servlet.Filter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.ValidatorFactory; @@ -114,6 +115,7 @@ import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.FormContentFilter; +import org.springframework.web.filter.ForwardedHeaderFilter; import org.springframework.web.filter.HiddenHttpMethodFilter; import org.springframework.web.filter.RequestContextFilter; import org.springframework.web.method.ControllerAdviceBean; @@ -661,6 +663,58 @@ void hiddenHttpMethodFilterDisabledByDefault() { this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(HiddenHttpMethodFilter.class)); } + @Test + void forwardedHeaderFilterIsNotConfiguredByDefault() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(FilterRegistrationBean.class)); + } + + @Test + void forwardedHeaderFilterIsNotConfiguredWhenStrategyIsNotFramework() { + this.contextRunner.withPropertyValues("server.forward-headers-strategy=native") + .run((context) -> assertThat(context).doesNotHaveBean(FilterRegistrationBean.class)); + } + + @Test + void forwardedHeaderFilterIsConfiguredWhenFrameworkStrategyIsUsed() { + this.contextRunner.withPropertyValues("server.forward-headers-strategy=framework").run((context) -> { + assertThat(context).hasSingleBean(FilterRegistrationBean.class); + Filter filter = context.getBean(FilterRegistrationBean.class).getFilter(); + assertThat(filter).isInstanceOf(ForwardedHeaderFilter.class); + assertThat(filter).extracting("useStandardHeader").isEqualTo(false); + assertThat(filter).extracting("useForwardedPrefix").isEqualTo(false); + }); + } + + @Test + void forwardedHeaderFilterAppliesConfiguredProperties() { + this.contextRunner + .withPropertyValues("server.forward-headers-strategy=framework", + "spring.mvc.forwarded-headers.header-format=standard", + "spring.mvc.forwarded-headers.use-forwarded-prefix=true") + .run((context) -> { + Filter filter = context.getBean(FilterRegistrationBean.class).getFilter(); + assertThat(filter).extracting("useStandardHeader").isEqualTo(true); + assertThat(filter).extracting("useForwardedPrefix").isEqualTo(true); + }); + } + + @Test + void forwardedHeaderFilterBacksOffWhenFilterBeanAlreadyRegistered() { + this.contextRunner.withUserConfiguration(ForwardedHeaderFilterConfiguration.class) + .withPropertyValues("server.forward-headers-strategy=framework") + .run((context) -> assertThat(context).hasSingleBean(FilterRegistrationBean.class)); + } + + @Test + void forwardedHeaderFilterCustomizerFromDeprecatedTypeIsApplied() { + this.contextRunner.withUserConfiguration(ForwardedHeaderFilterCustomizerConfiguration.class) + .withPropertyValues("server.forward-headers-strategy=framework") + .run((context) -> { + Filter filter = context.getBean(FilterRegistrationBean.class).getFilter(); + assertThat(filter).extracting("removeOnly").isEqualTo(true); + }); + } + @Test void customConfigurableWebBindingInitializer() { this.contextRunner.withUserConfiguration(CustomConfigurableWebBindingInitializer.class) @@ -1345,6 +1399,27 @@ FormContentFilter customFormContentFilter() { } + @Configuration(proxyBeanMethods = false) + static class ForwardedHeaderFilterConfiguration { + + @Bean + FilterRegistrationBean<ForwardedHeaderFilter> testForwardedHeaderFilter() { + return new FilterRegistrationBean<>(new ForwardedHeaderFilter(false)); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ForwardedHeaderFilterCustomizerConfiguration { + + @Bean + @SuppressWarnings("removal") + org.springframework.boot.web.server.autoconfigure.servlet.ForwardedHeaderFilterCustomizer forwardedHeaderFilterCustomizer() { + return (filter) -> filter.setRemoveOnly(true); + } + + } + @Configuration(proxyBeanMethods = false) static class CustomRequestMappingHandlerMapping { From 941045a2f4db88207f3475e25753a58cf50e3943 Mon Sep 17 00:00:00 2001 From: Brian Clozel <brian.clozel@broadcom.com> Date: Thu, 30 Jul 2026 15:12:48 +0200 Subject: [PATCH 804/930] Refine Forwarded headers configuration for Jetty servers This commit introduces new properties for configuring the HTTP headers that should be used for forward support. The new `server.jetty.forwarded-headers.header-format` property allows "standard" or "x_forwarded" to select "Forwarded" or "X-Forwarded-*" headers. This aligns the behavior of Jetty server with the "FRAMEWORK" strategy. Closes gh-51148 --- .../modules/how-to/pages/webserver.adoc | 22 ++++++- .../ConfigurableJettyWebServerFactory.java | 9 +++ .../boot/jetty/ForwardHeadersCustomizer.java | 27 ++++++++ .../boot/jetty/JettyWebServerFactory.java | 11 ++++ .../autoconfigure/JettyServerProperties.java | 46 +++++++++++++ .../JettyWebServerFactoryCustomizer.java | 11 +++- .../JettyReactiveWebServerFactory.java | 5 +- .../servlet/JettyServletWebServerFactory.java | 5 +- .../jetty/ForwardHeadersCustomizerTests.java | 66 +++++++++++++++++++ .../JettyServerPropertiesTests.java | 13 ++++ .../JettyWebServerFactoryCustomizerTests.java | 28 +++++++- .../JettyReactiveWebServerFactoryTests.java | 17 +++++ .../JettyServletWebServerFactoryTests.java | 16 +++++ ...AbstractReactiveWebServerFactoryTests.java | 12 ++++ .../AbstractServletWebServerFactoryTests.java | 8 +++ 15 files changed, 289 insertions(+), 7 deletions(-) create mode 100644 module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/ForwardHeadersCustomizerTests.java diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc index e61358d17c28..4536cd277665 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc @@ -517,8 +517,8 @@ If your web server of choice supports the set of HTTP headers you need, setting | xref:how-to:webserver.adoc#howto.webserver.use-behind-a-proxy-server.tomcat[] | Jetty -| `"X-Forwarded-*"` -| +| `"X-Forwarded-*"`, `"Forwarded"` +| xref:how-to:webserver.adoc#howto.webserver.use-behind-a-proxy-server.jetty[] | Reactor Netty | `"X-Forwarded-*"` @@ -569,6 +569,24 @@ You can take complete control of the configuration of Tomcat's javadoc:org.apach +[[howto.webserver.use-behind-a-proxy-server.jetty]] +=== Customize Jetty's Proxy Configuration + +If you use Jetty, you can additionally configure the type of the headers used to carry "`forwarded`" information. +By default, the `"X-Forwarded-*"` format is used, but you can opt into the standard RFC variant: + +[configprops,yaml] +---- +server: + jetty: + forwarded-headers: + header-format: "standard" +---- + +For more specific options, you can switch off the forwarded configuration (using `server.forward-headers-strategy=NONE`) and directly use Jetty's javadoc:org.eclipse.jetty.server.ForwardedRequestCustomizer[] to change the HTTP configuration. + + + [[howto.webserver.enable-multiple-connectors]] == Enable Multiple Connectors diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/ConfigurableJettyWebServerFactory.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/ConfigurableJettyWebServerFactory.java index 4e794b0ed94b..804f9fe02761 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/ConfigurableJettyWebServerFactory.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/ConfigurableJettyWebServerFactory.java @@ -56,6 +56,15 @@ public interface ConfigurableJettyWebServerFactory extends ConfigurableWebServer */ void setUseForwardHeaders(boolean useForwardHeaders); + /** + * Set if the RFC forwarded header should be processed. + * <p> + * {@link #setUseForwardHeaders(boolean)} will take precedence if enabled. + * @param useRfcForwardHeader if forwarded header should be used + * @since 4.2.0 + */ + void setUseRfcForwardHeader(boolean useRfcForwardHeader); + /** * Add {@link JettyServerCustomizer}s that will be applied to the {@link Server} * before it is started. diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/ForwardHeadersCustomizer.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/ForwardHeadersCustomizer.java index c1394ddcd061..2016719d0285 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/ForwardHeadersCustomizer.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/ForwardHeadersCustomizer.java @@ -30,9 +30,36 @@ */ public class ForwardHeadersCustomizer implements JettyServerCustomizer { + private final boolean useXForwarded; + + /** + * Create a new customizer instance. + * @param useXForwarded "X-Forwarded-*" headers are used if {@code true}, otherwise + * standard "Forwarded" are used instead. + * @since 4.2.0 + */ + public ForwardHeadersCustomizer(boolean useXForwarded) { + this.useXForwarded = useXForwarded; + } + + /** + * Create a new customizer instance with "X-Forwarded-*" support. + */ + public ForwardHeadersCustomizer() { + this(true); + } + @Override public void customize(Server server) { ForwardedRequestCustomizer customizer = new ForwardedRequestCustomizer(); + if (this.useXForwarded) { + // disable "Forwarded" support + customizer.setForwardedHeader(null); + } + else { + // disable "X-Forwarded-*" support + customizer.setForwardedOnly(true); + } for (Connector connector : server.getConnectors()) { for (ConnectionFactory connectionFactory : connector.getConnectionFactories()) { if (connectionFactory instanceof HttpConfiguration.ConnectionFactory jettyConnectionFactory) { diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyWebServerFactory.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyWebServerFactory.java index 13c569f995da..37829f4afc87 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyWebServerFactory.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyWebServerFactory.java @@ -64,6 +64,8 @@ public class JettyWebServerFactory extends AbstractConfigurableWebServerFactory private boolean useForwardHeaders; + private boolean useRfcForwardHeader; + private Set<JettyServerCustomizer> jettyServerCustomizers = new LinkedHashSet<>(); private int maxConnections = -1; @@ -180,6 +182,15 @@ public void setUseForwardHeaders(boolean useForwardHeaders) { this.useForwardHeaders = useForwardHeaders; } + public boolean isUseRfcForwardHeader() { + return this.useRfcForwardHeader; + } + + @Override + public void setUseRfcForwardHeader(boolean useRfcForwardHeader) { + this.useRfcForwardHeader = useRfcForwardHeader; + } + protected AbstractConnector createConnector(InetSocketAddress address, Server server) { return this.createConnector(address, server, null, null, null); } diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/autoconfigure/JettyServerProperties.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/autoconfigure/JettyServerProperties.java index cf6ab922c919..dba867211210 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/autoconfigure/JettyServerProperties.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/autoconfigure/JettyServerProperties.java @@ -86,6 +86,11 @@ public class JettyServerProperties { */ private final Accesslog accesslog = new Accesslog(); + /** + * Forwarded headers configuration. + */ + private final Forwardedheaders forwardedHeaders = new Forwardedheaders(); + /** * Thread related configuration. */ @@ -95,6 +100,10 @@ public Accesslog getAccesslog() { return this.accesslog; } + public Forwardedheaders getForwardedHeaders() { + return this.forwardedHeaders; + } + public Threads getThreads() { return this.threads; } @@ -268,6 +277,43 @@ public enum Format { } + /** + * Forwarded headers. + */ + public static class Forwardedheaders { + + /** + * Format of the forwarded headers to support. + */ + private HeaderFormat headerFormat = HeaderFormat.X_FORWARDED; + + public HeaderFormat getHeaderFormat() { + return this.headerFormat; + } + + public void setHeaderFormat(HeaderFormat headerFormat) { + this.headerFormat = headerFormat; + } + + /** + * Formats of forwarded headers supported by {@link Forwardedheaders}. + */ + public enum HeaderFormat { + + /** + * Use the standard "Forwarded" header, as defined by RFC 7239. + */ + STANDARD, + + /** + * Use the non-standard "X-Forwarded-*" headers. + */ + X_FORWARDED + + } + + } + /** * Jetty thread properties. */ diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/autoconfigure/JettyWebServerFactoryCustomizer.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/autoconfigure/JettyWebServerFactoryCustomizer.java index 8088cd354811..7e4e6127ca9c 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/autoconfigure/JettyWebServerFactoryCustomizer.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/autoconfigure/JettyWebServerFactoryCustomizer.java @@ -79,7 +79,9 @@ public int getOrder() { @Override public void customize(ConfigurableJettyWebServerFactory factory) { - factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders()); + if (getOrDeduceUseForwardHeaders()) { + configureForwardedSupport(factory); + } JettyServerProperties.Threads threadProperties = this.jettyProperties.getThreads(); factory.setThreadPool(JettyThreadPool.create(this.jettyProperties.getThreads())); PropertyMapper map = PropertyMapper.get(); @@ -121,6 +123,13 @@ private boolean getOrDeduceUseForwardHeaders() { return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE); } + private void configureForwardedSupport(ConfigurableJettyWebServerFactory factory) { + switch (this.jettyProperties.getForwardedHeaders().getHeaderFormat()) { + case X_FORWARDED -> factory.setUseForwardHeaders(true); + case STANDARD -> factory.setUseRfcForwardHeader(true); + } + } + private <T> Consumer<T> customizeHttpConfigurations(ConfigurableJettyWebServerFactory factory, BiConsumer<HttpConfiguration, T> action) { return customizeConnectionFactories(factory, HttpConfiguration.ConnectionFactory.class, diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/reactive/JettyReactiveWebServerFactory.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/reactive/JettyReactiveWebServerFactory.java index 82ee17f02c30..5018d4e7d7fe 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/reactive/JettyReactiveWebServerFactory.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/reactive/JettyReactiveWebServerFactory.java @@ -118,7 +118,10 @@ protected Server createJettyServer(ServletHttpHandlerAdapter servlet) { customizer.customize(server); } if (this.isUseForwardHeaders()) { - new ForwardHeadersCustomizer().customize(server); + new ForwardHeadersCustomizer(true).customize(server); + } + else if (this.isUseRfcForwardHeader()) { + new ForwardHeadersCustomizer(false).customize(server); } if (getShutdown() == Shutdown.GRACEFUL) { GracefulHandler gracefulHandler = new GracefulHandler(); diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java index 0dd9605395e0..4a06baccb967 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java @@ -177,7 +177,10 @@ public WebServer getWebServer(ServletContextInitializer... initializers) { customizer.customize(server); } if (this.isUseForwardHeaders()) { - new ForwardHeadersCustomizer().customize(server); + new ForwardHeadersCustomizer(true).customize(server); + } + else if (this.isUseRfcForwardHeader()) { + new ForwardHeadersCustomizer(false).customize(server); } if (getShutdown() == Shutdown.GRACEFUL) { GracefulHandler gracefulHandler = new GracefulHandler(); diff --git a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/ForwardHeadersCustomizerTests.java b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/ForwardHeadersCustomizerTests.java new file mode 100644 index 000000000000..4438eff24129 --- /dev/null +++ b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/ForwardHeadersCustomizerTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jetty; + +import org.eclipse.jetty.server.ForwardedRequestCustomizer; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Server; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ForwardHeadersCustomizer}. + * + * @author Brian Clozel + */ +class ForwardHeadersCustomizerTests { + + @Test + void defaultConstructorUsesXForwardedHeaders() { + ForwardedRequestCustomizer customizer = customize(new ForwardHeadersCustomizer()); + assertThat(customizer.getForwardedHeader()).isNull(); + assertThat(customizer.getForwardedForHeader()).isNotNull(); + } + + @Test + void useXForwardedTrueEnablesXForwardedAndDisablesForwarded() { + ForwardedRequestCustomizer customizer = customize(new ForwardHeadersCustomizer(true)); + assertThat(customizer.getForwardedHeader()).isNull(); + assertThat(customizer.getForwardedForHeader()).isNotNull(); + } + + @Test + void useXForwardedFalseEnablesForwardedAndDisablesXForwarded() { + ForwardedRequestCustomizer customizer = customize(new ForwardHeadersCustomizer(false)); + assertThat(customizer.getForwardedHeader()).isNotNull(); + assertThat(customizer.getForwardedForHeader()).isNull(); + } + + private ForwardedRequestCustomizer customize(ForwardHeadersCustomizer customizer) { + Server server = new Server(0); + customizer.customize(server); + HttpConfiguration httpConfiguration = ((HttpConfiguration.ConnectionFactory) server.getConnectors()[0] + .getConnectionFactories() + .stream() + .filter(HttpConfiguration.ConnectionFactory.class::isInstance) + .findFirst() + .orElseThrow()).getHttpConfiguration(); + return httpConfiguration.getCustomizer(ForwardedRequestCustomizer.class); + } + +} diff --git a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/JettyServerPropertiesTests.java b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/JettyServerPropertiesTests.java index 4793e7585883..dccbdb36f089 100644 --- a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/JettyServerPropertiesTests.java +++ b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/JettyServerPropertiesTests.java @@ -100,6 +100,19 @@ void testCustomizeJettyAccessLog() { assertThat(this.properties.getAccesslog().getIgnorePaths()).containsExactly("/a/path", "/b/path"); } + @Test + void forwardedHeadersDefaultToXForwarded() { + assertThat(this.properties.getForwardedHeaders().getHeaderFormat()) + .isEqualTo(JettyServerProperties.Forwardedheaders.HeaderFormat.X_FORWARDED); + } + + @Test + void testCustomizeJettyForwardedHeadersFormat() { + bind("server.jetty.forwarded-headers.header-format", "standard"); + assertThat(this.properties.getForwardedHeaders().getHeaderFormat()) + .isEqualTo(JettyServerProperties.Forwardedheaders.HeaderFormat.STANDARD); + } + @Test void jettyThreadPoolPropertyDefaultsShouldMatchServerDefault() { JettyServletWebServerFactory jettyFactory = new JettyServletWebServerFactory(0); diff --git a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/JettyWebServerFactoryCustomizerTests.java b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/JettyWebServerFactoryCustomizerTests.java index 14a9c85a6146..79fbb7d5f60a 100644 --- a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/JettyWebServerFactoryCustomizerTests.java +++ b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/autoconfigure/JettyWebServerFactoryCustomizerTests.java @@ -54,8 +54,10 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; /** * Tests for {@link JettyWebServerFactoryCustomizer}. @@ -94,7 +96,8 @@ void deduceUseForwardHeaders() { void defaultUseForwardHeaders() { ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class); this.customizer.customize(factory); - then(factory).should().setUseForwardHeaders(false); + then(factory).should(never()).setUseForwardHeaders(anyBoolean()); + then(factory).should(never()).setUseRfcForwardHeader(anyBoolean()); } @Test @@ -111,7 +114,28 @@ void forwardHeadersWhenStrategyIsNoneShouldNotConfigureValve() { this.serverProperties.setForwardHeadersStrategy(ServerProperties.ForwardHeadersStrategy.NONE); ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class); this.customizer.customize(factory); - then(factory).should().setUseForwardHeaders(false); + then(factory).should(never()).setUseForwardHeaders(anyBoolean()); + then(factory).should(never()).setUseRfcForwardHeader(anyBoolean()); + } + + @Test + void forwardedHeadersFormatXForwardedConfiguresUseForwardHeaders() { + this.environment.setProperty("DYNO", "-"); + bind("server.jetty.forwarded-headers.header-format=x_forwarded"); + ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class); + this.customizer.customize(factory); + then(factory).should().setUseForwardHeaders(true); + then(factory).should(never()).setUseRfcForwardHeader(anyBoolean()); + } + + @Test + void forwardedHeadersFormatStandardConfiguresUseRfcForwardHeader() { + this.environment.setProperty("DYNO", "-"); + bind("server.jetty.forwarded-headers.header-format=standard"); + ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class); + this.customizer.customize(factory); + then(factory).should().setUseRfcForwardHeader(true); + then(factory).should(never()).setUseForwardHeaders(anyBoolean()); } @Test diff --git a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/reactive/JettyReactiveWebServerFactoryTests.java b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/reactive/JettyReactiveWebServerFactoryTests.java index 3948dc53dcd6..ecc431ef87db 100644 --- a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/reactive/JettyReactiveWebServerFactoryTests.java +++ b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/reactive/JettyReactiveWebServerFactoryTests.java @@ -43,6 +43,7 @@ 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.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.ArgumentMatchers.any; @@ -121,6 +122,22 @@ void useForwardedHeaders() { assertForwardHeaderIsUsed(factory); } + @Test + void useRfcForwardHeader() { + JettyReactiveWebServerFactory factory = getFactory(); + factory.setUseRfcForwardHeader(true); + assertRfcForwardHeaderIsUsed(factory); + } + + @Test + void useForwardHeadersTakesPrecedenceOverUseRfcForwardHeader() { + JettyReactiveWebServerFactory factory = getFactory(); + factory.setUseForwardHeaders(true); + factory.setUseRfcForwardHeader(true); + assertForwardHeaderIsUsed(factory); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertRfcForwardHeaderIsUsed(factory)); + } + @Test void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() { JettyReactiveWebServerFactory factory = getFactory(); diff --git a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactoryTests.java b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactoryTests.java index 7fe513d3253b..57a3ff7f76b7 100644 --- a/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactoryTests.java +++ b/module/spring-boot-jetty/src/test/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactoryTests.java @@ -442,6 +442,22 @@ void useForwardHeaders() throws Exception { assertForwardHeaderIsUsed(factory); } + @Test + void useRfcForwardHeader() throws Exception { + JettyServletWebServerFactory factory = getFactory(); + factory.setUseRfcForwardHeader(true); + assertRfcForwardHeaderIsUsed(factory); + } + + @Test + void useForwardHeadersTakesPrecedenceOverUseRfcForwardHeader() throws Exception { + JettyServletWebServerFactory factory = getFactory(); + factory.setUseForwardHeaders(true); + factory.setUseRfcForwardHeader(true); + assertForwardHeaderIsUsed(factory); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertRfcForwardHeaderIsUsed(factory)); + } + @Test void defaultThreadPool() { JettyServletWebServerFactory factory = getFactory(); diff --git a/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/reactive/AbstractReactiveWebServerFactoryTests.java b/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/reactive/AbstractReactiveWebServerFactoryTests.java index ff6a636ce8b9..47ed49cd5b8e 100644 --- a/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/reactive/AbstractReactiveWebServerFactoryTests.java +++ b/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/reactive/AbstractReactiveWebServerFactoryTests.java @@ -677,6 +677,18 @@ protected void assertForwardHeaderIsUsed(ConfigurableReactiveWebServerFactory fa assertThat(body).isEqualTo("https"); } + protected void assertRfcForwardHeaderIsUsed(ConfigurableReactiveWebServerFactory factory) { + this.webServer = factory.getWebServer(new XForwardedHandler()); + this.webServer.start(); + String body = getWebClient(this.webServer.getPort()).build() + .get() + .header("Forwarded", "proto=https") + .retrieve() + .bodyToMono(String.class) + .block(Duration.ofSeconds(30)); + assertThat(body).isEqualTo("https"); + } + private <T> T doWithRetry(Callable<T> action) throws Exception { Exception lastFailure = null; for (int i = 0; i < 10; i++) { diff --git a/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/servlet/AbstractServletWebServerFactoryTests.java b/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/servlet/AbstractServletWebServerFactoryTests.java index c841c7de8876..78d6f0f8f5e3 100644 --- a/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/servlet/AbstractServletWebServerFactoryTests.java +++ b/module/spring-boot-web-server/src/testFixtures/java/org/springframework/boot/web/server/servlet/AbstractServletWebServerFactoryTests.java @@ -1617,6 +1617,14 @@ protected void assertForwardHeaderIsUsed(ServletWebServerFactory factory) throws .contains("remoteaddr=140.211.11.130"); } + protected void assertRfcForwardHeaderIsUsed(ServletWebServerFactory factory) + throws IOException, URISyntaxException { + this.webServer = factory.getWebServer(new ServletRegistrationBean<>(new ExampleServlet(true, false), "/hello")); + this.webServer.start(); + assertThat(getResponse(getLocalUrl("/hello"), "Forwarded:for=140.211.11.130")) + .contains("remoteaddr=140.211.11.130"); + } + protected abstract ConfigurableServletWebServerFactory getFactory(); protected abstract org.apache.jasper.servlet.@Nullable JspServlet getJspServlet() throws Exception; From 53099aec33067857c619b29910b52891babc1dfe Mon Sep 17 00:00:00 2001 From: Phillip Webb <phil.webb@broadcom.com> Date: Fri, 31 Jul 2026 09:16:13 +0100 Subject: [PATCH 805/930] Temporarily disable test caching to test CI --- build-plugin/spring-boot-gradle-plugin/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build-plugin/spring-boot-gradle-plugin/build.gradle b/build-plugin/spring-boot-gradle-plugin/build.gradle index 2e37a17007ee..67c82d6cf3e0 100644 --- a/build-plugin/spring-boot-gradle-plugin/build.gradle +++ b/build-plugin/spring-boot-gradle-plugin/build.gradle @@ -171,3 +171,8 @@ tasks.named("compileTestJava") { tasks.named("compileDockerTestJava") { options.nullability.checking = "tests" } + +tasks.named("test") { + outputs.upToDateWhen { false } + outputs.cacheIf { false } +} From a26ff992962ef576ca83e7024b81759db4cb2a53 Mon Sep 17 00:00:00 2001 From: Phillip Webb <phil.webb@broadcom.com> Date: Fri, 31 Jul 2026 09:35:50 +0100 Subject: [PATCH 806/930] Revert "Temporarily disable test caching to test CI" This reverts commit 53099aec33067857c619b29910b52891babc1dfe. --- build-plugin/spring-boot-gradle-plugin/build.gradle | 5 ----- 1 file changed, 5 deletions(-) diff --git a/build-plugin/spring-boot-gradle-plugin/build.gradle b/build-plugin/spring-boot-gradle-plugin/build.gradle index 67c82d6cf3e0..2e37a17007ee 100644 --- a/build-plugin/spring-boot-gradle-plugin/build.gradle +++ b/build-plugin/spring-boot-gradle-plugin/build.gradle @@ -171,8 +171,3 @@ tasks.named("compileTestJava") { tasks.named("compileDockerTestJava") { options.nullability.checking = "tests" } - -tasks.named("test") { - outputs.upToDateWhen { false } - outputs.cacheIf { false } -} From 607fc5cd544138a930e8a0065daaeda6418d0ed1 Mon Sep 17 00:00:00 2001 From: Brian Clozel <brian.clozel@broadcom.com> Date: Fri, 31 Jul 2026 14:01:28 +0200 Subject: [PATCH 807/930] Start building against Reactor 2026.0.0-SNAPSHOT See gh-51167 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7bf29d873286..e9b20a9b57ff 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2108,7 +2108,7 @@ bom { ] } } - library("Reactor Bom", "2025.0.6") { + library("Reactor Bom", "2026.0.0-SNAPSHOT") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From 66a093dbfa8129d717604cb0e2ad5947ec52863e Mon Sep 17 00:00:00 2001 From: Brian Clozel <brian.clozel@broadcom.com> Date: Fri, 31 Jul 2026 14:10:21 +0200 Subject: [PATCH 808/930] Refine Forwarded headers configuration for Reactor servers This commit introduces new properties for configuring the HTTP headers that should be used for forward support. The new `server.netty.forwarded-headers.header-format` property allows "standard" or "x_forwarded" to select "Forwarded" or "X-Forwarded-*" headers. This aligns the behavior of Netty server with the "FRAMEWORK" strategy. Closes gh-51031 --- .../modules/how-to/pages/webserver.adoc | 22 ++++++++- .../netty/NettyReactiveWebServerFactory.java | 25 +++++++++- ...ttyReactiveWebServerFactoryCustomizer.java | 11 ++++- .../autoconfigure/NettyServerProperties.java | 46 +++++++++++++++++++ .../NettyReactiveWebServerFactoryTests.java | 16 +++++++ ...activeWebServerFactoryCustomizerTests.java | 29 +++++++++++- .../NettyServerPropertiesTests.java | 13 ++++++ 7 files changed, 156 insertions(+), 6 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc index 4536cd277665..b17e9b697f4c 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc @@ -521,8 +521,8 @@ If your web server of choice supports the set of HTTP headers you need, setting | xref:how-to:webserver.adoc#howto.webserver.use-behind-a-proxy-server.jetty[] | Reactor Netty -| `"X-Forwarded-*"` -| +| `"X-Forwarded-*"`, `"Forwarded"` +| xref:how-to:webserver.adoc#howto.webserver.use-behind-a-proxy-server.reactor-netty[] |=== @@ -587,6 +587,24 @@ For more specific options, you can switch off the forwarded configuration (using +[[howto.webserver.use-behind-a-proxy-server.reactor-netty]] +=== Customize Reactor Netty's Proxy Configuration + +If you use Reactor Netty, you can additionally configure the type of the headers used to carry "`forwarded`" information. +By default, the `"X-Forwarded-*"` format is used, but you can opt into the standard RFC variant: + +[configprops,yaml] +---- +server: + netty: + forwarded-headers: + header-format: "standard" +---- + +For more specific options, you can switch off the forwarded configuration (using `server.forward-headers-strategy=NONE`) and directly use `reactor.netty.http.server.HttpServer` forwarded header support to change the HTTP configuration. + + + [[howto.webserver.enable-multiple-connectors]] == Enable Multiple Connectors diff --git a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactory.java b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactory.java index 515263e1fcb2..f3341f103d26 100644 --- a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactory.java +++ b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactory.java @@ -59,6 +59,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact private boolean useForwardHeaders; + private boolean useRfcForwardHeader; + private @Nullable ReactorResourceFactory resourceFactory; public NettyReactiveWebServerFactory() { @@ -139,6 +141,17 @@ public void setUseForwardHeaders(boolean useForwardHeaders) { this.useForwardHeaders = useForwardHeaders; } + /** + * Set if the RFC "Forwarded" header should be processed. + * <p> + * {@link #setUseForwardHeaders(boolean)} will take precedence if enabled. + * @param useRfcForwardHeader if the "Forwarded" header should be used + * @since 4.2.0 + */ + public void setUseRfcForwardHeader(boolean useRfcForwardHeader) { + this.useRfcForwardHeader = useRfcForwardHeader; + } + /** * Set the {@link ReactorResourceFactory} to get the shared resources from. * @param resourceFactory the server resources @@ -157,10 +170,20 @@ private HttpServer createHttpServer() { CompressionCustomizer compressionCustomizer = new CompressionCustomizer(getCompression()); server = compressionCustomizer.apply(server); } - server = server.protocol(listProtocols()).forwarded(this.useForwardHeaders); + server = configureForwardedHeaders(server.protocol(listProtocols())); return applyCustomizers(server); } + private HttpServer configureForwardedHeaders(HttpServer server) { + if (this.useForwardHeaders) { + return server.forwarded(false, false); + } + if (this.useRfcForwardHeader) { + return server.forwarded(true, false); + } + return server.noForwarded(); + } + private HttpServer customizeSslConfiguration(HttpServer httpServer, Ssl ssl) { SslServerCustomizer customizer = new SslServerCustomizer(getHttp2(), ssl.getClientAuth(), getSslBundle(), getServerNameSslBundles()); diff --git a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizer.java b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizer.java index f46c6d9a2db2..7774aa3c5310 100644 --- a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizer.java +++ b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizer.java @@ -59,7 +59,9 @@ public int getOrder() { @Override public void customize(NettyReactiveWebServerFactory factory) { - factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders()); + if (getOrDeduceUseForwardHeaders()) { + configureForwardedSupport(factory); + } PropertyMapper map = PropertyMapper.get(); map.from(this.nettyProperties::getConnectionTimeout) .to((connectionTimeout) -> customizeConnectionTimeout(factory, connectionTimeout)); @@ -81,6 +83,13 @@ private boolean getOrDeduceUseForwardHeaders() { return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE); } + private void configureForwardedSupport(NettyReactiveWebServerFactory factory) { + switch (this.nettyProperties.getForwardedHeaders().getHeaderFormat()) { + case X_FORWARDED -> factory.setUseForwardHeaders(true); + case STANDARD -> factory.setUseRfcForwardHeader(true); + } + } + private void customizeConnectionTimeout(NettyReactiveWebServerFactory factory, Duration connectionTimeout) { factory.addServerCustomizers((httpServer) -> httpServer.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) connectionTimeout.toMillis())); diff --git a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerProperties.java b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerProperties.java index 3273c84ec4cf..f37584cc5245 100644 --- a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerProperties.java +++ b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerProperties.java @@ -90,6 +90,11 @@ public class NettyServerProperties { */ private @Nullable Duration idleTimeout; + /** + * Forwarded headers configuration. + */ + private final Forwardedheaders forwardedHeaders = new Forwardedheaders(); + public @Nullable Duration getConnectionTimeout() { return this.connectionTimeout; } @@ -146,4 +151,45 @@ public void setIdleTimeout(@Nullable Duration idleTimeout) { this.idleTimeout = idleTimeout; } + public Forwardedheaders getForwardedHeaders() { + return this.forwardedHeaders; + } + + /** + * Forwarded headers. + */ + public static class Forwardedheaders { + + /** + * Format of the forwarded headers to support. + */ + private HeaderFormat headerFormat = HeaderFormat.X_FORWARDED; + + public HeaderFormat getHeaderFormat() { + return this.headerFormat; + } + + public void setHeaderFormat(HeaderFormat headerFormat) { + this.headerFormat = headerFormat; + } + + /** + * Formats of forwarded headers supported by {@link Forwardedheaders}. + */ + public enum HeaderFormat { + + /** + * Use the standard "Forwarded" header, as defined by RFC 7239. + */ + STANDARD, + + /** + * Use the non-standard "X-Forwarded-*" headers. + */ + X_FORWARDED + + } + + } + } diff --git a/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactoryTests.java b/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactoryTests.java index 35a820ffcbc5..d9e8acb5f3f4 100644 --- a/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactoryTests.java +++ b/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactoryTests.java @@ -146,6 +146,22 @@ void useForwardedHeaders() { assertForwardHeaderIsUsed(factory); } + @Test + void useRfcForwardHeader() { + NettyReactiveWebServerFactory factory = getFactory(); + factory.setUseRfcForwardHeader(true); + assertRfcForwardHeaderIsUsed(factory); + } + + @Test + void useForwardHeadersTakesPrecedenceOverUseRfcForwardHeader() { + NettyReactiveWebServerFactory factory = getFactory(); + factory.setUseForwardHeaders(true); + factory.setUseRfcForwardHeader(true); + assertForwardHeaderIsUsed(factory); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertRfcForwardHeaderIsUsed(factory)); + } + @Test @WithPackageResources("test.jks") void whenSslIsConfiguredWithAValidAliasARequestSucceeds() { diff --git a/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizerTests.java b/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizerTests.java index c3ee9666fc54..561fd5cbe0e5 100644 --- a/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizerTests.java +++ b/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizerTests.java @@ -39,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -85,7 +86,8 @@ void deduceUseForwardHeaders() { void defaultUseForwardHeaders() { NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); this.customizer.customize(factory); - then(factory).should().setUseForwardHeaders(false); + then(factory).should(never()).setUseForwardHeaders(anyBoolean()); + then(factory).should(never()).setUseRfcForwardHeader(anyBoolean()); } @Test @@ -102,7 +104,30 @@ void forwardHeadersWhenStrategyIsNoneShouldNotConfigureValve() { this.serverProperties.setForwardHeadersStrategy(ServerProperties.ForwardHeadersStrategy.NONE); NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); this.customizer.customize(factory); - then(factory).should().setUseForwardHeaders(false); + then(factory).should(never()).setUseForwardHeaders(anyBoolean()); + then(factory).should(never()).setUseRfcForwardHeader(anyBoolean()); + } + + @Test + void forwardedHeadersFormatXForwardedConfiguresUseForwardHeaders() { + this.environment.setProperty("DYNO", "-"); + this.nettyProperties.getForwardedHeaders() + .setHeaderFormat(NettyServerProperties.Forwardedheaders.HeaderFormat.X_FORWARDED); + NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); + this.customizer.customize(factory); + then(factory).should().setUseForwardHeaders(true); + then(factory).should(never()).setUseRfcForwardHeader(anyBoolean()); + } + + @Test + void forwardedHeadersFormatStandardConfiguresUseRfcForwardHeader() { + this.environment.setProperty("DYNO", "-"); + this.nettyProperties.getForwardedHeaders() + .setHeaderFormat(NettyServerProperties.Forwardedheaders.HeaderFormat.STANDARD); + NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); + this.customizer.customize(factory); + then(factory).should().setUseRfcForwardHeader(true); + then(factory).should(never()).setUseForwardHeaders(anyBoolean()); } @Test diff --git a/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerPropertiesTests.java b/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerPropertiesTests.java index d1953291e599..d046a9bf5df7 100644 --- a/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerPropertiesTests.java +++ b/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerPropertiesTests.java @@ -73,6 +73,19 @@ void nettyInitialBufferSizeMatchesHttpDecoderSpecDefault() { .isEqualTo(HttpDecoderSpec.DEFAULT_INITIAL_BUFFER_SIZE); } + @Test + void forwardedHeadersDefaultToXForwarded() { + assertThat(this.properties.getForwardedHeaders().getHeaderFormat()) + .isEqualTo(NettyServerProperties.Forwardedheaders.HeaderFormat.X_FORWARDED); + } + + @Test + void testCustomizeNettyForwardedHeadersFormat() { + bind("server.netty.forwarded-headers.header-format", "standard"); + assertThat(this.properties.getForwardedHeaders().getHeaderFormat()) + .isEqualTo(NettyServerProperties.Forwardedheaders.HeaderFormat.STANDARD); + } + private void bind(String name, String value) { bind(Collections.singletonMap(name, value)); } From edc6fa3f287f4310fb7a583ad1c14501d6422114 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson <andy.wilkinson@broadcom.com> Date: Fri, 31 Jul 2026 13:16:06 +0100 Subject: [PATCH 809/930] Use temp directory when path of build directory is too long Some gRPC-related tests fail on Windows when the path becomes too long. The failure is somewhat cryptic: > protoc: stdout: . stderr: --grpc_out: protoc-gen-grpc: The system cannot find the path specified. The failure occurs when the path to the protoc-gen-grpc-java executable gets too long. Our Gradle plugin's tests configure TestKit to use a directory beneath spring-boot-gradle-plugin/build and this can result in the path being too long. This commit updates GradleBuild to use a directory beneath java.io.tmpdir when the tests are running on Windows and the absolute path of spring-boot-gradle-plugin/build 80 characters or more in length. Closes gh-51169 --- .../testsupport/gradle/testkit/GradleBuild.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test-support/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleBuild.java b/test-support/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleBuild.java index 996ea9b0faf8..46cb31d2f5ba 100644 --- a/test-support/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleBuild.java +++ b/test-support/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleBuild.java @@ -210,12 +210,25 @@ private void copyTransformedScript(String script, File destination) throws IOExc } private File getTestKitDir() { - File build = this.buildOutput.getRootLocation(); - File testKitRoot = new File(build, "gradle-test-kit"); + File testKitRoot = getTestKitRoot(); String gradleVersion = (this.gradleVersion != null) ? this.gradleVersion : "default"; return new File(testKitRoot, gradleVersion).getAbsoluteFile(); } + private File getTestKitRoot() { + File build = this.buildOutput.getRootLocation(); + if (isWindows() && build.getAbsolutePath().length() >= 80) { + return new File(System.getProperty("java.io.tmpdir"), "gradle-test-kit"); + } + else { + return new File(build, "gradle-test-kit"); + } + } + + private boolean isWindows() { + return File.separatorChar == '\\'; + } + public File getProjectDir() { return this.projectDir; } From 435604f8d8e8a0faa0da6bed2f67fd0f252dcc15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 10:29:11 +0200 Subject: [PATCH 810/930] Upgrade to ActiveMQ 6.2.8 Closes gh-51184 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1ca624954fef..7e379b194e97 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -31,7 +31,7 @@ bom { ] } } - library("ActiveMQ", "6.2.7") { + library("ActiveMQ", "6.2.8") { group("org.apache.activemq") { modules = [ "activemq-console", From 569bc8cd811e63ab1bdf924121809684a9e61be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 10:29:18 +0200 Subject: [PATCH 811/930] Upgrade to CycloneDX Maven Plugin 2.9.3 Closes gh-51185 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7e379b194e97..3d8d2b12766d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -337,7 +337,7 @@ bom { ] } } - library("CycloneDX Maven Plugin", "2.9.2") { + library("CycloneDX Maven Plugin", "2.9.3") { group("org.cyclonedx") { plugins = [ "cyclonedx-maven-plugin" From 66b7e8da9ba3d691626842f3652be27765df9dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 10:29:26 +0200 Subject: [PATCH 812/930] Upgrade to Groovy 5.0.8 Closes gh-51186 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3d8d2b12766d..a8ea2523e6a0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -546,7 +546,7 @@ bom { releaseNotes("https://github.com/graphql-java/graphql-java/releases/tag/v{version}") } } - library("Groovy", "5.0.7") { + library("Groovy", "5.0.8") { prohibit { contains "-alpha-" because "we don't want alpha dependencies" From 172c0559b402f7d069e280d3e126b1eda72be59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 10:29:33 +0200 Subject: [PATCH 813/930] Upgrade to Hibernate Validator 9.1.3.Final Closes gh-51187 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a8ea2523e6a0..31ebd6a75049 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -673,7 +673,7 @@ bom { .formatted(version.major(), version.minor())) } } - library("Hibernate Validator", "9.1.2.Final") { + library("Hibernate Validator", "9.1.3.Final") { group("org.hibernate.validator") { modules = [ "hibernate-validator", From ae96f82c647406a1e522b84d96bce0b998623d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 10:29:39 +0200 Subject: [PATCH 814/930] Upgrade to HttpClient5 5.6.3 Closes gh-51188 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 31ebd6a75049..cf222ef945d9 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -727,7 +727,7 @@ bom { ] } } - library("HttpClient5", "5.6.2") { + library("HttpClient5", "5.6.3") { prohibit { contains "-alpha" contains "-beta" From bca7334ad9d5e9a3a28d5fee28bfe0b2bea5b556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 10:29:45 +0200 Subject: [PATCH 815/930] Upgrade to JSpecify 1.0.1 Closes gh-51189 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index cf222ef945d9..7331823a4a93 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1197,7 +1197,7 @@ bom { releaseNotes("https://github.com/skyscreamer/JSONassert/releases/tag/jsonassert-{version}") } } - library("JSpecify", "1.0.0") { + library("JSpecify", "1.0.1") { group("org.jspecify") { modules = [ "jspecify" From 533ac9168e0a6738311c9c73765c949dbb738153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 10:29:50 +0200 Subject: [PATCH 816/930] Upgrade to MariaDB 3.5.10 Closes gh-51190 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7331823a4a93..efd7505afe9d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1398,7 +1398,7 @@ bom { javadoc("https://projectlombok.org/api") } } - library("MariaDB", "3.5.9") { + library("MariaDB", "3.5.10") { group("org.mariadb.jdbc") { modules = [ "mariadb-java-client" From 4cae8fa66f5fe75d62ae69f2273ba4eb1ed11957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 10:29:55 +0200 Subject: [PATCH 817/930] Upgrade to Maven Jar Plugin 3.5.1 Closes gh-51191 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index efd7505afe9d..3ed78c019e4d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1533,7 +1533,7 @@ bom { releaseNotes("https://github.com/apache/maven-invoker-plugin/releases/tag/maven-invoker-plugin-{version}") } } - library("Maven Jar Plugin", "3.5.0") { + library("Maven Jar Plugin", "3.5.1") { prohibit { contains "-beta-" because "we don't want betas" From 74098b3670a9740778f2a31233c1ab6c501e581f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 10:30:01 +0200 Subject: [PATCH 818/930] Upgrade to Native Build Tools Plugin 1.1.6 Closes gh-51192 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 06222da173fa..f187bce843de 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ junitJupiterVersion=6.0.3 kotlinVersion=2.3.21 mavenVersion=3.9.13 mockitoVersion=5.23.0 -nativeBuildToolsVersion=1.1.5 +nativeBuildToolsVersion=1.1.6 nullabilityPluginVersion=0.0.14 snakeYamlVersion=2.6 springGrpcVersion=1.1.0 From cd659e3b14e505c052b79ab320032da1738a11ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 10:30:06 +0200 Subject: [PATCH 819/930] Upgrade to Oracle Database 23.26.3.0.0 Closes gh-51193 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3ed78c019e4d..97eaea3bedc0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1793,7 +1793,7 @@ bom { releaseNotes("https://github.com/open-telemetry/opentelemetry-java/releases/tag/v{version}") } } - library("Oracle Database", "23.26.2.0.0") { + library("Oracle Database", "23.26.3.0.0") { alignWith { dependencyManagementDeclaredIn("com.oracle.database.jdbc:ojdbc-bom") } From 0499051b4680814ea53aff275408bbb4f49cf6b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 10:30:12 +0200 Subject: [PATCH 820/930] Upgrade to R2DBC MySQL 1.4.3 Closes gh-51194 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 97eaea3bedc0..cf9931eabe14 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1998,7 +1998,7 @@ bom { releaseNotes("https://github.com/r2dbc/r2dbc-mssql/releases/tag/v{version}") } } - library("R2DBC MySQL", "1.4.2") { + library("R2DBC MySQL", "1.4.3") { group("io.asyncer") { modules = [ "r2dbc-mysql" From bad2873459483bb1d3d105612d1e489922acfcc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 10:30:17 +0200 Subject: [PATCH 821/930] Upgrade to SQLite JDBC 3.53.2.1 Closes gh-51195 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index cf9931eabe14..dfcbbd52d468 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2764,7 +2764,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ws/releases/tag/v{version}") } } - library("SQLite JDBC", "3.53.2.0") { + library("SQLite JDBC", "3.53.2.1") { group("org.xerial") { modules = [ "sqlite-jdbc" From b1250f1ff11bdebff1b3b1e83b335813f13dafd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 10:52:43 +0200 Subject: [PATCH 822/930] Upgrade to Pooled JMS 3.2.3 Closes gh-51196 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index dfcbbd52d468..3313e06c5c3a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1855,7 +1855,7 @@ bom { releaseNotes("https://github.com/ascopes/protobuf-maven-plugin/releases/tag/v{version}") } } - library("Pooled JMS", "3.2.2") { + library("Pooled JMS", "3.2.3") { group("org.messaginghub") { modules = [ "pooled-jms" From 6706def0e693cd388a475a0f27420ea05849754f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:50:19 +0200 Subject: [PATCH 823/930] Upgrade to ActiveMQ 6.3.0 Closes gh-51197 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a9c9fd3cca06..b289d1bd2d7e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -31,7 +31,7 @@ bom { ] } } - library("ActiveMQ", "6.2.7") { + library("ActiveMQ", "6.3.0") { group("org.apache.activemq") { modules = [ "activemq-console", From 6790e0c8a819f763e79ff8b01a42442f8b875348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:50:24 +0200 Subject: [PATCH 824/930] Upgrade to Commons Codec 1.22.1 Closes gh-51198 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6368c4350413..db6d9995e026 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8 assertjVersion=3.27.7 checkstyleToolVersion=10.12.4 -commonsCodecVersion=1.22.0 +commonsCodecVersion=1.22.1 graalVersion=25 hamcrestVersion=3.0 jackson2Version=2.21.5 From 303efeb90281c8804c632c4781b961df072aec86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:50:30 +0200 Subject: [PATCH 825/930] Upgrade to CycloneDX Maven Plugin 2.9.3 Closes gh-51199 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b289d1bd2d7e..8e6e711e4d9e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -337,7 +337,7 @@ bom { ] } } - library("CycloneDX Maven Plugin", "2.9.2") { + library("CycloneDX Maven Plugin", "2.9.3") { group("org.cyclonedx") { plugins = [ "cyclonedx-maven-plugin" From 5d02335fa344310246eef15cdb41fe06cbc7545f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:50:35 +0200 Subject: [PATCH 826/930] Upgrade to Groovy 5.0.8 Closes gh-51200 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8e6e711e4d9e..3b04390f39b7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -546,7 +546,7 @@ bom { releaseNotes("https://github.com/graphql-java/graphql-java/releases/tag/v{version}") } } - library("Groovy", "5.0.7") { + library("Groovy", "5.0.8") { prohibit { contains "-alpha-" because "we don't want alpha dependencies" From 5781684e7ee45cb7518a9a69d92ef5096c0aa682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:50:40 +0200 Subject: [PATCH 827/930] Upgrade to Hibernate Validator 9.1.3.Final Closes gh-51201 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3b04390f39b7..67d8aaad4ee5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -676,7 +676,7 @@ bom { .formatted(version.major(), version.minor())) } } - library("Hibernate Validator", "9.1.2.Final") { + library("Hibernate Validator", "9.1.3.Final") { group("org.hibernate.validator") { modules = [ "hibernate-validator", From 38c503fa99083bf027d5f5eba5dd4fde0d6331af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:50:46 +0200 Subject: [PATCH 828/930] Upgrade to HttpClient5 5.6.3 Closes gh-51202 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 67d8aaad4ee5..857ad73a9c81 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -730,7 +730,7 @@ bom { ] } } - library("HttpClient5", "5.6.2") { + library("HttpClient5", "5.6.3") { prohibit { contains "-alpha" contains "-beta" From d8f1b1a5334b3b042049df4d185537416a1b4275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:50:51 +0200 Subject: [PATCH 829/930] Upgrade to Infinispan 16.2.2 Closes gh-51203 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 857ad73a9c81..291330808b45 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -763,7 +763,7 @@ bom { ] } } - library("Infinispan", "16.2.1") { + library("Infinispan", "16.2.2") { group("org.infinispan") { bom("infinispan-bom") } From 99db9994e1bf1d0f4ebb5433f8398859b58ad710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:50:55 +0200 Subject: [PATCH 830/930] Upgrade to JSpecify 1.0.1 Closes gh-51204 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 291330808b45..8ac64e4ce40a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1208,7 +1208,7 @@ bom { releaseNotes("https://github.com/skyscreamer/JSONassert/releases/tag/jsonassert-{version}") } } - library("JSpecify", "1.0.0") { + library("JSpecify", "1.0.1") { group("org.jspecify") { modules = [ "jspecify" From 2fe899fd9d8417457bd9aab4f91099c3e2f70659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:50:59 +0200 Subject: [PATCH 831/930] Upgrade to Kotlin 2.4.10 Closes gh-51205 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index db6d9995e026..5bb50cd420b1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ jackson2Version=2.21.5 jacksonVersion=3.1.5 javaFormatVersion=0.0.47 junitJupiterVersion=6.1.2 -kotlinVersion=2.3.21 +kotlinVersion=2.4.10 mavenVersion=3.9.13 mockitoVersion=5.23.0 nativeBuildToolsVersion=1.1.5 From 9bc991a412be00c9ab4011f224504d316af97da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:51:03 +0200 Subject: [PATCH 832/930] Upgrade to Kotlin Coroutines 1.11.0 Closes gh-51206 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8ac64e4ce40a..0b5743aa6cd2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1311,7 +1311,7 @@ bom { releaseNotes("https://github.com/JetBrains/kotlin/releases/tag/v{version}") } } - library("Kotlin Coroutines", "1.10.2") { + library("Kotlin Coroutines", "1.11.0") { group("org.jetbrains.kotlinx") { bom("kotlinx-coroutines-bom") } From ddb8426ca672e31133981a1c7851158d8c9a8f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:51:09 +0200 Subject: [PATCH 833/930] Upgrade to Logback 1.6.1 Closes gh-51207 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0b5743aa6cd2..98df9a951432 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1386,7 +1386,7 @@ bom { releaseNotes("https://github.com/apache/logging-log4j2/releases/tag/rel%2F{version}") } } - library("Logback", "1.5.38") { + library("Logback", "1.6.1") { group("ch.qos.logback") { modules = [ "logback-classic", From 097d37bb6c5bb786187dac1afb17ae21a1e7c358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:51:15 +0200 Subject: [PATCH 834/930] Upgrade to MariaDB 3.5.10 Closes gh-51208 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 98df9a951432..79bd1256e70a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1409,7 +1409,7 @@ bom { javadoc("https://projectlombok.org/api") } } - library("MariaDB", "3.5.9") { + library("MariaDB", "3.5.10") { group("org.mariadb.jdbc") { modules = [ "mariadb-java-client" From e2e413cd2c60d37681301c19e1c3298e2704f0ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:51:20 +0200 Subject: [PATCH 835/930] Upgrade to Maven Jar Plugin 3.5.1 Closes gh-51209 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 79bd1256e70a..1e3a873f026f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1544,7 +1544,7 @@ bom { releaseNotes("https://github.com/apache/maven-invoker-plugin/releases/tag/maven-invoker-plugin-{version}") } } - library("Maven Jar Plugin", "3.5.0") { + library("Maven Jar Plugin", "3.5.1") { prohibit { contains "-beta-" because "we don't want betas" From 2c9afbfe50516479c9008f3bf9e2858e2d20f5f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:51:25 +0200 Subject: [PATCH 836/930] Upgrade to Native Build Tools Plugin 1.1.6 Closes gh-51210 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5bb50cd420b1..ff0a81428c28 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ junitJupiterVersion=6.1.2 kotlinVersion=2.4.10 mavenVersion=3.9.13 mockitoVersion=5.23.0 -nativeBuildToolsVersion=1.1.5 +nativeBuildToolsVersion=1.1.6 nullabilityPluginVersion=0.0.14 snakeYamlVersion=2.6 springGrpcVersion=1.1.0 From 56a726f4c7cfb524ee323cf943abe70506854466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:51:29 +0200 Subject: [PATCH 837/930] Upgrade to Oracle Database 23.26.3.0.0 Closes gh-51211 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1e3a873f026f..d4784db40b90 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1804,7 +1804,7 @@ bom { releaseNotes("https://github.com/open-telemetry/opentelemetry-java/releases/tag/v{version}") } } - library("Oracle Database", "23.26.2.0.0") { + library("Oracle Database", "23.26.3.0.0") { alignWith { dependencyManagementDeclaredIn("com.oracle.database.jdbc:ojdbc-bom") } From f3035f7cce057d2299c508a4b3049217677c6b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:51:33 +0200 Subject: [PATCH 838/930] Upgrade to Pooled JMS 3.2.3 Closes gh-51212 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d4784db40b90..fc8a91c13a8a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1866,7 +1866,7 @@ bom { releaseNotes("https://github.com/ascopes/protobuf-maven-plugin/releases/tag/v{version}") } } - library("Pooled JMS", "3.2.2") { + library("Pooled JMS", "3.2.3") { group("org.messaginghub") { modules = [ "pooled-jms" From bda659047cfc4935a39947013a29bf8760d1a78f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:51:39 +0200 Subject: [PATCH 839/930] Upgrade to Pulsar 4.2.4 Closes gh-51213 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index fc8a91c13a8a..b068f6094b79 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1915,7 +1915,7 @@ bom { releaseNotes("https://github.com/prometheus/client_java/releases/tag/parent-{version}") } } - library("Pulsar", "4.2.3") { + library("Pulsar", "4.2.4") { group("org.apache.pulsar") { bom("pulsar-bom") } From 17c55d56340ac151ba204e21d62a741ea1139977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:51:43 +0200 Subject: [PATCH 840/930] Upgrade to R2DBC MySQL 1.4.3 Closes gh-51214 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b068f6094b79..078f79000b09 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2009,7 +2009,7 @@ bom { releaseNotes("https://github.com/r2dbc/r2dbc-mssql/releases/tag/v{version}") } } - library("R2DBC MySQL", "1.4.2") { + library("R2DBC MySQL", "1.4.3") { group("io.asyncer") { modules = [ "r2dbc-mysql" From 53e60cc35b93ce89a2f451565a17c0fa0b03850e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:51:47 +0200 Subject: [PATCH 841/930] Upgrade to RabbitMQ AMQP Client 1.3.0 Closes gh-51215 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 078f79000b09..382822ac6daa 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2078,7 +2078,7 @@ bom { releaseNotes("https://github.com/rabbitmq/rabbitmq-java-client/releases/tag/v{version}") } } - library("RabbitMQ AMQP Client", "1.2.0") { + library("RabbitMQ AMQP Client", "1.3.0") { group("com.rabbitmq.client") { modules = [ "amqp-client" From 96022cf5f9da62bd5127e5934a139dfeb976c6cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:51:51 +0200 Subject: [PATCH 842/930] Upgrade to SQLite JDBC 3.53.2.1 Closes gh-51216 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 382822ac6daa..00860afa1a2b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2793,7 +2793,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ws/releases/tag/v{version}") } } - library("SQLite JDBC", "3.53.2.0") { + library("SQLite JDBC", "3.53.2.1") { group("org.xerial") { modules = [ "sqlite-jdbc" From a7390ac586e403ffb6cee1c382430c0c50a675a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 14:51:56 +0200 Subject: [PATCH 843/930] Upgrade to XmlUnit2 2.13.0 Closes gh-51217 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 00860afa1a2b..9ed27ac1bda5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2967,7 +2967,7 @@ bom { releaseNotes("https://github.com/mojohaus/xml-maven-plugin/releases/tag/{version}") } } - library("XmlUnit2", "2.12.0") { + library("XmlUnit2", "2.13.0") { group("org.xmlunit") { modules = [ "xmlunit-assertj", From 3380f9236b4043ad018f10990799a003c1bed07e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 16:02:19 +0200 Subject: [PATCH 844/930] Polish "Upgrade to Kotlin 2.4.10" Closes gh-51205 --- build-plugin/spring-boot-gradle-plugin/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build-plugin/spring-boot-gradle-plugin/build.gradle b/build-plugin/spring-boot-gradle-plugin/build.gradle index 2e37a17007ee..a9e0f3918713 100644 --- a/build-plugin/spring-boot-gradle-plugin/build.gradle +++ b/build-plugin/spring-boot-gradle-plugin/build.gradle @@ -65,6 +65,7 @@ dependencies { testImplementation("org.apache.httpcomponents.client5:httpclient5") testImplementation("org.graalvm.buildtools:native-gradle-plugin") testImplementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + testImplementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlinVersion") testImplementation("org.jetbrains.kotlin:kotlin-compiler-runner:$kotlinVersion") testImplementation("org.jetbrains.kotlin:kotlin-daemon-client:$kotlinVersion") testImplementation("org.tomlj:tomlj:1.0.0") From 7cc5fbaa0a97b8ed226ba332a14ec8a18a0422e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 17:24:40 +0200 Subject: [PATCH 845/930] Start building against Micrometer 1.18.0-M1 snapshots See gh-51219 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9ed27ac1bda5..30ce351180c1 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1626,7 +1626,7 @@ bom { releaseNotes("https://github.com/apache/maven-war-plugin/releases/tag/maven-war-plugin-{version}") } } - library("Micrometer", "1.17.0") { + library("Micrometer", "1.18.0-SNAPSHOT") { firstParty() group("io.micrometer") { modules = [ From 6d4b82b7355a8404453652fca409c090617213bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 17:24:45 +0200 Subject: [PATCH 846/930] Start building against Micrometer Tracing 1.8.0-M1 snapshots See gh-51220 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 30ce351180c1..769975677b48 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1649,7 +1649,7 @@ bom { releaseNotes("https://github.com/micrometer-metrics/micrometer/releases/tag/v{version}") } } - library("Micrometer Tracing", "1.7.0") { + library("Micrometer Tracing", "1.8.0-SNAPSHOT") { firstParty() group("io.micrometer") { bom("micrometer-tracing-bom") From 7f15ee9983a9e664d71f3346e48e95a701e26df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 17:24:50 +0200 Subject: [PATCH 847/930] Start building against Spring AMQP 4.2.0-M1 snapshots See gh-51221 --- ...RabbitConnectionFactoryBeanConfigurer.java | 9 ++- .../autoconfigure/RabbitProperties.java | 2 + .../SslBundleRabbitConnectionFactoryBean.java | 2 + .../RabbitAutoConfigurationTests.java | 58 +------------------ .../spring-boot-dependencies/build.gradle | 2 +- 5 files changed, 15 insertions(+), 58 deletions(-) diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java index c44e3e3b5268..39a02976be2d 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitConnectionFactoryBeanConfigurer.java @@ -25,6 +25,7 @@ import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.rabbitmq.autoconfigure.RabbitConnectionDetails.Address; +import org.springframework.boot.rabbitmq.autoconfigure.RabbitProperties.Ssl; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.core.io.ResourceLoader; @@ -133,7 +134,7 @@ public void configure(RabbitConnectionFactoryBean factory) { RabbitProperties.Ssl ssl = this.rabbitProperties.getSsl(); if (sslBundle != null || ssl.determineEnabled()) { factory.setUseSSL(true); - map.from(ssl::isVerifyHostname).to(factory::setEnableHostnameVerification); + configureDeprecatedHostNameVerification(factory, map, ssl); if (sslBundle != null) { applySslBundle(factory, sslBundle); } @@ -154,6 +155,12 @@ public void configure(RabbitConnectionFactoryBean factory) { .to(factory::setMaxInboundMessageBodySize); } + @SuppressWarnings("removal") + private static void configureDeprecatedHostNameVerification(RabbitConnectionFactoryBean factory, PropertyMapper map, + Ssl ssl) { + map.from(ssl::isVerifyHostname).to(factory::setEnableHostnameVerification); + } + private void applySslBundle(RabbitConnectionFactoryBean factory, SslBundle bundle) { if (factory instanceof SslBundleRabbitConnectionFactoryBean sslFactory) { sslFactory.setSslBundle(bundle); diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitProperties.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitProperties.java index 48449dca89e8..c410f41b1225 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitProperties.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitProperties.java @@ -28,6 +28,7 @@ import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.CacheMode; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; 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.InvalidConfigurationPropertyValueException; import org.springframework.boot.convert.DurationUnit; @@ -579,6 +580,7 @@ public void setValidateServerCertificate(boolean validateServerCertificate) { this.validateServerCertificate = validateServerCertificate; } + @DeprecatedConfigurationProperty(reason = "Hostname verification is always enabled", since = "4.2.0") public boolean isVerifyHostname() { return this.verifyHostname; } diff --git a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SslBundleRabbitConnectionFactoryBean.java b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SslBundleRabbitConnectionFactoryBean.java index c3f0b389ea99..ac0371478a2c 100644 --- a/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SslBundleRabbitConnectionFactoryBean.java +++ b/module/spring-boot-rabbitmq/src/main/java/org/springframework/boot/rabbitmq/autoconfigure/SslBundleRabbitConnectionFactoryBean.java @@ -50,6 +50,8 @@ void setSslBundle(@Nullable SslBundle sslBundle) { this.sslBundle = sslBundle; } + @Deprecated(since = "4.2.0", forRemoval = true) + @SuppressWarnings("removal") @Override public void setEnableHostnameVerification(boolean enable) { this.enableHostnameVerification = enable; diff --git a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfigurationTests.java b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfigurationTests.java index 787f078c9857..2b321d01bd4c 100644 --- a/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfigurationTests.java +++ b/module/spring-boot-rabbitmq/src/test/java/org/springframework/boot/rabbitmq/autoconfigure/RabbitAutoConfigurationTests.java @@ -16,7 +16,6 @@ package org.springframework.boot.rabbitmq.autoconfigure; -import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.List; @@ -24,14 +23,11 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSocketFactory; import com.rabbitmq.client.Address; import com.rabbitmq.client.Connection; import com.rabbitmq.client.JDKSaslConfig; -import com.rabbitmq.client.SslEngineConfigurator; import com.rabbitmq.client.impl.CredentialsProvider; import com.rabbitmq.client.impl.CredentialsRefreshService; import com.rabbitmq.client.impl.DefaultCredentialsProvider; @@ -43,7 +39,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.springframework.amqp.core.AcknowledgeMode; @@ -880,20 +875,6 @@ void enableSsl() { assertThat(rabbitConnectionFactory.isSSL()).isTrue(); assertThat(rabbitConnectionFactory.getSocketFactory()).as("SocketFactory must use SSL") .isInstanceOf(SSLSocketFactory.class); - assertThatHostnameVerificationIsEnabled(rabbitConnectionFactory); - }); - } - - @Test - void enableSslWithoutHostnameVerification() { - this.contextRunner.withUserConfiguration(TestConfiguration.class) - .withPropertyValues("spring.rabbitmq.ssl.enabled:true", "spring.rabbitmq.ssl.verify-hostname:false") - .run((context) -> { - com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); - assertThat(rabbitConnectionFactory.isSSL()).isTrue(); - assertThat(rabbitConnectionFactory.getSocketFactory()).as("SocketFactory must use SSL") - .isInstanceOf(SSLSocketFactory.class); - assertThatHostnameVerificationIsDisabled(rabbitConnectionFactory); }); } @@ -966,20 +947,6 @@ void enableSslWithBundle() { .run((context) -> { com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); assertThat(rabbitConnectionFactory.isSSL()).isTrue(); - assertThatHostnameVerificationIsEnabled(rabbitConnectionFactory); - }); - } - - @Test - void enableSslWithBundleAndWithoutHostnameVerification() { - this.contextRunner.withUserConfiguration(TestConfiguration.class) - .withPropertyValues("spring.rabbitmq.ssl.bundle=test-bundle", "spring.rabbitmq.ssl.verify-hostname=false", - "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/rabbitmq/autoconfigure/test.jks", - "spring.ssl.bundle.jks.test-bundle.keystore.password=secret") - .run((context) -> { - com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); - assertThat(rabbitConnectionFactory.isSSL()).isTrue(); - assertThatHostnameVerificationIsDisabled(rabbitConnectionFactory); }); } @@ -1002,7 +969,7 @@ void enableSslWithValidateServerCertificateFalse(CapturedOutput output) { .run((context) -> { com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); assertThat(rabbitConnectionFactory.isSSL()).isTrue(); - assertThat(output).contains("TrustEverythingTrustManager", "SECURITY ALERT"); + assertThat(output).contains("SECURITY ALERT", "this mode trusts every certificate"); }); } @@ -1013,7 +980,7 @@ void enableSslWithValidateServerCertificateDefault(CapturedOutput output) { .run((context) -> { com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); assertThat(rabbitConnectionFactory.isSSL()).isTrue(); - assertThat(output).doesNotContain("TrustEverythingTrustManager", "SECURITY ALERT"); + assertThat(output).doesNotContain("SECURITY ALERT", "this mode trusts every certificate"); }); } @@ -1143,27 +1110,6 @@ void whenADirectContainerCustomizerIsDefinedThenItIsCalledToConfigureTheContaine .configure(any(DirectMessageListenerContainer.class))); } - @SuppressWarnings("deprecation") - private void assertThatHostnameVerificationIsEnabled(com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory) - throws IOException { - SslEngineConfigurator sslEngineConfigurator = rabbitConnectionFactory.getNioParams().getSslEngineConfigurator(); - SSLEngine engine = mock(SSLEngine.class); - sslEngineConfigurator.configure(engine); - ArgumentCaptor<SSLParameters> sslParametersCaptor = ArgumentCaptor.forClass(SSLParameters.class); - then(engine).should().setSSLParameters(sslParametersCaptor.capture()); - SSLParameters sslParameters = sslParametersCaptor.getValue(); - assertThat(sslParameters.getEndpointIdentificationAlgorithm()).isEqualTo("HTTPS"); - } - - @SuppressWarnings("deprecation") - private void assertThatHostnameVerificationIsDisabled(com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory) - throws IOException { - SslEngineConfigurator sslEngineConfigurator = rabbitConnectionFactory.getNioParams().getSslEngineConfigurator(); - SSLEngine engine = mock(SSLEngine.class); - sslEngineConfigurator.configure(engine); - then(engine).shouldHaveNoMoreInteractions(); - } - private com.rabbitmq.client.ConnectionFactory getTargetConnectionFactory(AssertableApplicationContext context) { CachingConnectionFactory connectionFactory = context.getBean(CachingConnectionFactory.class); return connectionFactory.getRabbitConnectionFactory(); diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 769975677b48..df549b1e4a23 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2545,7 +2545,7 @@ bom { ] } } - library("Spring AMQP", "4.1.0") { + library("Spring AMQP", "4.2.0-SNAPSHOT") { firstParty() group("org.springframework.amqp") { bom("spring-amqp-bom") From a29a44e8e805c29516a17e50accb0701310f2283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 17:25:34 +0200 Subject: [PATCH 848/930] Start building against Spring Data Bom 2026.1.0-M1 snapshots See gh-51222 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index df549b1e4a23..777773f92059 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2575,7 +2575,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-batch/releases/tag/v{version}") } } - library("Spring Data Bom", "2026.0.0") { + library("Spring Data Bom", "2026.1.0-SNAPSHOT") { firstParty() calendarName = "Spring Data Release" group("org.springframework.data") { From 9c72f7252819d86449f6a98febd1b18b86bfa154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 17:30:34 +0200 Subject: [PATCH 849/930] Start building against Spring Integration 7.2.0-M1 snapshots See gh-51223 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 777773f92059..f79cf4b0741c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2657,7 +2657,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-hateoas/releases/tag/{version}") } } - library("Spring Integration", "7.1.0") { + library("Spring Integration", "7.2.0-SNAPSHOT") { firstParty() group("org.springframework.integration") { bom("spring-integration-bom") From c86c6444053c6e4f857d0e82c8181ab5d5092ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 17:30:39 +0200 Subject: [PATCH 850/930] Start building against Spring Kafka 4.2.0-M1 snapshots See gh-51224 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index f79cf4b0741c..227b2b24a8a6 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2672,7 +2672,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-integration/releases/tag/v{version}") } } - library("Spring Kafka", "4.1.0") { + library("Spring Kafka", "4.2.0-SNAPSHOT") { firstParty() group("org.springframework.kafka") { modules = [ From 35de2469fea5cd5e95ac59f85d9981a569fd9b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 18:07:22 +0200 Subject: [PATCH 851/930] Start building against Spring GraphQL 2.0.5 snapshots See gh-51225 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 227b2b24a8a6..080ad7bd47c7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2605,7 +2605,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-framework/releases/tag/v{version}") } } - library("Spring GraphQL", "2.0.4") { + library("Spring GraphQL", "2.0.5-SNAPSHOT") { firstParty() group("org.springframework.graphql") { modules = [ From 99118ff5a3a7008d9b1bc7a760ce999344d8703f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 18:07:26 +0200 Subject: [PATCH 852/930] Start building against Spring gRPC 1.1.1 snapshots See gh-51226 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ff0a81428c28..34ce0317f7ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ mockitoVersion=5.23.0 nativeBuildToolsVersion=1.1.6 nullabilityPluginVersion=0.0.14 snakeYamlVersion=2.6 -springGrpcVersion=1.1.0 +springGrpcVersion=1.1.1-SNAPSHOT springFrameworkVersion=7.1.0-SNAPSHOT springFramework60xVersion=6.0.23 tomcatVersion=11.0.24 From f5d45c4581a29aa409992959bd70c9d2d5762600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 18:24:47 +0200 Subject: [PATCH 853/930] Upgrade to Couchbase Client 3.12.1 Closes gh-51227 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 080ad7bd47c7..d488bc1b4dc5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -312,7 +312,7 @@ bom { site("https://commons.apache.org/proper/commons-pool") } } - library("Couchbase Client", "3.11.3") { + library("Couchbase Client", "3.12.1") { group("com.couchbase.client") { modules = [ "java-client" From f074a30e856ea346d25e559a8a4ff417af9826af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 18:24:51 +0200 Subject: [PATCH 854/930] Upgrade to Elasticsearch Client 9.4.4 Closes gh-51228 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d488bc1b4dc5..1118e33cb002 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -401,7 +401,7 @@ bom { releaseNotes("https://github.com/ehcache/ehcache3/releases/tag/v{version}") } } - library("Elasticsearch Client", "9.4.2") { + library("Elasticsearch Client", "9.4.4") { prohibit { contains "-alpha" contains "-beta" From 9a79759a10b8a9a9765d84743b58b0d2f2dbdd76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 18:24:55 +0200 Subject: [PATCH 855/930] Upgrade to Grpc Java 1.81.1 Closes gh-51229 --- platform/spring-boot-dependencies/build.gradle | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1118e33cb002..9752b2b3c00c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -558,16 +558,10 @@ bom { site("https://groovy-lang.org") } } - library("Grpc Java", "1.81.0") { + library("Grpc Java", "1.81.1") { group("io.grpc") { bom("grpc-bom") } - alignWith { - version { - of "io.grpc:grpc-core" - from "org.springframework.grpc:spring-grpc-core" - } - } links { github("https://github.com/grpc/grpc-java") docs("https://grpc.io/docs/languages/java/") From 82e1ecacc21ba4f57a31eefb7a3fc926bd756fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 18:25:00 +0200 Subject: [PATCH 856/930] Upgrade to MongoDB 5.9.0 Closes gh-51230 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9752b2b3c00c..670738ebfb9d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1665,7 +1665,7 @@ bom { releaseNotes("https://github.com/mockito/mockito/releases/tag/v{version}") } } - library("MongoDB", "5.8.0") { + library("MongoDB", "5.9.0") { alignWith { version { of "org.mongodb:mongodb-driver-core" From ba2e9f078c62203e65c5c2447a20ebbc7e605e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 18:25:04 +0200 Subject: [PATCH 857/930] Upgrade to OpenTelemetry 1.64.0 Closes gh-51231 --- .../autoconfigure/otlp/OtlpTracingAutoConfigurationTests.java | 2 +- platform/spring-boot-dependencies/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfigurationTests.java index a4379ac7b5e0..5499a6af5914 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfigurationTests.java @@ -23,8 +23,8 @@ import javax.net.ssl.X509TrustManager; import io.opentelemetry.api.metrics.MeterProvider; -import io.opentelemetry.exporter.internal.compression.GzipCompressor; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.exporter.otlp.internal.GzipCompressor; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.sdk.trace.export.SpanExporter; import okhttp3.HttpUrl; diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 670738ebfb9d..50637fc56656 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1775,7 +1775,7 @@ bom { releaseNotes("https://github.com/netty/netty/releases/tag/netty-{version}") } } - library("OpenTelemetry", "1.62.0") { + library("OpenTelemetry", "1.64.0") { group("io.opentelemetry") { bom("opentelemetry-bom") } From 3d884c74f235994a47eaad35bdf1109cddbabb3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 18:25:08 +0200 Subject: [PATCH 858/930] Upgrade to Prometheus Client 1.8.0 Closes gh-51232 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 50637fc56656..dd10f915601b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1882,7 +1882,7 @@ bom { releaseNotes("https://github.com/pgjdbc/pgjdbc/releases/tag/REL{version}") } } - library("Prometheus Client", "1.7.0") { + library("Prometheus Client", "1.8.0") { group("io.prometheus") { bom("prometheus-metrics-bom") } From bc64b9cd70ae9a274f25d9501f7d522987d65b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 18:25:12 +0200 Subject: [PATCH 859/930] Upgrade to Protobuf Java 4.35.1 Closes gh-51233 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index dd10f915601b..df9eb045fd6d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1933,7 +1933,7 @@ bom { releaseNotes("https://qpid.apache.org/releases/qpid-protonj2-{version}/release-notes.html") } } - library("Protobuf Java", "4.35.0") { + library("Protobuf Java", "4.35.1") { group("com.google.protobuf") { bom("protobuf-bom") } From 6dabe4afa02791fb5f7393fa530b9c3820fd41a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 3 Aug 2026 19:17:52 +0200 Subject: [PATCH 860/930] Upgrade to RabbitMQ Client 5.34.0 Closes gh-51069 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index df9eb045fd6d..e95f16f60c6c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2060,7 +2060,7 @@ bom { releaseNotes("https://github.com/r2dbc/r2dbc-spi/releases/tag/v{version}") } } - library("RabbitMQ Client", "5.30.0") { + library("RabbitMQ Client", "5.34.0") { group("com.rabbitmq") { modules = [ "amqp-client" From 15dcd8208b02cfc96ef6d82152d304bcb039d737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 07:40:20 +0200 Subject: [PATCH 861/930] Start building against Micrometer 1.17.1 snapshots See gh-51251 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 3313e06c5c3a..caecb0e6ed06 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1615,7 +1615,7 @@ bom { releaseNotes("https://github.com/apache/maven-war-plugin/releases/tag/maven-war-plugin-{version}") } } - library("Micrometer", "1.17.0") { + library("Micrometer", "1.17.1-SNAPSHOT") { firstParty() group("io.micrometer") { modules = [ From 3c9a94c0208ff9c5d9764a9d94a3b1c5c3244862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 07:40:24 +0200 Subject: [PATCH 862/930] Start building against Micrometer Tracing 1.7.1 snapshots See gh-51252 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index caecb0e6ed06..76f79e9bf454 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1638,7 +1638,7 @@ bom { releaseNotes("https://github.com/micrometer-metrics/micrometer/releases/tag/v{version}") } } - library("Micrometer Tracing", "1.7.0") { + library("Micrometer Tracing", "1.7.1-SNAPSHOT") { firstParty() group("io.micrometer") { bom("micrometer-tracing-bom") From 951b32b83c3af646566fd68df33b84edb43348c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 07:40:28 +0200 Subject: [PATCH 863/930] Start building against Reactor Bom 2025.0.7 snapshots See gh-51253 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 76f79e9bf454..4b7ae9286711 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2085,7 +2085,7 @@ bom { ] } } - library("Reactor Bom", "2025.0.6") { + library("Reactor Bom", "2025.0.7-SNAPSHOT") { firstParty() calendarName = "Reactor" group("io.projectreactor") { From fcb413998fe22a02feeaeb969bd1110afed2715d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 07:40:34 +0200 Subject: [PATCH 864/930] Start building against Spring AMQP 4.1.1 snapshots See gh-51254 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4b7ae9286711..eba18dfe2cc1 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2516,7 +2516,7 @@ bom { ] } } - library("Spring AMQP", "4.1.0") { + library("Spring AMQP", "4.1.1-SNAPSHOT") { firstParty() group("org.springframework.amqp") { bom("spring-amqp-bom") From 4b141a424c741504fbde920b8cc9b5eb16814578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 07:40:38 +0200 Subject: [PATCH 865/930] Start building against Spring Batch 6.0.5 snapshots See gh-51255 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index eba18dfe2cc1..5d057cddd664 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2531,7 +2531,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-amqp/releases/tag/v{version}") } } - library("Spring Batch", "6.0.4") { + library("Spring Batch", "6.0.5-SNAPSHOT") { firstParty() group("org.springframework.batch") { bom("spring-batch-bom") From 981feedfea3fc464335d904894abbcd66be67fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 07:40:43 +0200 Subject: [PATCH 866/930] Start building against Spring Framework 7.0.9 snapshots See gh-51256 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f187bce843de..5c417d4e981c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,7 +22,7 @@ nativeBuildToolsVersion=1.1.6 nullabilityPluginVersion=0.0.14 snakeYamlVersion=2.6 springGrpcVersion=1.1.0 -springFrameworkVersion=7.0.8 +springFrameworkVersion=7.0.9-SNAPSHOT springFramework60xVersion=6.0.23 tomcatVersion=11.0.24 protobufGradlePluginVersion=0.9.6 From d93c4bf777d588558e6c2fafe889c9d103766708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 07:40:48 +0200 Subject: [PATCH 867/930] Start building against Spring GraphQL 2.0.5 snapshots See gh-51257 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 5d057cddd664..48033571980f 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2576,7 +2576,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-framework/releases/tag/v{version}") } } - library("Spring GraphQL", "2.0.4") { + library("Spring GraphQL", "2.0.5-SNAPSHOT") { firstParty() group("org.springframework.graphql") { modules = [ From 5aef05e330f964acf5ac48c46f86f03325e90d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 07:40:49 +0200 Subject: [PATCH 868/930] Start building against Spring gRPC 1.1.1 snapshots See gh-51171 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5c417d4e981c..d54ad2feab80 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ mockitoVersion=5.23.0 nativeBuildToolsVersion=1.1.6 nullabilityPluginVersion=0.0.14 snakeYamlVersion=2.6 -springGrpcVersion=1.1.0 +springGrpcVersion=1.1.1-SNAPSHOT springFrameworkVersion=7.0.9-SNAPSHOT springFramework60xVersion=6.0.23 tomcatVersion=11.0.24 From 66d61df8c6c2c1ff74855aaf7fff963afbb68276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 07:40:53 +0200 Subject: [PATCH 869/930] Start building against Spring Integration 7.1.1 snapshots See gh-51258 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 48033571980f..a82bd75034c7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2628,7 +2628,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-hateoas/releases/tag/{version}") } } - library("Spring Integration", "7.1.0") { + library("Spring Integration", "7.1.1-SNAPSHOT") { firstParty() group("org.springframework.integration") { bom("spring-integration-bom") From f642576a3b715395629c6574adbfbb72620aeb2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 07:40:58 +0200 Subject: [PATCH 870/930] Start building against Spring Kafka 4.1.1 snapshots See gh-51259 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a82bd75034c7..51a9e9eb6ca7 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2643,7 +2643,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-integration/releases/tag/v{version}") } } - library("Spring Kafka", "4.1.0") { + library("Spring Kafka", "4.1.1-SNAPSHOT") { firstParty() group("org.springframework.kafka") { modules = [ From 32fccfc18843551f6a50276fefff7b240784fbd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 07:41:03 +0200 Subject: [PATCH 871/930] Start building against Spring LDAP 4.1.1 snapshots See gh-51260 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 51a9e9eb6ca7..8eb8ad2b9097 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2661,7 +2661,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-kafka/releases/tag/v{version}") } } - library("Spring LDAP", "4.1.0") { + library("Spring LDAP", "4.1.1-SNAPSHOT") { firstParty() group("org.springframework.ldap") { modules = [ From 3c744d92789a2ad807fb385d98301928279ccf62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 07:46:54 +0200 Subject: [PATCH 872/930] Start building against Spring Security 7.1.1 snapshots See gh-51261 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8eb8ad2b9097..7832a23a3cb5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2711,7 +2711,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-restdocs/releases/tag/v{version}") } } - library("Spring Security", "7.1.0") { + library("Spring Security", "7.1.1-SNAPSHOT") { firstParty() group("org.springframework.security") { bom("spring-security-bom") From 59a6f56f602fb5f367b377ecf209cd51fc3c7472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 07:46:58 +0200 Subject: [PATCH 873/930] Start building against Spring Session 4.1.1 snapshots See gh-51262 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7832a23a3cb5..85b9efe9ad50 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2726,7 +2726,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-security/releases/tag/{version}") } } - library("Spring Session", "4.1.0") { + library("Spring Session", "4.1.1-SNAPSHOT") { firstParty() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) From 9d9de25df26758a849c89e9e129f50011a177fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 08:23:57 +0200 Subject: [PATCH 874/930] Upgrade to Grpc Java 1.81.1 Closes gh-51263 --- platform/spring-boot-dependencies/build.gradle | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 85b9efe9ad50..7ac1b4747ec5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -558,16 +558,10 @@ bom { site("https://groovy-lang.org") } } - library("Grpc Java", "1.81.0") { + library("Grpc Java", "1.81.1") { group("io.grpc") { bom("grpc-bom") } - alignWith { - version { - of "io.grpc:grpc-core" - from "org.springframework.grpc:spring-grpc-core" - } - } links { github("https://github.com/grpc/grpc-java") docs("https://grpc.io/docs/languages/java/") From d2395ca5692fde64b17e6810e196e0d6975d8e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 08:25:41 +0200 Subject: [PATCH 875/930] Upgrade to Protobuf Java 4.35.1 Closes gh-51264 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7ac1b4747ec5..77e0c63432fc 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1922,7 +1922,7 @@ bom { releaseNotes("https://qpid.apache.org/releases/qpid-protonj2-{version}/release-notes.html") } } - library("Protobuf Java", "4.35.0") { + library("Protobuf Java", "4.35.1") { group("com.google.protobuf") { bom("protobuf-bom") } From bb7d09b2aad6227c33ecb74b08af36854fec164b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 08:32:58 +0200 Subject: [PATCH 876/930] Upgrade to Kafka 4.3.1 Closes gh-51054 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index e95f16f60c6c..7f7862d01fc4 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1248,7 +1248,7 @@ bom { releaseNotes("https://junit.org/junit5/docs/{version}/release-notes") } } - library("Kafka", "4.2.1") { + library("Kafka", "4.3.1") { group("org.apache.kafka") { modules = [ "connect", From 19181e7a917112b4a4d6bb13e7a0f3d499f2aba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 16:00:57 +0200 Subject: [PATCH 877/930] Start building against Spring Batch 6.1.0-M1 snapshots See gh-51269 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7f7862d01fc4..86b528e86f2e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2554,7 +2554,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-amqp/releases/tag/v{version}") } } - library("Spring Batch", "6.0.4") { + library("Spring Batch", "6.1.0-SNAPSHOT") { firstParty() group("org.springframework.batch") { bom("spring-batch-bom") From ffa332c5bc45615cc592084cbea12bc1e5e52a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 16:01:01 +0200 Subject: [PATCH 878/930] Start building against Spring LDAP 4.1.1 snapshots See gh-51270 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 86b528e86f2e..4b4e5a18e576 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2684,7 +2684,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-kafka/releases/tag/v{version}") } } - library("Spring LDAP", "4.1.0") { + library("Spring LDAP", "4.1.1-SNAPSHOT") { firstParty() group("org.springframework.ldap") { modules = [ From 14e88872437646b088a94cdbf24af97e87094f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 16:01:06 +0200 Subject: [PATCH 879/930] Start building against Spring Security 7.1.1 snapshots See gh-51271 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 4b4e5a18e576..c83ea838ed5d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2734,7 +2734,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-restdocs/releases/tag/v{version}") } } - library("Spring Security", "7.1.0") { + library("Spring Security", "7.1.1-SNAPSHOT") { firstParty() group("org.springframework.security") { bom("spring-security-bom") From c9efe39904c7e0bdd5de338fd2b3184075e50a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 4 Aug 2026 16:01:10 +0200 Subject: [PATCH 880/930] Start building against Spring Session 4.1.1 snapshots See gh-51272 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c83ea838ed5d..13503ba6eaa4 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2749,7 +2749,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-security/releases/tag/{version}") } } - library("Spring Session", "4.1.0") { + library("Spring Session", "4.1.1-SNAPSHOT") { firstParty() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) From bf461b085dbb318b7dce270590e4ce99da76891b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Fri, 7 Aug 2026 11:04:46 +0200 Subject: [PATCH 881/930] Backoff from exposing gRPC server port if gRPC is not available This moves the feature from a unconditional application listener that can load gRPC types to an auto-configuration that backs off if the necessary classes are not present. Also updated the smoke tests to actually use the feature. Closes gh-50825 --- ... GrpcServerPortInfoAutoConfiguration.java} | 42 ++++++++++------- .../main/resources/META-INF/spring.factories | 3 -- ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...ServerPortInfoAutoConfigurationTests.java} | 45 ++++++++++--------- .../build.gradle | 2 +- ...GrpcServerNettyShadedApplicationTests.java | 31 +++---------- .../build.gradle | 2 +- .../SampleGrpcServerApplicationTests.java | 34 +++----------- 8 files changed, 65 insertions(+), 95 deletions(-) rename module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/{GrpcPortInfoApplicationContextInitializer.java => GrpcServerPortInfoAutoConfiguration.java} (66%) delete mode 100644 module/spring-boot-grpc-test/src/main/resources/META-INF/spring.factories create mode 100644 module/spring-boot-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/{GrpcPortInfoApplicationContextInitializerTests.java => GrpcServerPortInfoAutoConfigurationTests.java} (65%) diff --git a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializer.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/GrpcServerPortInfoAutoConfiguration.java similarity index 66% rename from module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializer.java rename to module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/GrpcServerPortInfoAutoConfiguration.java index 76f1e0a30673..80f0109b182c 100644 --- a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializer.java +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/GrpcServerPortInfoAutoConfiguration.java @@ -21,11 +21,14 @@ import io.grpc.Server; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.env.ConfigTreePropertySource.Value; import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.env.MapPropertySource; @@ -36,34 +39,41 @@ import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent; /** - * {@link ApplicationContextInitializer} that sets {@link Environment} properties for the - * ports that {@link Server gRPC servers} are actually listening on. The property - * {@literal "local.grpc.server.port"} can be injected directly into tests using - * {@link Value @Value} or obtained through the {@link Environment}. + * {@link EnableAutoConfiguration Auto-configuration} for an {@link ApplicationListener} + * that sets {@link Environment} properties for the port that a {@link Server gRPC server} + * is actually listening on. The property {@value PROPERTY_NAME} can be injected directly + * into tests using {@link Value @Value} or obtained through the {@link Environment}. * <p> * Properties are automatically propagated up to any parent context. * * @author Dave Syer * @author Chris Bono * @author Phillip Webb + * @author Stephane Nicoll + * @since 4.1.1 */ -class GrpcPortInfoApplicationContextInitializer - implements ApplicationContextInitializer<ConfigurableApplicationContext> { - - @Override - public void initialize(ConfigurableApplicationContext applicationContext) { - applicationContext.addApplicationListener(new Listener(applicationContext)); +@AutoConfiguration(beforeName = "org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration") +@ConditionalOnClass(GrpcServerStartedEvent.class) +public final class GrpcServerPortInfoAutoConfiguration { + + /** + * Property that contains the port that a {@link Server gRPC server} is actually + * listening on. + */ + public static final String PROPERTY_NAME = "local.grpc.server.port"; + + @Bean + GrpcPortInfoApplicationListener grpcPortInfoApplicationListener(ConfigurableApplicationContext applicationContext) { + return new GrpcPortInfoApplicationListener(applicationContext); } - private static class Listener implements ApplicationListener<GrpcServerStartedEvent> { - - private static final String PROPERTY_NAME = "local.grpc.server.port"; + static class GrpcPortInfoApplicationListener implements ApplicationListener<GrpcServerStartedEvent> { private static final String PROPERTY_SOURCE_NAME = "server.ports"; private final ConfigurableApplicationContext applicationContext; - Listener(ConfigurableApplicationContext applicationContext) { + GrpcPortInfoApplicationListener(ConfigurableApplicationContext applicationContext) { this.applicationContext = applicationContext; } diff --git a/module/spring-boot-grpc-test/src/main/resources/META-INF/spring.factories b/module/spring-boot-grpc-test/src/main/resources/META-INF/spring.factories deleted file mode 100644 index a9432eff7478..000000000000 --- a/module/spring-boot-grpc-test/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Application Context Initializers -org.springframework.context.ApplicationContextInitializer=\ -org.springframework.boot.grpc.test.autoconfigure.GrpcPortInfoApplicationContextInitializer diff --git a/module/spring-boot-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000000..6ff75cfc88f0 --- /dev/null +++ b/module/spring-boot-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springframework.boot.grpc.test.autoconfigure.GrpcServerPortInfoAutoConfiguration diff --git a/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializerTests.java b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/GrpcServerPortInfoAutoConfigurationTests.java similarity index 65% rename from module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializerTests.java rename to module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/GrpcServerPortInfoAutoConfigurationTests.java index 7e37a43647cd..73815d948377 100644 --- a/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/GrpcPortInfoApplicationContextInitializerTests.java +++ b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/GrpcServerPortInfoAutoConfigurationTests.java @@ -20,10 +20,10 @@ import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.grpc.test.autoconfigure.GrpcServerPortInfoAutoConfiguration.GrpcPortInfoApplicationListener; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.grpc.server.InProcessGrpcServerFactory; import org.springframework.grpc.server.NettyGrpcServerFactory; @@ -35,22 +35,36 @@ import static org.mockito.Mockito.mock; /** - * Tests for {@link GrpcPortInfoApplicationContextInitializer}. + * Tests for {@link GrpcServerPortInfoAutoConfiguration}. * * @author Phillip Webb */ -class GrpcPortInfoApplicationContextInitializerTests { +class GrpcServerPortInfoAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcServerPortInfoAutoConfiguration.class)); private static final String PORT_PROPERTY = "local.grpc.server.port"; @Test - void whenServerHasAddressInitializerSetsPortProperty() { + void createsGrpcPortInfoApplicationListenerBean() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GrpcPortInfoApplicationListener.class)); + } + + @Test + void whenNoGrpcServerStartedEventClassDoesNotCreateBean() { + this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerStartedEvent.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcPortInfoApplicationListener.class)); + } + + @Test + void whenServerHasAddressListenerSetsPortProperty() { NettyGrpcServerFactory factory = mock(); testListener(factory, 65535, "65535"); } @Test - void whenServerHasNoAddressInitializerSetsNoPortProperty() { + void whenServerHasNoAddressListenerSetsNoPortProperty() { NettyGrpcServerFactory factory = mock(); testListener(factory, -1, null); } @@ -68,25 +82,14 @@ void whenTestGrpcServerFactorySetsNoPortProperty() { } private void testListener(GrpcServerFactory factory, int port, @Nullable String expected) { - try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(Config.class)) { - context.getBean(GrpcPortInfoApplicationContextInitializer.class).initialize(context); + this.contextRunner.run((context) -> { GrpcServerLifecycle lifecycle = mock(); Server server = mock(); given(lifecycle.getFactory()).willReturn(factory); GrpcServerStartedEvent event = new GrpcServerStartedEvent(lifecycle, server, "localhost", port); context.publishEvent(event); assertThat(context.getEnvironment().getProperty(PORT_PROPERTY)).isEqualTo(expected); - } - } - - @Configuration(proxyBeanMethods = false) - static class Config { - - @Bean - GrpcPortInfoApplicationContextInitializer grpcPortInfoApplicationContextInitializer() { - return new GrpcPortInfoApplicationContextInitializer(); - } - + }); } } diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/build.gradle index 76fbfdca0dfa..3997c3e266aa 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/build.gradle @@ -28,7 +28,7 @@ dependencies { } implementation("io.grpc:grpc-netty-shaded") - dockerTestImplementation(project(":starter:spring-boot-starter-test")) + dockerTestImplementation(project(":starter:spring-boot-starter-grpc-server-test")) dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") } diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/dockerTest/java/smoketest/grpcservernettyshaded/SampleGrpcServerNettyShadedApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/dockerTest/java/smoketest/grpcservernettyshaded/SampleGrpcServerNettyShadedApplicationTests.java index 63af451767b1..8aa6a7667558 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/dockerTest/java/smoketest/grpcservernettyshaded/SampleGrpcServerNettyShadedApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-grpc-server-netty-shaded/src/dockerTest/java/smoketest/grpcservernettyshaded/SampleGrpcServerNettyShadedApplicationTests.java @@ -21,13 +21,9 @@ import org.testcontainers.containers.startupcheck.IndefiniteWaitOneShotStartupCheckStrategy; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; -import smoketest.grpcservernettyshaded.SampleGrpcServerNettyShadedApplicationTests.GrpcServerStartedEventListener; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.ApplicationListener; -import org.springframework.context.annotation.Import; -import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent; import static org.assertj.core.api.Assertions.assertThat; @@ -38,18 +34,16 @@ */ @SpringBootTest(properties = "spring.grpc.server.port=0") @Testcontainers(disabledWithoutDocker = true) -@Import(GrpcServerStartedEventListener.class) class SampleGrpcServerNettyShadedApplicationTests { - @Autowired - private GrpcServerStartedEventListener startedEventListener; + @Value("${local.grpc.server.port}") + private int port; @Test @SuppressWarnings("resource") void test() { - int port = this.startedEventListener.getPort(); - String address = "host.testcontainers.internal:" + port; - org.testcontainers.Testcontainers.exposeHostPorts(port); + String address = "host.testcontainers.internal:" + this.port; + org.testcontainers.Testcontainers.exposeHostPorts(this.port); try (GenericContainer<?> container = new GenericContainer<>( DockerImageName.parse("fullstorydev/grpcurl:v1.9.3")) .withCommand("-d", "{\"name\": \"spring\"}", "--plaintext", address, "HelloWorld/SayHello") @@ -60,19 +54,4 @@ void test() { } - static class GrpcServerStartedEventListener implements ApplicationListener<GrpcServerStartedEvent> { - - private int port; - - @Override - public void onApplicationEvent(GrpcServerStartedEvent event) { - this.port = event.getPort(); - } - - int getPort() { - return this.port; - } - - } - } diff --git a/smoke-test/spring-boot-smoke-test-grpc-server/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server/build.gradle index 28ab9ef252bf..b14151d0f484 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc-server/build.gradle @@ -25,7 +25,7 @@ description = "Spring Boot gRPC server smoke test" dependencies { implementation(project(":starter:spring-boot-starter-grpc-server")) - dockerTestImplementation(project(":starter:spring-boot-starter-test")) + dockerTestImplementation(project(":starter:spring-boot-starter-grpc-server-test")) dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") } diff --git a/smoke-test/spring-boot-smoke-test-grpc-server/src/dockerTest/java/smoketest/grpcserver/SampleGrpcServerApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-server/src/dockerTest/java/smoketest/grpcserver/SampleGrpcServerApplicationTests.java index ca79ef5cf5dc..c36a5fa1aa81 100644 --- a/smoke-test/spring-boot-smoke-test-grpc-server/src/dockerTest/java/smoketest/grpcserver/SampleGrpcServerApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-grpc-server/src/dockerTest/java/smoketest/grpcserver/SampleGrpcServerApplicationTests.java @@ -21,35 +21,30 @@ import org.testcontainers.containers.startupcheck.IndefiniteWaitOneShotStartupCheckStrategy; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; -import smoketest.grpcserver.SampleGrpcServerApplicationTests.GrpcServerStartedEventListener; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.ApplicationListener; -import org.springframework.context.annotation.Import; -import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent; import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for the default Spring gRPC netty server. + * Integration tests for the default Spring gRPC Netty server. * * @author Phillip Webb + * @author Stephane Nicoll */ @SpringBootTest(properties = "spring.grpc.server.port=0") @Testcontainers(disabledWithoutDocker = true) -@Import(GrpcServerStartedEventListener.class) class SampleGrpcServerApplicationTests { - @Autowired - private GrpcServerStartedEventListener startedEventListener; + @Value("${local.grpc.server.port}") + private int port; @Test @SuppressWarnings("resource") void test() { - int port = this.startedEventListener.getPort(); - String address = "host.testcontainers.internal:" + port; - org.testcontainers.Testcontainers.exposeHostPorts(port); + String address = "host.testcontainers.internal:" + this.port; + org.testcontainers.Testcontainers.exposeHostPorts(this.port); try (GenericContainer<?> container = new GenericContainer<>( DockerImageName.parse("fullstorydev/grpcurl:v1.9.3")) .withNetworkAliases("") @@ -61,19 +56,4 @@ void test() { } - static class GrpcServerStartedEventListener implements ApplicationListener<GrpcServerStartedEvent> { - - private int port; - - @Override - public void onApplicationEvent(GrpcServerStartedEvent event) { - this.port = event.getPort(); - } - - int getPort() { - return this.port; - } - - } - } From 41c857c4dd4eaae07128b40e42acc85166293780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Fri, 7 Aug 2026 14:17:17 +0200 Subject: [PATCH 882/930] Upgrade to Couchbase Client 3.12.2 Closes gh-51295 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 13503ba6eaa4..f0147bcbd7db 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -312,7 +312,7 @@ bom { site("https://commons.apache.org/proper/commons-pool") } } - library("Couchbase Client", "3.12.1") { + library("Couchbase Client", "3.12.2") { group("com.couchbase.client") { modules = [ "java-client" From 2af4680e4203890f0fe109f29bc825593302bc55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Fri, 7 Aug 2026 14:17:22 +0200 Subject: [PATCH 883/930] Upgrade to Netty 4.2.17.Final Closes gh-51296 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index f0147bcbd7db..1d13cc79855c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1759,7 +1759,7 @@ bom { releaseNotes("https://github.com/neo4j/neo4j-java-driver/releases/tag/{version}") } } - library("Netty", "4.2.16.Final") { + library("Netty", "4.2.17.Final") { prohibit { contains ".Alpha" contains ".Beta" From c35043626cde1db852f0510de3af0beff14dc935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Fri, 7 Aug 2026 14:23:48 +0200 Subject: [PATCH 884/930] Upgrade to Jetty 12.1.12 Closes gh-51297 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1d13cc79855c..9a3777676543 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1126,7 +1126,7 @@ bom { ] } } - library("Jetty", "12.1.11") { + library("Jetty", "12.1.12") { prohibit { contains ".alpha" because "we don't want alpha dependencies" From 50ecfcbe5bc12939d629867f7ff3aaffccbb4edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Fri, 7 Aug 2026 15:59:26 +0200 Subject: [PATCH 885/930] Upgrade to Jetty 12.1.12 Closes gh-51302 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 77e0c63432fc..758c2d9dafae 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1115,7 +1115,7 @@ bom { ] } } - library("Jetty", "12.1.11") { + library("Jetty", "12.1.12") { prohibit { contains ".alpha" because "we don't want alpha dependencies" From 62397abb5765548937aeb033a15d0289b3ec2d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Fri, 7 Aug 2026 15:59:31 +0200 Subject: [PATCH 886/930] Upgrade to jOOQ 3.21.7 Closes gh-51303 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 758c2d9dafae..1d10c2dbfd87 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1139,7 +1139,7 @@ bom { ] } } - library("jOOQ", "3.21.6") { + library("jOOQ", "3.21.7") { group("org.jooq") { bom("jooq-bom") plugins = [ From d8621eb4ef304d3a02cd27cdaba3c2b85df4edef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Fri, 7 Aug 2026 15:59:36 +0200 Subject: [PATCH 887/930] Upgrade to Native Build Tools Plugin 1.1.8 Closes gh-51304 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d54ad2feab80..aea308b692c8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ junitJupiterVersion=6.0.3 kotlinVersion=2.3.21 mavenVersion=3.9.13 mockitoVersion=5.23.0 -nativeBuildToolsVersion=1.1.6 +nativeBuildToolsVersion=1.1.8 nullabilityPluginVersion=0.0.14 snakeYamlVersion=2.6 springGrpcVersion=1.1.1-SNAPSHOT From db56096c614b40611cd0a8ed17fca6e19642b2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Fri, 7 Aug 2026 15:59:42 +0200 Subject: [PATCH 888/930] Upgrade to Netty 4.2.17.Final Closes gh-51305 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1d10c2dbfd87..a6c74bf8306a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1748,7 +1748,7 @@ bom { releaseNotes("https://github.com/neo4j/neo4j-java-driver/releases/tag/{version}") } } - library("Netty", "4.2.16.Final") { + library("Netty", "4.2.17.Final") { prohibit { contains ".Alpha" contains ".Beta" From 7ce8f7b01dff6b03b2518848083327fce5548e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Fri, 7 Aug 2026 15:59:47 +0200 Subject: [PATCH 889/930] Upgrade to Pulsar 4.2.4 Closes gh-51306 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a6c74bf8306a..8fe0b7cfd142 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1898,7 +1898,7 @@ bom { releaseNotes("https://github.com/prometheus/client_java/releases/tag/parent-{version}") } } - library("Pulsar", "4.2.3") { + library("Pulsar", "4.2.4") { group("org.apache.pulsar") { bom("pulsar-bom") } From c968710576bb265297eccce017ab1468cd9300e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Fri, 7 Aug 2026 15:59:51 +0200 Subject: [PATCH 890/930] Upgrade to Yasson 3.0.5 Closes gh-51307 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 8fe0b7cfd142..6fb41cc3fe86 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2949,7 +2949,7 @@ bom { releaseNotes("https://github.com/xmlunit/xmlunit/releases/tag/v{version}") } } - library("Yasson", "3.0.4") { + library("Yasson", "3.0.5") { group("org.eclipse") { modules = [ "yasson" From bb23ef72b6a72fd5bfedb4775646072510533313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Fri, 7 Aug 2026 14:15:02 +0200 Subject: [PATCH 891/930] Add testClasspath option to run and start goals Deprecate the `useTestClasspath` flag in favor of a `testClasspath` parameter on the `run` and `start` goals. It accepts `off`, `dependencies`, or `all`, with `all` also adding the project's test classes directory to the classpath so that a main class located in `src/test/java` can be found and run, matching the `test-run` goal's existing behaviour. Closes gh-36115 --- .../boot/maven/RunIntegrationTests.java | 23 ++++++++ .../boot/maven/StartStopIntegrationTests.java | 12 +++- .../projects/run-test-classpath-all/pom.xml | 33 +++++++++++ .../test/java/org/test/SampleApplication.java | 33 +++++++++++ .../run-test-classpath-dependencies/pom.xml | 33 +++++++++++ .../main/java/org/test/SampleApplication.java | 38 ++++++++++++ .../src/test/java/org/test/SampleTest.java | 21 +++++++ .../projects/run-test-classpath-off/pom.xml | 33 +++++++++++ .../main/java/org/test/SampleApplication.java | 39 ++++++++++++ .../src/test/java/org/test/SampleTest.java | 21 +++++++ .../intTest/projects/start-stop-test/pom.xml | 59 +++++++++++++++++++ .../java/org/test/SpringApplicationAdmin.java | 37 ++++++++++++ .../test/SpringApplicationAdminMXBean.java | 25 ++++++++ .../test/java/org/test/SampleApplication.java | 54 +++++++++++++++++ .../boot/maven/AbstractRunMojo.java | 50 +++++++++++++++- .../springframework/boot/maven/RunMojo.java | 34 +++++++++-- .../springframework/boot/maven/StartMojo.java | 33 +++++++++-- .../boot/maven/TestRunMojo.java | 20 +------ 18 files changed, 568 insertions(+), 30 deletions(-) create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-all/pom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-all/src/test/java/org/test/SampleApplication.java create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/pom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/src/main/java/org/test/SampleApplication.java create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/src/test/java/org/test/SampleTest.java create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/pom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/src/main/java/org/test/SampleApplication.java create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/src/test/java/org/test/SampleTest.java create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/pom.xml create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/main/java/org/test/SpringApplicationAdmin.java create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/main/java/org/test/SpringApplicationAdminMXBean.java create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/test/java/org/test/SampleApplication.java diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java index bf9e6a214e37..f5d0bd15e53a 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java @@ -95,8 +95,31 @@ void whenProfilesAreConfiguredTheyArePassedToTheApplication(MavenBuild mavenBuil } @TestTemplate + @Deprecated(since = "4.2.0", forRemoval = true) void whenUseTestClasspathIsEnabledTheApplicationHasTestDependenciesOnItsClasspath(MavenBuild mavenBuild) { mavenBuild.project("run-use-test-classpath") + .goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run") + .contains("useTestClasspath is deprecated, use testClasspath instead.")); + } + + @TestTemplate + void whenTestClasspathIsDependenciesTheApplicationHasTestDependenciesOnItsClasspath(MavenBuild mavenBuild) { + mavenBuild.project("run-test-classpath-dependencies") + .goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); + } + + @TestTemplate + void whenTestClasspathIsAllTheApplicationHasTestAndClassesDependenciesOnItsClasspath(MavenBuild mavenBuild) { + mavenBuild.project("run-test-classpath-all") + .goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); + } + + @TestTemplate + void whenTestClasspathIsOffTheApplicationHasNeitherTestNorClassesDependenciesOnItsClasspath(MavenBuild mavenBuild) { + mavenBuild.project("run-test-classpath-off") .goals("spring-boot:run") .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); } diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java index 5e6b8efe1b04..acd0b6c215e1 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java @@ -34,7 +34,17 @@ class StartStopIntegrationTests { @TestTemplate void startStopWaitsForApplicationToBeReadyAndThenRequestsShutdown(MavenBuild mavenBuild) { - mavenBuild.project("start-stop") + testStartStop(mavenBuild, "start-stop"); + + } + + @TestTemplate + void startStopWithTestClasspathWaitsForApplicationToBeReadyAndThenRequestsShutdown(MavenBuild mavenBuild) { + testStartStop(mavenBuild, "start-stop-test"); + } + + private void testStartStop(MavenBuild mavenBuild, String projectId) { + mavenBuild.project(projectId) .goals("verify") .execute((project) -> assertThat(buildLog(project)).contains("isReady: true") .contains("Shutdown requested")); diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-all/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-all/pom.xml new file mode 100644 index 000000000000..4fedfc6053c9 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-all/pom.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>org.springframework.boot.maven.it</groupId> + <artifactId>run-test-classpath-all</artifactId> + <version>0.0.1.BUILD-SNAPSHOT</version> + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <maven.compiler.source>@java.version@</maven.compiler.source> + <maven.compiler.target>@java.version@</maven.compiler.target> + </properties> + <build> + <plugins> + <plugin> + <groupId>@project.groupId@</groupId> + <artifactId>@project.artifactId@</artifactId> + <version>@project.version@</version> + <configuration> + <testClasspath>all</testClasspath> + </configuration> + </plugin> + </plugins> + </build> + <dependencies> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-context</artifactId> + <version>@spring-framework.version@</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-all/src/test/java/org/test/SampleApplication.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-all/src/test/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..19e05c51e088 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-all/src/test/java/org/test/SampleApplication.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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) { + + Class<?> appContext = null; + try { + appContext = Class.forName("org.springframework.context.ApplicationContext"); + } + catch (ClassNotFoundException e) { + throw new IllegalStateException("Test dependencies not added to classpath", e); + } + System.out.println("I haz been run"); + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/pom.xml new file mode 100644 index 000000000000..5bfc72a9110f --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/pom.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>org.springframework.boot.maven.it</groupId> + <artifactId>run-test-classpath-dependencies</artifactId> + <version>0.0.1.BUILD-SNAPSHOT</version> + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <maven.compiler.source>@java.version@</maven.compiler.source> + <maven.compiler.target>@java.version@</maven.compiler.target> + </properties> + <build> + <plugins> + <plugin> + <groupId>@project.groupId@</groupId> + <artifactId>@project.artifactId@</artifactId> + <version>@project.version@</version> + <configuration> + <testClasspath>dependencies</testClasspath> + </configuration> + </plugin> + </plugins> + </build> + <dependencies> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-context</artifactId> + <version>@spring-framework.version@</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/src/main/java/org/test/SampleApplication.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..e2f44e459209 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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) { + try { + Class.forName("org.springframework.context.ApplicationContext"); + } + catch (ClassNotFoundException e) { + throw new IllegalStateException("Test dependencies not added to classpath", e); + } + try { + Class.forName("org.test.SampleTest"); + throw new IllegalStateException("Test classes should not have been added to classpath"); + } + catch (ClassNotFoundException e) { + // OK + } + System.out.println("I haz been run"); + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/src/test/java/org/test/SampleTest.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/src/test/java/org/test/SampleTest.java new file mode 100644 index 000000000000..0fd0a1474a30 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/src/test/java/org/test/SampleTest.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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 SampleTest { + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/pom.xml new file mode 100644 index 000000000000..69d1b00a2fd4 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/pom.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>org.springframework.boot.maven.it</groupId> + <artifactId>run-test-classpath-off</artifactId> + <version>0.0.1.BUILD-SNAPSHOT</version> + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <maven.compiler.source>@java.version@</maven.compiler.source> + <maven.compiler.target>@java.version@</maven.compiler.target> + </properties> + <build> + <plugins> + <plugin> + <groupId>@project.groupId@</groupId> + <artifactId>@project.artifactId@</artifactId> + <version>@project.version@</version> + <configuration> + <testClasspath>off</testClasspath> + </configuration> + </plugin> + </plugins> + </build> + <dependencies> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-context</artifactId> + <version>@spring-framework.version@</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/src/main/java/org/test/SampleApplication.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..725d88351caa --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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) { + try { + Class.forName("org.springframework.context.ApplicationContext"); + throw new IllegalStateException("Test dependencies should not have been added to classpath"); + } + catch (ClassNotFoundException e) { + // OK + } + try { + Class.forName("org.test.SampleTest"); + throw new IllegalStateException("Test classes should not have been added to classpath"); + } + catch (ClassNotFoundException e) { + // OK + } + System.out.println("I haz been run"); + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/src/test/java/org/test/SampleTest.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/src/test/java/org/test/SampleTest.java new file mode 100644 index 000000000000..0fd0a1474a30 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/src/test/java/org/test/SampleTest.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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 SampleTest { + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/pom.xml new file mode 100644 index 000000000000..6613cb47a999 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/pom.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>org.springframework.boot.maven.it</groupId> + <artifactId>start-stop-test</artifactId> + <version>0.0.1.BUILD-SNAPSHOT</version> + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <maven.compiler.source>@java.version@</maven.compiler.source> + <maven.compiler.target>@java.version@</maven.compiler.target> + </properties> + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <version>@build-helper-maven-plugin.version@</version> + <executions> + <execution> + <id>reserve-jmx-port</id> + <goals> + <goal>reserve-network-port</goal> + </goals> + <phase>process-resources</phase> + <configuration> + <portNames> + <portName>jmx.port</portName> + </portNames> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>@project.groupId@</groupId> + <artifactId>@project.artifactId@</artifactId> + <version>@project.version@</version> + <executions> + <execution> + <id>pre-integration-test</id> + <goals> + <goal>start</goal> + </goals> + </execution> + <execution> + <id>post-integration-test</id> + <goals> + <goal>stop</goal> + </goals> + </execution> + </executions> + <configuration> + <jmxPort>${jmx.port}</jmxPort> + <testClasspath>all</testClasspath> + </configuration> + </plugin> + </plugins> + </build> +</project> diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/main/java/org/test/SpringApplicationAdmin.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/main/java/org/test/SpringApplicationAdmin.java new file mode 100644 index 000000000000..b56865a700c5 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/main/java/org/test/SpringApplicationAdmin.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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; + +final class SpringApplicationAdmin implements SpringApplicationAdminMXBean { + + boolean ready; + + boolean shutdownInvoked; + + @Override + public boolean isReady() { + System.out.println("isReady: " + this.ready); + return this.ready; + } + + @Override + public void shutdown() { + this.shutdownInvoked = true; + System.out.println("Shutdown requested"); + } + +} \ No newline at end of file diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/main/java/org/test/SpringApplicationAdminMXBean.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/main/java/org/test/SpringApplicationAdminMXBean.java new file mode 100644 index 000000000000..8c240fdaab64 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/main/java/org/test/SpringApplicationAdminMXBean.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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 interface SpringApplicationAdminMXBean { + + boolean isReady(); + + void shutdown(); + +} \ No newline at end of file diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/test/java/org/test/SampleApplication.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/test/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..f2008431c359 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/test/java/org/test/SampleApplication.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.lang.management.ManagementFactory; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +/** + * This sample app simulates the JMX Mbean that is exposed by the Spring Boot application. + */ +public class SampleApplication { + + private static final Object lock = new Object(); + + public static void main(String[] args) throws Exception { + MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); + ObjectName name = new ObjectName( + "org.springframework.boot:type=Admin,name=SpringApplication"); + SpringApplicationAdmin mbean = new SpringApplicationAdmin(); + mbs.registerMBean(mbean, name); + + // Flag the app as ready + mbean.ready = true; + + int waitAttempts = 0; + while (!mbean.shutdownInvoked) { + if (waitAttempts > 30) { + throw new IllegalStateException( + "Shutdown should have been invoked by now"); + } + synchronized (lock) { + lock.wait(250); + } + waitAttempts++; + } + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java index 08feb075567f..cb1ce5f26461 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java @@ -205,6 +205,16 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { @SuppressWarnings("NullAway.Init") private File classesDirectory; + /** + * Directory containing the test classes and resource files that can be included when + * running the application. + * + * @since 4.2.0 + */ + @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true) + @SuppressWarnings("NullAway.Init") + private File testClassesDirectory; + /** * Skip the execution. * @@ -241,10 +251,19 @@ private String determineMainClass() throws MojoExecutionException { * @since 3.1.0 */ protected List<File> getClassesDirectories() { - return List.of(this.classesDirectory); + List<File> classesDirectories = new ArrayList<>(); + if (testClasspath() == TestClasspath.ALL) { + classesDirectories.add(this.testClassesDirectory); + } + classesDirectories.add(this.classesDirectory); + return Collections.unmodifiableList(classesDirectories); } - protected abstract boolean isUseTestClasspath(); + /** + * Return the {@link TestClasspath} strategy to use. + * @return the test classpath strategy + */ + protected abstract TestClasspath testClasspath(); private void run(String startClassName) throws MojoExecutionException, MojoFailureException { List<String> args = new ArrayList<>(); @@ -416,7 +435,9 @@ private void addProjectClasses(List<URL> urls) throws MalformedURLException { } private void addDependencies(List<URL> urls) throws MalformedURLException, MojoExecutionException { - Set<Artifact> artifacts = (isUseTestClasspath()) ? filterDependencies(this.project.getArtifacts()) + TestClasspath testClasspath = testClasspath(); + Set<Artifact> artifacts = ((testClasspath == TestClasspath.ALL || testClasspath == TestClasspath.DEPENDENCIES)) + ? filterDependencies(this.project.getArtifacts()) : filterDependencies(this.project.getArtifacts(), new ExcludeTestScopeArtifactFilter()); for (Artifact artifact : artifacts) { if (artifact.getFile() != null) { @@ -432,4 +453,27 @@ private void logArguments(String name, String[] args) { } } + /** + * Strategies to handle the test classpath when running an application. + */ + protected enum TestClasspath { + + /** + * Do not include the test classpath at all. + */ + OFF, + + /** + * Include only dependencies with test scope. + */ + DEPENDENCIES, + + /** + * Include the full test classpath (test classes and dependencies with test + * scope). + */ + ALL + + } + } diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java index 8992ed3d55f6..ee6332ca0760 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java @@ -18,6 +18,7 @@ import java.io.File; import java.util.List; +import java.util.Locale; import java.util.Map; import javax.inject.Inject; @@ -30,6 +31,7 @@ import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.toolchain.ToolchainManager; +import org.jspecify.annotations.Nullable; import org.springframework.boot.loader.tools.RunProcess; @@ -57,9 +59,24 @@ public class RunMojo extends AbstractRunMojo { /** * Flag to include the test classpath when running. * @since 1.3.0 + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of {@code testClasspath} */ - @Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false") - private boolean useTestClasspath; + @Parameter(property = "spring-boot.run.useTestClasspath") + @Deprecated(since = "4.2.0", forRemoval = true) + private @Nullable Boolean useTestClasspath; + + /** + * Strategy to determine how the test classpath should be included when running the + * application. Available values are {@code off} to not include the test classpath at + * all, {@code dependencies} to include only dependencies with test scope, and + * {@code all} to include the full test classpath (test classes and dependencies with + * test scope). + * @since 4.2.0 + */ + @Parameter(property = "spring-boot.run.testClasspath", defaultValue = "off") + private String testClasspath = "off"; + + private @Nullable TestClasspath resolvedTestClasspath; @Inject public RunMojo(ToolchainManager toolchainManager) { @@ -85,8 +102,17 @@ protected void run(JavaProcessExecutor processExecutor, File workingDirectory, L } @Override - protected boolean isUseTestClasspath() { - return this.useTestClasspath; + protected TestClasspath testClasspath() { + if (this.resolvedTestClasspath == null) { + if (this.useTestClasspath != null) { + getLog().warn("useTestClasspath is deprecated, use testClasspath instead."); + this.resolvedTestClasspath = (this.useTestClasspath) ? TestClasspath.DEPENDENCIES : TestClasspath.OFF; + } + else { + this.resolvedTestClasspath = TestClasspath.valueOf(this.testClasspath.toUpperCase(Locale.ROOT)); + } + } + return this.resolvedTestClasspath; } private static final class RunProcessKiller implements Runnable { diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java index 728e663546c0..39fd8a6605f0 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java @@ -21,6 +21,7 @@ import java.net.ConnectException; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.Callable; @@ -91,9 +92,24 @@ public class StartMojo extends AbstractRunMojo { /** * Flag to include the test classpath when running. + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of {@code testClasspath} */ - @Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false") - private boolean useTestClasspath; + @Parameter(property = "spring-boot.run.useTestClasspath") + @Deprecated(since = "4.2.0", forRemoval = true) + private @Nullable Boolean useTestClasspath; + + /** + * Strategy to determine how the test classpath should be included when running the + * application. Available values are {@code off} to not include the test classpath at + * all, {@code dependencies} to include only dependencies with test scope, and + * {@code all} to include the full test classpath (test classes and dependencies with + * test scope). + * @since 4.2.0 + */ + @Parameter(property = "spring-boot.run.testClasspath", defaultValue = "off") + private String testClasspath = "off"; + + private @Nullable TestClasspath resolvedTestClasspath; @Inject public StartMojo(ToolchainManager toolchainManager) { @@ -205,8 +221,17 @@ public <T> T execute(long wait, int maxAttempts, Callable<@Nullable T> callback) } @Override - protected boolean isUseTestClasspath() { - return this.useTestClasspath; + protected TestClasspath testClasspath() { + if (this.resolvedTestClasspath == null) { + if (this.useTestClasspath != null) { + getLog().warn("useTestClasspath is deprecated, use testClasspath instead."); + this.resolvedTestClasspath = (this.useTestClasspath) ? TestClasspath.DEPENDENCIES : TestClasspath.OFF; + } + else { + this.resolvedTestClasspath = TestClasspath.valueOf(this.testClasspath.toUpperCase(Locale.ROOT)); + } + } + return this.resolvedTestClasspath; } private class CreateJmxConnector implements Callable<@Nullable JMXConnector> { diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java index 4be0c6185e6a..9ece33a41c63 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java @@ -17,7 +17,6 @@ package org.springframework.boot.maven; import java.io.File; -import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -57,29 +56,14 @@ public class TestRunMojo extends AbstractRunMojo { @Parameter(property = "spring-boot.test-run.optimizedLaunch", defaultValue = "true") private boolean optimizedLaunch; - /** - * Directory containing the test classes and resource files that should be used to run - * the application. - */ - @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true) - @SuppressWarnings("NullAway.Init") - private File testClassesDirectory; - @Inject public TestRunMojo(ToolchainManager toolchainManager) { super(toolchainManager); } @Override - protected List<File> getClassesDirectories() { - ArrayList<File> classesDirectories = new ArrayList<>(super.getClassesDirectories()); - classesDirectories.add(0, this.testClassesDirectory); - return classesDirectories; - } - - @Override - protected boolean isUseTestClasspath() { - return true; + protected TestClasspath testClasspath() { + return TestClasspath.ALL; } @Override From 68fcf1cc87493ed0cfd891e50f9bc2093ac21eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Sat, 8 Aug 2026 18:43:33 +0200 Subject: [PATCH 892/930] Upgrade to HtmlUnit 5.4.0 Closes gh-51313 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 9a3777676543..6495889e2a70 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -700,7 +700,7 @@ bom { ] } } - library("HtmlUnit", "5.3.0") { + library("HtmlUnit", "5.4.0") { group("org.htmlunit") { modules = [ "htmlunit" From 8e4ded44ea327cd8ab27e9e81f6b362389c22136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Sat, 8 Aug 2026 18:43:38 +0200 Subject: [PATCH 893/930] Upgrade to jOOQ 3.21.7 Closes gh-51314 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6495889e2a70..30af317c885b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1150,7 +1150,7 @@ bom { ] } } - library("jOOQ", "3.21.6") { + library("jOOQ", "3.21.7") { group("org.jooq") { bom("jooq-bom") plugins = [ From cca26a6d0e17fa3c33837c93f6b557ad2064f4af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Sat, 8 Aug 2026 18:43:43 +0200 Subject: [PATCH 894/930] Upgrade to JUnit Jupiter 6.1.3 Closes gh-51315 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 34ce0317f7ee..b3c64ea1e6b8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ hamcrestVersion=3.0 jackson2Version=2.21.5 jacksonVersion=3.1.5 javaFormatVersion=0.0.47 -junitJupiterVersion=6.1.2 +junitJupiterVersion=6.1.3 kotlinVersion=2.4.10 mavenVersion=3.9.13 mockitoVersion=5.23.0 From 9dedbf52bf71cbd083a805e2a025f6f2d7537c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Sat, 8 Aug 2026 18:43:48 +0200 Subject: [PATCH 895/930] Upgrade to Native Build Tools Plugin 1.1.8 Closes gh-51316 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b3c64ea1e6b8..7142a8223e76 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ junitJupiterVersion=6.1.3 kotlinVersion=2.4.10 mavenVersion=3.9.13 mockitoVersion=5.23.0 -nativeBuildToolsVersion=1.1.6 +nativeBuildToolsVersion=1.1.8 nullabilityPluginVersion=0.0.14 snakeYamlVersion=2.6 springGrpcVersion=1.1.1-SNAPSHOT From f1f4a345a88f7f0a78b293d415b673e467260881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Sat, 8 Aug 2026 18:43:51 +0200 Subject: [PATCH 896/930] Upgrade to Qpid ProtonJ2 1.2.0 Closes gh-51317 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 30af317c885b..7e3087dd38e3 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1920,7 +1920,7 @@ bom { releaseNotes("https://pulsar.apache.org/release-notes/versioned/pulsar-{version}") } } - library("Qpid ProtonJ2", "1.1.0") { + library("Qpid ProtonJ2", "1.2.0") { group("org.apache.qpid") { modules = [ "protonj2", From c38bfe4a81bbb7eade7f83923d0d7737c0a0553a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Sat, 8 Aug 2026 18:43:56 +0200 Subject: [PATCH 897/930] Upgrade to RabbitMQ AMQP Client 1.4.0 Closes gh-51318 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7e3087dd38e3..b482cb114038 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2072,7 +2072,7 @@ bom { releaseNotes("https://github.com/rabbitmq/rabbitmq-java-client/releases/tag/v{version}") } } - library("RabbitMQ AMQP Client", "1.3.0") { + library("RabbitMQ AMQP Client", "1.4.0") { group("com.rabbitmq.client") { modules = [ "amqp-client" From 522d3cf21364bdd6a789813eb6503a798963559a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Sat, 8 Aug 2026 18:44:01 +0200 Subject: [PATCH 898/930] Upgrade to RabbitMQ Stream Client 1.9.0 Closes gh-51319 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index b482cb114038..7dcf38270470 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2084,7 +2084,7 @@ bom { releaseNotes("https://github.com/rabbitmq/rabbitmq-amqp-java-client/releases/tag/v{version}") } } - library("RabbitMQ Stream Client", "1.8.0") { + library("RabbitMQ Stream Client", "1.9.0") { group("com.rabbitmq") { modules = [ "stream-client" From 5129a44c652f3f59180e0e79aa4f03526c980ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Sat, 8 Aug 2026 18:44:05 +0200 Subject: [PATCH 899/930] Upgrade to Yasson 3.0.5 Closes gh-51320 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7dcf38270470..93d6b96a20e4 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2978,7 +2978,7 @@ bom { releaseNotes("https://github.com/xmlunit/xmlunit/releases/tag/v{version}") } } - library("Yasson", "3.0.4") { + library("Yasson", "3.0.5") { group("org.eclipse") { modules = [ "yasson" From ee3c48d00ffa6efda14e730c02ba145dc15e0fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Sat, 8 Aug 2026 18:55:17 +0200 Subject: [PATCH 900/930] Upgrade to Hazelcast 5.7.0 Closes gh-51309 --- platform/spring-boot-dependencies/build.gradle | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 93d6b96a20e4..ceff499904b9 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -618,11 +618,7 @@ bom { releaseNotes("https://github.com/hamcrest/JavaHamcrest/releases/tag/v{version}") } } - library("Hazelcast", "5.5.0") { - prohibit { - versionRange "[5.6.0,)" - because "https://github.com/hazelcast/hazelcast/issues/26546" - } + library("Hazelcast", "5.7.0") { group("com.hazelcast") { modules = [ "hazelcast", From 84f8a6ff357ba307e704dbc19a4df7d33161e74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 14:37:10 +0200 Subject: [PATCH 901/930] Polish "Backoff from exposing gRPC server port if gRPC is not available" See gh-50825 --- .../autoconfigure/GrpcServerPortInfoAutoConfiguration.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/GrpcServerPortInfoAutoConfiguration.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/GrpcServerPortInfoAutoConfiguration.java index 80f0109b182c..74e92610c076 100644 --- a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/GrpcServerPortInfoAutoConfiguration.java +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/GrpcServerPortInfoAutoConfiguration.java @@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.env.ConfigTreePropertySource.Value; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; @@ -52,7 +53,7 @@ * @author Stephane Nicoll * @since 4.1.1 */ -@AutoConfiguration(beforeName = "org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration") +@AutoConfiguration(before = GrpcServerAutoConfiguration.class) @ConditionalOnClass(GrpcServerStartedEvent.class) public final class GrpcServerPortInfoAutoConfiguration { From 90df8591bb09ca189b4ff9d1e75e2be97a81bcda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 16:53:33 +0200 Subject: [PATCH 902/930] Start building against Spring Pulsar 2.0.7 snapshots See gh-51328 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6fb41cc3fe86..bc2f33622330 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2675,7 +2675,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ldap/releases/tag/{version}") } } - library("Spring Pulsar", "2.0.6") { + library("Spring Pulsar", "2.0.7-SNAPSHOT") { firstParty() group("org.springframework.pulsar") { bom("spring-pulsar-bom") From 230afd26763a46c93aea1bed4daa1278e6e6dd4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 16:54:48 +0200 Subject: [PATCH 903/930] Upgrade to ActiveMQ 6.2.9 Closes gh-51329 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index bc2f33622330..0d253752fe1b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -31,7 +31,7 @@ bom { ] } } - library("ActiveMQ", "6.2.8") { + library("ActiveMQ", "6.2.9") { group("org.apache.activemq") { modules = [ "activemq-console", From c9edd316230dbb4d6c7aafaf5c58ff903d28573b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 16:54:53 +0200 Subject: [PATCH 904/930] Upgrade to HttpClient5 5.6.4 Closes gh-51330 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0d253752fe1b..eb49d9b674db 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -721,7 +721,7 @@ bom { ] } } - library("HttpClient5", "5.6.3") { + library("HttpClient5", "5.6.4") { prohibit { contains "-alpha" contains "-beta" From 559a9819f077b21f2614e0ff06cf4d302658c404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 16:54:58 +0200 Subject: [PATCH 905/930] Upgrade to Protobuf Maven Plugin 5.1.8 Closes gh-51331 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index eb49d9b674db..bf22f188a77a 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1838,7 +1838,7 @@ bom { releaseNotes("https://github.com/oracle/oracle-r2dbc/releases/tag/{version}") } } - library("Protobuf Maven Plugin", "5.1.7") { + library("Protobuf Maven Plugin", "5.1.8") { group("io.github.ascopes") { plugins = [ "protobuf-maven-plugin" From 6f1e02102b983f8b2b8d0fa850183458699e2068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 16:55:33 +0200 Subject: [PATCH 906/930] Start building against Spring HATEOAS 3.2.0-M1 snapshots See gh-51332 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ceff499904b9..d9688e899bcf 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2630,7 +2630,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-grpc/releases/tag/v{version}") } } - library("Spring HATEOAS", "3.1.1") { + library("Spring HATEOAS", "3.2.0-SNAPSHOT") { firstParty() group("org.springframework.hateoas") { modules = [ From f566ac74a2eb4fb94a5b35269d489463c518c553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 16:55:37 +0200 Subject: [PATCH 907/930] Start building against Spring Pulsar 2.0.7 snapshots See gh-51333 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d9688e899bcf..c2a839d26b9d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2700,7 +2700,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ldap/releases/tag/{version}") } } - library("Spring Pulsar", "2.0.6") { + library("Spring Pulsar", "2.0.7-SNAPSHOT") { firstParty() group("org.springframework.pulsar") { bom("spring-pulsar-bom") From 613932cc7c9eb861d5b2bebc0ca1bbeb3f004e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 16:57:38 +0200 Subject: [PATCH 908/930] Upgrade to ActiveMQ 6.3.1 Closes gh-51334 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index c2a839d26b9d..2cca5a88c3d5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -31,7 +31,7 @@ bom { ] } } - library("ActiveMQ", "6.3.0") { + library("ActiveMQ", "6.3.1") { group("org.apache.activemq") { modules = [ "activemq-console", From 1778ec5832b03da15defd2ccd892cf2bb7799077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 16:57:44 +0200 Subject: [PATCH 909/930] Upgrade to Elasticsearch Client 9.5.0 Closes gh-51335 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 2cca5a88c3d5..191eff2d7997 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -401,7 +401,7 @@ bom { releaseNotes("https://github.com/ehcache/ehcache3/releases/tag/v{version}") } } - library("Elasticsearch Client", "9.4.4") { + library("Elasticsearch Client", "9.5.0") { prohibit { contains "-alpha" contains "-beta" From 4805159156b17832d5d00cdcae30614599b12337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 16:57:49 +0200 Subject: [PATCH 910/930] Upgrade to HttpClient5 5.6.4 Closes gh-51336 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 191eff2d7997..7b34d815cc89 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -720,7 +720,7 @@ bom { ] } } - library("HttpClient5", "5.6.3") { + library("HttpClient5", "5.6.4") { prohibit { contains "-alpha" contains "-beta" From 127e4c93df4605187095d1f90fb3dd52b661d850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 16:57:53 +0200 Subject: [PATCH 911/930] Upgrade to Jedis 8.0.0 Closes gh-51337 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 7b34d815cc89..1f79d10fd66d 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1087,7 +1087,7 @@ bom { releaseNotes("https://github.com/hunterhacker/jdom/releases/tag/JDOM-{version}") } } - library("Jedis", "7.4.1") { + library("Jedis", "8.0.0") { alignWith { property { name "jedis" From 5774779658353f69d973b0648fc669074d87c02e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 16:57:57 +0200 Subject: [PATCH 912/930] Upgrade to Lettuce 7.6.0.RELEASE Closes gh-51338 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 1f79d10fd66d..fbcc124238c2 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1319,7 +1319,7 @@ bom { releaseNotes("https://github.com/Kotlin/kotlinx.serialization/releases/tag/v{version}") } } - library("Lettuce", "7.5.2.RELEASE") { + library("Lettuce", "7.6.0.RELEASE") { prohibit { contains ".BETA" because "we don't want betas" From 7027d1f0ce25f203e59e6b578f4eccbf48fd4b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 16:58:02 +0200 Subject: [PATCH 913/930] Upgrade to MongoDB 5.9.1 Closes gh-51339 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index fbcc124238c2..15aea8cb51e6 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1661,7 +1661,7 @@ bom { releaseNotes("https://github.com/mockito/mockito/releases/tag/v{version}") } } - library("MongoDB", "5.9.0") { + library("MongoDB", "5.9.1") { alignWith { version { of "org.mongodb:mongodb-driver-core" From 01a9ac023be1be1e2039def3cdc7a04c0bbd263e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 16:58:06 +0200 Subject: [PATCH 914/930] Upgrade to Neo4j Java Driver 6.2.1 Closes gh-51340 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 15aea8cb51e6..380c9574143e 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1732,7 +1732,7 @@ bom { ] } } - library("Neo4j Java Driver", "6.1.0") { + library("Neo4j Java Driver", "6.2.1") { alignWith { version { of "org.neo4j.driver:neo4j-java-driver" From 48a24638afc7fa903c9d4f369e412d8c223a4eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 16:58:12 +0200 Subject: [PATCH 915/930] Upgrade to Protobuf Maven Plugin 5.1.8 Closes gh-51341 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 380c9574143e..a6fe8b6a915b 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1845,7 +1845,7 @@ bom { releaseNotes("https://github.com/oracle/oracle-r2dbc/releases/tag/{version}") } } - library("Protobuf Maven Plugin", "5.1.7") { + library("Protobuf Maven Plugin", "5.1.8") { group("io.github.ascopes") { plugins = [ "protobuf-maven-plugin" From a31bf32ae636883454c822a2810d3247f5368818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Mon, 10 Aug 2026 17:53:06 +0200 Subject: [PATCH 916/930] Adapt to changes in micrometer snapshots See https://github.com/micrometer-metrics/micrometer/issues/2297 --- .../export/datadog/DatadogProperties.java | 13 +++++++++++++ .../datadog/DatadogPropertiesConfigAdapter.java | 5 +++++ .../DatadogPropertiesConfigAdapterTests.java | 7 +++++++ .../export/datadog/DatadogPropertiesTests.java | 1 + 4 files changed, 26 insertions(+) diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogProperties.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogProperties.java index d1aed8e68440..0fa2361446d2 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogProperties.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogProperties.java @@ -43,6 +43,11 @@ public class DatadogProperties extends StepRegistryProperties { */ private @Nullable String applicationKey; + /** + * Whether payload should be compressed. + */ + private boolean compress = true; + /** * Whether to publish descriptions metadata to Datadog. Turn this off to minimize the * amount of metadata sent. @@ -76,6 +81,14 @@ public void setApplicationKey(@Nullable String applicationKey) { this.applicationKey = applicationKey; } + public boolean isCompress() { + return this.compress; + } + + public void setCompress(boolean compress) { + this.compress = compress; + } + public boolean isDescriptions() { return this.descriptions; } diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogPropertiesConfigAdapter.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogPropertiesConfigAdapter.java index 789aa96ddcc2..37083f9c053b 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogPropertiesConfigAdapter.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogPropertiesConfigAdapter.java @@ -49,6 +49,11 @@ public String apiKey() { return get(DatadogProperties::getApplicationKey, DatadogConfig.super::applicationKey); } + @Override + public boolean compress() { + return obtain(DatadogProperties::isCompress, DatadogConfig.super::compress); + } + @Override public @Nullable String hostTag() { return get(DatadogProperties::getHostTag, DatadogConfig.super::hostTag); diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogPropertiesConfigAdapterTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogPropertiesConfigAdapterTests.java index 982d92af88df..1e51a8a25a6e 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogPropertiesConfigAdapterTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogPropertiesConfigAdapterTests.java @@ -59,6 +59,13 @@ void whenPropertiesApplicationKeyIsSetAdapterApplicationKeyReturnsIt() { assertThat(createConfigAdapter(properties).applicationKey()).isEqualTo("my-application-key"); } + @Test + void whenPropertiesCompressIsSetAdapterCompressReturnsIt() { + DatadogProperties properties = createProperties(); + properties.setCompress(false); + assertThat(createConfigAdapter(properties).compress()).isFalse(); + } + @Test void whenPropertiesDescriptionsIsSetAdapterDescriptionsReturnsIt() { DatadogProperties properties = createProperties(); diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogPropertiesTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogPropertiesTests.java index faf666c34071..47db230dff5a 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogPropertiesTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/datadog/DatadogPropertiesTests.java @@ -35,6 +35,7 @@ void defaultValuesAreConsistent() { DatadogProperties properties = new DatadogProperties(); DatadogConfig config = (key) -> null; assertStepRegistryDefaultValues(properties, config); + assertThat(properties.isCompress()).isEqualTo(config.compress()); assertThat(properties.isDescriptions()).isEqualTo(config.descriptions()); assertThat(properties.getHostTag()).isEqualTo(config.hostTag()); assertThat(properties.getUri()).isEqualTo(config.uri()); From f8243f59911ccc7123082ca066ca21cd084d4b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 11 Aug 2026 11:20:19 +0200 Subject: [PATCH 917/930] Start building against Spring Data Bom 2026.0.1 snapshots See gh-51357 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index bf22f188a77a..816660337866 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2540,7 +2540,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-batch/releases/tag/v{version}") } } - library("Spring Data Bom", "2026.0.0") { + library("Spring Data Bom", "2026.0.1-SNAPSHOT") { firstParty() calendarName = "Spring Data Release" group("org.springframework.data") { From 3ff6ad8dfb5d737687a9bc9d8c1689d85c198e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 11 Aug 2026 11:20:23 +0200 Subject: [PATCH 918/930] Start building against Spring HATEOAS 3.1.2 snapshots See gh-51358 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 816660337866..072b0fd03916 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2605,7 +2605,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-grpc/releases/tag/v{version}") } } - library("Spring HATEOAS", "3.1.1") { + library("Spring HATEOAS", "3.1.2-SNAPSHOT") { firstParty() group("org.springframework.hateoas") { modules = [ From 87932c4456dcd913e1a2db8f95ec45025e0af24f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 11 Aug 2026 11:28:06 +0200 Subject: [PATCH 919/930] Upgrade to Elasticsearch Client 9.4.4 Closes gh-51359 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 072b0fd03916..36770a71db9c 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -401,7 +401,7 @@ bom { releaseNotes("https://github.com/ehcache/ehcache3/releases/tag/v{version}") } } - library("Elasticsearch Client", "9.4.2") { + library("Elasticsearch Client", "9.4.4") { prohibit { contains "-alpha" contains "-beta" From 0c52364e0d77ef89f12b8dc48b330f10eb4b7e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 11 Aug 2026 11:28:10 +0200 Subject: [PATCH 920/930] Upgrade to MongoDB 5.8.1 Closes gh-51360 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 36770a71db9c..d98b6f0348cc 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1654,7 +1654,7 @@ bom { releaseNotes("https://github.com/mockito/mockito/releases/tag/v{version}") } } - library("MongoDB", "5.8.0") { + library("MongoDB", "5.8.1") { alignWith { version { of "org.mongodb:mongodb-driver-core" From e665232b96cf451b8f01cca6c068554564ec3b52 Mon Sep 17 00:00:00 2001 From: Phillip Webb <phil.webb@broadcom.com> Date: Tue, 11 Aug 2026 10:40:58 +0100 Subject: [PATCH 921/930] Upgrade to Grpc Java 1.83.1 Closes gh-51361 --- .../boot/grpc/client/autoconfigure/ServiceConfig.java | 6 ++++-- platform/spring-boot-dependencies/build.gradle | 8 +++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java index bc1749efb01c..c7520906b230 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ServiceConfig.java @@ -189,7 +189,7 @@ Map<String, Object> grpcJavaConfig() { */ public record WeightedRoundRobinLoadBalancingConfig(Duration blackoutPeriod, Duration weightExpirationPeriod, Duration outOfBandReportingPeriod, Boolean enableOutOfBandLoadReport, Duration weightUpdatePeriod, - Float errorUtilizationPenalty) { + Double errorUtilizationPenalty) { Map<String, Object> grpcJavaConfig() { // Aligned with WeightedRoundRobinLoadBalancerProvider @@ -208,7 +208,9 @@ Map<String, Object> grpcJavaConfig() { map.from(this::weightUpdatePeriod) .as(ServiceConfig::durationString) .to(grpcJavaConfig.in("weightUpdatePeriod")); - map.from(this::errorUtilizationPenalty).to(grpcJavaConfig.in("errorUtilizationPenalty")); + map.from(this::errorUtilizationPenalty) + .as(Object::toString) + .to(grpcJavaConfig.in("errorUtilizationPenalty")); return grpcJavaConfig.asMap(); } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index d98b6f0348cc..6d8927c2ab38 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -558,10 +558,16 @@ bom { site("https://groovy-lang.org") } } - library("Grpc Java", "1.81.1") { + library("Grpc Java", "1.83.1") { group("io.grpc") { bom("grpc-bom") } + alignWith { + version { + of "io.grpc:grpc-core" + from "org.springframework.grpc:spring-grpc-core" + } + } links { github("https://github.com/grpc/grpc-java") docs("https://grpc.io/docs/languages/java/") From 328390acee5eab8a36dbe2a0f9e72eb59637ae42 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com> Date: Tue, 11 Aug 2026 15:14:38 +0700 Subject: [PATCH 922/930] Remove code tag in property description See gh-51348 Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com> --- .../boot/graphql/autoconfigure/GraphQlProperties.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/GraphQlProperties.java b/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/GraphQlProperties.java index 90bbe2283faf..aea400b8153e 100644 --- a/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/GraphQlProperties.java +++ b/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/GraphQlProperties.java @@ -244,7 +244,7 @@ public static class Websocket { private @Nullable String path; /** - * Time within which the initial {@code CONNECTION_INIT} type message must be + * Time within which the initial CONNECTION_INIT type message must be * received. */ private Duration connectionInitTimeout = Duration.ofSeconds(60); From 4518e11bf55f58c0471163c35ac420a37ac2dfe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 11 Aug 2026 14:43:45 +0200 Subject: [PATCH 923/930] Polish "Remove code tag in property description" See gh-51348 --- .../boot/graphql/autoconfigure/GraphQlProperties.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/GraphQlProperties.java b/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/GraphQlProperties.java index aea400b8153e..553eed0aa03e 100644 --- a/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/GraphQlProperties.java +++ b/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/GraphQlProperties.java @@ -244,8 +244,7 @@ public static class Websocket { private @Nullable String path; /** - * Time within which the initial CONNECTION_INIT type message must be - * received. + * Time within which the initial CONNECTION_INIT type message must be received. */ private Duration connectionInitTimeout = Duration.ofSeconds(60); From 76521bb7ac2eb0a46fe862d05720626c643f7e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 11 Aug 2026 17:27:28 +0200 Subject: [PATCH 924/930] Upgrade to Elasticsearch Client 9.4.5 Closes gh-51368 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6d8927c2ab38..89168c8072e5 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -401,7 +401,7 @@ bom { releaseNotes("https://github.com/ehcache/ehcache3/releases/tag/v{version}") } } - library("Elasticsearch Client", "9.4.4") { + library("Elasticsearch Client", "9.4.5") { prohibit { contains "-alpha" contains "-beta" From fc6acdbc21b10916cef7f74b8c68543eaaee87b9 Mon Sep 17 00:00:00 2001 From: Sumit Kumar Das <skdas5405@gmail.com> Date: Wed, 29 Jul 2026 16:46:02 +0000 Subject: [PATCH 925/930] Reset cached AppendableByteArray before it is reused AppendableByteArray.reset() was only called from toByteArray(), so an encode that failed part-way left its partial output in the thread-local cached instance. The next value encoded on that thread was then prefixed with it, which in structured logging corrupted the log event following a failed one. Reset the cached instance when it is handed out instead, so the buffer is clean regardless of how the previous use ended. This also covers the early return in toByteArray() for empty content, which returns without resetting. See gh-51156 Signed-off-by: Sumit Kumar Das <skdas5405@gmail.com> --- .../boot/json/AppendableByteArray.java | 6 ++++++ .../boot/json/AppendableByteArrayTests.java | 12 ++++++++++++ .../springframework/boot/json/WritableJsonTests.java | 11 +++++++++++ 3 files changed, 29 insertions(+) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java b/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java index a5f271352d28..5f212ce915d3 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java @@ -129,6 +129,12 @@ static AppendableByteArray get(Charset charset) { result = new AppendableByteArray(charset); cache.set(new SoftReference<>(result)); } + else { + // The cached instance is reused, so it must be clean before it is + // handed out again. A previous use may have been abandoned part-way, + // for example when writing the value threw, leaving content behind. + result.reset(); + } return result; } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java b/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java index 22cb350b450a..c2195ea438fa 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java @@ -68,6 +68,18 @@ void writeUsingCache() throws IOException { assertByteArray(StandardCharsets.UTF_8, AppendableByteArray::get, (appendable) -> appendable.append(string)); } + @Test + void getWhenPreviousUseWasAbandonedReturnsCleanInstance() throws IOException { + AppendableByteArray abandoned = AppendableByteArray.get(StandardCharsets.UTF_8); + abandoned.append("partial content"); + // The instance is never converted to a byte array, as happens when writing + // the value throws part-way through. + AppendableByteArray reused = AppendableByteArray.get(StandardCharsets.UTF_8); + assertThat(reused).isSameAs(abandoned); + reused.append("clean"); + assertThat(reused.toByteArray()).isEqualTo("clean".getBytes(StandardCharsets.UTF_8)); + } + private void assertByteArray(Charset charset, ThrowingConsumer<Appendable> action) throws Exception { assertByteArray(4, 4, charset, action); } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/json/WritableJsonTests.java b/core/spring-boot/src/test/java/org/springframework/boot/json/WritableJsonTests.java index aa3ea133f04c..6d7bfbc1a602 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/json/WritableJsonTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/json/WritableJsonTests.java @@ -66,6 +66,17 @@ void toByteArrayReturnsByteArray() { assertThat(writable.toByteArray()).isEqualTo("{}".getBytes()); } + @Test + void toByteArrayWhenPreviousWriteFailedDoesNotIncludePartialContent() { + WritableJson failing = (out) -> { + out.append("{\"partial\":"); + throw new IllegalStateException("bad"); + }; + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(failing::toByteArray); + WritableJson healthy = (out) -> out.append("{\"ok\":true}"); + assertThat(healthy.toByteArray()).isEqualTo("{\"ok\":true}".getBytes(StandardCharsets.UTF_8)); + } + @Test void toResourceWritesJson() throws Exception { File file = new File(this.temp, "out.json"); From dafadea2ab85b50b4299046049cb56f1641610fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Tue, 11 Aug 2026 15:45:32 +0200 Subject: [PATCH 926/930] Polish "Reset cached AppendableByteArray before it is reused" See gh-51156 --- .../boot/json/AppendableByteArray.java | 30 +++++++++--- .../boot/json/WritableJson.java | 4 +- .../boot/json/AppendableByteArrayTests.java | 49 ++++++++++++------- .../boot/json/WritableJsonTests.java | 11 ----- 4 files changed, 55 insertions(+), 39 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java b/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java index 5f212ce915d3..e0274f6be61c 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java @@ -28,6 +28,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; +import org.springframework.util.function.ThrowingConsumer; /** * {@link Appendable} implementation that can be used to return a byte array. Designed to @@ -35,6 +36,7 @@ * cached buffer scoped to the thread. * * @author Phillip Webb + * @author Stephane Nicoll */ class AppendableByteArray implements Appendable { @@ -112,7 +114,6 @@ byte[] toByteArray() { } byte[] result = new byte[size]; System.arraycopy(this.out.array(), this.out.arrayOffset() + position, result, 0, size); - reset(); return result; } @@ -121,7 +122,26 @@ private void reset() { this.encoder.reset(); } - static AppendableByteArray get(Charset charset) { + static byte[] toByteArray(Charset charset, ThrowingConsumer<Appendable> appendable) throws IOException { + Assert.notNull(charset, "'charset' must not be null"); + Assert.notNull(appendable, "'appendable' must not be null"); + AppendableByteArray appendableByteArray = get(charset); + try { + appendable.acceptWithException(appendableByteArray); + return appendableByteArray.toByteArray(); + } + catch (IOException | RuntimeException ex) { + throw ex; + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + finally { + appendableByteArray.reset(); + } + } + + private static AppendableByteArray get(Charset charset) { Assert.notNull(charset, "'charset' must not be null"); SoftReference<AppendableByteArray> cached = cache.get(); AppendableByteArray result = (cached != null) ? cached.get() : null; @@ -129,12 +149,6 @@ static AppendableByteArray get(Charset charset) { result = new AppendableByteArray(charset); cache.set(new SoftReference<>(result)); } - else { - // The cached instance is reused, so it must be clean before it is - // handed out again. A previous use may have been abandoned part-way, - // for example when writing the value threw, leaving content behind. - result.reset(); - } return result; } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/json/WritableJson.java b/core/spring-boot/src/main/java/org/springframework/boot/json/WritableJson.java index 0593bbc0960d..457b4848309d 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/json/WritableJson.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/json/WritableJson.java @@ -76,9 +76,7 @@ default byte[] toByteArray() { default byte[] toByteArray(Charset charset) { Assert.notNull(charset, "'charset' must not be null"); try { - AppendableByteArray appendable = AppendableByteArray.get(charset); - to(appendable); - return appendable.toByteArray(); + return AppendableByteArray.toByteArray(charset, this::to); } catch (IOException ex) { throw new UncheckedIOException(ex); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java b/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java index c2195ea438fa..5746991f0a07 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java @@ -33,6 +33,7 @@ * Tests for {@link AppendableByteArray}. * * @author Phillip Webb + * @author Stephane Nicoll */ class AppendableByteArrayTests { @@ -61,23 +62,32 @@ void writesMultipleSmallStrings() throws Exception { @Test void writeUsingCache() throws IOException { - assertByteArray(StandardCharsets.UTF_8, AppendableByteArray::get, (appendable) -> appendable.append(string)); - assertByteArray(StandardCharsets.UTF_8, AppendableByteArray::get, (appendable) -> appendable.append(string)); - assertByteArray(StandardCharsets.UTF_16, AppendableByteArray::get, (appendable) -> appendable.append(string)); - assertByteArray(StandardCharsets.UTF_16, AppendableByteArray::get, (appendable) -> appendable.append(string)); - assertByteArray(StandardCharsets.UTF_8, AppendableByteArray::get, (appendable) -> appendable.append(string)); + testWriteUsingCache(StandardCharsets.UTF_8, (appendable) -> appendable.append(string)); + testWriteUsingCache(StandardCharsets.UTF_8, (appendable) -> appendable.append(string)); + testWriteUsingCache(StandardCharsets.UTF_16, (appendable) -> appendable.append(string)); + testWriteUsingCache(StandardCharsets.UTF_16, (appendable) -> appendable.append(string)); + testWriteUsingCache(StandardCharsets.UTF_8, (appendable) -> appendable.append(string)); + } + + private void testWriteUsingCache(Charset charset, ThrowingConsumer<Appendable> action) throws IOException { + byte[] baseLine = createFreshByteArray(charset, action); + assertThat(AppendableByteArray.toByteArray(charset, action)).isEqualTo(baseLine); } @Test - void getWhenPreviousUseWasAbandonedReturnsCleanInstance() throws IOException { - AppendableByteArray abandoned = AppendableByteArray.get(StandardCharsets.UTF_8); - abandoned.append("partial content"); - // The instance is never converted to a byte array, as happens when writing - // the value throws part-way through. - AppendableByteArray reused = AppendableByteArray.get(StandardCharsets.UTF_8); - assertThat(reused).isSameAs(abandoned); - reused.append("clean"); - assertThat(reused.toByteArray()).isEqualTo("clean".getBytes(StandardCharsets.UTF_8)); + void toByteArrayWhenPreviousUseWasAbandonedReturnsCleanInstance() throws IOException { + try { + AppendableByteArray.toByteArray(StandardCharsets.UTF_8, (appendable) -> { + appendable.append("partial content"); + throw new IllegalStateException("Interrupted"); + }); + } + catch (IllegalStateException ex) { + // Ignore + } + byte[] reused = AppendableByteArray.toByteArray(StandardCharsets.UTF_8, + (appendable) -> appendable.append("clean")); + assertThat(reused).isEqualTo("clean".getBytes(StandardCharsets.UTF_8)); } private void assertByteArray(Charset charset, ThrowingConsumer<Appendable> action) throws Exception { @@ -91,13 +101,18 @@ private void assertByteArray(int initialSize, int expansionSize, Charset charset private void assertByteArray(Charset charset, Function<Charset, AppendableByteArray> factory, ThrowingConsumer<Appendable> action) throws IOException { + byte[] baseline = createFreshByteArray(charset, action); + AppendableByteArray appendableByteArray = factory.apply(charset); + action.accept(appendableByteArray); + assertThat(appendableByteArray.toByteArray()).isEqualTo(baseline); + } + + private byte[] createFreshByteArray(Charset charset, ThrowingConsumer<Appendable> action) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); try (OutputStreamWriter writer = new OutputStreamWriter(out, charset)) { action.accept(writer); } - AppendableByteArray appendableByteArray = factory.apply(charset); - action.accept(appendableByteArray); - assertThat(appendableByteArray.toByteArray()).isEqualTo(out.toByteArray()); + return out.toByteArray(); } } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/json/WritableJsonTests.java b/core/spring-boot/src/test/java/org/springframework/boot/json/WritableJsonTests.java index 6d7bfbc1a602..aa3ea133f04c 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/json/WritableJsonTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/json/WritableJsonTests.java @@ -66,17 +66,6 @@ void toByteArrayReturnsByteArray() { assertThat(writable.toByteArray()).isEqualTo("{}".getBytes()); } - @Test - void toByteArrayWhenPreviousWriteFailedDoesNotIncludePartialContent() { - WritableJson failing = (out) -> { - out.append("{\"partial\":"); - throw new IllegalStateException("bad"); - }; - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(failing::toByteArray); - WritableJson healthy = (out) -> out.append("{\"ok\":true}"); - assertThat(healthy.toByteArray()).isEqualTo("{\"ok\":true}".getBytes(StandardCharsets.UTF_8)); - } - @Test void toResourceWritesJson() throws Exception { File file = new File(this.temp, "out.json"); From 3575c12ae5f26e20a419ef6fb631cc7800a224fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Wed, 12 Aug 2026 08:09:49 +0200 Subject: [PATCH 927/930] Start building against Spring Security 7.2.0-M1 snapshots See gh-51271 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 0518c385f2ed..5fe26df15a25 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2736,7 +2736,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-restdocs/releases/tag/v{version}") } } - library("Spring Security", "7.1.1-SNAPSHOT") { + library("Spring Security", "7.2.0-SNAPSHOT") { firstParty() group("org.springframework.security") { bom("spring-security-bom") From 188860f7305fe7f3d003f6d034bda684bace70a7 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov <jonatan.ivanov@gmail.com> Date: Fri, 14 Aug 2026 02:01:18 +0200 Subject: [PATCH 928/930] Adapt to changes in micrometer snapshots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/micrometer-metrics/micrometer/issues/2297 Co-authored-by: Stéphane Nicoll <stephane.nicoll@broadcom.com> --- ...lpMetricsExportAutoConfigurationTests.java | 16 ++------ .../OtlpExemplarsAutoConfigurationTests.java | 40 ++++++++++--------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java index f4c267b8f19e..49283bbb74df 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java @@ -151,10 +151,8 @@ void testConnectionFactoryWithOverridesWhenUsingCustomConnectionDetails() { @Test void allowsCustomMetricsSenderToBeUsed() { this.contextRunner.withUserConfiguration(BaseConfiguration.class, CustomMetricsSenderConfiguration.class) - .run((context) -> { - assertHasCustomMetricsSender(context); - assertThat(context).doesNotHaveBean(OtlpHttpMetricsSender.class); - }); + .run((context) -> assertThat(context).hasSingleBean(OtlpMetricsSender.class) + .doesNotHaveBean(OtlpHttpMetricsSender.class)); } @Test @@ -169,7 +167,8 @@ void allowsCustomExemplarContextProviderToBeUsed() { void allowsCustomMetricsSenderToBeUsedWithVirtualThreads() { this.contextRunner.withUserConfiguration(BaseConfiguration.class, CustomMetricsSenderConfiguration.class) .withPropertyValues("spring.threads.virtual.enabled=true") - .run(this::assertHasCustomMetricsSender); + .run((context) -> assertThat(context).hasSingleBean(OtlpMetricsSender.class) + .doesNotHaveBean(OtlpHttpMetricsSender.class)); } @Test @@ -274,13 +273,6 @@ private HttpClient extractHttpClient(OtlpHttpMetricsSender metricsSender) { return (HttpClient) httpClient; } - private void assertHasCustomMetricsSender(AssertableApplicationContext context) { - assertThat(context).hasSingleBean(OtlpMeterRegistry.class); - OtlpMeterRegistry registry = context.getBean(OtlpMeterRegistry.class); - assertThat(registry).extracting("metricsSender") - .satisfies((sender) -> assertThat(sender).isSameAs(CustomMetricsSenderConfiguration.customMetricsSender)); - } - private void assertHasCustomExemplarContextProvider(AssertableApplicationContext context) { assertThat(context).hasSingleBean(OtlpMeterRegistry.class); OtlpMeterRegistry registry = context.getBean(OtlpMeterRegistry.class); diff --git a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java index 2bd4bb96d49a..705d028527ac 100644 --- a/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-brave/src/test/java/org/springframework/boot/micrometer/tracing/brave/autoconfigure/OtlpExemplarsAutoConfigurationTests.java @@ -16,7 +16,10 @@ package org.springframework.boot.micrometer.tracing.brave.autoconfigure; +import java.util.List; + import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; import io.micrometer.observation.Observation; import io.micrometer.observation.Observation.Context; @@ -24,6 +27,7 @@ import io.micrometer.registry.otlp.ExemplarContextProvider; import io.micrometer.registry.otlp.OtlpMeterRegistry; import io.micrometer.registry.otlp.OtlpMetricsSender; +import io.micrometer.registry.otlp.shaded.io.opentelemetry.sdk.metrics.data.DoubleExemplarData; import io.micrometer.tracing.Tracer; import io.micrometer.tracing.handler.TracingAwareMeterObservationHandler; import org.junit.jupiter.api.Test; @@ -38,6 +42,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.assertThat; import static org.mockito.Mockito.mock; @@ -85,12 +90,15 @@ void otlpOutputShouldContainExemplars() { ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); Observation.start("test.observation", observationRegistry).stop(); OtlpMeterRegistry otlpMeterRegistry = context.getBean(OtlpMeterRegistry.class); - TestOtlpMetricsSender metricsSender = context.getBean(TestOtlpMetricsSender.class); + Timer timer = otlpMeterRegistry.get("test.observation").timer(); otlpMeterRegistry.close(); - assertThat(metricsSender.getOtlpRequest()).containsOnlyOnce("name: \"test.observation\"") - .containsOnlyOnce("exemplars") - .containsOnlyOnce("span_id") - .containsOnlyOnce("trace_id"); + + List<DoubleExemplarData> exemplars = ReflectionTestUtils.invokeMethod(timer, "exemplars"); + assertThat(exemplars).hasSize(1).first().satisfies((exemplar) -> { + assertThat(exemplar).isNotNull(); + assertThat(exemplar.getSpanContext().getSpanId()).isNotEmpty(); + assertThat(exemplar.getSpanContext().getTraceId()).isNotEmpty(); + }); }); } @@ -104,12 +112,15 @@ void otlpOutputShouldContainExemplarsWhenIncludeIsAllAndSpanIsNotSampled() { ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); Observation.start("test.observation", observationRegistry).stop(); OtlpMeterRegistry otlpMeterRegistry = context.getBean(OtlpMeterRegistry.class); - TestOtlpMetricsSender metricsSender = context.getBean(TestOtlpMetricsSender.class); + Timer timer = otlpMeterRegistry.get("test.observation").timer(); otlpMeterRegistry.close(); - assertThat(metricsSender.getOtlpRequest()).containsOnlyOnce("name: \"test.observation\"") - .containsOnlyOnce("exemplars") - .containsOnlyOnce("span_id") - .containsOnlyOnce("trace_id"); + + List<DoubleExemplarData> exemplars = ReflectionTestUtils.invokeMethod(timer, "exemplars"); + assertThat(exemplars).hasSize(1).first().satisfies((exemplar) -> { + assertThat(exemplar).isNotNull(); + assertThat(exemplar.getSpanContext().getSpanId()).isNotEmpty(); + assertThat(exemplar.getSpanContext().getTraceId()).isNotEmpty(); + }); }); } @@ -151,15 +162,8 @@ TestOtlpMetricsSender testOtlpMetricsSender() { static class TestOtlpMetricsSender implements OtlpMetricsSender { - private String request = ""; - @Override - public void send(Request request) throws Exception { - this.request = request.toString(); - } - - String getOtlpRequest() { - return this.request; + public void send(Request ignored) { } } From e3d4b1ceb6d8971936f484bae39c7d6659a81c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= <stephane.nicoll@broadcom.com> Date: Fri, 14 Aug 2026 08:41:22 +0200 Subject: [PATCH 929/930] Upgrade to MongoDB 5.9.2 Closes gh-51380 --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 5fe26df15a25..6a490a2406af 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1667,7 +1667,7 @@ bom { releaseNotes("https://github.com/mockito/mockito/releases/tag/v{version}") } } - library("MongoDB", "5.9.1") { + library("MongoDB", "5.9.2") { alignWith { version { of "org.mongodb:mongodb-driver-core" From 16f5fffdf935d0d7237e6b3bf1740cdfbd812f7d Mon Sep 17 00:00:00 2001 From: dlwldn30 <dlwldn30@naver.com> Date: Sun, 16 Aug 2026 23:44:13 +0900 Subject: [PATCH 930/930] Close the stream when comparing AOT file content RequireNewOrMatchingContentFileHandler reads the already generated file through content.getInputStream().readAllBytes(). InputStream.readAllBytes does not close the stream, and the stream is never assigned, so it cannot be closed at all. During AOT processing the content is a FileSystemResource, so each comparison leaks a file handle. FileSystemGeneratedFiles already uses try-with-resources when it consumes an InputStreamSource. Read the existing content inside a try-with-resources block. See gh-51385 Signed-off-by: dlwldn30 <dlwldn30@naver.com> --- .../logback/SpringBootJoranConfigurator.java | 5 +- .../SpringBootJoranConfiguratorTests.java | 63 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java index ac1d5cc0bd3c..570c09db4bec 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java @@ -442,7 +442,10 @@ public void acceptWithException(FileHandler file) throws Exception { if (file.exists()) { InputStreamSource content = file.getContent(); Assert.state(content != null, "Unable to get file content"); - byte[] existingContent = content.getInputStream().readAllBytes(); + byte[] existingContent; + try (InputStream inputStream = content.getInputStream()) { + existingContent = inputStream.readAllBytes(); + } if (!Arrays.equals(this.newContent, existingContent)) { throw new IllegalStateException( "Logging configuration differs from the configuration that has already been written. " diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java index 0b08bbe8ac2d..afb3f7d951f5 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java @@ -16,10 +16,17 @@ package org.springframework.boot.logging.logback; +import java.io.ByteArrayInputStream; +import java.io.FilterInputStream; +import java.io.IOException; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import ch.qos.logback.classic.BasicConfigurator; import ch.qos.logback.classic.LoggerContext; @@ -32,7 +39,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.aot.generate.ClassNameGenerator; +import org.springframework.aot.generate.DefaultGenerationContext; +import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.generate.GeneratedFiles.FileHandler; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; @@ -40,10 +52,14 @@ import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.context.aot.AbstractAotProcessor; +import org.springframework.core.io.InputStreamSource; +import org.springframework.javapoet.ClassName; import org.springframework.mock.env.MockEnvironment; import org.springframework.test.context.support.TestPropertySourceUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; /** * Tests for {@link SpringBootJoranConfigurator}. @@ -283,6 +299,29 @@ void addsAotContributionToContextDuringAotProcessing() throws Exception { }); } + @Test + @WithPropertyXmlResource + void aotContributionClosesExistingFileContent() throws Exception { + withSystemProperty(AbstractAotProcessor.AOT_PROCESSING, "true", () -> { + initialize("property.xml"); + BeanFactoryInitializationAotContribution contribution = (BeanFactoryInitializationAotContribution) this.context + .getObject(BeanFactoryInitializationAotContribution.class.getName()); + assertThat(contribution).isNotNull(); + List<AtomicBoolean> closed = new ArrayList<>(); + GeneratedFiles generatedFiles = (kind, path, handler) -> { + AtomicBoolean fileClosed = new AtomicBoolean(); + closed.add(fileClosed); + handler.accept(new ClosableContentFileHandler(fileClosed)); + }; + DefaultGenerationContext generationContext = new DefaultGenerationContext( + new ClassNameGenerator(ClassName.get(Object.class)), generatedFiles); + assertThatIllegalStateException() + .isThrownBy(() -> contribution.applyTo(generationContext, mock(BeanFactoryInitializationCode.class))) + .withMessageContaining("Logging configuration differs"); + assertThat(closed).isNotEmpty().allMatch(AtomicBoolean::get); + }); + } + private void withSystemProperty(String name, String value, Action action) throws Exception { System.setProperty(name, value); try { @@ -318,6 +357,30 @@ private interface Action { } + /** + * {@link FileHandler} whose existing content records when its stream is closed. + */ + private static final class ClosableContentFileHandler extends FileHandler { + + private ClosableContentFileHandler(AtomicBoolean closed) { + super(true, () -> (InputStreamSource) () -> new FilterInputStream( + new ByteArrayInputStream("existing".getBytes(StandardCharsets.UTF_8))) { + + @Override + public void close() throws IOException { + closed.set(true); + super.close(); + } + + }); + } + + @Override + protected void copy(InputStreamSource content, boolean override) { + } + + } + @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @WithResource(name = "property-default-value.xml", content = """