提到 MySQL 的 Java 驱动,很多老项目里躺着的是mysql:mysql-connector-java:8.0.33,而新项目或新版本驱动用到的却是com.mysql:mysql-connector-j。我第一次在依赖树里同时看到这两个坐标时,第一反应是有人把 artifactId 写错了——毕竟少了java后缀,groupId 也变了。但翻完中央仓库的产物列表和官方发布说明之后,我发现这不是拼写漂移,而是一次有计划的坐标重定位。这篇文章我就围绕 mysql-connector-java 和 mysql-connector-j 的区别,把自己从排查、验证到迁移的完整过程写下来,适合还在用旧坐标、或者正打算升级 MySQL 驱动版本的同学参考。
1. 版本时间线:旧坐标停在 8.0.33,新坐标从 8.0.31 开始接管
1.1 旧坐标什么时候不再更新
先说结论:mysql:mysql-connector-java这个坐标是 MySQL Connector/J 驱动在 Maven 中央仓库里的老写法,早期官方发布 Java 驱动时一直用这个坐标,一路更新到 8.0.x。以中央仓库的实际产物为准,这个坐标的最后一个版本是 8.0.33。注意,不是 8.0.33 之后驱动不更新了,而是驱动依然在更新,只是不再以mysql:mysql-connector-java这个名字发布。
很多项目到现在还在用旧坐标,最大的原因是 Spring Boot 2.x 的依赖管理锁定了mysql:mysql-connector-java。比如 Spring Boot 2.7.x 里,starter-data-jpa、starter-jdbc这些 starter 默认管理的 MySQL 驱动就是mysql:mysql-connector-java:8.0.33。开发者通常不会在 pom 里显式声明 MySQL 驱动的版本,完全依赖父 pom 的 dependencyManagement,所以项目的依赖树里就一直保留着旧坐标。这个版本的驱动其实也没问题,能正常工作,只是它已经到了旧坐标的终点。
我遇到过一个比较典型的场景:某支付系统的老服务用 Spring Boot 2.7,pom 里没有任何 MySQL 驱动的显式声明,但mvn dependency:tree一看,mysql:mysql-connector-java:8.0.33 被间接带进来。等我把这个服务升级到 Spring Boot 3.x 之后,依赖树里的驱动坐标自动变成了com.mysql:mysql-connector-j,版本也跟着走。这说明驱动本身没换,换的是坐标体系和版本管理策略。
1.2 新坐标从哪个版本开始出现
com.mysql:mysql-connector-j不是凭空冒出来的。从 8.0.31 开始,官方在中央仓库同时发布两个坐标的 8.0.x 版本;从 8.0.34 以及 9.0.0 开始,旧坐标就不再提供新产物,只有新坐标在持续更新。
我给一张版本对照表,方便你判断自己项目卡在哪个阶段:
| 版本 | mysql:mysql-connector-java | com.mysql:mysql-connector-j |
|---|---|---|
| 8.0.30 及更早 | 有 | 无 |
| 8.0.31 - 8.0.33 | 有 | 有 |
| 8.0.34 及之后 | 无 | 有 |
| 9.x 系列 | 无 | 有 |
这张表的意思是,如果你只是想停留在 8.0.33,那新旧坐标都可以用,驱动内容几乎一致;如果你想用 8.0.34 以上的维护版本,或者直接上 9.x,那就没有选择余地了,只能用com.mysql:mysql-connector-j。很多人在升级时遇到"找不到 mysql-connector-java 9.0.0"的报错,原因就是旧坐标根本没发布 9.x,而不是网络或仓库配置问题。
1.3 别把两个坐标理解成两个驱动
我在排查这个问题的过程中,一度以为mysql:mysql-connector-java是老驱动、com.mysql:mysql-connector-j是新驱动。实际不是。它们打包出来的类、连接协议、com.mysql.cj.jdbc.Driver驱动类都是同一套代码,区别主要在于 Maven 坐标的命名、发布策略和后续演进方向。可以用一个生活化类比:同一个产品,老名字叫"某品牌 Java 连接器",新名字叫"某品牌连接器",包装换了,里面的连接协议和核心逻辑是同源的。旧坐标的 jar 文件名是mysql-connector-java-8.0.33.jar,新坐标是mysql-connector-j-8.0.33.jar,拆开来对比会发现 class 结构基本一致,连Automatic-Module-Name都是同一个值。
这一点对排查问题很重要:如果你的项目同时引入了两个坐标,表现出的现象可能是类冲突,也可能只是冗余依赖,但不会是"两个完全不同的驱动在打架"。理解同源关系之后,处理起来会简单很多——排除旧坐标、保留新坐标,就能让依赖收敛。
2. 官方为什么在这个时间点切换坐标:命名规范、语义统一与模块化需求
2.1 groupId 从 mysql 到 com.mysql 的历史遗留
Maven 中央仓库的 groupId 通常遵循反向域名约定,比如com.alibaba、org.apache、io.netty。老驱动的 groupId 是mysql,这是早期 Maven 时代的写法,简单直接,但和后来的规范不一致。至于为什么当初用mysql,业界普遍认为和早期中央仓库没有那么严格的域名校验有关,很多开源项目都经历过这种"先上线、后规范"的阶段。
到了 8.0.31 附近,官方明显在整理坐标体系,把 groupId 统一到com.mysql。这样做的好处是,在仓库里看目录结构更规整,依赖打印也更符合预期。或者说,这更像是一次"坐标洁癖"式的整理——把历史上不那么规范的部分修掉。对我这种经常查依赖的人来说,反而省事很多,因为com.mysql一眼就能看出是官方域名体系下的产物。
2.2 artifactId 去掉 java 后缀的语义变化
旧坐标的 artifactId 是mysql-connector-java,新坐标是mysql-connector-j。表面上看只是少了一个 "ava",实际上语义更准确。Connector/J 这个产品本身就是 Java 驱动,名字里带 Java 是历史习惯,去掉后缀之后反而和官方文档里的下载文件名、README 里的引用方式对齐。
举个例子,MySQL 官网的 Connector/J 下载页面,文件名通常就是mysql-connector-j-x.x.xx.tar.gz,从来不带java后缀。旧坐标相当于把普通人容易叫错的口语名写进了 artifactId,新坐标则回归到产品本身的命名。另外,从生态统一的角度看,MySQL 官方还维护了其他语言的连接器,比如 Python 的是mysql-connector-python,把这些放在一起看,Java 驱动用mysql-connector-j会更清爽,不会让人误以为只有 java 版本才有连接器。
2.3 Java 9 模块化与 Multi-Release JAR 的推动
从 8.0.31 开始,Connector/J 的 jar 变成了 Multi-Release JAR,也就是在同一个 jar 里的META-INF/versions目录下,会根据不同 Java 版本提供更合适的实现。这个变化其实比坐标切换更有技术含量。之前的单版本 jar 只能面向最低支持的 Java 版本编译,时区处理、字符集映射等逻辑在 Java 8 和 Java 17 上跑,很多分支没办法做到最优;多版本 jar 可以针对 Java 9+ 提供专门优化过的类,同时保留 Java 8 class 版本作为基础。
新坐标在这个时间点切换,和模块化清理是配套的。新坐标的 jar 里有明确的Automatic-Module-Name: com.mysql.cj,在 JPMS 项目里做requires com.mysql.cj会更规范。旧坐标虽然也包含同样的 JAR,但官方明显希望把"新形态的驱动"和"新坐标"绑定在一起,避免大家继续在老坐标的惯性里打转。这一点对普通业务项目影响不大,但对那些要拆模块、用 jlink 定制运行时的工程,影响直接。
3. 替换旧坐标的最稳操作:Maven、Gradle 与 Spring Boot 三套姿势
3.1 Maven 项目里怎么改
如果你用的是 Maven,最直接的改法是把依赖声明从这样:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency>换成这样:
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency>如果你的项目是 Spring Boot,而且版本是 3.x,那更省事,直接写:
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>不写 version,让 Spring Boot 的 dependencyManagement 接管版本。Spring Boot 3.0、3.1、3.2 这些版本里,管理的就是新坐标,所以显式写版本反而容易和父 pom 产生冲突。
但这里有一个特别容易踩的坑:如果父项目是 Spring Boot 2.7,而你只改了依赖坐标但没升级 Spring Boot 版本,那你必须自己写 version,否则会提示找不到依赖管理项。我见过有人在 Spring Boot 2.7 的项目里纯写坐标不写版本,结果编译期报错,最后又加回8.0.33。这种情况我建议要么一起升级 Spring Boot,要么就用旧坐标继续跑,不必为了换坐标而换坐标。
3.2 Gradle 项目里怎么改
Gradle 项目改起来同样直接。用 Kotlin DSL 的话:
runtimeOnly("com.mysql:mysql-connector-j")用 Groovy DSL:
runtimeOnly 'com.mysql:mysql-connector-j'如果项目没有依赖管理插件,那需要显式带上版本号,比如runtimeOnly("com.mysql:mysql-connector-j:8.3.0")。这里要注意的是,Gradle 默认的依赖解析不会自动把新旧坐标视为同一个模块,所以如果你原本直接依赖mysql:mysql-connector-java,改桥之后最好跑一遍./gradlew dependencies --configuration runtimeClasspath看看结果,确认没有同时保留两个 MySQL 相关依赖。
3.3 替换之后的验证清单
坐标改完了,不代表万事大吉。我给自己的项目做切换时,会按下面这份清单逐项检查:
| 检查项 | 旧写法 | 新写法 | 备注 |
|---|---|---|---|
| Maven 坐标 | mysql:mysql-connector-java | com.mysql:mysql-connector-j | groupId 和 artifactId 都要换 |
| 驱动类名 | com.mysql.jdbc.Driver(老,不推荐) | com.mysql.cj.jdbc.Driver | 8.0 以后都用这个 |
| JDBC URL 前缀 | jdbc:mysql://... | jdbc:mysql://... | 不变 |
| 依赖版本来源 | Spring Boot 2.x 管理 | Spring Boot 3.x 管理 | 注意版本归属 |
我会专门跑一次mvn dependency:tree -Dincludes=mysql或者 Gradle 的依赖报告,确认最终生效的是com.mysql:mysql-connector-j,以及版本号符合预期。然后启动应用,看日志里有没有 DRIVER 相关异常。如果项目里用了数据库连接池,还要观察连接池初始化是否正常,最好执行一条简单的SELECT 1来验证真正能拿到连接。
4. 依赖树里的隐藏问题:两个坐标同框时的版本冲突与排除
4.1 为什么会出现两个坐标并存
新坐标出现之后,最典型的问题不是"改不掉",而是"改完之后依赖树里同时出现了两个 mysql 驱动"。为什么?因为很多中间件和 starter 仍然在传递依赖里引用旧坐标。比如某个数据访问组件、某个连接池封装,它们的 pom 里锁定的是mysql:mysql-connector-java,你的业务代码里又显式引用了com.mysql:mysql-connector-j,结果 classpath 里就会同时存在mysql-connector-java-8.0.33.jar和mysql-connector-j-8.0.33.jar。
各种情况都可能导致。有的项目里旧坐标是父 pom 统一管理版本,子模块显式引入新坐标,但父 pom 的 dependencyManagement 没删除旧坐标;还有的是因为某个第三方的依赖没有及时跟上官方坐标迁移,继续传递旧坐标。别觉得这个问题少见,Spring Boot 2.7 的 starter 里就带着旧坐标,你如果在这个基础上升级驱动到 9.x,不处理传递依赖,两个 jar 就会安居在依赖树里。
两个 jar 同框的危害不是马上能看到。因为两个 jar 里的类结构大量相同,类加载器在按需加载时可能从旧 jar 加载某些类、从新 jar 加载另一些类,导致奇怪的运行期错误。比如ClassNotFoundException不报,但报NoSuchMethodError、NoClassDefFoundError,或者连接池初始化成功但某些 MySQL 8 特性不可用。追这种问题非常耗费时间,所以最好的策略是主动排除。
4.2 用 dependency:tree 定位并排除旧坐标
排查工具很简单,Maven 项目执行:
mvn dependency:tree -Dincludes=mysql这个命令会把所有和 mysql 相关的依赖树节点打出来。理想状态下应该只看到一行com.mysql:mysql-connector-j:jar:8.3.0:runtime;如果看到mysql:mysql-connector-java:jar:8.0.33:compile和com.mysql:mysql-connector-j:jar:8.3.0:runtime两行甚至多行,就要再向上追溯是谁带进来的。
Gradle 项目执行:
./gradlew dependencies --configuration runtimeClasspath | grep mysql找到引入旧坐标的传递依赖后,在显式依赖里加 exclusion。Maven 的写法:
<dependency> <groupId>某个中间件</groupId> <artifactId>某个包</artifactId> <exclusions> <exclusion> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </exclusion> </exclusions> </dependency>Gradle 的写法:
implementation("某个中间件:某个包") { exclude(group = "mysql", module = "mysql-connector-java") }排除的时候要特别注意 groupId 是mysql而不是com.mysql,写反了排除不掉。我有一次排除时把 groupId 写成了com.mysql,结果 excluded 一直没生效,依赖树里旧坐标纹丝不动,后来对着坐标逐个核对才发现问题。
4.3 连接池与 ORM 框架的传递依赖处理
如果是 Spring Boot + HikariCP 的项目,HikariCP 本身不依赖具体数据库驱动,问题不大。但如果你用了一些自动装配数据库驱动的组件,比如某些 ORM 的 starter,或者自己封装了数据库模块,就要留意传递依赖。MyBatis 的 starter 不会强制带 MySQL 驱动,一般由使用者自己引入,但很多公司内部的基础组件会顺手带一个驱动版本,导致升级时躲都躲不开。
处理这类问题我建议按"显式声明、主动收敛"的原则来:业务模块不要依赖中间件的传递驱动,统一显式声明新坐标。这样以后排查依赖时,MySQL 驱动的来源一目了然。另一个技巧是,在父 pom 的dependencyManagement里直接管理两个坐标的版本,即使某些模块还在用旧坐标,也能确保版本一致,减少双 jar 时的版本差问题。等所有模块都切换完,再把父 pom 里的旧坐标管理项删掉,做一次彻底清理。
5. 换完坐标后,代码层真正需要留神的点:驱动类名、URL 参数与旧 API 移除
5.1 com.mysql.jdbc.Driver 这个历史类名的去处
坐标切换不会改变com.mysql.cj.jdbc.Driver这个类,但会影响com.mysql.jdbc.Driver这个旧类。在 8.0 时代,旧坐标的 jar 里还保留了一个com.mysql.jdbc.Driver兼容类,用于照顾远古代码,但官方文档从 8.0 开始就明确推荐com.mysql.cj.jdbc.Driver。到了 9.x 的新坐标里,com.mysql.jdbc.Driver这个旧类已经被移除。
这里的区别很实际:如果你的应用里有一段硬编码的Class.forName("com.mysql.jdbc.Driver"),在旧坐标的 8.0.33 下可能还能跑,最多打印点警告;切换到新坐标 9.x 之后,直接就会抛ClassNotFoundException。连接池配置里的driverClassName也会遇到同样问题。我见过一个项目的配置写的是driverClassName=com.mysql.jdbc.Driver,连接池启动时报错,排查半天才意识到是这个类没了。
正确的配置是:
driverClassName=com.mysql.cj.jdbc.Driver或者干脆不配 driverClassName,让连接池根据 JDBC URL 自动识别驱动类。JDBC 4.0 之后驱动支持 SPI 自动注册,META-INF/services 里已经有com.mysql.cj.jdbc.Driver,很多情况下不需要手动写类名。
5.2 serverTimezone 与连接参数的几个高频问题
MySQL Connector/J 8.0 系列对时区的处理比老版本严格,新旧坐标在这方面没有差异,但切换坐标和升级版本时特别容易碰到。比较典型的是连接 URL 没有显式指定时区,驱动抛类似 "The server time zone value 'UTC' is unrecognized or represents more than one time zone" 的异常。这个问题的根源是驱动和 MySQL 服务端时区配置不一致。
我实际项目里常用的 JDBC URL 是这样:
jdbc:mysql://192.168.1.10:3306/某个业务库?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true其中serverTimezone=Asia/Shanghai基本必带,allowPublicKeyRetrieval=true是在 MySQL 8 使用caching_sha2_password认证时可能需要的参数。如果你用默认的sha256_password或者caching_sha2_password插件,登录时 public key 获取有可能被驱动安全策略拦截,导致连接失败,这时候除了allowPublicKeyRetrieval=true,还可以用useSSL=false配合。不过这些参数和坐标无关,属于驱动升级时共同要注意的参数集合。
5.3 常见升级报错与排查思路
把我在实际升级中遇到过的报错整理成表,供你对照:
| 报错 | 原因 | 处理 |
|---|---|---|
ClassNotFoundException: com.mysql.jdbc.Driver | 配置了旧驱动类名,新坐标 9.x 已移除 | 改成com.mysql.cj.jdbc.Driver |
The server time zone value ... unrecognized | URL 未指定 serverTimezone | 加serverTimezone=Asia/Shanghai |
Public Key Retrieval is not allowed | caching_sha2_password 认证被安全配置拦截 | URL 加allowPublicKeyRetrieval=true |
NoSuchMethodError: com.mysql.cj.conf.PropertySet | 双 jar 版本不一致,加载了旧 jar 的类 | 排除旧坐标,收敛到单一版本 |
连接池初始化成功后查询报Unknown system variable 'xxx' | 驱动版本和服务端版本不匹配 | 确认驱动版本支持对应 MySQL 服务端版本 |
排查报错有一个通用顺序:先看依赖树里是不是存在新旧坐标并存,再看驱动类名配没配对,最后看 URL 参数和时区。绝大多数升级问题都能被这三个角度覆盖。
最后讲一条我自己的实操心得:换 MySQL 驱动坐标,不要只改 pom 就行,改完之后必须做两件事——查依赖树确认只有一个 mysql 相关 jar,然后跑一次真实的连接初始化。为什么强调这个?因为很多编译期不报错的依赖问题,只在运行期连接池创建连接时才爆发。我经手的模拟项目里,有一次双 jar 并存,服务能正常启动,但 MySQL 8 的身份认证插件行为一直不对,查了两天才发现旧 jar 混在 classpath 里。从那以后,我新项目一律直接用com.mysql:mysql-connector-j,老项目升级前先dependency:tree看一眼,再动手改配置,整个切换过程会顺畅很多。