做过javaweb开发的同学,对mysql的使用一定不陌生。今天,我们来聊一聊jdbc连接mysql。

mysql的驱动注册

学习jdbc时,网上有一大堆例子,教你如何创建连接。大部分例子如下所示:

Connection conn = null;

String sql;

// MySQL的JDBC URL编写方式:jdbc:mysql://主机名称:连接端口/数据库的名称?参数=值

// 避免中文乱码要指定useUnicode和characterEncoding

String url = "jdbc:mysql://localhost:3306/test?"

        + "user=root&password=root&useUnicode=true&characterEncoding=UTF8";

try {

    // 之所以要使用下面这条语句,是因为要使用MySQL的驱动,所以我们要把它驱动起来,

    // 可以通过Class.forName把它加载进去,也可以通过初始化来驱动起来,下面三种形式都可以

    Class.forName("com.mysql.jdbc.Driver");// 动态加载mysql驱动

    // or:

    // com.mysql.jdbc.Driver driver = new com.mysql.jdbc.Driver();

    // or:

    // new com.mysql.jdbc.Driver();

    System.out.println("成功加载MySQL驱动程序");

    // 一个Connection代表一个数据库连接

    conn = DriverManager.getConnection(url);

    // Statement里面带有很多方法,比如executeUpdate可以实现插入,更新和删除等

    Statement stmt = conn.createStatement();

    // Do something

    }

} catch (SQLException e) {

    System.out.println("MySQL操作错误");

    e.printStackTrace();

} catch (Exception e) {

    e.printStackTrace();

} finally {

    try {
        conn.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }

}

上边的例子中,我们需要加载mysql驱动。Class.forName("com.mysql.jdbc.Driver")。这是为什么呢?让我们看看com.mysql.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!");
        }
    }
}

在代码中,我们发现有一个静态代码块,字面意思就是向驱动管理器注册mysql驱动。猜想一下,也就是只有向驱动管理器注册的驱动才能被我们使用。那么,我们再看看驱动管理器做了什么事。

private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

public static synchronized void registerDriver(java.sql.Driver driver)
    throws SQLException {
     registerDriver(driver, null);
}

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);
}

很简单吧,就是如果CopyOnWriteArrayList写时复制列表中没有该驱动,就向其中添加添加驱动。

将jdbc的连接设计成驱动管理的好处是什么呢?我理解就是能够统一管理驱动,避免在一个项目中创建多个同样的驱动。那为什么要设置成在项目启动时动态加载驱动呢?原因有两个:

  1. 项目的测试环境和线上环境有一定差别,线上的驱动可能是在运行环境的classpath中的,如tomcat/lib。
  2. jdbc是java的一种规范,通俗一点说就是JDK在java.sql.*下提供了一系列的接口,但没有提供任何实现。对于在代码中写死Class.forName("com.mysql.jdbc.Driver")也是没有解耦的,以后我们想换成别的驱动也不太方便,一般对实现类的加载都是写在properties中,如果需要更换驱动,我们只需要修改properties文件,重启项目就可以了。

获得Connection连接

如果你了解了驱动的注册,那Connection的获取就更简单了。让我们看看源码吧!

public static Connection getConnection(String url,
    java.util.Properties info) throws SQLException {
     return (getConnection(url, info, Reflection.getCallerClass()));
}


private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
    /*
     * When callerCl is null, we should check the application's
     * (which is invoking this class indirectly)
     * classloader, so that the JDBC driver class outside rt.jar
     * can be loaded from here.
     */
    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.
        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());
        }

    }

    // if we got here nobody could connect.
    if (reason != null)    {
        println("getConnection failed: " + reason);
        throw reason;
    }

    println("getConnection: no suitable driver found for "+ url);
    throw new SQLException("No suitable driver found for "+ url, "08001");
}

因为驱动都在CopyOnWriteArrayList写时复制列表,我们只要遍历列表中的驱动,只要其中一个驱动能够给我们连接,那我们就得到了连接,如果都没得到连接,那就要报错了。

至于驱动的实现是如何判断能否给出连接,那就凭各自驱动的本事了,什么意思呢?在上边的代码中,我们看到给驱动的有效参数就两个,一个是url,一个是info。驱动的实现需要靠这两个参数决定是否生成连接。

我们来看看mysql的实现吧!

//com.mysql.jdbc.NonRegisteringDriver
Properties props = null;
if ((props = this.parseURL(url, info)) == null) {
    return null;
} else {
    try {
        com.mysql.jdbc.Connection newConn = ConnectionImpl.getInstance(this.host(props), this.port(props), props, this.database(props), url);
        return newConn;
    } catch (SQLException var6) {
        throw var6;
    } catch (Exception var7) {
        SQLException sqlEx = SQLError.createSQLException(Messages.getString("NonRegisteringDriver.17") + var7.toString() + Messages.getString("NonRegisteringDriver.18"), "08001");
        sqlEx.initCause(var7);
        throw sqlEx;
    }
}

我们看到,mysql的实现中,会先解析url和info,生成props,如果没有生成,那说明给定的url不能被mysql的驱动解析。

再谈驱动注册

我们在第一节中说到,必须要注册驱动Class.forName("com.mysql.jdbc.Driver"),笔者这里展示两种不用在代码里注册驱动也能正常加载驱动的方法。

  1. 在启动项目时指定虚拟机参数-Djdbc.drivers=com.mysql.jdbc.Driver
  2. 使用mysql-connector-java版本>=5.1.6的jar包。

不信可以试一试。

为什么这样也可以正常加载驱动呢?再来看看驱动管理器和mysql-connector-java的jar吧

// 驱动管理器

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    // If the driver is packaged as a Service Provider, load it.
    // Get all the drivers through the classloader
    // exposed as a java.sql.Driver.class service.
    // ServiceLoader.load() replaces the sun.misc.Providers()

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            /* Load these drivers, so that they can be instantiated.
             * It may be the case that the driver class may not be there
             * i.e. there may be a packaged driver with the service class
             * as implementation of java.sql.Driver but the actual class
             * may be missing. In that case a java.util.ServiceConfigurationError
             * will be thrown at runtime by the VM trying to locate
             * and load the service.
             *
             * Adding a try catch block to catch those runtime errors
             * if driver not available in classpath but it's
             * packaged as service and that service is there in classpath.
             */
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

驱动管理器中提供了两种默认加载驱动的方法,一个就是通过虚拟机参数jdbc.drivers来加载驱动。另一个是通过java的spi接口加载驱动。第一种方法比较简单就不用探讨了,第二种方法我们来具体了解一下。

SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。有很多开源框架都用到了这种服务或者是这种服务的自定义扩展。它允许在JAR包的"src/META-INF/services/"目录下建立一个文件,文件名是接口的全限定名,文件的内容可以有多行,每行都是该接口对应的具体实现类的全限定名。

再来看看我们的jar包,在版本大于等于5.1.6的jar包中,有这么一个文件:mysql-connector-java\5.1.6\mysql-connector-java-5.1.6.jar!\META-INF\services\java.sql.Driver。在这个文件中,就是Driver接口实现类的全限定名com.mysql.jdbc.Driver

关于SPI,笔者还会专门写一篇文章来介绍,欢迎大家来信探讨。

标签: java, Mysql, SPI, 源码阅读

添加新评论