写点什么

以 MySQL 为例,来看看 maven-shade-plugin 如何解决多版本驱动共存的问题?

作者:EquatorCoco
  • 2024-09-02
    福建
  • 本文字数:9698 字

    阅读完需:约 32 分钟

前提说明


假设 MySQL 5.7.36 的库 qsl_datax



有表 qsl_datax_source 和 数据


CREATE TABLE `qsl_datax_source`  (  `id` bigint(20) NOT NULL COMMENT '自增主键',  `username` varchar(255) NOT NULL COMMENT '姓名',  `password` varchar(255) NOT NULL COMMENT '密码',  `birth_day` date NOT NULL COMMENT '出生日期',  `remark` text,  PRIMARY KEY (`id`)) ENGINE = InnoDB ;INSERT INTO `qsl_datax_source` VALUES (1, '张三', 'z123456', '1991-01-01', '张三');INSERT INTO `qsl_datax_source` VALUES (2, '李四', 'l123456', '1992-01-01', '李四');INSERT INTO `qsl_datax_source` VALUES (3, '王五', 'w123456', '1993-01-01', '王五');INSERT INTO `qsl_datax_source` VALUES (4, '麻子', 'm123456', '1994-01-01', '麻子');
复制代码


需要将表中数据同步到 MySQL 8.0.30


sql_db 库的 qsl_datax_source 表中,并且只用 JDBC 的方式,该如何实现?你们可能觉得非常简单,直接引入 mysql-connector-j 依赖


<dependency>    <groupId>com.mysql</groupId>    <artifactId>mysql-connector-j</artifactId>    <version>8.0.33</version></dependency>
复制代码


然后直接写同步代码


public static void main(String[] args) throws Exception {    String url5 = "jdbc:mysql://192.168.2.118:3307/qsl_datax?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai";    String url8 = "jdbc:mysql://192.168.2.118:3311/sql_db?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai";    Properties pro = new Properties();    pro.put("user", "root");    pro.put("password", "123456");    // 加载驱动类    Class.forName("com.mysql.cj.jdbc.Driver");    // 建立连接    Connection conn5 = DriverManager.getConnection(url5, pro);    // 查数据    Statement statement = conn5.createStatement();    ResultSet resultSet = statement.executeQuery("SELECT * FROM qsl_datax_source");    StringBuilder insertSql = new StringBuilder("INSERT INTO qsl_datax_source(id,username,password,birth_day,remark) VALUES ");    while (resultSet.next()) {        // 拼接sql        insertSql.append("(")                .append(resultSet.getLong("id")).append(",")                .append("'").append(resultSet.getString("username")).append("',")                .append("'").append(resultSet.getString("password")).append("',")                .append("'").append(resultSet.getString("birth_day")).append("',")                .append("'").append(resultSet.getString("remark")).append("'")                .append("),");    }    // 因为mysql5和mysql8的账密是一样的,所以用的同一个 pro    Connection conn8 = DriverManager.getConnection(url8, pro);    Statement stmt = conn8.createStatement();    int count = stmt.executeUpdate(insertSql.substring(0, insertSql.length() - 1));    System.out.println("新插入记录数:" + count);
resultSet.close(); statement.close(); stmt.close(); conn5.close(); conn8.close();}
复制代码


执行后输出


新插入记录数:4


在 MySQL 8.0.30 的库 sql_db 查看表 qsl_datax_source 的数据



同步完成,这不是有手就行吗?



一般来说,高版本的驱动会兼容低版本的数据库,但也不绝对,或者说兼容不全;MySQL 版本、驱动版本、JDK 版本对应关系如下



mysql-connector-j 8.0.33 驱动兼容 MySQL 5.7.36,所以上面的同步没问题,但如果 MySQL 版本很低(比如:5.1.x),例如从 MySQL 5.1.8 同步到 MySQL 8.0.30 ,如上同步代码还能同步成功吗(我就不去试了,你们也别去试了,因为引申目的已经达到了),所以保险做法是


mysql-connector-j 8.0.33 操作 MySQL 8.0.30

mysql-connector-java 5.1.49 操作 MySQL 5.7.36

mysql-connector-java 5.0.x 操作 MySQL 5.0.x


所以问题就来了


如何用 mysql-connector-java 5.1.49 从 MySQL 5.7.36 查数据后,用 mysql-connector-j 8.0.33 将数据插入 MySQL 8.0.30


多驱动操作


你们肯定也觉得简单,继续引入 mysql-connector-java 5.1.49 依赖


<dependency>    <groupId>com.mysql</groupId>    <artifactId>mysql-connector-j</artifactId>    <version>8.0.33</version></dependency><dependency>    <groupId>mysql</groupId>    <artifactId>mysql-connector-java</artifactId>    <version>5.1.49</version></dependency>
复制代码


然后调整代码


public static void main(String[] args) throws Exception {    String url5 = "jdbc:mysql://192.168.2.118:3307/qsl_datax?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai";    String url8 = "jdbc:mysql://192.168.2.118:3311/sql_db?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai";    Properties pro = new Properties();    pro.put("user", "root");    pro.put("password", "123456");    // 加载驱动类    Class.forName("com.mysql.jdbc.Driver");    // 建立连接    Connection conn5 = DriverManager.getConnection(url5, pro);    // 查数据    Statement statement = conn5.createStatement();    ResultSet resultSet = statement.executeQuery("SELECT * FROM qsl_datax_source");    StringBuilder insertSql = new StringBuilder("INSERT INTO qsl_datax_source(id,username,password,birth_day,remark) VALUES ");    while (resultSet.next()) {        // 拼接sql        insertSql.append("(")                .append(resultSet.getLong("id")).append(",")                .append("'").append(resultSet.getString("username")).append("',")                .append("'").append(resultSet.getString("password")).append("',")                .append("'").append(resultSet.getString("birth_day")).append("',")                .append("'").append(resultSet.getString("remark")).append("'")                .append("),");    }    Class.forName("com.mysql.cj.jdbc.Driver");    // 因为mysql5和mysql8的账密是一样的,所以用的同一个 pro    Connection conn8 = DriverManager.getConnection(url8, pro);    Statement stmt = conn8.createStatement();    int count = stmt.executeUpdate(insertSql.substring(0, insertSql.length() - 1));    System.out.println("新插入记录数:" + count);
resultSet.close(); statement.close(); stmt.close(); conn5.close(); conn8.close();}
复制代码


和之前代码对比下



调整甚微;执行后输出结果如下


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.新插入记录数:4
复制代码


如果只从结果来看,确实同步成功了,但第一行的 警告 值得得我们琢磨下


类 com.mysql.jdbc.Driver 加载中。这个类已经被弃用。新的驱动类是 com.mysql.cj.jdbc.Driver,这个驱动通过 SPI 机制已经自动注册了,不需要手动加载


从中我们会产生 2 个疑问


  1. com.mysql.jdbc.Driver 不应该是 mysql-connector-java 5.1.49 的吗,怎么会被弃用

  2. SPI 机制是什么,com.mysql.cj.jdbc.Driver 什么时候加载的


我们先来看问题 2,关于 SPI 机制可查看


记一次 JDK SPI 配置不生效的问题 → 这么简单都不会,还是回家养猪吧


DriverManager 有静态代码块


static {    loadInitialDrivers();    println("JDBC DriverManager initialized");}
复制代码


loadInitialDrivers() 中有这样一段代码



自动加载了驱动,而驱动类中往往有类似如下代码



将驱动实例注册给 DriverManager,所以不需要再去手动加载驱动类了


从 JDBC 4.0 开始,JDBC 驱动支持自动加载功能,不再需要调用 Class.forName 来加载驱动


我们回到问题 1,同步的告警信息


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.
复制代码


肯定是 mysql-connector-j 8.0.33 告警出来的,因为 mysql-connector-java 5.1.49 没有类


com.mysql.cj.jdbc.Driver


对不对?全局搜索下


This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'



点进去,我们会发现 mysql-connector-j 8.0.33 也有类


com.mysql.jdbc.Driver

大家看仔细了,这个 Driver 是没有把自己的实例注册进 java.sql.DriverManager 的



这说明什么,说明是从 mysql-connector-j 8.0.33 加载的类:com.mysql.jdbc.Driver,而不是从 mysql-connector-java 5.1.49 加载


我们来捋一捋整个同步流程


1、通过 SPI 机制,会加载文件 META-INF/services/java.sql.Driver 中配置的类


mysql-connector-j 8.0.33 的 java.sql.Driver 文件内容



mysql-connector-java 5.1.49 的 java.sql.Driver 文件内容



类加载器加载 com.mysql.cj.jdbc.Driver 的时候,毫无疑问找到的肯定是 mysql-connector-j 8.0.33 jar 包中的,而加载 com.mysql.jdbc.Driver 的时候,类加载器找到的却是 mysql-connector-j 8.0.33 jar 包中的,而非 mysql-connector-java 5.1.49 jar 包中的,所以告警了


2、代码中手动调用 Class.forName("com.mysql.jdbc.Driver"); 进行类加载,根据 双亲委派模型,已经加载过的类不会再加载,所以相当于没做任何操作


前面的告警信息不是这里触发出来的!!!不信的话可以注释掉该行代码执行下,你们会发现仍有同样的告警信息


3、从 MySQL5 查数据,用的驱动实际是 com.mysql.cj.jdbc.Driver



因为 DriverManager 中合适的驱动只有这一个


4、代码中手动调用 Class.forName("com.mysql.cj.jdbc.Driver"); 进行类加载,根据 双亲委派模型,已经加载过的类不会再加载,所以相当于没做任何操作


5、从 MySQL8 查数据,用的驱动毫无疑问也只能是 com.mysql.cj.jdbc.Driver


所以整个同步,用的都是 mysql-connector-j 8.0.33 下的驱动,mysql-connector-java 5.1.49 压根就没用到,是不是在你们的意料之外?



所以如何实现我们最初的想法?


如何用 mysql-connector-java 5.1.49 从 MySQL 5.7.36 查数据后,用 mysql-connector-j 8.0.33 将数据插入 MySQL 8.0.30


maven-shade-plugin


甲方扔给两个存在包名与类名均相同的Jar包,要在工程中同时使用怎么办? 中谈到了好些解决办法,但 maven-shade-plugin 相对而言是最优解,其具体使用可参考


maven 插件之 maven-shade-plugin,解决同包同名 class 共存问题的神器

那如何应该到当前案例中来了,其实很简单,只需要用到 maven-shade-plugin 的 重定位 class 功能即可,请看我表演


1、对 mysql-connector-j 8.0.33 进行 class 重定位


新建一个工程 mysql-jdbc,没有任何代码和配置文件



只有一个 pom.xml


<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>
<groupId>com.qsl</groupId> <artifactId>mysql-jdbc8</artifactId> <version>8.0.33</version>
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
<dependencies> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.6.0</version> <executions> <execution> <!-- 和 package 阶段绑定 --> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <relocations> <relocation> <pattern>com.mysql.jdbc</pattern> <shadedPattern>com.mysql.jdbc8</shadedPattern> </relocation> </relocations> <filters> <filter> <artifact>com.qsl:mysql-jdbc8</artifact> <excludes> <exclude>META-INF/*.*</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin> </plugins> </build></project>
复制代码


mvn install 一下,将重新打包后的 jar 部署到本地仓库



2、调整示例代码的 maven 依赖


mysql-connector-j 8.0.33 调整成 mysql-jdbc8 8.0.33,mysql-connector-java 5.1.49 原样保留


<dependencies>    <dependency>        <groupId>com.qsl</groupId>        <artifactId>mysql-jdbc8</artifactId>        <version>8.0.33</version>    </dependency>    <dependency>        <groupId>mysql</groupId>        <artifactId>mysql-connector-java</artifactId>        <version>5.1.49</version>    </dependency></dependencies>
复制代码


3、调整同步代码


去掉手动加载驱动,增加 connection 驱动信息版本输出


public static void main(String[] args) throws Exception {    String url5 = "jdbc:mysql://192.168.2.118:3307/qsl_datax?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai";    String url8 = "jdbc:mysql://192.168.2.118:3311/sql_db?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai";    Properties pro = new Properties();    pro.put("user", "root");    pro.put("password", "123456");    // 建立连接    Connection conn5 = DriverManager.getConnection(url5, pro);    // 查数据    Statement statement = conn5.createStatement();    System.out.println("conn5 driver version: " + conn5.getMetaData().getDriverVersion());    ResultSet resultSet = statement.executeQuery("SELECT * FROM qsl_datax_source");    StringBuilder insertSql = new StringBuilder("INSERT INTO qsl_datax_source(id,username,password,birth_day,remark) VALUES ");    while (resultSet.next()) {        // 拼接sql        insertSql.append("(")                .append(resultSet.getLong("id")).append(",")                .append("'").append(resultSet.getString("username")).append("',")                .append("'").append(resultSet.getString("password")).append("',")                .append("'").append(resultSet.getString("birth_day")).append("',")                .append("'").append(resultSet.getString("remark")).append("'")                .append("),");    }    // 因为mysql5和mysql8的账密是一样的,所以用的同一个 pro    Connection conn8 = DriverManager.getConnection(url8, pro);    System.out.println("conn8 driver version: " + conn8.getMetaData().getDriverVersion());    Statement stmt = conn8.createStatement();    int count = stmt.executeUpdate(insertSql.substring(0, insertSql.length() - 1));    System.out.println("新插入记录数:" + count);
resultSet.close(); statement.close(); stmt.close(); conn5.close(); conn8.close();}
复制代码


处理就算完成,我们执行一下看结果



之前的警告确实没了,但新的问题又来了:为什么驱动用的是同一个,mysql-connector-java 5.1.49 中的驱动为什么没有被用到?



mysql-connector-java 5.1.49 中的 com.mysql.jdbc.Driver 肯定是被正常加载了,并且注册到了 DriverManager 中,这点大家认同不?(不认同也没关系,后面会得到证明)那它为什么没有被使用了,我们需要跟一下 DriverManager.getConnection 的源码了;源码跟进去比较简单,我就带你们一步一步跟了,最终回来到如下方法


java.sql.DriverManager#getConnection(java.lang.String, java.util.Properties, java.lang.Class<?>)


这个方法里面有这么一段代码


for(DriverInfo aDriver : registeredDrivers) {    // If the caller does not have permission to load the driver then    // skip it.    if(isDriverAllowed(aDriver.driver, callerCL)) {        try {            println("    trying " + aDriver.driver.getClass().getName());            Connection con = aDriver.driver.connect(url, info);            if (con != null) {                // Success!                println("getConnection returning " + aDriver.driver.getClass().getName());                return (con);            }        } catch (SQLException ex) {            if (reason == null) {                reason = ex;            }        }
} else { println(" skipping: " + aDriver.getClass().getName()); }
}
复制代码


我们打个断点跟一下(com.mysql.cj.jdbc.Driver 排在 com.mysql.jdbc.Driver 前面!!!)


isDriverAllowed 作用是检查一个给定的Driver对象是否被允许通过指定的ClassLoader加载,我们不需要关注,而我们需要关注的是


Connection con = aDriver.driver.connect(url, info);


跟进去来到 com.mysql.cj.jdbc.NonRegisteringDriver#connect



感兴趣的可以继续跟进 ConnectionUrl.acceptsUrl(url),但我觉得没必要了,很明显就是根据正则表达式去匹配 url,看看是否适配,因为 MySQL5 的 url 与 MySQL8 的 URL 格式一致


String url5 = "jdbc:mysql://192.168.2.118:3307/qsl_datax?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai";String url8 = "jdbc:mysql://192.168.2.118:3311/sql_db?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai";
复制代码


因为 com.mysql.cj.jdbc.Driver 排在 com.mysql.jdbc.Driver 前面,所以用它连接了 MySQL5 和 MySQL8,前面的问题


为什么驱动用的是同一个,mysql-connector-java 5.1.49 中的驱动为什么没有被用到?


是不是就清楚了?你们可能又有疑问了:为什么不是 com.mysql.jdbc.Driver 排在前面?这个跟类加载的顺序有关,超出了本文范围,你们自行去查阅。那还能实现最初的目的吗


用 mysql-connector-java 5.1.49 从 MySQL 5.7.36 查数据后,用 mysql-connector-j 8.0.33 将数据插入 MySQL 8.0.30


肯定是能的,看我调整下代码


public static void main(String[] args) throws Exception {    String url5 = "jdbc:mysql://192.168.2.118:3307/qsl_datax?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai";    String url8 = "jdbc:mysql://192.168.2.118:3311/sql_db?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai";    Properties pro = new Properties();    pro.put("user", "root");    pro.put("password", "123456");
// 建立连接 Driver driver5 = getDriver("com.mysql.jdbc.Driver"); Connection conn5 = driver5.connect(url5, pro); // 查数据 Statement statement = conn5.createStatement(); System.out.println("conn5 driver version: " + conn5.getMetaData().getDriverVersion()); ResultSet resultSet = statement.executeQuery("SELECT * FROM qsl_datax_source"); StringBuilder insertSql = new StringBuilder("INSERT INTO qsl_datax_source(id,username,password,birth_day,remark) VALUES "); while (resultSet.next()) { // 拼接sql insertSql.append("(") .append(resultSet.getLong("id")).append(",") .append("'").append(resultSet.getString("username")).append("',") .append("'").append(resultSet.getString("password")).append("',") .append("'").append(resultSet.getString("birth_day")).append("',") .append("'").append(resultSet.getString("remark")).append("'") .append("),"); } // 因为mysql5和mysql8的账密是一样的,所以用的同一个 pro Driver driver8 = getDriver("com.mysql.cj.jdbc.Driver"); Connection conn8 = driver8.connect(url8, pro); System.out.println("conn8 driver version: " + conn8.getMetaData().getDriverVersion()); Statement stmt = conn8.createStatement(); int count = stmt.executeUpdate(insertSql.substring(0, insertSql.length() - 1)); System.out.println("新插入记录数:" + count);
resultSet.close(); statement.close(); stmt.close(); conn5.close(); conn8.close();}
private static Driver getDriver(String driverClassName) { Enumeration<Driver> drivers = DriverManager.getDrivers(); while (drivers.hasMoreElements()) { Driver driver = drivers.nextElement(); if (driver.getClass().getName().equals(driverClassName)) { return driver; } } throw new RuntimeException("未找到驱动:" + driverClassName);}
复制代码


执行一下看结果



此时我就想说一句:还有谁?



总结


1、示例代码:mysql-driver-demo


不包括 mysql-jdbc8 的代码


2、就 MySQL 而言,mysql-connector-j 8 驱动兼容 MySQL 5.5、5.6、5.7,实际工作中是可以用 mysql-connector-j 8 去连 MySQL 5.7 的


3、SQL Server 就存在驱动不兼容的情况


Microsoft JDBC Driver for SQL Server 支持矩阵


4、maven-shade-plugin 来实现多版本驱动的共存,简单高效,值得掌握!


文章转载自:青石路

原文链接:https://www.cnblogs.com/youzhibing/p/18390525

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
以MySQL为例,来看看maven-shade-plugin如何解决多版本驱动共存的问题?_MySQL_EquatorCoco_InfoQ写作社区