Mybatis資料庫驅動

Jame!發表於2023-03-15

Mybatis資料庫驅動

最近在學習mybatis的原始碼,有一個databaseIdProvider根據不同資料庫執行不同sql的功能,我正好有一個mysql還有一個瀚高資料庫,就去試了一下,使用如下

pom檔案匯入兩個資料庫的驅動

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.13</version>
</dependency>

<dependency>
    <groupId>com.highgo</groupId>
    <artifactId>HgdbJdbc</artifactId>
    <version>6.2.2</version>
</dependency>

主啟動類.java

public class MybatisHelloWorld {
    public static void main(String[] args) throws Exception {
        String resource = "org/mybatis/config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession session = sqlSessionFactory.openSession();
        UserMapper mapper = session.getMapper(UserMapper.class);
        List<User> users = mapper.getUsers(1);
        session.close();
    }
}

User.java


public class User {
    private Integer id;
    private String name;
    private Integer age;
    //....getter setter 構造..
}

UserMapper.java

public interface UserMapper {
    List<User> getUsers(int age);
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.mapper.UserMapper" >

    <select id="getUsers" resultType="org.mybatis.pojo.User" databaseId="mysql">
        select * from user where age = #{age}
    </select>

    <select id="getUsers" resultType="org.mybatis.pojo.User" databaseId="postgresql">
        select * from mybatis.user where age = #{age}
    </select>

</mapper>

Mybatis配置檔案

<configuration>
    <environments default="mysql">
        <environment id="mysql">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>

        <environment id="highgo">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:highgo://localhost:5866/highgo?currentSchema=mybatis"/>
                <property name="username" value="highgo"/>
                <property name="password" value="Hello@123"/>
            </dataSource>
        </environment>
    </environments>


    <databaseIdProvider type="DB_VENDOR">
        <property name="MySQL" value="mysql"/>
        <property name="PostgreSQL" value="postgresql"/>
    </databaseIdProvider>


    <mappers>
        <package name="org.mybatis.mapper"/>
    </mappers>

</configuration>

當我把mybatis配置檔案中的環境設定為<environments default="mysql">,程式碼執行結果如下

然後修改環境設定為<environments default="highgo">後,程式碼執行結果如下

不知道您有沒有看出問題所在,在上面的mybatis配置檔案中highgo環境的驅動是com.mysql.cj.jdbc.Driver 但是能連線上瀚高的資料庫並且能正常執行sql

當時我也發現這個問題了,於是想研究下原因

首先要找到是哪一段程式碼進行的操作,那麼這裡肯定是建立連線的時候,因為驅動不對的話是連線不上的,於是跟著這個思路就去尋找

最後找到方法棧如下

  • doGetConnection:200, UnpooledDataSource (org.apache.ibatis.datasource.unpooled)
  • doGetConnection:196, UnpooledDataSource (org.apache.ibatis.datasource.unpooled)
  • getConnection:93, UnpooledDataSource (org.apache.ibatis.datasource.unpooled)
  • popConnection:407, PooledDataSource (org.apache.ibatis.datasource.pooled)
  • getConnection:89, PooledDataSource (org.apache.ibatis.datasource.pooled)
  • getDatabaseProductName:82, VendorDatabaseIdProvider (org.apache.ibatis.mapping)
  • getDatabaseName:66, VendorDatabaseIdProvider (org.apache.ibatis.mapping)
  • getDatabaseId:53, VendorDatabaseIdProvider (org.apache.ibatis.mapping)
  • databaseIdProviderElement:305, XMLConfigBuilder (org.apache.ibatis.builder.xml)
  • parseConfiguration:123, XMLConfigBuilder (org.apache.ibatis.builder.xml)
  • parse:97, XMLConfigBuilder (org.apache.ibatis.builder.xml)
  • build:82, SqlSessionFactoryBuilder (org.apache.ibatis.session)
  • build:67, SqlSessionFactoryBuilder (org.apache.ibatis.session)
  • main:32, MybatisHelloWorld (org.mybatis)

UnpooledDataSource.java

private Connection doGetConnection(Properties properties) throws SQLException {
    initializeDriver();
    Connection connection = DriverManager.getConnection(url, properties);
    configureConnection(connection);
    return connection;
}
private synchronized void initializeDriver() throws SQLException {
    //判斷這個驅動是否註冊過
    if (!registeredDrivers.containsKey(driver)) {
        Class<?> driverType;
        try {
            if (driverClassLoader != null) {
                driverType = Class.forName(driver, true, driverClassLoader);
            } else {
                driverType = Resources.classForName(driver);
            }
            Driver driverInstance = (Driver)driverType.newInstance();
            DriverManager.registerDriver(new DriverProxy(driverInstance));
            registeredDrivers.put(driver, driverInstance);
        } catch (Exception e) {
            throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
        }
    }
}

先判斷需要載入的驅動是否已經註冊了

那這裡面的兩個驅動是從哪裡來的呢?

就在這個UnpooledDataSource類中的靜態塊裡面

static {
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) {
        Driver driver = drivers.nextElement();
        registeredDrivers.put(driver.getClass().getName(), driver);
    }
}

而DriverManager中有一個集合用來儲存所有已經註冊的資料庫連線驅動

public class DriverManager {

    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
	//....
    public static java.util.Enumeration<Driver> getDrivers() {
        java.util.Vector<Driver> result = new java.util.Vector<>();

        Class<?> callerClass = Reflection.getCallerClass();

        // Walk through the loaded registeredDrivers.
        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerClass)) {
                result.addElement(aDriver.driver);
            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }
        }
        return (result.elements());
    }
    //......
}

那麼問題又來了,DriverManager裡面的瀚高資料庫驅動啥時候放進去的呢

在學java基礎的jdbc時,肯定都寫過類似這樣的程式碼

public static void main(String[] args) throws Exception {
    Class.forName("com.mysql.cj.jdbc.Driver");
    Connection con= DriverManager.getConnection("jdbc:mysql://localhost:3306/xxx","root","XXXXXX");
    Statement stat=con.createStatement();
	//......
}

當時這段Class.forName("com.mysql.cj.jdbc.Driver");就告訴你是載入驅動,有的部落格寫了這段程式碼,有的沒寫,具體操作一直都不清楚

首先JDK5版本以後可以不用顯式呼叫這段話,DriverManager會自己去載入合適的驅動,前提是這個驅動存在於CLASSPATH下

其次,它是怎麼載入的呢?為啥Class.forName就能載入呢?

當一個類被載入到JVM時會執行靜態程式碼塊,我們以mysql的驅動舉例子

package com.mysql.cj.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

所以最終呼叫的還是DriverManager.registerDriver(new Driver());註冊一個驅動,底層就是放入到registeredDrivers這個集合中

以瀚高的資料庫驅動來看,當呼叫DriverManager.getDrivers時,DriverManager會去載入驅動類,繼而驅動類執行static程式碼塊

最終還是使用DriverManager.registerDriver註冊了瀚高的數庫驅動

那麼回到UnpooledDataSource類中

public class UnpooledDataSource implements DataSource {
    private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<String, Driver>();
    //.....
    static {
        //這裡就會獲取到mysql和瀚高的驅動
        Enumeration<Driver> drivers = DriverManager.getDrivers();
        while (drivers.hasMoreElements()) {
            Driver driver = drivers.nextElement();
            registeredDrivers.put(driver.getClass().getName(), driver);
        }
    }
    //.....
	
    private Connection doGetConnection(Properties properties) throws SQLException {
        initializeDriver();
        Connection connection = DriverManager.getConnection(url, properties);
        configureConnection(connection);
        return connection;
    }
}

initializeDriver()載入一些其他的驅動,例如我們自定義一個類,實現Driver介面,然後在<property name="driver" value="com.drive.MyDrive"/>使用

那麼Connection connection = DriverManager.getConnection(url, properties);不就是基礎的JDBC連線資料庫的操作嗎

現在還有一個問題,DriverManager是怎麼確定使用哪個資料庫驅動呢

DriverManager.java

private static Connection getConnection(
	//......
    for(DriverInfo aDriver : registeredDrivers) {
        //檢查是否能載入這個驅動到jvm,不能就跳過,底層使用Class.forName 沒出異常就是能載入
        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());
        }
    }
	//.....
}

底層也很簡單,就是遍歷驅動集合,每個驅動都去連線一下資料庫,如果能連線上說明這個驅動是對的,返回這個驅動建立的連線

也解答了我自己以前的疑惑和錯誤的理解

  1. 一直不清楚Class.forName("xxx.Driver")是怎麼載入驅動的
  2. 以為mybatis配置檔案中的<property name="driver" value="com.mysql.cj.jdbc.Driver"/> 寫給哪個環境,哪個環境就使用這個驅動

現在是明白了

相關文章