以MySQL為例,來看看maven-shade-plugin如何解決多版本驅動共存的問題?

青石路發表於2024-09-02

開心一刻

清明節那天,看到一小孩在路邊燒紙
時不時地偷偷往火堆裡扔幾張考試卷子
邊燒邊唸叨:爺爺呀,你歲數大了,在那邊多做做題吧,對腦子好,要是有不懂的地方,就把我老師帶走,讓他教您!

開心一刻

前提說明

假設 MySQL 5.7.36 的庫 qsl_datax

mysql5

有表 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

mysql8

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版本驅動版本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() 中有這樣一段程式碼

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

mysql8驅動com_mysql_jdbc_Driver類

這說明什麼,說明是從 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 檔案內容

    mysql8驅動java_sql_Driver

    mysql-connector-java 5.1.49 的 java.sql.Driver 檔案內容

    mysql5驅動_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

    連線mysql5的實際驅動

    因為 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,沒有任何程式碼和配置檔案

    mysql-jdbc8

    只有一個 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 部署到本地倉庫

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

處理就算完成,我們執行一下看結果

maven-shade-plugin改造後執行結果

之前的警告確實沒了,但新的問題又來了:為什麼驅動用的是同一個,mysql-connector-java 5.1.49 中的驅動為什麼沒有被用到?

一個bug改一天

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 前面!!!)

debug_驅動列表

isDriverAllowed 作用是檢查一個給定的Driver物件是否被允許透過指定的ClassLoader載入,我們不需要關注,而我們需要關注的是

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

跟進去來到 com.mysql.cj.jdbc.NonRegisteringDriver#connect

debug_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 支援矩陣

    SQLServer驅動相容情況
  4. maven-shade-plugin 來實現多版本驅動的共存,簡單高效,值得掌握!

    maven 外掛之 maven-shade-plugin,解決同包同名 class 共存問題的神器

相關文章