服务提供者框架在jdbc中的应用

java数据库无缝切换原理及其设计思路

大家在使用jdbc连接数据库时应该都感觉到了,在java中切换一种数据库,通常只需要改变某个加载类和一些配置文件即可。其具体输入和实现的呢?在此我结合源码来分析一波。

jdbc连接oracle

        Connection connect = null;
        Statement statement = null;
        ResultSet resultSet = null;

        try {
            //第一步:注册驱动
            //第一种方式:类加载(常用)
            //Class.forName("oracle.jdbc.OracleDriver");

            //第二种方式:利用Driver对象
            Driver driver = new OracleDriver();
            DriverManager.deregisterDriver(driver);

            //第三种方式:利用系统参数  需在idea中配置program arguments为下面的参数
            //-Djdbc.drivers = oracle.jdbc.OracleDriver



            //第二步:获取连接
            //第一种方式:利用DriverManager(常用)
            //connect = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:XE", "你的oracle数据库用户名", "用户名密码");

            //第二种方式:直接使用Driver
            Properties pro = new Properties();
            pro.put("user", "你的oracle数据库用户名");
            pro.put("password", "用户名密码");
            connect = driver.connect("jdbc:oracle:thin:@localhost:1521:XE", pro);

jdbc连接mysql

      // MySQL 8.0 以下版本 - JDBC 驱动名及数据库 URL
    static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";  
    static final String DB_URL = "jdbc:mysql://localhost:3306/RUNOOB";
 
    // MySQL 8.0 以上版本 - JDBC 驱动名及数据库 URL
    //static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";  
    //static final String DB_URL = "jdbc:mysql://localhost:3306/RUNOOB?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC";
 
 
    // 数据库的用户名与密码,需要根据自己的设置
    static final String USER = "root";
    static final String PASS = "123456";
 
    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        try{
            // 注册 JDBC 驱动
            Class.forName(JDBC_DRIVER);
        
            // 打开链接
            System.out.println("连接数据库...");
            conn = DriverManager.getConnection(DB_URL,USER,PASS);
    }

2者之间的区别就是加载了不一样的驱动,各自驱动的具体实现由各自的生厂商决定,但由于java给了其具体实现定义了接口,所以当我们使用 Class.forName(“xxx”);来加载驱动类时,我们甚至可以不改变代买来实现无缝切换。但是其具体时如何实现的呢?

具体实现

Class.forName(JDBC_DRIVER) ,是加载类的几种方式之一,调用这个方法会将相应的类加载到我们的程序中。

可能此时你会疑惑因为调用了此方法后,我们只要使用conn = DriverManager.getConnection(DB_URL,USER,PASS);即可获得相应的数据库连接,这是如何做到的呢?

当我们执行完Class.forName(JDBC_DRIVER);时,这个类的类加载过程已经执行完毕,于是我们可以去看一看这个类里面有什么静态块,看在其中是否有什么玄机

源码如下

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());方法,相信从名字上你就能猜出,他将此driver注册到了DriverManager中,而你的connection也是从DriverManager中获取。继续往下走

    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

registeredDrivers 是一个能保证原子性队列。DriverManager.registerDriver(new Driver())也就是将其加入到了此队列中。

    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {

        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }

接下来看DriverManager.getConnection(DB_URL,USER,PASS)这一行代码,他干了啥

 public static Connection getConnection(String url,
        String user, String password) throws SQLException {
        java.util.Properties info = new java.util.Properties();

        if (user != null) {
            info.put("user", user);
        }
        if (password != null) {
            info.put("password", password);
        }

        return (getConnection(url, info, Reflection.getCallerClass()));
    }

Reflection.getCallerClass() 获得获得调用此方法的类。(参数 1 - 返回自己的类 2 - 返回调用者的类 \3. 4. ....层层上传 )

下面源码我解释部分关键性的代码,并省略了一些代码

    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
   		//获得调用类的类加载器
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;

        //遍历已经加载过的数据库驱动类
        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            //当callerCL和driver的类加载器是同一个时则继续往下走(即如果这个driver的类加载器和调用类的类加载器一致时,则返回这个driver的con)
            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());
            }

        }

    }
    private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
        boolean result = false;
        if(driver != null) {
            Class<?> aClass = null;
            try {
                aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
            } catch (Exception ex) {
                result = false;
            }

             result = ( aClass == driver.getClass() ) ? true : false;
        }

        return result;
    }

源码阅读到此你应该已经大概清楚了他是如何确立并返回对应的connection。在此我在梳理一下java提供的接口,在java.sql 的包下面你能看到 driver接口,Connection接口,Statement接口,这几个核心接口的实现类由对应的数据库厂商编写。java编写了DriverManager类,DriverInfo类

设计思路

java服务接口 Driver,让厂商编写对应的实现类,并在接口中定义了静态代码块,将其注册到DriverManager中去。

Connection,Statement接口同理也是让对应的厂商根据其厂商的内部逻辑来编写不一样的实现,但对于调用者来说,这一部分是屏蔽了的,也无需关心。

java中的DriverManager,提供了2个核心api

  1. registerDriver(Driver driver) , 提供者的注册API,将对应的对象加入其队列中
  2. getConnection(),服务访问API,其实就是一个工厂模式产生对象的工厂方法。在jdbc中通过类加载器进行判断,获得哪一种connection。

说到这你可能就会想到这就是服务提供者框架,因为其三大要素有意齐全。

  1. 服务接口(这是提供者实现)
  2. 提供者注册API(系统用于注册实现)
  3. 服务访问API(客户端用于获取服务实例)

以前跟人对工厂模式的理解为 他能创造单例,能造出子类对象,能根据不同参数生产出不同的对象,且在名字上看起来更加直观。但此次在源码阅读中我直观的感受到了其魅力,如果接口提供的好,通过注册可以在日后不需要修改代码的情况下进行功能的拓展。实现对拓展开放,修改关闭的设计原理

go中的应用

import (
    "database/sql"
    _ "github.com/lib/pq"              // enable support for Postgres
    _ "github.com/go-sql-driver/mysql" // enable support for MySQL
)

db, err = sql.Open("postgres", dbname) // OK
db, err = sql.Open("mysql", dbname)    // OK
db, err = sql.Open("sqlite3", dbname)  // returns error: unknown driver "sqlite3"