天天看点

JDBC规范三-Connection详解

作者:小雨在进步

一个Connection对象表示通过JDBC驱动与数据源建立的连接,这里的数据源可以是关系型数据库管理系统(DBMS)、文件系统或者其他通过JDBC驱动访问的数据。使用JDBC API的应用程序可能需要维护多个Connection对象,一个Connection对象可能访问多个数据源,也可能访问单个数据源。

从JDBC驱动的角度来看,Connection对象表示客户端会话,因此它需要一些相关的状态信息,例如用户Id、一组SQL语句和会话中使用的结果集以及事务隔离级别等信息。

我们可以通过两种方式获取JDBC中的Connection对象:

(1)通过JDBC API中提供的DriverManager类获取。

(2)通过DataSource接口的实现类获取。

使用DataSource的具体实现获取Connection对象是比较推荐的一种方式,因为它增强了应用程序的可移植性,使代码维护更加容易,并且使应用程序能够透明地使用连接池和处理分布式事务。几乎在所有的Java EE项目中都是使用DataSource的具体实现来维护应用程序和数据库连接的。目前使用比较广泛的数据库连接池C3P0、DBCP、Druid等都是javax.sql.DataSource接口的具体实现。

本节会详细介绍Connection接口相关的内容,例如JDBC驱动的类型、DriverManager类、Driver接口以及DataSource接口等。

1.JDBC驱动类型

JDBC驱动程序有很多可能的实现,这些驱动实现类型主要包括以下几种:

1.JDBC-ODBC Bridge Driver

SUN发布JDBC规范时,市场上可用的JDBC驱动程序并不多,但是已经逐渐成熟的ODBC方案使得通过ODBC驱动程序几乎可以连接所有类型的数据源。所以SUN发布了JDBC-ODBC的桥接驱动,利用现成的ODBC架构将JDBC调用转换为ODBC调用,避免了JDBC无驱动可用的窘境,如图所示。但是,由于桥接的限制,并非所有功能都能直接转换并正常调用,而多层调用转换对性能也有一定的影响,除非没有其他解决方案,否则不采用桥接架构。

JDBC规范三-Connection详解

2.Native API Driver

这类驱动程序会直接调用数据库提供的原生链接库或客户端,因为没有中间过程,访问速度通常表现良好,如图所示。但是驱动程序与数据库和平台绑定无法达到JDBC跨平台的基本目的。在JDBC规范中也是不被推荐的选择。

JDBC规范三-Connection详解

3.JDBC-Net Driver

这类驱动程序会将JDBC调用转换为独立于数据库的协议,然后通过特定的中间组件或服务器转换为数据库通信协议,主要目的是获得更好的架构灵活性,如图所示。例如,更换数据库时可以通过更换中间组件实现。数据库厂商开发的驱动通常还提供额外的功能,例如高级安全特性等,而且通过中间服务器转换会对性能有一定影响。JDBC领域这种类型驱动并不常见,而微软的ADO.NET是这种架构的典型。

JDBC规范三-Connection详解

2.java.sql.Driver接口

所有的JDBC驱动都必须实现Driver接口,而且实现类必须包含一个静态初始化代码块。我们知道,类的静态初始化代码块会在类初始化时调用,驱动实现类需要在静态初始化代码块中向DriverManager注册自己的一个实例,例如:

public static final JDBCDriver driverInstance = new JDBCDriver();

    static {
        try {
            DriverManager.registerDriver(driverInstance);
        } catch (Exception e) {
        }
    }           

当我们加载驱动实现类时,上面的静态初始化代码块就会被调用,向DriverManager中注册一个驱动类的实例。这就是为什么我们使用JDBC操作数据库时一般会先加载驱动,例如:

// 加载驱动
 Class.forName("org.hsqldb.jdbcDriver");           

为了确保驱动程序可以使用这种机制加载,Driver实现类需要提供一个无参数的构造方法。

DriverManager类与注册的驱动程序进行交互时会调用Driver接口中提供的方法。Driver接口中提供了一个acceptsURL()方法,DriverManager类可以通过Driver实现类的acceptsURL()来判断一个给定的URL是否能与数据库成功建立连接。当我们试图使用DriverManager与数据库建立连接时,会调用Driver接口中提供的connect()方法,具体如下:

Connection connect(String url, java.util.Properties info)
        throws SQLException;           

该方法有两个参数:第一个参数为驱动能够识别的URL;第二个参数为与数据库建立连接需要的额外参数,例如用户名、密码等。

当Driver实现类能够与数据库建立连接时,就会返回一个Connection对象,当Driver实现类无法识别URL时则会返回null。

注意在DriverManager类初始化时,会试图加载所有jdbc.drivers属性指定的驱动类,因此我们可以通过jdbc.drivers属性来加载驱动, Class.forName("org.hsqldb.jdbcDriver"); 这句可以不需要

JDBC 4.0以上的版本对DriverManager类的getConnection()方法做了增强,可以通过Java的SPI机制加载驱动。符合JDBC 4.0以上版本的驱动程序的JAR包中必须存在一个META-INF/services/java.sql.Driver文件,在java.sql.Driver文件中必须指定Driver接口的实现类。

3.Java SPI机制简介

在JDBC 4.0版本之前,使用DriverManager获取Connection对象之前都需要通过代码显式地加载驱动实现类,例如:

// 加载驱动
            Class.forName("org.hsqldb.jdbcDriver");           

JDBC 4.0之后的版本对此做了改进,我们不再需要显式地加载驱动实现类。这得益于Java中的SPI机制,本节我们就来简单地了解SPI机制。

SPI(Service Provider Interface)是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制。比如有一个接口,想在运行时动态地给它添加实现,只需要添加一个实现,SPI机制在程序运行时就会发现该实现类,整体流程如图所示。

JDBC规范三-Connection详解

当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services目录中创建一个以服务接口命名的文件,这个文件中的内容就是这个接口具体的实现类。当其他的程序需要这个服务的时候,就可以查找这个JAR包中META-INF/services目录的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名加载服务实现类,然后就可以使用该服务了。

JDK中查找服务实现的工具类是java.util.ServiceLoader。接下来我们看一下ServiceLoader类的使用,代码如下:

@Test
    public void testSPI() {
        ServiceLoader<Driver> drivers = ServiceLoader.load(java.sql.Driver.class);
        for (Driver driver : drivers ) {
            System.out.println(driver.getClass().getName());
        }
    }           

ServiceLoader类提供了一个静态的load()方法,用于加载指定接口的所有实现类。调用该方法后,classpath下META-INF/services目录的java.sql.Driver文件中指定的所有实现类都会被加载。

最后我们再来了解一下DriverManager加载驱动实现类的过程。符合JDBC 4.0以上版本的JDBC驱动都会在META-INF/services目录的java.sql.Driver文件中指定驱动实现类的完全限定名。

/**
     * 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");
    }
           

如上面的代码所示,DriverManager类的静态代码块会在我们调用DriverManager的getConnection()方法之前调用。静态代码块中调用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;
        }
        // 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);
            }
        }
    }           

如上面的代码所示,在loadInitialDrivers()方法中,通过JDK内置的ServiceLoader机制加载java.sql.Driver接口的实现类,然后对所有实现类进行遍历,这样就完成了驱动类的加载。驱动实现类会在自己的静态代码块中将驱动实现类的实例注册到DriverManager中,这样就取代了通过调用Class.forName()方法加载驱动的过程。

4.java.sql.DriverAction接口

前面我们了解到,Driver实现类在被加载时会调用DriverManager类的registerDriver()方法注册驱动。我们也可以在应用程序中显式地调用DriverManager类的deregisterDriver()方法来解除注册。JDBC驱动可以通过实现DriverAction接口来监听DriverManager类的deregisterDriver()方法的调用。

JDBC规范中不建议DriverAction接口的实现类在应用程序中被使用,因此DriverAction实现类通常会作为私有的内部类,从而避免被其他程序使用。

JDBC驱动的静态初始化代码块可以调用DriverManager.registerDriver(java.sql.Driver,java.sql.DriverAction)方法来确保DriverManager类的deregisterDriver()方法调用被监听

DriverAction用于监听驱动类被解除注册事件,是驱动提供者需要关注的范畴,作为JDBC的使用者,我们只需要了解即可

5.java.sql.DriverManager类

DriverManager类通过Driver接口为JDBC客户端管理一组可用的驱动实现,当客户端通过DriverManager类和数据库建立连接时,DriverManager类会根据getConnection()方法参数中的URL找到对应的驱动实现类,然后使用具体的驱动实现连接到对应的数据库。

DriverManager类提供了两个关键的静态方法:

registerDriver():该方法用于将驱动的实现类注册到DriverManager类中,这个方法会在驱动加载时隐式地调用,而且通常在每个驱动实现类的静态初始化代码块中调用。

getConnection():这个方法是提供给JDBC客户端调用的,可以接收一个JDBC URL作为参数,DriverManager类会对所有注册驱动进行遍历,调用Driver实现的connect()方法找到能够识别JDBC URL的驱动实现后,会与数据库建立连接,然后返回Connection对象。

JDBC URL的格式如下:jdbc:<subprotocol>:<subname>

subprotocol用于指定数据库连接机制由一个或者多个驱动程序提供支持,subname的内容取决于subprotocol。常用的数据库驱动程序的驱动实现类名及JDBC URL如下:

(1)Oracle驱动程序类名:

oracle.jdbc.driver.OracleDriver。

JDBC URL:jdbc: oracle:thin:@//<host>:<port>/ServiceName或jdbc:oracle:thin:@<host>:<port>:<SID>。

例如:jdbc:oracle:thin:@localhost:1521:orcl。

(2)MySQL驱动程序类名:com.mysql.jdbc.Driver。

JDBC URL:jdbc: mysql://<host>:<port>/<database_name>。

例如:jdbc:mysql://localhost/sample。

DriverManager类还提供了两个重载的getConnection方法:getConnection(String url):当数据库不需要用户名和密码时,我们可以调用该方法与数据库建立连接。getConnection(String url, java.util.Properties prop):如果建立数据库连接除了需要用户名、密码外,还需要一些额外的信息,我们可以使用Properties类来描述建立连接需要的所有配置信息。

6.javax.sql.DataSource接口

javax.sql.DataSource接口最早是由JDBC 2.0版本扩展包提供的,它是比较推荐的获取数据源连接的一种方式,JDBC驱动程序都会实现DataSource接口,通过DataSource实现类的实例,返回一个Connection接口的实现类的实例。

使用DataSource对象可以提高应用程序的可移植性。在应用程序中,可以通过逻辑名称来获取DataSource对象,而不用为特定的驱动指定特定的信息。我们可以使用JNDI(Java Naming and Directory Interface)把一个逻辑名称和数据源对象建立映射关系。

DataSource对象用于表示能够提供数据库连接的数据源对象。如果数据库相关的信息发生了变化,则可以简单地修改DataSource对象的属性来反映这种变化,而不用修改应用程序的任何代码。

DataSource接口可以被实现,提供如下两种功能:

通过连接池提高系统性能和伸缩性。

通过XADataSource接口支持分布式事务。

DataSource接口的实现必须包含一个无参构造方法。

JDBC API中定义了一组属性来表示和描述数据源实现。具体有哪些属性,取决于DataSource对象的类型,包括DataSource、ConnectionPoolDataSource和XADataSource。如下是DataSource所有标准属性及其描述。

JDBC规范三-Connection详解

DataSource属性遵循JavaBeans 1.01规范中对JavaBean组件属性的约定,可以在这些属性的基础上增加一些特定的属性扩展(这些扩展的属性不能与标准属性冲突)。DataSource实现类必须为支持的每个属性提供对应的Getter和Setter方法,而且这些属性需要在创建DataSource对象时初始化。

DataSource对象的属性不建议被JDBC客户端直接访问,可以通过增强DataSource实现类的属性访问方法来实现,而不是在应用程序中使用DataSource接口时控制。此外,客户端所操作的对象可以是实现了DataSource接口的包装类,它的属性对应的Setter和Getter方法不需要暴露给客户端。一些管理工具如果需要访问DataSource实现类的属性,则可以使用Java的内省机制。

7.使用JNDI API增强应用的可移植性

JNDI(Java Naming and Directory Interface,Java命名和目录接口)为应用程序提供了一种通过网络访问远程服务的方式。本节我们学习如何通过JNDI API注册和访问JDBC数据源对象。读者如果需要了解更多JNDI相关细节,则可参考JNDI规范文档。

JNDI API的命名服务可以把一个逻辑名称和一个具体的对象绑定。使用JNDI API,应用程序可以通过与DataSource对象绑定的逻辑名称来获取DataSource对象,这种方式在很大程度上提高了应用的可移植性,因为当DataSource对象的属性(例如端口号、服务器地址等)被修改时,不会影响JDBC客户端代码。实际上,当修改DataSource的配置,使它连接到其他数据库时,应用程序是没有任何感知的。

接下来我们就以一个实际的案例介绍如何使用JNDI API提供一个命名服务,然后使用JNDI API查找该命名服务,代码如下

@Before
    public void before() throws IOException {
        DataSourceFactory dsf = new UnpooledDataSourceFactory();
        Properties properties = new Properties();
        InputStream configStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("database.properties");
        properties.load(configStream);
        dsf.setProperties(properties);
        DataSource dataSource = dsf.getDataSource();
        try {
            Properties jndiProps = new Properties();
            jndiProps.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.naming.java.javaURLContextFactory");
            jndiProps.put(Context.URL_PKG_PREFIXES, "org.apache.naming");
            Context ctx = new InitialContext(jndiProps);
            ctx.bind("java:TestDC", dataSource);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void testJndi() {
        try {
            Properties jndiProps = new Properties();
            jndiProps.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.naming.java.javaURLContextFactory");
            jndiProps.put(Context.URL_PKG_PREFIXES, "org.apache.naming");
            Context ctx = new InitialContext(jndiProps);
            DataSource dataSource = (DataSource) ctx.lookup("java:TestDC");
            Connection conn = dataSource.getConnection();
            Assert.assertNotNull(conn);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }           

如上面的代码所示,在MyBatis源码中提供了javax.sql.DataSource接口的实现,分别为UnpooledDataSource和PooledDataSource类。UnpooledDataSource未实现连接池功能,而PooledDataSource则采用装饰器模式对UnpooledDataSource功能进行了增强,增加了连接池管理功能。

面的代码中,我们使用UnpooledDataSourceFactory创建了一个UnpooledDataSource实例,其中database.properties文件为数据源相关配置、然后创建一个javax.naming.InitialContext实例,调用该实例的bind()方法创建命名服务,命名服务创建完成后就可以通过javax.naming.InitialContext实例的lookup()方法来查找服务了。

需要注意的是,JDK中只提供了JNDI规范,具体的实现由不同的厂商来完成。这里我们使用的是Apache Tomcat中提供的JNDI实现,因此需要在项目中添加相关依赖,例如:

<dependency>
            <groupId>tomcat</groupId>
            <artifactId>naming-java</artifactId>
            <version>5.0.28</version>
        </dependency>
        <dependency>
            <groupId>tomcat</groupId>
            <artifactId>naming-common</artifactId>
            <version>5.0.28</version>
        </dependency>           

在实际的Java EE项目中,JNDI命名服务的创建通常由应用服务器来完成。在应用程序中,我们只需要查找命名服务并使用即可。例如,在Apache Tomcat服务器中,我们可以通过如下代码配置JNDI数据源:

JDBC规范三-Connection详解

8 关闭Connection对象

当我们使用完Connection对象后,需要显式地关闭该对象。Connection接口中提供了一个close()方法,用于关闭Connection对象;还提供了一个isClosed()方法,判断连接是否关闭;同时可以通过isValid()方法判断连接是否有效。

下面详细介绍这几个方法。java.sql.Connection#close():当应用程序使用完Connection对象后,应该显式地调用java.sql.Connection对象的close()方法。调用该方法后,由该Connection对象创建的所有Statement对象都会被关闭。一旦Connection对象关闭后,调用Connection的常用方法(例如createStatement()方法)将会抛出SQLException异常。

java.sql.Connection#isClosed():Connection接口中提供的isClosed()方法用于判断应用中是否调用了close()方法关闭该Connection对象,这个方法不能用于判断数据库连接是否有效。

有些JDBC驱动实现厂商对isClosed()方法做了增强,可以用它判断数据库连接是否有效。但是为了程序的可移植性,需要判断连接是否有效时还是建议使用isValid()方法。

java.sql.Connection#isValid():Connection接口提供的isValid()方法用于判断连接是否有效,如果连接依然有效,则返回true,否则返回false。当该方法返回false时,调用除了close()、isClosed()、isValid()以外的其他方法将会抛出SQLException异常。

继续阅读