From 61e11cd361cbdbb731a15daab4bf17c21afe6743 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 16 Sep 2022 16:24:57 +0100 Subject: [PATCH] Ensure that TCCL is clean when DataSource is accessed Previously, when using Tomcat, its web app class loader was the thread context class loader when H2ConsoleAutoConfiguration triggered initialization of Hikari's pool. This was the case because it's done in the bean method of a ServletRegistrationBean. Such Servlet-related beans are intentionally created with Tomcat's web app classloader as the TCCL. This arrangement results in the pool's threads using Tomcat's web app class loader as their TCCL which is not desirable. One consequence of this was that Tomcat could log a warning at shutdown about the thread being left running when it will, in fact, be stopped as part of the context being closed. This commit updates H2ConsoleAutoConfiguration to set the TCCL to its own ClassLoader while the DataSource information is being logged. Closes gh-32382 --- .../h2/H2ConsoleAutoConfiguration.java | 15 +++++++++-- .../h2/H2ConsoleAutoConfigurationTests.java | 26 +++++++++++++++---- 2 files changed, 34 insertions(+), 7 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 85a7fdc8d5b8..8d4385cd80b5 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-2021 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,11 +67,22 @@ public ServletRegistrationBean h2Console(H2ConsoleProperties propert ServletRegistrationBean registration = new ServletRegistrationBean<>(new WebServlet(), urlMapping); configureH2ConsoleSettings(registration, properties.getSettings()); if (logger.isInfoEnabled()) { - logDataSources(dataSource, path); + withThreadContextClassLoader(getClass().getClassLoader(), () -> logDataSources(dataSource, path)); } 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((available) -> { try (Connection connection = available.getConnection()) { 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 132b37a0d180..e3c371954969 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-2021 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.h2; +import java.net.URL; +import java.net.URLClassLoader; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.SQLException; @@ -24,6 +26,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.springframework.beans.factory.BeanCreationException; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -137,7 +141,8 @@ void noDataSourceIsLoggedWhenNoneAvailable(CapturedOutput output) { @Test @ExtendWith(OutputCaptureExtension.class) void allDataSourceUrlsAreLoggedWhenMultipleAvailable(CapturedOutput output) { - this.contextRunner + ClassLoader webAppClassLoader = new URLClassLoader(new URL[0]); + this.contextRunner.withClassLoader(webAppClassLoader) .withUserConfiguration(FailingDataSourceConfiguration.class, MultiDataSourceConfiguration.class) .withPropertyValues("spring.h2.console.enabled=true").run((context) -> assertThat(output).contains( "H2 console available at '/h2-console'. Databases available at 'someJdbcUrl', 'anotherJdbcUrl'")); @@ -179,9 +184,20 @@ DataSource someDataSource() throws SQLException { private DataSource mockDataSource(String url) throws SQLException { DataSource dataSource = mock(DataSource.class); - given(dataSource.getConnection()).willReturn(mock(Connection.class)); - given(dataSource.getConnection().getMetaData()).willReturn(mock(DatabaseMetaData.class)); - given(dataSource.getConnection().getMetaData().getURL()).willReturn(url); + given(dataSource.getConnection()).will(new Answer() { + + @Override + public Connection answer(InvocationOnMock invocation) throws Throwable { + assertThat(Thread.currentThread().getContextClassLoader()).isEqualTo(getClass().getClassLoader()); + Connection connection = mock(Connection.class); + DatabaseMetaData metadata = mock(DatabaseMetaData.class); + given(connection.getMetaData()).willReturn(metadata); + given(metadata.getURL()).willReturn(url); + return connection; + } + + }); + return dataSource; }