Skip to content

Commit

Permalink
Ensure that TCCL is clean when DataSource is accessed
Browse files Browse the repository at this point in the history
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
  • Loading branch information
wilkinsona committed Sep 16, 2022
1 parent ab26050 commit 61e11cd
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 7 deletions.
@@ -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.
Expand Down Expand Up @@ -67,11 +67,22 @@ public ServletRegistrationBean<WebServlet> h2Console(H2ConsoleProperties propert
ServletRegistrationBean<WebServlet> 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> dataSource, String path) {
List<String> urls = dataSource.orderedStream().map((available) -> {
try (Connection connection = available.getConnection()) {
Expand Down
@@ -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.
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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'"));
Expand Down Expand Up @@ -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<Connection>() {

@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;
}

Expand Down

0 comments on commit 61e11cd

Please sign in to comment.