转载

彻底搞明白JAVA中JDBC连接

一开始我们在学习JDBC的时候,老师就教我们了以下几步来建立JDBC连接.

public static void main(String[] args) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/springboot", "root", "redhat");
        PreparedStatement statement = connection.prepareStatement("select * from tag");
        ResultSet resultSet = statement.executeQuery();
        while (resultSet.next()) {
            //do something
        }
 
    }
  1. Class.forName到底干了什么

首先Class.forName来加载注册我们的mysql驱动,执行完这个Class.forName, 到底我们的类里,哪些方法会被执行呢.我们写个类来测试一下,测试类如下

package org.linuxsogood.boot.jdbc;
public class TestInit {
    static {
        System.out.println("load me");
    }
 
    {
        System.out.println("nomal load me");
    }
 
    public TestInit() {
        System.out.println("construct method is execute");
    }
}

最后我们使用Class.forName(“org.linuxsogood.boot.jdbc.TestInit”)测试的结果是,只有静态代码块里的代码被执行了. 

我们再看一下myql的Driver类里是怎么写的

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
   
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
 
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

mysql的Driver类,其实还是调用了DriverManager的注册驱动方法,new了一个自己的类.仅此. 所以我们在使用JDBC连接MySQL的时候,可以完全不用Class.forName, 我们可以直接使用DriverManager的registerDriver方法手动来注册,测试下来,结果也确实是一样的.

然后我们再重点关注一下DriverManager类

2. 为什么我不写Class.forName, 直接使用DriverManager.getConnection也可以获取MySQL连接?

我测试着把Class.forName给注释掉,发现程序依然能正常运行,这和我们初学的时候,老师教我们的,好像有点违背,不是说一定要按这个步骤吗? 太奇怪了.那下面我们就重点关注一下DriverManager这个类,到底是在搞什么飞机.

首先看一下源码上的注释

 * <P>As part of its initialization, the <code>DriverManager</code> class will
 * attempt to load the driver classes referenced in the "jdbc.drivers"
 * system property. This allows a user to customize the JDBC Drivers
 * used by their applications. For example in your
 * ~/.hotjava/properties file you might specify:
 * <pre>
 * <CODE>jdbc.drivers=foo.bah.Driver:wombat.sql.Driver:bad.taste.ourDriver</CODE>
 * </pre>
 *<P> The <code>DriverManager</code> methods <code>getConnection</code> and
 * <code>getDrivers</code> have been enhanced to support the Java Standard Edition
 * <a href="../../../technotes/guides/jar/jar.html#Service%20Provider">Service Provider</a> mechanism. JDBC 4.0 Drivers must
 * include the file <code>META-INF/services/java.sql.Driver</code>. This file contains the name of the JDBC drivers
 * implementation of <code>java.sql.Driver</code>.  For example, to load the <code>my.sql.Driver</code> class,
 * the <code>META-INF/services/java.sql.Driver</code> file would contain the entry:
 * <pre>
 * <code>my.sql.Driver</code>
 * </pre>
 * <P>Applications no longer need to explicitly load JDBC drivers using <code>Class.forName()</code>. Existing programs
 * which currently load JDBC drivers using <code>Class.forName()</code> will continue to work without
 * modification.
 *
 * <P>When the method <code>getConnection</code> is called,
 * the <code>DriverManager</code> will attempt to
 * locate a suitable driver from amongst those loaded at
 * initialization and those loaded explicitly using the same classloader
 * as the current applet or application.
 *

重点读一下以上说明,大概意思是说:

DriverManager会先读取系统环境变量jdbc.drivers属性, 我们可以通过设置这个属性的值,来达到让DriverManager来帮我们注册数据库驱动的目的,如果有多个数据库驱动,多个之间使用:号分隔

DriverManager的getConnection方法做了增强, 自从JDBC 4.0开始,就不需要再使用Class.forName的方式来注册数据库驱动了, 使用的是一java里面的Service Loader的机制, Service Loader机制要求在classpath下面有一个META-INF/services的目录,并且在这个目录下,建立一个接口全路径名的文件,比如java.sql.Driver的文件,内容写上

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

这样在使用ServiceLoader.load(Driver.class)的时候,文件里面这两个类,会被系统加载,并且延迟执行.

和Class.forName不同的就是这个是延迟执行的,只有当你把这个类取出来的时候,里面的静态方法,才会被执行.

我们继续看getConnection方法,到底是怎么搞的

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 + "/")");
 
       SQLException reason = null;
 
        for(DriverInfo aDriver : registeredDrivers) {
          
            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 (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");
    }

我们发现他是从registeredDrivers这个集合中去找对应的驱动.是根据你数据库连接的URL来找的.那这个集合是啥时候被初始化的呢.我们代码只写了一个getConnection方法,别的没写,应该不会跑出DriverManager这个类,当我们调用这个类的方法的时候,我们知道,静态代码块应该会被调用,看一下这个类的静态代码块.

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

跟踪loadInitialDrivers方法

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;
        }
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
 
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                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, 其次我们看到了一个ServiceLoader.load方法,load了Driver.class类. 这个就是前面注释上讲过的ServiceLoader, 它会将实现Driver接口的classPath中找META-INF/service中的java.sql.Driver文件中的驱动,并且加载他们.为什么是META-INF/service这个路径,这是因为ServiceLoader的实现机制就是这样的,去在这样一个路径下寻找你的接口的全路径名的文件,并加载其中的每一行. 因为java对JDBC的驱动都必须实现java.sql.Driver接口,所以就是找这个文件.然后依次循环. 如果没有后面的循环,我们的类只是被加载进内存,并不会触发里面的静态代码块的方法.这是因为ServiceLoader的懒加载机制.

3.模拟java.sql.Driver自己写一个使用ServiceLoader的例子

首先定义一个接口

package org.linuxsogood.boot.serviceloader;
 
public interface IService {
 
    String sayHello();
 
    String getSchema();
}

然后定义实现类

package org.linuxsogood.boot.serviceloader;
 
public class HDFSServiceImpl implements IService {
    static {
        System.out.println("load HDFSServiceImpl");
    }
    @Override
    public String sayHello() {
        return "hello hdfs";
    }
 
    @Override
    public String getSchema() {
        return "hdfs";
    }
}

再定义一个

package org.linuxsogood.boot.serviceloader;
 
public class NTFSServiceImpl implements IService {
    @Override
    public String sayHello() {
        return "hello ntfs";
    }
 
    @Override
    public String getSchema() {
        return "ntfs";
    }
}

在classpath目录下,我这里是maven项目,就在resources目录下,创建META-INF/services目录

然后建立一个org.linuxsogood.boot.serviceloader.IService名字的文件,里面写入以下两行

org.linuxsogood.boot.serviceloader.HDFSServiceImpl
org.linuxsogood.boot.serviceloader.NTFSServiceImpl

写一个测试类来测试

package org.linuxsogood.boot.serviceloader;
 
import java.util.Iterator;
import java.util.ServiceLoader;
 
public class TestServiceLoader {
    public static void main(String[] args) {
        ServiceLoader<IService> load = ServiceLoader.load(IService.class);
        Iterator<IService> iterator = load.iterator();
        while (iterator.hasNext()) {
            iterator.next();
        }
    }
}

我们发现,HDFSServiceImpl里面的静态代码块被执行了. 如果云掉下面的遍历操作,加载能成功,但是静态代码块不会被调用.这也就是为什么,MySQL的Driver里面,初始化要在静态代码块里面来做了

原文  http://linuxsogood.org/1638.html
正文到此结束
Loading...