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 4a9c98f4dac9..728e663546c0 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 @@ -233,7 +233,7 @@ private class CreateJmxConnector implements Callable<@Nullable JMXConnector> { } private boolean hasCauseWithType(Throwable t, Class type) { - return type.isAssignableFrom(t.getClass()) || t.getCause() != null && hasCauseWithType(t.getCause(), type); + return type.isInstance(t) || t.getCause() != null && hasCauseWithType(t.getCause(), type); } } diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 1f2778c9e920..84dda76682a6 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -88,6 +88,18 @@ value="Please use static AssertJ imports." /> + + + + + + + + + + + + diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ExpressionTree.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ExpressionTree.java index 9eb0caba5138..1b154d92bd6e 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ExpressionTree.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ExpressionTree.java @@ -55,14 +55,14 @@ String getKind() throws Exception { } Object getLiteralValue() throws Exception { - if (this.literalTreeType.isAssignableFrom(getInstance().getClass())) { + if (this.literalTreeType.isInstance(getInstance())) { return this.literalValueMethod.invoke(getInstance()); } return null; } Object getFactoryValue() throws Exception { - if (this.methodInvocationTreeType.isAssignableFrom(getInstance().getClass())) { + if (this.methodInvocationTreeType.isInstance(getInstance())) { List arguments = (List) this.methodInvocationArgumentsMethod.invoke(getInstance()); if (arguments.size() == 1) { return new ExpressionTree(arguments.get(0)).getLiteralValue(); @@ -72,7 +72,7 @@ Object getFactoryValue() throws Exception { } Member getSelectedMember() throws Exception { - if (this.memberSelectTreeType.isAssignableFrom(getInstance().getClass())) { + if (this.memberSelectTreeType.isInstance(getInstance())) { String expression = this.memberSelectTreeExpressionMethod.invoke(getInstance()).toString(); String identifier = this.memberSelectTreeIdentifierMethod.invoke(getInstance()).toString(); if (expression != null && identifier != null) { @@ -83,7 +83,7 @@ Member getSelectedMember() throws Exception { } List getArrayExpression() throws Exception { - if (this.newArrayTreeType.isAssignableFrom(getInstance().getClass())) { + if (this.newArrayTreeType.isInstance(getInstance())) { List elements = (List) this.arrayValueMethod.invoke(getInstance()); List result = new ArrayList<>(); if (elements == null) { diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestImportTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestImportTests.java index 549dc58e2374..d485f14dba16 100644 --- a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestImportTests.java +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestImportTests.java @@ -24,6 +24,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.env.Environment; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration; import org.springframework.test.context.TestPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -68,6 +70,38 @@ void containingClassTestPropertySourceIsHonored() { } + @Nested + @SpringBootTest(classes = Config.class) + @Import(ImportedByNestedTests.class) + @NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) + class OverrideNestedTests { + + @Autowired(required = false) + private ImportedByContainingTests importedByContainingTests; + + @Autowired(required = false) + private ImportedByNestedTests importedByNestedTests; + + @Autowired + private Environment environment; + + @Test + void sameClassImportIsHonored() { + assertThat(this.importedByNestedTests).isNotNull(); + } + + @Test + void containingClassImportIsNotInherited() { + assertThat(this.importedByContainingTests).isNull(); + } + + @Test + void containingClassTestPropertySourceIsNotInherited() { + assertThat(this.environment.getProperty("a")).isNull(); + } + + } + @Configuration(proxyBeanMethods = false) static class Config { diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java index 06e4708019cb..bfd8340c0410 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java @@ -34,7 +34,6 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; import org.springframework.core.ResolvableType; -import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.validation.AbstractBindingResult; import org.springframework.validation.BeanPropertyBindingResult; @@ -115,8 +114,7 @@ private void validate(ConfigurationPropertyName name, Bindable target, BindCo if (this.exception == null) { Object validationTarget = getValidationTarget(target, context, result); Class validationType = target.getBoxedType().resolve(); - if (validationTarget != null) { - Assert.state(validationType != null, "'validationType' must not be null"); + if (validationTarget != null && validationType != null) { validateAndPush(name, validationTarget, validationType); } } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/AbstractInjectionFailureAnalyzer.java b/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/AbstractInjectionFailureAnalyzer.java index 6e357c13edc4..8107c1059edd 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/AbstractInjectionFailureAnalyzer.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/AbstractInjectionFailureAnalyzer.java @@ -64,7 +64,7 @@ public abstract class AbstractInjectionFailureAnalyzer exte Throwable candidate = root; C result = null; while (candidate != null) { - if (type.isAssignableFrom(candidate.getClass())) { + if (type.isInstance(candidate)) { result = (C) candidate; } candidate = candidate.getCause(); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/json/AbstractJsonParser.java b/core/spring-boot/src/main/java/org/springframework/boot/json/AbstractJsonParser.java index 78e9a0000b8a..56591ea5ee36 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/json/AbstractJsonParser.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/json/AbstractJsonParser.java @@ -55,7 +55,7 @@ protected final T tryParse(Callable parser, Class ch return parser.call(); } catch (Exception ex) { - if (check.isAssignableFrom(ex.getClass())) { + if (check.isInstance(ex)) { throw new JsonParseException(ex); } ReflectionUtils.rethrowRuntimeException(ex); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java index c9ac953d5539..d8f645541e9b 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java @@ -238,6 +238,15 @@ void validateMapValuesWithNonUniformSource() { this.binder.bind(ConfigurationPropertyName.of("test"), Bindable.of(ExampleWithMap.class), this.handler); } + @Test + void bindShouldValidateMapWithWildcardValue() { + this.sources.add(new MockConfigurationPropertySource("test.params.toto", "titi")); + ExampleWithWildcardMap result = this.binder + .bind(ConfigurationPropertyName.of("test"), Bindable.of(ExampleWithWildcardMap.class), this.handler) + .get(); + assertThat(result.getParams().get("toto")).isEqualTo("titi"); + } + private Validator getMapValidator() { return new Validator() { @@ -425,6 +434,21 @@ Map getItems() { } + @Validated + static class ExampleWithWildcardMap { + + private Map params = new LinkedHashMap<>(); + + Map getParams() { + return this.params; + } + + void setParams(Map params) { + this.params = params; + } + + } + static class ExampleMapValue { private @Nullable String number; 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[] diff --git a/integration-test/spring-boot-loader-integration-tests/build.gradle b/integration-test/spring-boot-loader-integration-tests/build.gradle index 1a2eea9cf62d..fd63ebb620bd 100644 --- a/integration-test/spring-boot-loader-integration-tests/build.gradle +++ b/integration-test/spring-boot-loader-integration-tests/build.gradle @@ -82,6 +82,17 @@ tasks.register("buildSignedJarRsaApp", GradleBuild) { tasks = ["build"] } +tasks.register("syncWarAppSource", org.springframework.boot.build.SyncAppSource) { + sourceDirectory = file("spring-boot-loader-tests-war") + destinationDirectory = file(layout.buildDirectory.dir("spring-boot-loader-tests-war")) +} + +tasks.register("buildWarApp", GradleBuild) { + dependsOn syncWarAppSource, syncMavenRepository + dir = layout.buildDirectory.dir("spring-boot-loader-tests-war") + startParameter.buildCacheEnabled = false + tasks = ["build"] +} tasks.register("downloadJdk", Download) { def destFolder = new File(project.gradle.gradleUserHomeDir, "caches/springboot/downloads/jdk/oracle") @@ -105,5 +116,5 @@ tasks.named("processDockerTestResources").configure { } tasks.named("dockerTest").configure { - dependsOn buildApp, buildSignedJarApp, buildSignedJarRsaApp + dependsOn buildApp, buildWarApp, buildSignedJarApp, buildSignedJarRsaApp } diff --git a/integration-test/spring-boot-loader-integration-tests/spring-boot-loader-tests-war/build.gradle b/integration-test/spring-boot-loader-integration-tests/spring-boot-loader-tests-war/build.gradle new file mode 100644 index 000000000000..a3e0b11f58e6 --- /dev/null +++ b/integration-test/spring-boot-loader-integration-tests/spring-boot-loader-tests-war/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 "war" + id "org.springframework.boot" +} + +java { + sourceCompatibility = '17' + targetCompatibility = '17' +} + +repositories { + maven { url = layout.projectDirectory.dir("../docker-test-maven-repository") } + mavenCentral() + spring.mavenRepositories() +} + +dependencies { + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + implementation("org.springframework.boot:spring-boot-starter-webmvc") + + providedRuntime(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + providedRuntime("org.springframework.boot:spring-boot-starter-tomcat-runtime") + providedRuntime("org.glassfish.web:jakarta.servlet.jsp.jstl") + providedRuntime("org.apache.tomcat.embed:tomcat-embed-jasper") +} diff --git a/integration-test/spring-boot-loader-integration-tests/spring-boot-loader-tests-war/settings.gradle b/integration-test/spring-boot-loader-integration-tests/spring-boot-loader-tests-war/settings.gradle new file mode 100644 index 000000000000..d5cca12cf132 --- /dev/null +++ b/integration-test/spring-boot-loader-integration-tests/spring-boot-loader-tests-war/settings.gradle @@ -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. + */ + +pluginManagement { + evaluate(new File("${gradle.parent.rootProject.rootDir}/buildSrc/SpringRepositorySupport.groovy")).apply(this) + repositories { + maven { url = layout.settingsDirectory.dir("../docker-test-maven-repository") } + mavenCentral() + spring.mavenRepositories() + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == "org.springframework.boot") { + useModule "org.springframework.boot:spring-boot-gradle-plugin:${requested.version}" + } + } + } +} diff --git a/integration-test/spring-boot-loader-integration-tests/spring-boot-loader-tests-war/src/main/java/org/springframework/boot/loaderwarapp/LoaderWarTestApplication.java b/integration-test/spring-boot-loader-integration-tests/spring-boot-loader-tests-war/src/main/java/org/springframework/boot/loaderwarapp/LoaderWarTestApplication.java new file mode 100644 index 000000000000..6d9e218ccf5a --- /dev/null +++ b/integration-test/spring-boot-loader-integration-tests/spring-boot-loader-tests-war/src/main/java/org/springframework/boot/loaderwarapp/LoaderWarTestApplication.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.loaderwarapp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +@SpringBootApplication +public class LoaderWarTestApplication extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(LoaderWarTestApplication.class); + } + + public static void main(String[] args) { + SpringApplication.run(LoaderWarTestApplication.class, args).close(); + } + +} diff --git a/integration-test/spring-boot-loader-integration-tests/spring-boot-loader-tests-war/src/main/webapp/WEB-INF/jsp/welcome.jsp b/integration-test/spring-boot-loader-integration-tests/spring-boot-loader-tests-war/src/main/webapp/WEB-INF/jsp/welcome.jsp new file mode 100644 index 000000000000..811f3e7d00f2 --- /dev/null +++ b/integration-test/spring-boot-loader-integration-tests/spring-boot-loader-tests-war/src/main/webapp/WEB-INF/jsp/welcome.jsp @@ -0,0 +1,13 @@ + + +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> + + + + + + Spring URL: ${springUrl} + JSTL URL: ${url} + + diff --git a/integration-test/spring-boot-loader-integration-tests/src/dockerTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java b/integration-test/spring-boot-loader-integration-tests/src/dockerTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java index 54d662e738ab..56c1fecd4e29 100644 --- a/integration-test/spring-boot-loader-integration-tests/src/dockerTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java +++ b/integration-test/spring-boot-loader-integration-tests/src/dockerTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java @@ -63,6 +63,21 @@ void runJar(JavaRuntime javaRuntime) { } } + @ParameterizedTest + @MethodSource("javaRuntimes") + void runWarWithTldScanning(JavaRuntime javaRuntime) { + try (GenericContainer container = createContainer(javaRuntime, "spring-boot-loader-tests-war", null, + "war")) { + container.start(); + System.out.println(this.output.toUtf8String()); + assertThat(this.output.toUtf8String()).contains("Started LoaderWarTestApplication") + .doesNotContain("no entry name specified") + .doesNotContain("Failed to scan") + .doesNotContain("ZipException") + .doesNotContain("WARNING:"); + } + } + @ParameterizedTest @MethodSource("javaRuntimes") void runSignedJar(JavaRuntime javaRuntime) { @@ -97,31 +112,37 @@ void runSignedJarWhenRsa(JavaRuntime javaRuntime) { } private GenericContainer createContainer(JavaRuntime javaRuntime, String name, String classifier) { + return createContainer(javaRuntime, name, classifier, "jar"); + } + + private GenericContainer createContainer(JavaRuntime javaRuntime, String name, String classifier, + String extension) { + String application = "app." + extension; return javaRuntime.getContainer() .withLogConsumer(this.output) - .withCopyFileToContainer(findApplication(name, classifier), "/app.jar") + .withCopyFileToContainer(findApplication(name, classifier, extension), "/" + application) .withStartupCheckStrategy(new OneShotStartupCheckStrategy().withTimeout(Duration.ofMinutes(5))) - .withCommand(command()); + .withCommand(command(application)); } - private String[] command() { + private String[] command(String application) { List command = new ArrayList<>(); command.add("java"); command.add("-jar"); - command.add("app.jar"); + command.add(application); return command.toArray(new String[0]); } - private MountableFile findApplication(String name, String classifier) { - return MountableFile.forHostPath(findJarFile(name, classifier).toPath()); + private MountableFile findApplication(String name, String classifier, String extension) { + return MountableFile.forHostPath(findArchiveFile(name, classifier, extension).toPath()); } - private File findJarFile(String name, String classifier) { + private File findArchiveFile(String name, String classifier, String extension) { classifier = (classifier != null) ? "-" + classifier : ""; - String path = String.format("build/%1$s/build/libs/%1$s%2$s.jar", name, classifier); - File jar = new File(path); - Assert.state(jar.isFile(), () -> "Could not find " + path + ". Have you built it?"); - return jar; + String path = String.format("build/%1$s/build/libs/%1$s%2$s.%3$s", name, classifier, extension); + File archive = new File(path); + Assert.state(archive.isFile(), () -> "Could not find " + path + ". Have you built it?"); + return archive; } static Stream javaRuntimes() { diff --git a/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java b/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java index 7de31c52698c..51a390501a88 100644 --- a/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java +++ b/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java @@ -337,7 +337,7 @@ else if (item instanceof List) { if (value == null || ClassUtils.isPrimitiveOrWrapper(value.getClass()) || value instanceof String) { return value; } - if (CharSequence.class.isAssignableFrom(value.getClass())) { + if (value instanceof CharSequence) { return value.toString(); } return "Complex property value " + value.getClass().getName(); diff --git a/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java b/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java index 4b979f5c573e..807ceb0761b6 100644 --- a/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java +++ b/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java @@ -208,11 +208,10 @@ private void extract(String root, Map> map, PropertySo } protected @Nullable Object stringifyIfNecessary(@Nullable Object value) { - if (value == null || ClassUtils.isPrimitiveOrWrapper(value.getClass()) - || Number.class.isAssignableFrom(value.getClass())) { + if (value == null || ClassUtils.isPrimitiveOrWrapper(value.getClass()) || value instanceof Number) { return value; } - if (CharSequence.class.isAssignableFrom(value.getClass())) { + if (value instanceof CharSequence) { return value.toString(); } return "Complex property type " + value.getClass().getName(); diff --git a/module/spring-boot-http-codec/src/test/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfigurationTests.java b/module/spring-boot-http-codec/src/test/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfigurationTests.java index 6c221bcbd685..1bdcd559c2cb 100644 --- a/module/spring-boot-http-codec/src/test/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfigurationTests.java +++ b/module/spring-boot-http-codec/src/test/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfigurationTests.java @@ -169,7 +169,7 @@ private T findEncoder(AssertableApplicationContext context, Class encoder .filter((writer) -> writer instanceof EncoderHttpMessageWriter) .map((writer) -> (EncoderHttpMessageWriter) writer) .map(EncoderHttpMessageWriter::getEncoder) - .filter((encoder) -> encoderClass.isAssignableFrom(encoder.getClass())) + .filter(encoderClass::isInstance) .findFirst() .orElseThrow(); } 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 4e7177dc03a5..ec35b5e71df8 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 @@ -465,18 +465,14 @@ private ApplicationContextRunner allOptionsRunner() { private void assertConverterIsRegistered(AssertableApplicationContext context, Class> converterType) { - assertThat(getClientConverters(context)).filteredOn((c) -> converterType.isAssignableFrom(c.getClass())) - .hasSize(1); - assertThat(getServerConverters(context)).filteredOn((c) -> converterType.isAssignableFrom(c.getClass())) - .hasSize(1); + assertThat(getClientConverters(context)).filteredOn(converterType::isInstance).hasSize(1); + assertThat(getServerConverters(context)).filteredOn(converterType::isInstance).hasSize(1); } private void assertConverterIsNotRegistered(AssertableApplicationContext context, Class> converterType) { - assertThat(getClientConverters(context)).filteredOn((c) -> converterType.isAssignableFrom(c.getClass())) - .isEmpty(); - assertThat(getServerConverters(context)).filteredOn((c) -> converterType.isAssignableFrom(c.getClass())) - .isEmpty(); + assertThat(getClientConverters(context)).filteredOn(converterType::isInstance).isEmpty(); + assertThat(getServerConverters(context)).filteredOn(converterType::isInstance).isEmpty(); } private void assertBeanExists(AssertableApplicationContext context, Class type, String beanName) { @@ -504,7 +500,7 @@ private HttpMessageConverters getServerConverters(ApplicationContext context) { private > T findConverter(HttpMessageConverters converters, Class> type) { for (HttpMessageConverter converter : converters) { - if (type.isAssignableFrom(converter.getClass())) { + if (type.isInstance(converter)) { return (T) converter; } } @@ -589,8 +585,7 @@ static class JacksonConverterConfig { @Bean JacksonJsonHttpMessageConverter customJacksonMessageConverter(JsonMapper jsonMapperMapper) { - JacksonJsonHttpMessageConverter converter = new JacksonJsonHttpMessageConverter(jsonMapperMapper); - return converter; + return new JacksonJsonHttpMessageConverter(jsonMapperMapper); } } diff --git a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/autoconfigure/JdbcConnectionDetailsBeanPostProcessor.java b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/autoconfigure/JdbcConnectionDetailsBeanPostProcessor.java index 23d1f7378246..cb7989d775ce 100644 --- a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/autoconfigure/JdbcConnectionDetailsBeanPostProcessor.java +++ b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/autoconfigure/JdbcConnectionDetailsBeanPostProcessor.java @@ -49,7 +49,7 @@ abstract class JdbcConnectionDetailsBeanPostProcessor implements BeanPostProc @Override @SuppressWarnings("unchecked") public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - if (this.dataSourceClass.isAssignableFrom(bean.getClass()) && "dataSource".equals(beanName)) { + if (this.dataSourceClass.isInstance(bean) && "dataSource".equals(beanName)) { JdbcConnectionDetails connectionDetails = this.connectionDetailsProvider.getObject(); if (!(connectionDetails instanceof PropertiesJdbcConnectionDetails)) { return processDataSource((T) bean, connectionDetails); diff --git a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/TracingAndMeterObservationHandlerGroup.java b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/TracingAndMeterObservationHandlerGroup.java index 35872707f621..0c0e11e76ed0 100644 --- a/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/TracingAndMeterObservationHandlerGroup.java +++ b/module/spring-boot-micrometer-tracing/src/main/java/org/springframework/boot/micrometer/tracing/autoconfigure/TracingAndMeterObservationHandlerGroup.java @@ -47,7 +47,7 @@ class TracingAndMeterObservationHandlerGroup implements ObservationHandlerGroup @Override public boolean isMember(ObservationHandler handler) { - return MeterObservationHandler.class.isInstance(handler) || TracingObservationHandler.class.isInstance(handler); + return handler instanceof MeterObservationHandler || handler instanceof TracingObservationHandler; } @Override diff --git a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoHealthIndicator.java b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoHealthIndicator.java index 541a9a996f56..afb9e07db211 100644 --- a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoHealthIndicator.java +++ b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoHealthIndicator.java @@ -17,9 +17,7 @@ package org.springframework.boot.mongodb.health; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import com.mongodb.client.MongoClient; import org.bson.Document; @@ -34,10 +32,13 @@ * MongoDB. * * @author Christian Dupuis + * @author Seonwoo Jung * @since 4.0.0 */ public class MongoHealthIndicator extends AbstractHealthIndicator { + private static final String ADMIN_DATABASE = "admin"; + private static final Document HELLO_COMMAND = Document.parse("{ hello: 1 }"); private final MongoClient mongoClient; @@ -50,15 +51,19 @@ public MongoHealthIndicator(MongoClient mongoClient) { @Override protected void doHealthCheck(Health.Builder builder) throws Exception { - Map details = new LinkedHashMap<>(); List databases = new ArrayList<>(); - details.put("databases", databases); - this.mongoClient.listDatabaseNames().forEach((database) -> { - Document result = this.mongoClient.getDatabase(database).runCommand(HELLO_COMMAND); - databases.add(database); - details.putIfAbsent("maxWireVersion", result.getInteger("maxWireVersion")); - }); - builder.up().withDetails(details); + this.mongoClient.listDatabaseNames().forEach(databases::add); + Document result = this.mongoClient.getDatabase(getDatabaseName(databases)).runCommand(HELLO_COMMAND); + builder.up() + .withDetail("databases", databases) + .withDetail("maxWireVersion", result.getInteger("maxWireVersion")); + } + + private static String getDatabaseName(List databases) { + if (databases.contains(ADMIN_DATABASE)) { + return ADMIN_DATABASE; + } + return (!databases.isEmpty()) ? databases.get(0) : ADMIN_DATABASE; } } diff --git a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicator.java b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicator.java index c73cdd50e6f6..cc4e60356bb5 100644 --- a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicator.java +++ b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicator.java @@ -16,10 +16,7 @@ package org.springframework.boot.mongodb.health; -import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import com.mongodb.reactivestreams.client.MongoClient; import org.bson.Document; @@ -35,10 +32,13 @@ * A {@link ReactiveHealthIndicator} for Mongo. * * @author Yulin Qin + * @author Seonwoo Jung * @since 4.0.0 */ public class MongoReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + private static final String ADMIN_DATABASE = "admin"; + private static final Document HELLO_COMMAND = Document.parse("{ hello: 1 }"); private final MongoClient mongoClient; @@ -51,24 +51,20 @@ public MongoReactiveHealthIndicator(MongoClient mongoClient) { @Override protected Mono doHealthCheck(Health.Builder builder) { - Mono> healthDetails = Flux.from(this.mongoClient.listDatabaseNames()) - .flatMap((database) -> Mono.from(this.mongoClient.getDatabase(database).runCommand(HELLO_COMMAND)) - .map((document) -> new HelloResponse(database, document))) - .collectList() - .map((responses) -> { - Map databaseDetails = new LinkedHashMap<>(); - List databases = new ArrayList<>(); - databaseDetails.put("databases", databases); - for (HelloResponse response : responses) { - databases.add(response.database()); - databaseDetails.putIfAbsent("maxWireVersion", response.document().getInteger("maxWireVersion")); - } - return databaseDetails; - }); - return healthDetails.map((details) -> builder.up().withDetails(details).build()); + Mono> databases = Flux.from(this.mongoClient.listDatabaseNames()).collectList(); + return databases.flatMap((databaseNames) -> Mono + .from(this.mongoClient.getDatabase(getDatabaseName(databaseNames)).runCommand(HELLO_COMMAND)) + .map((result) -> builder.up() + .withDetail("databases", databaseNames) + .withDetail("maxWireVersion", result.getInteger("maxWireVersion")) + .build())); } - private record HelloResponse(String database, Document document) { + private static String getDatabaseName(List databases) { + if (databases.contains(ADMIN_DATABASE)) { + return ADMIN_DATABASE; + } + return (!databases.isEmpty()) ? databases.get(0) : ADMIN_DATABASE; } } diff --git a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoHealthIndicatorTests.java b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoHealthIndicatorTests.java index 27d1f4f2a96e..7973cb146d23 100644 --- a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoHealthIndicatorTests.java +++ b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoHealthIndicatorTests.java @@ -35,12 +35,14 @@ import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; /** * Tests for {@link MongoHealthIndicator}. * * @author Christian Dupuis * @author Andy Wilkinson + * @author Seonwoo Jung */ class MongoHealthIndicatorTests { @@ -52,19 +54,45 @@ void mongoIsUp() { MongoClient mongoClient = mock(MongoClient.class); MongoIterable databaseNames = mock(MongoIterable.class); willAnswer((invocation) -> { - ((Consumer) invocation.getArgument(0)).accept("db"); + ((Consumer) invocation.getArgument(0)).accept("test"); + ((Consumer) invocation.getArgument(0)).accept("admin"); return null; }).given(databaseNames).forEach(any()); given(mongoClient.listDatabaseNames()).willReturn(databaseNames); - MongoDatabase mongoDatabase = mock(MongoDatabase.class); - given(mongoClient.getDatabase("db")).willReturn(mongoDatabase); - given(mongoDatabase.runCommand(Document.parse("{ hello: 1 }"))).willReturn(commandResult); + MongoDatabase adminDatabase = mock(MongoDatabase.class); + given(mongoClient.getDatabase("admin")).willReturn(adminDatabase); + given(adminDatabase.runCommand(Document.parse("{ hello: 1 }"))).willReturn(commandResult); MongoHealthIndicator healthIndicator = new MongoHealthIndicator(mongoClient); Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getDetails()).containsEntry("maxWireVersion", 10); - assertThat(health.getDetails()).containsEntry("databases", List.of("db")); + assertThat(health.getDetails()).containsEntry("databases", List.of("test", "admin")); then(commandResult).should().getInteger("maxWireVersion"); + // the hello command must only be run once, never per listed database + then(mongoClient).should(never()).getDatabase("test"); + } + + @Test + @SuppressWarnings("unchecked") + void mongoUsesFirstDatabaseWhenAdminIsNotVisible() { + Document commandResult = mock(Document.class); + given(commandResult.getInteger("maxWireVersion")).willReturn(10); + MongoClient mongoClient = mock(MongoClient.class); + MongoIterable databaseNames = mock(MongoIterable.class); + willAnswer((invocation) -> { + ((Consumer) invocation.getArgument(0)).accept("test"); + return null; + }).given(databaseNames).forEach(any()); + given(mongoClient.listDatabaseNames()).willReturn(databaseNames); + MongoDatabase database = mock(MongoDatabase.class); + given(mongoClient.getDatabase("test")).willReturn(database); + given(database.runCommand(Document.parse("{ hello: 1 }"))).willReturn(commandResult); + MongoHealthIndicator healthIndicator = new MongoHealthIndicator(mongoClient); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsEntry("maxWireVersion", 10); + assertThat(health.getDetails()).containsEntry("databases", List.of("test")); + then(mongoClient).should(never()).getDatabase("admin"); } @Test diff --git a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicatorTests.java b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicatorTests.java index f6f115e40a41..8fd46cfeda68 100644 --- a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicatorTests.java +++ b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicatorTests.java @@ -24,6 +24,7 @@ import com.mongodb.reactivestreams.client.MongoDatabase; import org.bson.Document; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -32,23 +33,26 @@ 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; +import static org.mockito.Mockito.never; /** * Tests for {@link MongoReactiveHealthIndicator}. * * @author Yulin Qin + * @author Seonwoo Jung */ class MongoReactiveHealthIndicatorTests { @Test void mongoIsUp() { MongoClient mongoClient = mock(MongoClient.class); - given(mongoClient.listDatabaseNames()).willReturn(Mono.just("db")); - MongoDatabase mongoDatabase = mock(MongoDatabase.class); - given(mongoClient.getDatabase("db")).willReturn(mongoDatabase); + given(mongoClient.listDatabaseNames()).willReturn(Flux.just("test", "admin")); + MongoDatabase adminDatabase = mock(MongoDatabase.class); + given(mongoClient.getDatabase("admin")).willReturn(adminDatabase); Document commandResult = mock(Document.class); - given(mongoDatabase.runCommand(Document.parse("{ hello: 1 }"))).willReturn(Mono.just(commandResult)); + given(adminDatabase.runCommand(Document.parse("{ hello: 1 }"))).willReturn(Mono.just(commandResult)); given(commandResult.getInteger("maxWireVersion")).willReturn(10); MongoReactiveHealthIndicator mongoReactiveHealthIndicator = new MongoReactiveHealthIndicator(mongoClient); Mono health = mongoReactiveHealthIndicator.health(); @@ -56,14 +60,40 @@ void mongoIsUp() { assertThat(h.getStatus()).isEqualTo(Status.UP); assertThat(h.getDetails()).containsOnlyKeys("maxWireVersion", "databases"); assertThat(h.getDetails()).containsEntry("maxWireVersion", 10); - assertThat(h.getDetails()).containsEntry("databases", List.of("db")); + assertThat(h.getDetails()).containsEntry("databases", List.of("test", "admin")); }).expectComplete().verify(Duration.ofSeconds(30)); + // the hello command must only be run once, never per listed database + then(mongoClient).should(never()).getDatabase("test"); + } + + @Test + void mongoUsesFirstDatabaseWhenAdminIsNotVisible() { + MongoClient mongoClient = mock(MongoClient.class); + given(mongoClient.listDatabaseNames()).willReturn(Flux.just("test")); + MongoDatabase database = mock(MongoDatabase.class); + given(mongoClient.getDatabase("test")).willReturn(database); + Document commandResult = mock(Document.class); + given(database.runCommand(Document.parse("{ hello: 1 }"))).willReturn(Mono.just(commandResult)); + given(commandResult.getInteger("maxWireVersion")).willReturn(10); + MongoReactiveHealthIndicator mongoReactiveHealthIndicator = new MongoReactiveHealthIndicator(mongoClient); + Mono health = mongoReactiveHealthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> { + assertThat(h.getStatus()).isEqualTo(Status.UP); + assertThat(h.getDetails()).containsOnlyKeys("maxWireVersion", "databases"); + assertThat(h.getDetails()).containsEntry("maxWireVersion", 10); + assertThat(h.getDetails()).containsEntry("databases", List.of("test")); + }).expectComplete().verify(Duration.ofSeconds(30)); + then(mongoClient).should(never()).getDatabase("admin"); } @Test void mongoIsDown() { MongoClient mongoClient = mock(MongoClient.class); - given(mongoClient.listDatabaseNames()).willThrow(new MongoException("Connection failed")); + given(mongoClient.listDatabaseNames()).willReturn(Flux.just("admin")); + MongoDatabase adminDatabase = mock(MongoDatabase.class); + given(mongoClient.getDatabase("admin")).willReturn(adminDatabase); + given(adminDatabase.runCommand(Document.parse("{ hello: 1 }"))) + .willReturn(Mono.error(new MongoException("Connection failed"))); MongoReactiveHealthIndicator mongoReactiveHealthIndicator = new MongoReactiveHealthIndicator(mongoClient); Mono health = mongoReactiveHealthIndicator.health(); StepVerifier.create(health).consumeNextWith((h) -> { 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 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 3ffe57f0331c..82601e4332b5 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 @@ -314,7 +314,7 @@ private void customizeHandler(ConfigurableTomcatWebS Class type, ObjIntConsumer consumer) { factory.addConnectorCustomizers((connector) -> { ProtocolHandler handler = connector.getProtocolHandler(); - if (type.isAssignableFrom(handler.getClass())) { + if (type.isInstance(handler)) { consumer.accept(type.cast(handler), value); } });