fix(jdbc): restore persisted legacy driver class aliases

Closes #7144
This commit is contained in:
t8y2
2026-10-02 09:05:15 +00:00
parent 0b360aac7d
commit 012915da2c
4 changed files with 416 additions and 2 deletions
@@ -732,7 +732,17 @@ public final class DbxJdbcPlugin {
throw new SQLException("JDBC logical sessions require the same selected driver");
}
if (connectionState() != DEFAULT_CONNECTION_STATE) logicalDriverKey = driverKey;
String driverClass = optionalText(connection, "jdbc_driver_class");
if (driverKey.equals(registeredDriverKey) && registeredDriver != null) {
if (driverClass != null) {
LegacyJdbcDriverClass.load(
driverClass,
optionalText(connection, "connection_string"),
registeredDriverClassLoader != null
? registeredDriverClassLoader
: Thread.currentThread().getContextClassLoader()
);
}
if (registeredDriverClassLoader != null) {
Thread.currentThread().setContextClassLoader(registeredDriverClassLoader);
}
@@ -757,9 +767,10 @@ public final class DbxJdbcPlugin {
Thread.currentThread().setContextClassLoader(loader);
registeredDriverClassLoader = loader;
String driverClass = optionalText(connection, "jdbc_driver_class");
if (driverClass != null) {
Constructor<?> constructor = Class.forName(driverClass, true, loader).getDeclaredConstructor();
Constructor<?> constructor = LegacyJdbcDriverClass.load(
driverClass, optionalText(connection, "connection_string"), loader
).getDeclaredConstructor();
constructor.setAccessible(true);
Driver driver = (Driver) constructor.newInstance();
registeredDriver = new DriverShim(driver);
@@ -0,0 +1,29 @@
package app.dbx.jdbc;
final class LegacyJdbcDriverClass {
private LegacyJdbcDriverClass() {}
static Class<?> load(String driverClass, String jdbcUrl, ClassLoader loader) throws ClassNotFoundException {
try {
return Class.forName(driverClass, true, loader);
} catch (ClassNotFoundException error) {
if (!driverClass.equals(error.getMessage()) || error.getCause() != null || jdbcUrl == null) {
throw error;
}
String resolved = switch (driverClass) {
case "teradata" -> hasUrlPrefix(jdbcUrl, "jdbc:teradata://") ? "com.teradata.jdbc.TeraDriver" : null;
case "db2" -> hasUrlPrefix(jdbcUrl, "jdbc:db2:") ? "com.ibm.db2.jcc.DB2Driver" : null;
case "h2_embedded", "h2_embedded_v2" -> hasUrlPrefix(jdbcUrl, "jdbc:h2:") ? "org.h2.Driver" : null;
default -> null;
};
if (resolved == null) {
throw error;
}
return Class.forName(resolved, true, loader);
}
}
private static boolean hasUrlPrefix(String url, String prefix) {
return url.startsWith(prefix) && url.length() > prefix.length();
}
}
@@ -0,0 +1,301 @@
package app.dbx.jdbc;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import javax.tools.ToolProvider;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Driver;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import static org.junit.jupiter.api.Assertions.*;
final class LegacyJdbcDriverClassTest {
private static final String H2_URL = "jdbc:h2:mem:legacy_class_resolution";
private static final String TERADATA_URL = "jdbc:teradata://127.0.0.1/DATABASE=fixture,DBS_PORT=1025";
private static final String DB2_URL = "jdbc:db2://127.0.0.1:50000/FIXTURE";
@TempDir
Path temporaryDirectory;
@Test
void resolvesHistoricalH2AliasesUsingTheSelectedJar() throws Exception {
try (URLClassLoader loader = h2Loader()) {
for (String alias : List.of("h2_embedded", "h2_embedded_v2")) {
assertThrows(ClassNotFoundException.class, () -> Class.forName(alias, true, loader));
Class<?> resolved = LegacyJdbcDriverClass.load(alias, H2_URL, loader);
assertEquals("org.h2.Driver", resolved.getName());
assertSame(loader, resolved.getClassLoader());
assertInstanceOf(Driver.class, resolved.getDeclaredConstructor().newInstance());
}
}
}
@Test
void resolvesHistoricalTeradataAndDb2Identities() throws Exception {
for (String[] fixture : List.of(
new String[] { "teradata", TERADATA_URL, "com.teradata.jdbc.TeraDriver" },
new String[] { "db2", DB2_URL, "com.ibm.db2.jcc.DB2Driver" }
)) {
try (URLClassLoader loader = fixtureLoader(fixture[2], "")) {
assertThrows(ClassNotFoundException.class, () -> Class.forName(fixture[0], true, loader));
Class<?> resolved = LegacyJdbcDriverClass.load(fixture[0], fixture[1], loader);
assertEquals(fixture[2], resolved.getName());
assertSame(loader, resolved.getClassLoader());
assertInstanceOf(Driver.class, resolved.getDeclaredConstructor().newInstance());
}
}
}
@Test
void preservesExplicitClassesRegardlessOfUrl() throws Exception {
try (URLClassLoader loader = h2Loader()) {
Class<?> explicit = Class.forName("org.h2.Driver", true, loader);
assertSame(explicit, LegacyJdbcDriverClass.load("org.h2.Driver", TERADATA_URL, loader));
assertSame(explicit, LegacyJdbcDriverClass.load("org.h2.Driver", null, loader));
}
}
@Test
void preservesLoadableUnqualifiedCustomClassesIncludingAliases() throws Exception {
for (String name : List.of("teradata", "db2", "h2_embedded", "h2_embedded_v2", "CustomDriver")) {
try (URLClassLoader loader = fixtureLoader(name, "")) {
Class<?> explicit = Class.forName(name, true, loader);
assertSame(explicit, LegacyJdbcDriverClass.load(name, TERADATA_URL, loader));
assertSame(explicit, LegacyJdbcDriverClass.load(name, H2_URL, loader));
assertInstanceOf(Driver.class, explicit.getDeclaredConstructor().newInstance());
}
}
}
@Test
void rejectsUnknownAndMalformedAliases() throws Exception {
try (URLClassLoader loader = h2Loader()) {
for (String name : List.of("custom.MissingDriver", "unknown", "h2_embedded_v3", "H2_embedded",
" h2_embedded", "h2_embedded ", "", "com.example.teradata")) {
ClassNotFoundException error = assertThrows(ClassNotFoundException.class,
() -> LegacyJdbcDriverClass.load(name, H2_URL, loader));
assertEquals(name, error.getMessage());
}
}
}
@Test
void rejectsMissingMalformedAndMismatchedUrls() throws Exception {
try (URLClassLoader loader = h2Loader()) {
for (String[] fixture : List.of(
new String[] { "teradata", H2_URL },
new String[] { "db2", TERADATA_URL },
new String[] { "h2_embedded", DB2_URL },
new String[] { "h2_embedded_v2", "jdbc:h2other:mem:test" },
new String[] { "teradata", "jdbc:teradatax://localhost" },
new String[] { "db2", "jdbc:db20://localhost/TEST" },
new String[] { "h2_embedded", "prefix:jdbc:h2:mem:test" },
new String[] { "h2_embedded", "jdbc:h2:" },
new String[] { "teradata", "jdbc:teradata://" },
new String[] { "db2", "jdbc:db2:" },
new String[] { "h2_embedded", "" },
new String[] { "teradata", null }
)) {
ClassNotFoundException error = assertThrows(ClassNotFoundException.class,
() -> LegacyJdbcDriverClass.load(fixture[0], fixture[1], loader));
assertEquals(fixture[0], error.getMessage());
}
}
}
@Test
void doesNotReplaceMissingSelectedJarsWithContextDrivers() throws Exception {
ClassLoader previous = Thread.currentThread().getContextClassLoader();
try (URLClassLoader context = h2Loader();
URLClassLoader selected = new URLClassLoader(
new URL[] { temporaryDirectory.resolve("missing.jar").toUri().toURL() },
ClassLoader.getPlatformClassLoader())) {
Thread.currentThread().setContextClassLoader(context);
ClassNotFoundException error = assertThrows(ClassNotFoundException.class,
() -> LegacyJdbcDriverClass.load("h2_embedded_v2", H2_URL, selected));
assertEquals("org.h2.Driver", error.getMessage());
assertSame(context, Thread.currentThread().getContextClassLoader());
} finally {
Thread.currentThread().setContextClassLoader(previous);
}
}
@Test
void usesTheSuppliedContextLoaderWhenNoPathsAreSelected() throws Exception {
ClassLoader previous = Thread.currentThread().getContextClassLoader();
try (URLClassLoader context = h2Loader()) {
Thread.currentThread().setContextClassLoader(context);
Class<?> resolved = LegacyJdbcDriverClass.load("h2_embedded", H2_URL,
Thread.currentThread().getContextClassLoader());
assertSame(context, resolved.getClassLoader());
assertSame(context, Thread.currentThread().getContextClassLoader());
} finally {
Thread.currentThread().setContextClassLoader(previous);
}
}
@Test
void preservesTheSuppliedLoadersParentDelegation() throws Exception {
try (URLClassLoader parent = h2Loader();
URLClassLoader child = new URLClassLoader(new URL[0], parent)) {
Class<?> resolved = LegacyJdbcDriverClass.load("h2_embedded", H2_URL, child);
assertSame(parent, resolved.getClassLoader());
}
}
@Test
void preservesClassNotFoundForADifferentClass() {
ClassNotFoundException failure = new ClassNotFoundException("vendor.RequiredDependency");
ClassLoader loader = new ClassLoader(ClassLoader.getPlatformClassLoader()) {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw failure;
}
};
assertSame(failure, assertThrows(ClassNotFoundException.class,
() -> LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader)));
}
@Test
void preservesClassNotFoundThatWrapsALoadingFailure() {
ClassNotFoundException failure = new ClassNotFoundException("teradata",
new NoClassDefFoundError("vendor/RequiredDependency"));
ClassLoader loader = new ClassLoader(ClassLoader.getPlatformClassLoader()) {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw failure;
}
};
assertSame(failure, assertThrows(ClassNotFoundException.class,
() -> LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader)));
}
@Test
void propagatesInitializerFailuresWithoutAliasFallback() throws Exception {
try (URLClassLoader loader = fixtureLoader("teradata",
"static { if (true) throw new IllegalStateException(\"initializer failed\"); }")) {
ExceptionInInitializerError error = assertThrows(ExceptionInInitializerError.class,
() -> LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader));
assertEquals("initializer failed", error.getCause().getMessage());
assertThrows(NoClassDefFoundError.class,
() -> LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader));
}
}
@Test
void propagatesLinkageErrorsWithoutAliasFallback() {
LinkageError failure = new UnsupportedClassVersionError("driver requires a newer Java version");
ClassLoader loader = new ClassLoader(ClassLoader.getPlatformClassLoader()) {
@Override
protected Class<?> findClass(String name) {
throw failure;
}
};
assertSame(failure, assertThrows(LinkageError.class,
() -> LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader)));
}
@Test
void propagatesFailuresInTheResolvedVendorClass() throws Exception {
try (URLClassLoader loader = fixtureLoader("com.teradata.jdbc.TeraDriver",
"static { if (true) throw new UnsatisfiedLinkError(\"vendor library missing\"); }")) {
UnsatisfiedLinkError error = assertThrows(UnsatisfiedLinkError.class,
() -> LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader));
assertEquals("vendor library missing", error.getMessage());
}
try (URLClassLoader loader = fixtureLoader("com.ibm.db2.jcc.DB2Driver",
"static { if (true) throw new IllegalStateException(\"vendor initialization failed\"); }")) {
ExceptionInInitializerError error = assertThrows(ExceptionInInitializerError.class,
() -> LegacyJdbcDriverClass.load("db2", DB2_URL, loader));
assertEquals("vendor initialization failed", error.getCause().getMessage());
}
}
@Test
void rechecksTheUrlOnRepeatedResolution() throws Exception {
try (URLClassLoader loader = h2Loader()) {
Class<?> expected = LegacyJdbcDriverClass.load("h2_embedded", H2_URL, loader);
ClassNotFoundException error = assertThrows(ClassNotFoundException.class,
() -> LegacyJdbcDriverClass.load("h2_embedded", DB2_URL, loader));
assertEquals("h2_embedded", error.getMessage());
assertSame(expected, LegacyJdbcDriverClass.load("h2_embedded", H2_URL, loader));
}
}
@Test
void leavesConstructionFailuresAndDriverTypeValidationToTheCaller() throws Exception {
try (URLClassLoader loader = fixtureLoader("teradata",
"public teradata() { throw new IllegalStateException(\"constructor failed\"); }")) {
Class<?> resolved = LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader);
InvocationTargetException error = assertThrows(InvocationTargetException.class,
() -> resolved.getDeclaredConstructor().newInstance());
assertEquals("constructor failed", error.getCause().getMessage());
}
try (URLClassLoader loader = compile("teradata", "public class teradata {}")) {
Class<?> resolved = LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader);
assertThrows(ClassCastException.class, () -> Driver.class.cast(resolved.getDeclaredConstructor().newInstance()));
}
}
@Test
void isolatesConcurrentResolutionsByLoader() throws Exception {
try (URLClassLoader first = h2Loader(); URLClassLoader second = h2Loader();
var executor = Executors.newFixedThreadPool(4)) {
List<Callable<Class<?>>> tasks = new ArrayList<>();
for (int index = 0; index < 20; index++) {
ClassLoader selected = index % 2 == 0 ? first : second;
tasks.add(() -> LegacyJdbcDriverClass.load("h2_embedded_v2", H2_URL, selected));
}
var results = executor.invokeAll(tasks);
assertNotSame(results.get(0).get(), results.get(1).get());
for (int index = 0; index < results.size(); index++) {
assertSame(index % 2 == 0 ? first : second, results.get(index).get().getClassLoader());
}
}
}
private static URLClassLoader h2Loader() {
return new URLClassLoader(new URL[] { org.h2.Driver.class.getProtectionDomain().getCodeSource().getLocation() },
ClassLoader.getPlatformClassLoader());
}
private URLClassLoader fixtureLoader(String name, String body) throws Exception {
int separator = name.lastIndexOf('.');
String packageDeclaration = separator < 0 ? "" : "package " + name.substring(0, separator) + ";";
String simpleName = name.substring(separator + 1);
return compile(name, packageDeclaration + "public class " + simpleName + " implements java.sql.Driver {"
+ body + """
public java.sql.Connection connect(String url, java.util.Properties properties) {
throw new AssertionError("Registration must not connect");
}
public boolean acceptsURL(String url) { return false; }
public java.sql.DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties properties) {
return new java.sql.DriverPropertyInfo[0];
}
public int getMajorVersion() { return 1; }
public int getMinorVersion() { return 0; }
public boolean jdbcCompliant() { return false; }
public java.util.logging.Logger getParentLogger() { return java.util.logging.Logger.getGlobal(); }
}
""");
}
private URLClassLoader compile(String name, String source) throws Exception {
Path root = Files.createTempDirectory(temporaryDirectory, "driver-");
Path file = root.resolve(name.replace('.', '/') + ".java");
Files.createDirectories(file.getParent());
Files.writeString(file, source);
var compiler = ToolProvider.getSystemJavaCompiler();
assertNotNull(compiler, "Driver classloading fixtures require the Java 21 JDK");
assertEquals(0, compiler.run(null, null, null, "-proc:none", "-d", root.toString(), file.toString()));
return new URLClassLoader(new URL[] { root.toUri().toURL() }, ClassLoader.getPlatformClassLoader());
}
}
@@ -0,0 +1,73 @@
package app.dbx.jdbc;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class LegacyJdbcRegistrationTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
@AfterEach
void closeConnection() throws Exception {
request("close", configuration("org.h2.Driver"), null);
}
@Test
void legacyAliasesAndExplicitOrDefaultClassesReachTheProductionQueryPath() throws Exception {
for (String driverClass : new String[] {"h2_embedded", "h2_embedded_v2", "org.h2.Driver", ""}) {
ObjectNode connection = configuration(driverClass);
JsonNode connected = request("connect", connection, null);
assertFalse(connected.has("error"), connected.toString());
JsonNode result = request("executeQuery", connection, "SELECT 1");
assertFalse(result.has("error"), result.toString());
assertEquals(1, result.path("result").path("rows").path(0).path(0).asInt());
request("close", connection, null);
}
}
@Test
void cachedLegacyAliasCannotBypassUrlValidationAndRecoversAfterAnError() throws Exception {
ObjectNode connection = configuration("h2_embedded_v2");
JsonNode connected = request("connect", connection, null);
assertFalse(connected.has("error"), connected.toString());
ObjectNode mismatched = connection.deepCopy();
mismatched.put("connection_string", "jdbc:postgresql://127.0.0.1/unused");
JsonNode rejected = request("connect", mismatched, null);
assertTrue(rejected.has("error"), rejected.toString());
assertTrue(rejected.path("error").toString().contains("Missing Java class h2_embedded_v2."), rejected.toString());
JsonNode recovered = request("executeQuery", connection, "SELECT 2");
assertFalse(recovered.has("error"), recovered.toString());
assertEquals(2, recovered.path("result").path("rows").path(0).path(0).asInt());
}
private static ObjectNode configuration(String driverClass) {
ObjectNode connection = MAPPER.createObjectNode();
connection.put("connection_string", "jdbc:h2:mem:legacy_registration_test");
connection.put("username", "sa");
connection.put("password", "");
connection.put("jdbc_driver_class", driverClass);
return connection;
}
private static JsonNode request(String method, ObjectNode connection, String sql) throws Exception {
ObjectNode message = MAPPER.createObjectNode();
message.put("id", 1);
message.put("method", method);
ObjectNode params = message.putObject("params");
params.set("connection", connection);
if (sql != null) params.put("sql", sql);
Method handler = DbxJdbcPlugin.class.getDeclaredMethod("handleLine", String.class);
handler.setAccessible(true);
return (JsonNode) handler.invoke(null, MAPPER.writeValueAsString(message));
}
}