ChatGPT, Java 8 documentation, and MySQL all said that JDBC does not need `Class.forName()`, and the result is an error...

Some time ago, some students encountered an error when using Apache ShardingSphere-JDBCjava.sql.SQLException: No suitable driver . After investigation, it was found that the problem was related to deploying the application using standalone Tomcat.

I believe that students who do Java are familiar with the following code. JDBC obtains a database connection, and then uses this connection to perform addition, deletion, modification, and query operations.

Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306");

Since Java introduced the SPI mechanism, JDBC drivers can be registered as SPI implementation classes, and generally do not need to be Class.forNameloaded. However, some scenarios

Explore the root cause of java.sql.SQLException: No suitable driver reported by independent Tomcat

Review the problem that the database driver cannot be found when Tomcat deploys the WAR application

Developers who have experienced Tomcat deploying WAR packages may have encountered it more or less. There is already a database driver in the lib directory of the project, but the application still reports an error No suitable driver:

getConnection: no suitable driver found for jdbc:mysql://127.0.0.1:3306
java.sql.SQLException: No suitable driver found for jdbc:mysql://127.0.0.1:3306
        at java.sql.DriverManager.getConnection(DriverManager.java:689)
        at java.sql.DriverManager.getConnection(DriverManager.java:247)
        at icu.wwj.hello.tomcat.driverdemo.HelloServlet.doGet(HelloServlet.java:18)

Of course, if you search the Internet, there will be many search results for solutions, mainly:

  • Make sure the driver has been added;
  • Check the JDBC URL to make sure there is no typo;
  • Put the driver into Tomcat's lib directory (there is even a way to put it into the JRE lib directory);
  • Called before getting a connection Class.forName("com.mysql.jdbc.Driver");
  • ……

So far, the problem that the driver cannot be found in most scenarios can be solved. But, why putting the driver into Tomcat's lib directory, or calling first Class.forName()can solve the problem?

Check out the answers provided by ChatGPT:
insert image description here

It seems to be related to Tomcat's class loader and deployment mechanism.

ChatGPT, Javadoc and MySQL driver all say it's not necessaryClass.forName()

What ChatGPT says:
insert image description here

There is such a passage in the javadoc of JDK 8's DriverManager :

Applications no longer need to explicitly load JDBC drivers using Class.forName(). Existing programs which currently load JDBC drivers using Class.forName() will continue to work without modification.

It means that the application program no longer needs to Class.forName()explicitly load the JDBC driver, and the existing programs can do this without any problem.

insert image description here

Tracing the history a bit, this passage already exists in the initial commit of the GitHub OpenJDK repository , so it is not known who wrote it and when.

Even MySQL Connector/J 8.0.x outputs a warning that drivers are automatically registered via SPI, there is no need to manually load driver classes :

package com.mysql.jdbc;

import java.sql.SQLException;

/**
 * Backwards compatibility to support apps that call <code>Class.forName("com.mysql.jdbc.Driver");</code>.
 */
public class Driver extends com.mysql.cj.jdbc.Driver {
    
    
    public Driver() throws SQLException {
    
    
        super();
    }

    static {
    
    
        System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. "
                + "The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
    }
}

In JDK 9, this rhetoric has been changed:

insert image description here

The driver loading of DriverManager becomes lazy loading, and the thread context accumulator is used to trigger the SPI mechanism to load the driver. The drivers loaded and available to the application will depend on the thread context class loader of the thread that triggered the driver initialization.

Although the timing of SPI loading the driver has been adjusted, the point of not needing to call explicitly in general Class.forNameremains the same.

experiment

Create a demo that minimally reproduces the problem

Prepare an independent Tomcat and write a simple Servlet, through debugging and source code analysis why using independent Tomcat will encounter the problem of No suitable driver.

Digression: I never used annotations when I was learning Servlet before, and I was web.xmlwriting a bunch of configurations. web.xmlUsing annotations from Servlet 3.0, it is much faster to write a Servlet hello, world, and there is no need to write a bunch of configurations like before .

Implement an HTTP GET method and try to get a database connection inside.

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;

@WebServlet(name = "driver", value = "/driver")
public class HelloServlet extends HttpServlet {
    
    
    
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    
    
        response.setContentType("text/plain");
        DriverManager.setLogWriter(response.getWriter()); // 将 JDBC 日志直接输出到 HTTP 响应
        // try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { response.getWriter().println(e); }
        try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306", "root", "root")) {
    
    
            response.getWriter().println("Connected to MySQL " + c.getMetaData().getDatabaseProductVersion());
        } catch (Exception ex) {
    
    
            response.getWriter().println();
        }
    }
}

not callClass.forName("com.mysql.cj.jdbc.Driver")

Considering that there are differences in the DriverManager of different versions of Java, the following versions have been tried, and the same error occurs:

  • Java 8
  • Java 17
  • Java 20 (latest version of Java released as of this writing)

The output of Java 8 is as follows. The output of Java 17 and Java 20 are exactly the same except for the number of lines in DriverManager.java.

$ curl -s 127.0.0.1:8080/driver

DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306")
getConnection: no suitable driver found for jdbc:mysql://127.0.0.1:3306
java.sql.SQLException: No suitable driver found for jdbc:mysql://127.0.0.1:3306
	at java.sql.DriverManager.getConnection(DriverManager.java:689)
	at java.sql.DriverManager.getConnection(DriverManager.java:247)
	at icu.wwj.hello.tomcat.driverdemo.HelloServlet.doGet(HelloServlet.java:18)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:529)
# ... 省略 Tomcat 调用栈	
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:750)
SQLException: SQLState(08001)

transferClass.forName("com.mysql.cj.jdbc.Driver")

Database operations execute normally.

$ curl -s 127.0.0.1:8080/driver

registerDriver: com.mysql.cj.jdbc.Driver@547a2eb2
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306")
    trying com.mysql.cj.jdbc.Driver
getConnection returning com.mysql.cj.jdbc.Driver
Connected to MySQL 5.7.36

Next, I will explore the root cause of the problem.

Why can't I find the driver? the reason is simple

Java 8 source code

The source code of this section is selected from JDK Azul Zulu Community 1.8.0_372

DriverManager loads JDBC drivers through system variables, configuration files, and SPI in static methods.

java.sql.DriverManagersource code excerpt

public class DriverManager {
    
    

    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

// 省略部分代码
	
    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
    
    
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
// 省略其余代码

Hit a breakpoint to debug and found that loadInitialDrivers()after the method is executed, registeredDriversit is empty. Looking at the code call stack, it is found that Tomcat is currently in the startup phase. The MySQL JDBC driver is in the application WAR package, and the application WAR package is loaded after Tomcat is started.

insert image description here

That is, the DriverManager class has been initialized when Tomcat is starting, but the WAR package has not been deployed at this time, so DriverManager cannot load any driver through SPI .

Java 17 source code

The DriverManager of Java 17 no longer staticcalls SPI to load the JDBC driver class in the code block, but the performance of the problem is consistent with that of Java 8. Debugging found that the problem is actually related to Tomcat.
insert image description here
Tomcat's JreMemoryLeakPreventionListener.javawill call the method once DriverManager.getDrivers(), and it is this method call that triggers the DriverManager to use the SPI to load the driver.

insert image description here

Although the timing of DriverManager loading the driver via SPI has changed, the loading will still only happen once. Therefore, after deploying the application through the WAR package, DriverManager will no longer load the driver through SPI.

in conclusion

  • java.sql.DriverManager will only call SPI once to load JDBC driver;
  • When Tomcat starts, before deploying the WAR package application, it calls the DriverManager method and triggers the SPI loading mechanism;

Therefore, the JDBC driver in the WAR package misses the SPI JDBC driver loading, and the driver cannot be registered automatically.

solution

Method 1: Turn off Tomcat driverManagerProtection

Find the file in the Tomcat directory conf/server.xmland modify the following content:

 <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />

Turn off driverManagerProtection:

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" driverManagerProtection="false" />

Reference: https://github.com/apache/tomcat/blob/a25cb7910d6622e7b4e1507cfbf2600dacf350d3/webapps/docs/config/listeners.xml#L224

Method 2: Explicitly specify the initialization sequence of the driver class in the Bean

Adding the driver bean and ensuring its order is before creating the data source is equivalent to the calling code Class.forName:

<bean id="h2Driver" class="org.h2.Driver" />

<bean id="yourDataSource" depends-on="h2Driver">

Method 3: Call Class.forName before creating the data source

Class.forName("org.h2.Driver");

Guess you like

Origin blog.csdn.net/wu_weijie/article/details/128927303