Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

java.lang.NoClassDefFoundError: java/sql/ShardingKey running Hibernate 5.3.37, c3p0 0.10.1, and Java 8 #184

Open
ForceRs opened this issue Jan 24, 2025 · 20 comments

Comments

@ForceRs
Copy link

ForceRs commented Jan 24, 2025

I opened an issue with Hibernate for this, but now believe the issue is with c3p0.
When running Hibernate 5.3.37, c3p0 0.10.1, Tomcat/9.0.63, and Java 1.8.0_391-b13, Hibernate fails to initiate as follows:

java.lang.NoClassDefFoundError: java/sql/ShardingKey
	at java.lang.Class.getDeclaredMethods0(Native Method) ~[?:1.8.0_391]
	at java.lang.Class.privateGetDeclaredMethods(Class.java:2701) ~[?:1.8.0_391]
	at java.lang.Class.privateGetMethodRecursive(Class.java:3048) ~[?:1.8.0_391]
	at java.lang.Class.getMethod0(Class.java:3018) ~[?:1.8.0_391]
	at java.lang.Class.getMethod(Class.java:1784) ~[?:1.8.0_391]
	at org.hibernate.engine.jdbc.env.internal.DefaultSchemaNameResolver.determineAppropriateResolverDelegate(DefaultSchemaNameResolver.java:44) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.engine.jdbc.env.internal.DefaultSchemaNameResolver.resolveSchemaName(DefaultSchemaNameResolver.java:75) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentImpl.determineCurrentSchemaName(JdbcEnvironmentImpl.java:298) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentImpl.<init>(JdbcEnvironmentImpl.java:232) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:114) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:35) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:94) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:237) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:214) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.id.factory.internal.DefaultIdentifierGeneratorFactory.injectServices(DefaultIdentifierGeneratorFactory.java:152) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.service.internal.AbstractServiceRegistryImpl.injectDependencies(AbstractServiceRegistryImpl.java:286) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:243) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:214) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:179) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:119) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.build(MetadataBuildingProcess.java:84) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.boot.internal.MetadataBuilderImpl.build(MetadataBuilderImpl.java:474) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.boot.internal.MetadataBuilderImpl.build(MetadataBuilderImpl.java:85) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:689) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:724) ~[hibernate-core-5.3.37.Final.jar:5.3.37.Final]
	at com.company.test.persistence.hibernate.HibernateUtils.initSessionFactory(HibernateUtils.java:130) [classes/:?]
	at com.company.test.server.DBServer.init(DBServer.java:603) [classes/:?]
	at com.company.test.shared.SpringReplacementHelper.getDbServer(SpringReplacementHelper.java:326) [classes/:?]
	at com.company.test.shared.SpringReplacementHelper.bootstrap(SpringReplacementHelper.java:393) [classes/:?]
	at com.company.test.server.testServlet.init(testServlet.java:231) [classes/:?]
	at org.apache.catalina.core.StandardWrapper.initServlet(StandardWrapper.java:1164) [catalina.jar:9.0.63]
	at org.apache.catalina.core.StandardWrapper.loadServlet(StandardWrapper.java:1117) [catalina.jar:9.0.63]
	at org.apache.catalina.core.StandardWrapper.load(StandardWrapper.java:1010) [catalina.jar:9.0.63]
	at org.apache.catalina.core.StandardContext.loadOnStartup(StandardContext.java:4957) [catalina.jar:9.0.63]
	at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5264) [catalina.jar:9.0.63]
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) [catalina.jar:9.0.63]
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1396) [catalina.jar:9.0.63]
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1386) [catalina.jar:9.0.63]
	at java.util.concurrent.FutureTask.run(FutureTask.java:266) [?:1.8.0_391]
	at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) [tomcat-util.jar:9.0.63]
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134) [?:1.8.0_391]
	at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:919) [catalina.jar:9.0.63]
	at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:835) [catalina.jar:9.0.63]
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) [catalina.jar:9.0.63]
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1396) [catalina.jar:9.0.63]
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1386) [catalina.jar:9.0.63]
	at java.util.concurrent.FutureTask.run(FutureTask.java:266) [?:1.8.0_391]
	at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) [tomcat-util.jar:9.0.63]
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134) [?:1.8.0_391]
	at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:919) [catalina.jar:9.0.63]
	at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:263) [catalina.jar:9.0.63]
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) [catalina.jar:9.0.63]
	at org.apache.catalina.core.StandardService.startInternal(StandardService.java:432) [catalina.jar:9.0.63]
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) [catalina.jar:9.0.63]
	at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:927) [catalina.jar:9.0.63]
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) [catalina.jar:9.0.63]
	at org.apache.catalina.startup.Catalina.start(Catalina.java:772) [catalina.jar:9.0.63]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_391]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_391]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_391]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_391]
	at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345) [bootstrap.jar:9.0.63]
	at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476) [bootstrap.jar:9.0.63]
Caused by: java.lang.ClassNotFoundException: java.sql.ShardingKey
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1412) ~[catalina.jar:9.0.63]
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1220) ~[catalina.jar:9.0.63]
	... 64 more
Jan 23, 2025 8:52:51 AM org.apache.catalina.core.ApplicationContext log
SEVERE: Servlet.init() for servlet [testServlet] threw exception
java.lang.ExceptionInInitializerError
	at com.company.test.persistence.hibernate.HibernateUtils.initSessionFactory(HibernateUtils.java:136)
	at com.company.test.server.DBServer.init(DBServer.java:603)
	at com.company.test.shared.SpringReplacementHelper.getDbServer(SpringReplacementHelper.java:326)
	at com.company.test.shared.SpringReplacementHelper.bootstrap(SpringReplacementHelper.java:393)
	at com.company.test.server.testServlet.init(testServlet.java:231)
	at org.apache.catalina.core.StandardWrapper.initServlet(StandardWrapper.java:1164)
	at org.apache.catalina.core.StandardWrapper.loadServlet(StandardWrapper.java:1117)
	at org.apache.catalina.core.StandardWrapper.load(StandardWrapper.java:1010)
	at org.apache.catalina.core.StandardContext.loadOnStartup(StandardContext.java:4957)
	at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5264)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1396)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1386)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134)
	at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:919)
	at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:835)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1396)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1386)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134)
	at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:919)
	at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:263)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
	at org.apache.catalina.core.StandardService.startInternal(StandardService.java:432)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
	at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:927)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
	at org.apache.catalina.startup.Catalina.start(Catalina.java:772)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
	at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
Caused by: java.lang.NoClassDefFoundError: java/sql/ShardingKey
	at java.lang.Class.getDeclaredMethods0(Native Method)
	at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
	at java.lang.Class.privateGetMethodRecursive(Class.java:3048)
	at java.lang.Class.getMethod0(Class.java:3018)
	at java.lang.Class.getMethod(Class.java:1784)
	at org.hibernate.engine.jdbc.env.internal.DefaultSchemaNameResolver.determineAppropriateResolverDelegate(DefaultSchemaNameResolver.java:44)
	at org.hibernate.engine.jdbc.env.internal.DefaultSchemaNameResolver.resolveSchemaName(DefaultSchemaNameResolver.java:75)
	at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentImpl.determineCurrentSchemaName(JdbcEnvironmentImpl.java:298)
	at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentImpl.<init>(JdbcEnvironmentImpl.java:232)
	at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:114)
	at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:35)
	at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:94)
	at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263)
	at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:237)
	at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:214)
	at org.hibernate.id.factory.internal.DefaultIdentifierGeneratorFactory.injectServices(DefaultIdentifierGeneratorFactory.java:152)
	at org.hibernate.service.internal.AbstractServiceRegistryImpl.injectDependencies(AbstractServiceRegistryImpl.java:286)
	at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:243)
	at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:214)
	at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:179)
	at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:119)
	at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.build(MetadataBuildingProcess.java:84)
	at org.hibernate.boot.internal.MetadataBuilderImpl.build(MetadataBuilderImpl.java:474)
	at org.hibernate.boot.internal.MetadataBuilderImpl.build(MetadataBuilderImpl.java:85)
	at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:689)
	at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:724)
	at com.company.test.persistence.hibernate.HibernateUtils.initSessionFactory(HibernateUtils.java:130)
	... 37 more
Caused by: java.lang.ClassNotFoundException: java.sql.ShardingKey
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1412)
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1220)
	... 64 more

Using Java 11, everything is fine.
Hibernate support suspected: "a JDBC driver that was compiled for a newer Java version (i.e. it uses java.sql.ShardingKey that was added in Java 9), but still using the Java 8 classfile version."
To that end, I determined that the com.mchange.v2.c3p0.impl.NewProxyConnection is being used by Hibernate to make this reflective call:
final Method getSchemaMethod = jdbcConnectionClass.getMethod( "getSchema" ); // [where jdbcConnectionClass is com.mchange.v2.c3p0.impl.NewProxyConnection]
Thinking that class com.mchange.v2.c3p0.impl.NewProxyConnection must have been compiled using Java 9 or greater, I ran "javap.exe -verbose NewProxyConnection.class" to determine the major version, assuming it would be that of Java 9 or greater, but it is major version 51 (which is Java 7).
However, I did see many references to java/sql/ShardingKey. Maybe they're in there because c3p0 was compiled using Java 11, but targeted Java 7?
At any rate, is this a bug with c3p0? I mean, can c3p0 run in Java 8 when it has references to Java 9 classes?
Perhaps I'm missing some required configuration to c3p0 and/or Hibernate?

@swaldman
Copy link
Owner

Hi!

This is interesting. Yeah, c3p0 generally compiles in the earliest JDK using the latest version of JDBC, but targets an earlier classfile format. The reason for that is it wants both to be able to support new JDBC methods and old installations. Usually it's fine. If someone runs something that hits new functionality on an older JVM they see NoSuchMethodError or NoClassDefFoundError, but as long as an old application sticks to old functionality, nothing breaks.

c3p0-10.x compiles under JDK11, targets the JDK7 classfile format (major version 51).

I can reproduce your issue very easily. I don't know why ordinary access to getSchema succeeds while reflective access fails under Java 8. I can only really offer two solutions:

  1. If you can, reflect on the interface rather than the implementation class. Connection.class.getMethod("getSchema") works fine under JDK 8, and the method invokes fine.
  2. Downgrade to c3p0-0.9.5.5

Older c3p0's used the same trick, targeting older classfile versions, but introduction of the new class ShardingKey referenced from NewProxyConnection seems to have unsettled things, under reflective access of the class. This was a JDBC 4.3 / JDK 11 introduction, supported by c3p0 only as of 0.10.x.

Looking at postgres jdbc, the current version of their classes is JDK 8 (major version 52). But they are not supporting JDBC 4.3 methods, so they can just build under Java 8.

c3p0 internally tests for what JDBC methods a driver supports reflectively. JDBC 4.3 beginRequest and endRequest are checked reflectively, although only against the interface. (I'm surprised I haven't seen complaints of Exceptions in the logs for people running under new JVMs and old drivers. Nothing would break, but it would squawk.) I need to look into this more.

@swaldman
Copy link
Owner

Oh, that last wasn't quite right, we do reflexively check the classes for the beginRequest and endRequest as well. And under Java 8, that does trigger the error. We catch any Exception those reflexive checks yield, but not NoSuchClassError. I'm surprised this hasn't seemed to give people more grief. We also reflexively check via physical Connection classes for "setHoldability", "setReadOnly", and "setTypeMap", and don't expect NoSuchClassError there either.

@swaldman
Copy link
Owner

The surprise is that, while it is easy to reproduce the OP's problem in a REPL, running tests under Java 8 do not fail in this way, the getMethod call on raw Connection objects succeed or fails as expected (with an Exception that is handled, rather than a NoSuchClassError). I suspect this is a ClassLoader issue, under Java 8 and below, whether classes are "resolved" on loading and how the relevant ClassLoader handled that.

It's weird to have a bug that reproduces trivially in a REPL that doesn't reproduce in the test environment. But we can certainly make our reflective tests for Connection methods more resilient to this case.

@swaldman
Copy link
Owner

The problem does not reproduce under Java 9. But JDBC 4.3 classes were introduced in Java 9, so that's not surprising.

As with the test applications, the problem does not reproduce under Java 8 jshell, with c3p0, mchange-commons-java, and the postgres driver in the System classpath. This is consistent with the hypothesis that more eager Class resolution by some ClassLoaders is the source of the problem.

@ForceRs Any hope you could retry your application placing c3p0-0.10.1.jar and mchange-commons-java-0.3.1.jar in the System CLASSPATH?

@ForceRs
Copy link
Author

ForceRs commented Jan 27, 2025

@swaldman Since Hibernate's doing the reflection, I have no control to "reflect on the interface rather than the implementation class".
mchange-commons-java-0.3.1.jar is already in the classpath, alongside c3p0-0.10.1.jar; I should have mentioned that in the initial post -- appologies.

For context, here's the offending method's source code from Hibernate 5,3,37's "org\hibernate\engine\jdbc\env\internal\DefaultSchemaNameResolver.java" class:

private SchemaNameResolver determineAppropriateResolverDelegate(Connection connection) {
	// unfortunately Connection#getSchema is only available in Java 1.7 and above
	// and Hibernate still baselines on 1.6.  So for now, use reflection and
	// leverage the Connection#getSchema method if it is available.
	try {
		final Class<? extends Connection> jdbcConnectionClass = connection.getClass();
		final Method getSchemaMethod = jdbcConnectionClass.getMethod( "getSchema" );
		if ( getSchemaMethod != null && getSchemaMethod.getReturnType().equals( String.class ) ) {
			try {
				// If the JDBC driver does not implement the Java 7 spec, but the JRE is Java 7
				// then the getSchemaMethod is not null but the call to getSchema() throws an java.lang.AbstractMethodError
				connection.getSchema();
				return new SchemaNameResolverJava17Delegate();
			}
			catch (java.lang.AbstractMethodError e) {
				log.debugf( "Unable to use Java 1.7 Connection#getSchema" );
				return SchemaNameResolverFallbackDelegate.INSTANCE;
			}
		}
		else {
			log.debugf( "Unable to use Java 1.7 Connection#getSchema" );
			return SchemaNameResolverFallbackDelegate.INSTANCE;
		}
	}
	catch (Exception ignore) {
		log.debugf(
				"Unable to use Java 1.7 Connection#getSchema : An error occurred trying to resolve the connection default schema resolver: "
						+ ignore.getMessage() );
		return SchemaNameResolverFallbackDelegate.INSTANCE;
	}
}

And here are the inner class Delegates referenced (also from "org\hibernate\engine\jdbc\env\internal\DefaultSchemaNameResolver.java"):

public static class SchemaNameResolverJava17Delegate implements SchemaNameResolver {
	@Override
	public String resolveSchemaName(Connection connection, Dialect dialect) throws SQLException {
		return connection.getSchema();
	}
}

public static class SchemaNameResolverFallbackDelegate implements SchemaNameResolver {
	/**
	 * Singleton access
	 */
	public static final SchemaNameResolverFallbackDelegate INSTANCE = new SchemaNameResolverFallbackDelegate();
		@Override
	public String resolveSchemaName(Connection connection, Dialect dialect) throws SQLException {
		final String command = dialect.getCurrentSchemaCommand();
		if ( command == null ) {
			throw new HibernateException(
					"Use of DefaultSchemaNameResolver requires Dialect to provide the " +
							"proper SQL statement/command but provided Dialect [" +
							dialect.getClass().getName() + "] did not return anything " +
							"from Dialect#getCurrentSchemaCommand"
			);
		}
		try (
			final Statement statement = connection.createStatement();
			final ResultSet resultSet = statement.executeQuery( dialect.getCurrentSchemaCommand() )
		) {
			return resultSet.next() ? resultSet.getString( 1 ) : null;
		}
	}
}

Maybe Hibernate could do a better job assigning its schema name resolver?

I am able to run with c3p0-0.9.5.5 on Java 8 and Java 17. Since c3p0-0.9.5.5 reports no vulnerabilities, I guess I'm fine.

I'm more than willing to continue getting c3p0-0.10.1 to work with Java 8. I can certainly advise changes to their code above to perhaps do a simple java version check and return the SchemaNameResolverJava17Delegate (i.e., Java 1.7 delegate, not to be confused with Java 17.0) before blindly reflecting upon getSchema. I'm no Hibernate or c3p0 internals expert.
@swaldman, what would your recommendation be for Hibernate's determineAppropriateResolverDelegate method?

@swaldman
Copy link
Owner

I'd love to be able to blame hibernate, but they're using the same tricks I am, trying to support very old JVMs (c3p0-0.10.x also wants to support Java 7) while bringing forward the APIs, using reflection to decide when it's safe to hit newer functionality.

It's just the combination of the fact that JDBC 4.3 introduces new interfaces referenced from Connection (e.g. ShardingKey) and some ClassLoader implementations under Java 8 eagerly load referenced classes that turns these tricks dangerous using API >= Java 9.

mchange-commons-java-0.3.1.jar is already in the classpath, alongside c3p0-0.10.1.jar; I should have mentioned that in the initial post -- appologies.

You've nothing to apologize for! Thank you for the careful report. So they are set as CLASSPATH in an environment variable? In my testing (on a Mac), the issue does not appear when the classes are available only from CLASSPATH, but does appear if classes are loaded by some sort of launcher.

I most easily reproduce your issue in the scala-cli REPL, which I run like scala-cli --dep "com.mchange:c3p0:0.10.1" --dep "org.postgresql:postgresql:42.6.0". For this REPL, it doesn't matter if the jar files are also in the CLASSPATH, because it ignores the system CLASSPATH. jshell, on the other hand, uses the System CLASSPATH, as set in the environment. The issue does not appear under Java 8 jshell with the jars on the CLASSPATH. It also doesn't appear when I run my usual test programs under Java 8, again with the jars in the CLASSPATH environment variable.

We can make this more concrete by just comparing the ClassLoader implementations. conn is a c3p0 Connection. Under scala-cli, where the reflective call blows up...

scala> conn.getClass().getClassLoader()
val res1: ClassLoader = java.net.URLClassLoader@70fab835

Under jshell, where it does not...

jshell> conn.getClass().getClassLoader();
$5 ==> jdk.internal.loader.ClassLoaders$AppClassLoader@1cf4f579

To solve the problem under 0.10.x, I think, we want to find some way to place the classes so that jdk.internal.loader.ClassLoaders$AppClassLoader is what loads it.

This is just a hypothesis, a guess. It could be wrong.

If it's not hard for you to check, I'd be curious what the value of connection.getClass().getClassLoader(), anywhere your application has a c3p0 Connection (when connection.getClass().getName() is com.mchange.v2.c3p0.impl.NewProxyConnection).

I don't think I can "fix" this, except maybe with a Java 7/8 specific build (which is an option!). But it would be good to know if, for common applications like hibernate, there's a configuration or setup under which things don't break.

Thanks again for the help!

@ForceRs
Copy link
Author

ForceRs commented Jan 27, 2025

I just built c3p0-0.10.1 from source. All I did was change the imports in class src\main\java\com\mchange\v2\c3p0\impl\NewProxyConnection.java from:

import com.mchange.v2.c3p0.C3P0ProxyConnection;
import java.lang.Class;
import java.lang.Object;
import java.lang.String;
import java.sql.Array;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Savepoint;
import java.sql.ShardingKey;
import java.sql.Statement;
import java.sql.Struct;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.sql.*;
import javax.sql.*;
import com.mchange.v2.log.*;
import java.lang.reflect.Method;
import com.mchange.v2.sql.SqlUtils;
import java.util.concurrent.locks.ReentrantLock;
import java.lang.reflect.InvocationTargetException;
import com.mchange.v2.util.ResourceClosedException;

To:

import com.mchange.v2.c3p0.C3P0ProxyConnection;
import java.lang.Class;
import java.lang.Object;
import java.lang.String;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.sql.*;
import javax.sql.*;
import com.mchange.v2.log.*;
import java.lang.reflect.Method;
import com.mchange.v2.sql.SqlUtils;
import java.util.concurrent.locks.ReentrantLock;
import java.lang.reflect.InvocationTargetException;
import com.mchange.v2.util.ResourceClosedException;

That is, I used a wildcard for all java.sql. imports.
And all is well. I can now run with c3p0-0.10.1 and Java 8. I think having the explicit import for java.sql.ShardingKey triggers the overly eager class resolution.

Please confirm that this works for you. And, of course, if this is even a viable workaroud/fix.

UPDATE: I ran my test with Java 17. When I run with Java 8, it fails as expected: java.lang.NoClassDefFoundError: java/sql/ShardingKey

@ForceRs
Copy link
Author

ForceRs commented Jan 27, 2025

Here's what get for the classloader information you requested:

jdbcConnectionClass=class com.mchange.v2.c3p0.impl.NewProxyConnection

connection.getClass().getClassLoader()=ParallelWebappClassLoader
context: test
delegate: false
----------> Parent Classloader:
java.net.URLClassLoader@7c0e2abd

@ForceRs
Copy link
Author

ForceRs commented Jan 27, 2025

Crap -- I'm a dummy. I was running with Java 17 after modifying the imports for c3p0. The import change did not work with Java 8. Sorry about that.

@swaldman
Copy link
Owner

Well, even if under Java 17, that's consistent with the hypothesis that the fragility results from a more eager ClassLoader than the one that typically loads classes from CLASSPATH that breaks things.

What import change did you make that broke?

You are far from a dummy. I appreciate the help.

@ForceRs
Copy link
Author

ForceRs commented Jan 27, 2025

The classloader test information above was gathered running Java 8.

I always run Tomcat from eclipse as a server. Not familiar with any explicit classpaths. I just drop jars into the WEB-INF/lib folder and they get used.

The import change was a red-herring; I'd hoped that not explicitly referencing java.sql.ShardingKey in the imports would resolve the issue, but it didn't. I'd hoped that since we weren't calling the methods with java.sql.ShardingKey, the classloader wouldn't care.
I even just ran a quick test exporting -Xverify:none, but that didn't help.

Please let me know if you want me to test more stuff. I do have a custom-built version of Hibernate that I've been using to tweak class determineAppropriateResolverDelegate.java.

@ForceRs
Copy link
Author

ForceRs commented Jan 27, 2025

HikariCP had the same problem per this issue. Maybe you can look at that source for answers? His CHANGES log states: "fixed 1578 build change to ensure that proxies are generated using Java 8, otherwise we end up with class references to Java 11 classes, which fail to load on Java 8."

@swaldman
Copy link
Owner

Ah! Thank you again for all the help, and for the explanation.

And for tracking down the HikariCP analogue! Just like c3p0, they generate proxies for JDBC classes from java.sql.Connection and other java.sql interfaces. They also generate from more recent JDKs and target old ones, and thus faced the same issue when Java 9 introduced new classes referenced from Connection. Their solution at the time was to generate proxies from Java 8, rendering Java 9 / JDBC 4.3 functionality unsupported. (I suspect since 2020 they've moved on to generating from Java 8, eventually people care.)

It might be too much trouble, but I guess what we might try to address your problem is taking those jars out of WEB-INF/lib, and then placing a path to wherever else we've put them in the CLASSPATH environment variable when tomcat is launched. Usually that launcher would be a file like catalina.sh or catalina.bat, perhaps itself launched from startup.sh or startup.bat.

In my little world, I launch tomcat from systemd on Linux. It runs startup.sh, and I could add a line like Environment=CLASSPATH=/opt/miscjars/c3p0-0.10.1.jar:/opt/miscjars/mchange-commons-java-0.3.1.jar:/opt/miscjars/postgresql-42.7.5.jar to my systemd unit file. But you may launch tomcat very differently.

If this is right, it's a pretty annoying way to have to address the issue. I completely understand if you'd prefer to just downgrade to 0.9.5.5 for now and move on. Even if it works, I'll have to look into generating separate builds for Java 8 going forward. It's too annoying to insist users place jars in the native CLASSPATH. Modern JVM apps place code like you have, in WEB-INF/lib, or dynamically download libraries from Maven central and load them with ClassLoaders. All of that will break under Java 8 / c3p0-0.10.x.

If you do give this a try, let me know how it works, but please don't feel obliged to take the time or do anything disruptive.

Thank you again for the careful work.

@ForceRs
Copy link
Author

ForceRs commented Jan 27, 2025

I spent some time and came up with a working solution that uses generics.
By working, I mean it works for my use case in Java 8 and Java 17. That is, I have a custom c3p0-0.10.1.jar with everything else stock and unmodified.
I wasn't able to drive any of the offending ShardingKey methods in Java 17. I don't know what a ShardingKey is or how to generate one to test with -- over my pay grade.
@swaldman, should I send you the files or post them here for your evaluation. It may be that my fix is working only for my use case, as I don't use ShardingKey. I modified file com\mchange\v2\c3p0\impl\NewProxyConnection.java and created a new file named com\mchange\v2\c3p0\impl\NewProxyConnectionJava9.java.
Let me know.

@swaldman
Copy link
Owner

I'm glad to take a look at the files, but probably most helpful would just be to explain what you've done. Have you removed from the class the two setShardingKey and two setShardingKeyIfValid methods?

Thanks again for all your help.

@ForceRs
Copy link
Author

ForceRs commented Jan 27, 2025 via email

@swaldman
Copy link
Owner

Please do attach your work, if you don't mind sharing. I'm interested!

@ForceRs
Copy link
Author

ForceRs commented Jan 27, 2025 via email

@ForceRs
Copy link
Author

ForceRs commented Jan 28, 2025

I've attached c3p0-Issue_184.zip, which contains three files:

  1. Modified com\mchange\v2\c3p0\impl\NewProxyConnection.java.
  2. New com\mchange\v2\c3p0\impl\NewProxyConnectionJava9.java.
  3. Updated c3p0-0.10.1.jar, built from the modified and new file.

The purpose of this proof-of-concept is to see if moving the four methods that reference java.sql.ShardingKey to a separate class is viable.
To accomplish this, the four methods remain in NewProxyConnection, but their method signatures utilize generics instead of direct references to java.sql.ShardingKey. The hope is that those four methods are now JVM-agnostic.

For example, NewProxyConnection's method that was:
public final boolean setShardingKeyIfValid(ShardingKey a, ShardingKey b, int c) throws SQLException
is now:
public final boolean setShardingKeyIfValid(T a, T b, int c) throws SQLException
The other three methods follow the same pattern.

The new class, NewProxyConnectionJava9, has corresponding methods as follows:
protected final boolean setShardingKeyIfValid(T a, T b, int c) throws SQLException
which casts and calls:
setShardingKeyIfValid((ShardingKey)a, (ShardingKey)b, c);
whose signature is:
private final boolean setShardingKeyIfValid(ShardingKey a, ShardingKey b, int c) throws SQLException

The code compiles and runs in Java 8 and Java 17.
I don't know how to drive the four methods that were moved for this proof-of-concept.
The real question is: Will whoever utilizes methods setShardingKeyIfValid(ShardingKey a, ShardingKey b, int c), setShardingKeyIfValid(ShardingKey a, int b), setShardingKey(ShardingKey a, ShardingKey b), and setShardingKey(ShardingKey a) be able to access them given that they are now represented with generics as setShardingKeyIfValid(T a, T b, int c), setShardingKeyIfValid(T a, int b), setShardingKey(T a, T b), and setShardingKey(T a) in NewProxyConnection?

@swaldman, I placed the modified NewProxyConnection code between these two eye-catchers:
// Begin methods added to isolate us from directly referencing objects that are undefined in Java 8 and 7 (like java.sql.ShardingKey), thus preventing java.lang.NoClassDefFoundError.
// End methods added to isolate us from directly referencing objects that are undefined in Java 8 and 7 (like java.sql.ShardingKey), thus preventing java.lang.NoClassDefFoundError.

You might want to run quick tests using the attached c3p0-0.10.1.jar. It's got all the changes in it. I see a comment in NewProxyConnection stating: "This class generated by com.mchange.v2.c3p0.codegen.JdbcProxyGenerator$NewProxyConnectionGenerator. DO NOT HAND EDIT!!!!", so I assume your normal build would completely clobber the attached, modified, NewProxyConnection.java.
Fingers crossed...

@ForceRs
Copy link
Author

ForceRs commented Jan 28, 2025

I'm pretty sure this won't work. The only reason it compiled is that the four methods being implemented for the Connection interface utilize the default keyword, meaning they're optional to implement. Adding the @OverRide method annotation results in a compilation error. I should mention that was my first attempt at generics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants