ChatGPT、Java 8 文档、MySQL都说 JDBC 没必要 `Class.forName()`,结果报错了……

news2024/11/24 16:49:08

文章目录

    • 回顾 Tomcat 部署 WAR 应用报错找不到数据库驱动的问题
    • ChatGPT、Javadoc 和 MySQL 驱动都说没必要 `Class.forName()`
    • 实验
      • 创建一个最小复现问题的 Demo
      • 不调用 `Class.forName("com.mysql.cj.jdbc.Driver")`
      • 调用 `Class.forName("com.mysql.cj.jdbc.Driver")`
    • 为什么找不到驱动?原因很简单
      • Java 8 源码
      • Java 17 源码
      • 结论

前段时间,有同学在使用 Apache ShardingSphere-JDBC 时遇到报错 java.sql.SQLException: No suitable driver。经过调查,发现这个问题和使用独立 Tomcat 部署应用有关。

相信做 Java 的同学对以下这段代码都不陌生。JDBC 获取一个数据库连接,然后使用这个连接做增删改查操作。

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

自 Java 引入 SPI 机制后,JDBC 的驱动可以注册为 SPI 的实现类,一般情况无须再通过 Class.forName 加载。但是,有些场景

探究独立 Tomcat 报错 java.sql.SQLException: No suitable driver 的根本原因

回顾 Tomcat 部署 WAR 应用报错找不到数据库驱动的问题

经历过 Tomcat 部署 WAR 包的开发者,可能多少都有遇到过,明明项目的 lib 目录下已经有了数据库驱动,但是应用却仍然报错 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)

当然,去网上查一下会有很多解决方法的搜索结果,主要都是:

  • 确保已经添加了驱动;
  • 检查 JDBC URL 确保没有写错;
  • 把驱动放进 Tomcat 的 lib 目录(甚至有放进 JRE lib 目录的方法);
  • 获取连接前先调用 Class.forName("com.mysql.jdbc.Driver")
  • ……

至此,基本大多数场景找不到驱动的问题都能解决。但是,为什么把驱动放进 Tomcat 的 lib 目录,或者先调用 Class.forName() 能够解决问题?

看看 ChatGPT 提供的答案:
在这里插入图片描述

看起来与 Tomcat 的类加载器、部署机制有关系。

ChatGPT、Javadoc 和 MySQL 驱动都说没必要 Class.forName()

ChatGPT 的说法:
在这里插入图片描述

JDK 8 的 DriverManager 的 javadoc 中有这么一段话:

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.

意思就是应用程序不再需要通过 Class.forName() 显式加载 JDBC 驱动了,已有的程序这么干了也没问题。

在这里插入图片描述

追踪了一下历史,这段话在 GitHub OpenJDK 仓库的初始提交 中就已经存在了,所以是谁在什么时候写的也就不知道了。

就连 MySQL Connector/J 8.0.x 也输出警告说:驱动自动通过 SPI 注册,没有必要手动加载驱动类了:

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.");
    }
}

在 JDK 9,这套说辞被改掉了:

在这里插入图片描述

DriverManager 的驱动加载变成了懒加载,并使用线程上下文累加载器触发 SPI 机制加载驱动。应用程序加载和可用的驱动程序将取决于触发驱动程序初始化的线程的线程上下文类加载器。

虽然 SPI 加载驱动的时机做了调整,但一般情况下不需要显式调用 Class.forName 这点还是不变的。

实验

创建一个最小复现问题的 Demo

准备一个独立 Tomcat 并写一个简单的 Servlet,通过调试和源码分析为什么使用独立 Tomcat 会遇到 No suitable driver 的问题。

题外话:以前学习 Servlet 的时候都没有用过注解,都是在 web.xml 写一堆配置。使用自 Servlet 3.0 的注解,写一个 Servlet 的 hello, world 快多了,不需要像以前那样在 web.xml 写一堆配置了。

实现一个 HTTP GET 方法,在里面尝试获取数据库连接。

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();
        }
    }
}

不调用 Class.forName("com.mysql.cj.jdbc.Driver")

考虑到不同版本 Java 的 DriverManager 存在差异,尝试过以下版本,均发生同样的错误:

  • Java 8
  • Java 17
  • Java 20(本文编写时 Java 已发布的最新版本)

Java 8 输出结果如下,Java 17 与 Java 20 的输出除了 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)

调用 Class.forName("com.mysql.cj.jdbc.Driver")

数据库操作正常执行。

$ 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

接下来笔者将探究问题的根本原因。

为什么找不到驱动?原因很简单

Java 8 源码

本节源码选自 JDK Azul Zulu Community 1.8.0_372

DriverManager 在 static 方法中通过系统变量、配置文件、SPI 加载 JDBC 驱动。

java.sql.DriverManager 源码节选

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");
    }
// 省略其余代码

打个断点调试发现,loadInitialDrivers() 方法执行完后,registeredDrivers 却是空的。查看代码调用栈,发现目前正处于 Tomcat 的启动阶段。MySQL JDBC 驱动在应用 WAR 包内,而应用的 WAR 包是在 Tomcat 启动完成后才开始加载的。

在这里插入图片描述

也就是,Tomcat 正在启动的时候就已经初始化了类 DriverManager,但此时 WAR 包还没有部署,所以 DriverManager 通过 SPI 加载不到任何驱动

Java 17 源码

Java 17 的 DriverManager 已经不在 static 代码块中调用 SPI 加载 JDBC 驱动类了,但问题的表现与 Java 8 却是一致的。调试发现,问题其实还是与 Tomcat 有关。
在这里插入图片描述
Tomcat 的 JreMemoryLeakPreventionListener.java 会调用一次 DriverManager.getDrivers() 方法,正是这个方法调用触发了 DriverManager 使用 SPI 加载驱动。

在这里插入图片描述

虽然 DriverManager 通过 SPI 加载驱动的时机变化了,但加载还是只会进行一次。所以后续通过 WAR 包部署应用,DriverManager 不会再通过 SPI 加载驱动了。

结论

  • java.sql.DriverManager 只会调用一次 SPI 加载 JDBC 驱动;
  • Tomcat 在启动时,部署 WAR 包应用前,调用了 DriverManager 的方法,触发了 SPI 加载机制;

于是,WAR 包中的 JDBC 驱动错过了 SPI JDBC 驱动加载,驱动无法自动注册。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/574993.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

chatgpt赋能python:Python的输出功能及其应用

Python的输出功能及其应用 Python是一种高级编程语言&#xff0c;它具有动态类型的解释性能力&#xff0c;是一种简洁、易于学习和易于阅读的编程语言。Python拥有非常强大的输出功能&#xff0c;使得开发者可以以多种形式输出数据结果&#xff0c;这对于数据分析、数据处理和…

人工智能值不值得学习?人工智能就业方向及前景

人工智能值不值得学习? 一、人工智能值得学吗&#xff1f; 很多同学想要知道人工智能值得学吗&#xff1f;小编认为是值得的&#xff0c;具体原因有以下两点&#xff1a; 1、人工智能专业前景好&#xff0c;但人才紧缺 根据人工智能行业的专家预计&#xff0c;到2020年&am…

华为CE12808/S9700交换机istack/CSS堆叠主备倒换操作命令步骤

一、华为CE12808交换机&#xff0c;istack堆叠状态 1、设备型号&#xff1a; 交换机一&#xff1a; HUAWEI CE12808 交换机二&#xff1a; HUAWEI CE12808 2、istack堆叠主备倒换操作步骤&#xff1a; 2.1、设备当前配置保存并进行备份。 2.2、切换所用命令。 执行命令display…

红黑树的插入。

一&#xff0c;一颗红黑树满足的性质 ①每个结点或是红色&#xff0c;或是黑色。 ②根结点是黑色的。 ③叶结点&#xff08;虚构的外部节点NULL结点&#xff09;都是黑色的。 ④不存在两个相邻的红结点。 ⑤对每个结点&#xff0c;从该结点到任一结点的简单路径上&#xff0c;…

【JavaSE】Java基础语法(二十):多态

文章目录 1. 多态的概述2 .多态中的成员访问特点3. 多态的好处和弊端4. 多态中的转型5. 多态中转型存在的风险和解决方案 (应用)6. 多态的实用价值 1. 多态的概述 什么是多态 同一个对象&#xff0c;在不同时刻表现出来的不同形态 多态的前提 要有继承或实现关系要有方法的重写…

SQLite 数据库入门教程(GO)

文章目录 SQLite数据库入门教程一、SQLite 简介1、什么是 SQLite&#xff1f;2、为什么要用 SQLite&#xff1f; 二、SQLite 安装1、在 Windows 上安装 SQLite2、在 Linux 、Mac OS上安装 SQLite 三、SQLite 命令四、SQLite 使用1、SQLite 数据类型2、SQLite 语法3、SQLite 可视…

chatgpt赋能python:Python百分比怎么算?-从基础到逐层深入

Python百分比怎么算&#xff1f; - 从基础到逐层深入 Python是目前全球最流行的编程语言之一&#xff0c;由于其易学易用的特点&#xff0c;广泛地应用于数据处理、Web开发、自动化测试等不同领域。当我们需要对一些数据进行计算和分析时&#xff0c;经常需要对百分比进行计算…

Linux第二天

上传 scp -r 本地文件路劲 用户名目标主机地址&#xff1a;路径 下载&#xff1a;scp -r 用户名目标主机地址&#xff1a;路径 本地目录 ls -A /root //查看root文件下所有的隐藏文件 命令&#xff1a;ls 选项&#xff1a; -l&#xff1a;查看文件属性 -h&#xff1a;文…

【Python】继承与多态

知识目录 一、写在前面✨二、继承三、多态四、Account和FreeChecking类的实现五、总结撒花&#x1f60a; 一、写在前面✨ 大家好&#xff01;我是初心&#xff0c;希望我们一路走来能坚守初心&#xff01; 今天跟大家分享的文章是 Python中的继承与多态 &#xff0c;希望能帮…

在windows下安装linux和kali子系统

目录 安装子系统 报错解决官方方法 步骤 1 - 启用适用于 Linux 的 Windows 子系统 步骤 2 - 检查运行 WSL 2 的要求 步骤 3 - 启用虚拟机功能 步骤 4 - 下载 Linux 内核更新包 步骤 5 - 将 WSL 2 设置为默认版本 步骤 6 - 安装所选的 Linux 分发 排查安装问题 下载发行…

【JavaSE】Java基础语法(二十一):内部类

文章目录 1. 内部类的基本使用2. 成员内部类3. 局部内部类4. 匿名内部类5. 匿名内部类在开发中的使用&#xff08;应用&#xff09; 1. 内部类的基本使用 内部类概念 在一个类中定义一个类。举例&#xff1a;在一个类A的内部定义一个类B&#xff0c;类B就被称为内部类 内部类定…

YOLOv8目标检测实战:Android手机部署 (视频课程)

课程链接&#xff1a;https://edu.csdn.net/course/detail/38595 YOLOv8 目标检测基于先前 YOLO 版本的成功&#xff0c;引入了新功能和改进&#xff0c;进一步提升了性能和灵活性。 本课程在Windows上手把手演示YOLOv8&#xff08;YOLOv8n和YOLOv8s&#xff09;目标检测在An…

jvm 分析调优工具-学习笔记

jvm 分析调优工具 jps命令查看java进程pid jps 列出java进程名(不完整类名) 和pid jps -l 列出java进程名(完整类名) 和pid jmap命令查看java进程占用的jvm资源情况 jmap -histo pid 查看内存情况 jmap -heap pid 查看java程序的堆占用信息 jmap -dump 导出heap快照分析文件he…

22 VueComponent 响应式处理

前言 这是最近的碰到的那个 和响应式相关的问题 特定的操作之后响应式对象不“响应“了 引起的一系列的文章 主要记录的是 vue 的相关实现机制 呵呵 理解本文需要 vue 的使用基础, js 的使用基础 测试用例 测试用例如下, 一个简单的 按钮事件的触发 问题的调试 数据…

老macbook升级新版本(Big sur、Monterey)

老macbook升级新版本&#xff08;Big sur、Monterey&#xff09; 一、前期须知以及准备1.摘要2.设备3.升级方法3.前期准备 二、引导U盘的搭建1.下载安装程序2.U盘格式问题3.下载系统镜像并写入U盘 三、系统安装结束语 一、前期须知以及准备 1.摘要 对于老版本的macbook一系列…

代码随想录训练营Day53| 1143.最长公共子序列 1035.不相交的线 53. 最大子序和 动态规划

目录 学习目标 学习内容 1143.最长公共子序列 1035.不相交的线 53. 最大子序和 动态规划 学习目标 1143.最长公共子序列 1035.不相交的线 53. 最大子序和 动态规划 学习内容 1143.最长公共子序列 1143. 最长公共子序列 - 力扣&#xff08;LeetCode&#xff09;ht…

PS磨皮插件免费电脑版Portraiture4.03下载及使用教程

Portraiture是一款智能磨皮插件&#xff0c;为Photoshop和Lightroom添加一键磨皮美化功能&#xff0c;快速对照片中皮肤、头发、眉毛等部位进行美化&#xff0c;Portraiture 4用于人像图片润色&#xff0c;磨皮等&#xff0c;减少了人工选择图像区域的重复劳动。它能智能地对图…

chatgpt赋能python:如何在Python中输入字符

如何在Python中输入字符 Python是一种非常流行的编程语言&#xff0c;它被广泛应用于各种领域&#xff0c;包括Web开发、人工智能、数据分析、科学计算等。在Python中输入字符是一项基本的操作&#xff0c;本文将介绍常见的输入字符方式以及注意事项。 使用input()函数输入字…

[数据结构] AVL树的插入旋转 和 概念理解

文章目录 定义 && 性质定义性质 实现思路架构节点AVL树框架Insert&#xff08;插入&#xff09;左单旋右单旋左右双旋右左双旋 定义 && 性质 定义 二叉搜索树虽可以缩短查找的效率&#xff0c;但 如果数据有序或接近有序二叉搜索树将退化为单支树&#xff0c…

初学Qt(Day04)

今日目标 创建一个类似游戏手柄的窗口&#xff0c;每次鼠标点击拖动手柄&#xff0c;在qt开发界面输出坐标&#xff0c;每当松开鼠标&#xff0c;手柄自动复位。 目标是实现类似下面这种 先说结论&#xff08;免得我又忘记了&#xff09;&#xff1a;没写完&#xff0c;是一…