From b340c855c031ec25b3db37bc74ad4cc47f11a549 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 2 Dec 2024 19:02:02 -0800 Subject: [PATCH] Prevent H2 console from causing early DataSource initialization Update `H2ConsoleAutoConfiguration` so that DataSource connection logging occurs outside of the `ServletRegistrationBean`. Fixes gh-43337 --- .../h2/H2ConsoleAutoConfiguration.java | 90 ++++++++++++------- .../h2/H2ConsoleAutoConfigurationTests.java | 35 +++++++- 2 files changed, 90 insertions(+), 35 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java index 22720348a2b..7f2c096264f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ import org.springframework.core.log.LogMessage; * @author Andy Wilkinson * @author Marten Deinum * @author Stephane Nicoll + * @author Phillip Webb * @since 1.3.0 */ @AutoConfiguration(after = DataSourceAutoConfiguration.class) @@ -57,46 +58,25 @@ public class H2ConsoleAutoConfiguration { private static final Log logger = LogFactory.getLog(H2ConsoleAutoConfiguration.class); + private final H2ConsoleProperties properties; + + H2ConsoleAutoConfiguration(H2ConsoleProperties properties) { + this.properties = properties; + } + @Bean - public ServletRegistrationBean h2Console(H2ConsoleProperties properties, - ObjectProvider dataSource) { - String path = properties.getPath(); + public ServletRegistrationBean h2Console() { + String path = this.properties.getPath(); String urlMapping = path + (path.endsWith("/") ? "*" : "/*"); ServletRegistrationBean registration = new ServletRegistrationBean<>(new JakartaWebServlet(), urlMapping); - configureH2ConsoleSettings(registration, properties.getSettings()); - if (logger.isInfoEnabled()) { - withThreadContextClassLoader(getClass().getClassLoader(), () -> logDataSources(dataSource, path)); - } + configureH2ConsoleSettings(registration, this.properties.getSettings()); return registration; } - private void withThreadContextClassLoader(ClassLoader classLoader, Runnable action) { - ClassLoader previous = Thread.currentThread().getContextClassLoader(); - try { - Thread.currentThread().setContextClassLoader(classLoader); - action.run(); - } - finally { - Thread.currentThread().setContextClassLoader(previous); - } - } - - private void logDataSources(ObjectProvider dataSource, String path) { - List urls = dataSource.orderedStream().map(this::getConnectionUrl).filter(Objects::nonNull).toList(); - if (!urls.isEmpty()) { - logger.info(LogMessage.format("H2 console available at '%s'. %s available at %s", path, - (urls.size() > 1) ? "Databases" : "Database", String.join(", ", urls))); - } - } - - private String getConnectionUrl(DataSource dataSource) { - try (Connection connection = dataSource.getConnection()) { - return "'" + connection.getMetaData().getURL() + "'"; - } - catch (Exception ex) { - return null; - } + @Bean + H2ConsoleLogger h2ConsoleLogger(ObjectProvider dataSource) { + return new H2ConsoleLogger(dataSource, this.properties.getPath()); } private void configureH2ConsoleSettings(ServletRegistrationBean registration, @@ -112,4 +92,46 @@ public class H2ConsoleAutoConfiguration { } } + static class H2ConsoleLogger { + + H2ConsoleLogger(ObjectProvider dataSources, String path) { + if (logger.isInfoEnabled()) { + ClassLoader classLoader = getClass().getClassLoader(); + withThreadContextClassLoader(classLoader, () -> log(getConnectionUrls(dataSources), path)); + } + } + + private void withThreadContextClassLoader(ClassLoader classLoader, Runnable action) { + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(classLoader); + action.run(); + } + finally { + Thread.currentThread().setContextClassLoader(previous); + } + } + + private List getConnectionUrls(ObjectProvider dataSources) { + return dataSources.orderedStream().map(this::getConnectionUrl).filter(Objects::nonNull).toList(); + } + + private String getConnectionUrl(DataSource dataSource) { + try (Connection connection = dataSource.getConnection()) { + return "'" + connection.getMetaData().getURL() + "'"; + } + catch (Exception ex) { + return null; + } + } + + private void log(List urls, String path) { + if (!urls.isEmpty()) { + logger.info(LogMessage.format("H2 console available at '%s'. %s available at %s", path, + (urls.size() > 1) ? "Databases" : "Database", String.join(", ", urls))); + } + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java index f478262c816..d577b59baed 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,15 +30,20 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.BeanCreationException; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationPropertiesBindException; import org.springframework.boot.context.properties.bind.BindException; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -51,6 +56,7 @@ import static org.mockito.Mockito.mock; * @author Marten Deinum * @author Stephane Nicoll * @author Shraddha Yeole + * @author Phillip Webb */ class H2ConsoleAutoConfigurationTests { @@ -163,6 +169,22 @@ class H2ConsoleAutoConfigurationTests { .run((context) -> assertThat(context.isRunning()).isTrue()); } + @Test + @ExtendWith(OutputCaptureExtension.class) + void dataSourceIsNotInitializedEarly(CapturedOutput output) { + new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(H2ConsoleAutoConfiguration.class, + ServletWebServerFactoryAutoConfiguration.class)) + .withUserConfiguration(EarlyInitializationConfiguration.class) + .withPropertyValues("spring.h2.console.enabled=true", "server.port=0") + .run((context) -> { + try (Connection connection = context.getBean(DataSource.class).getConnection()) { + assertThat(output).contains("H2 console available at '/h2-console'. Database available at '" + + connection.getMetaData().getURL() + "'"); + } + }); + } + @Configuration(proxyBeanMethods = false) static class FailingDataSourceConfiguration { @@ -206,4 +228,15 @@ class H2ConsoleAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class EarlyInitializationConfiguration { + + @Bean + DataSource dataSource(ConfigurableApplicationContext applicationContext) { + assertThat(applicationContext.getBeanFactory().isConfigurationFrozen()).isTrue(); + return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build(); + } + + } + }