Mybatis源码学习(13)-数据源模块

导读:本篇文章讲解 Mybatis源码学习(13)-数据源模块,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一、概述

  数据源是系统中数据持久层最重要的概念之一,甚至没有之一。数据源组件提供了系统访问数据库的能力。在Java中定义了数据源接口javax.sql.DataSource,常见的数据源组件一般都会实现该接口。

二、工厂模式简介

  因为在Mybatis的数据源模块的代码中,采用了工厂设计模式,工厂模式有几种变化形式,这里不做过多分析,只是简单回顾一下工厂模式的概念。UML图如下:
在这里插入图片描述
  通过上面的UML图可以看出,工厂模式主要包括了四类角色,分别是:

  • 工厂接口(Creator)
    工厂接口是工厂方法模式的核心接口,调用者会直接与工厂接口交互用于获取具体的产品实现类。
  • 具体工厂类(ConcreteCreator)
    具体工厂类是工厂接口的实现类,用于实例化产品对象,不同的具体工厂类会根据需求实例化不同的产品实现类。
  • 产品接口(Product)
    产品接口用于定义产品类的功能,具体工厂类产生的所有产品对象都必须实现该接口。调用者一般会面向产品接口进行编程,所以产品接口会与调用者直接交互,也是调用者最为关心的接口。
  • 具体产品类(ConcreteProduct)
    实现产品接口的实现类,具体产品类中定义了具体的业务逻辑。
三、类结构

  Mybatis的数据源模块的包目录:org.apache.ibatis.datasource。具体包结构如下图所示:
在这里插入图片描述
  其中,Mybatis数据源模块就是采用了工厂模式。各个类在工厂模式中扮演的角色如下:

  • DataSourceFactory 是工厂接口;
  • 然后有三类数据源分别对应的目录是unpooled、pooled、jndi,其中具体工厂实现类就在三个目录中实现,分别是UnpooledDataSourceFactory、PooledDataSourceFactory、JndiDataSourceFactory;
  • 对应的产品接口即javax.sql.DataSource
  • 具体的产品类也分别在这三个目录中实现,分别是UnpooledDataSource、PooledDataSource。其中,Jndi中没有具体的产品类。
四、UnpooledDataSource数据源

下面分析三类数据源的用法和三者之间的关系。

  1. 工厂接口 DataSourceFactory
    DataSourceFactory接口扮演着工厂模式中工厂接口的角色。其中定义了两个方法,代码如下所示:
public interface DataSourceFactory {

  /**
   * 设置属性,被XMLConfigBuilder所调用,是为了将配置文件中配置的数据源属性信息填充到DataSource中
   * @param props
   */
  void setProperties(Properties props);
  /**
   * 生产数据源,直接得到javax.sql.DataSource
   * @return
   */
  DataSource getDataSource();

}
  1. 具体工厂实现类 UnpooledDataSourceFactory
    在Mybatis中,数据源模块一共有三个雷具体工厂实现类,分别是UnpooledDataSourceFactory、PooledDataSourceFactory、JndiDataSourceFactory。

这里首先介绍最简单的UnpooledDataSourceFactory。

  • 字段
  //属性前缀
  private static final String DRIVER_PROPERTY_PREFIX = "driver.";
  //属性前缀的长度
  private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length();
  //数据源,需要由该工厂生产的数据源
  protected DataSource dataSource;
  //构造器,实例化一个非池型数据源UnpooledDataSource,这将用于供getDataSource()方法获取数据源实例
  public UnpooledDataSourceFactory() {
    this.dataSource = new UnpooledDataSource();
  }
  • 设置数据源驱动器属性 setProperties()方法
    在这个方法中主要处理<dataSource>元素下对应的<property>元素,其中<property>元素又分为两类,一类是name的命名以driver.为前缀命名的参数,一类是不以driver.前缀命名的参数。其中以driver.为前缀的参数,主要是为UnpooledDataSource对象的driverProperties字段赋值,最后通 过 DriverManager.getConnection(url,driverProperties)方法传递给数据库驱动。不以driver.为前缀的参数,主要是为UnpooledDataSource对象的其他字段赋值,比如:driver、url、username、password等。
<dataSource type="POOLED">
 <property name="driver" value="${driver}" />
    <property name="url" value="${url}" />
    <property name="username" value="${username}" />
    <property name="password" value="${password?:123456}" />
    <property name="driver.encoding" value="UTF8"/> 
</dataSource>
@Override
  public void setProperties(Properties properties) {
    Properties driverProperties = new Properties();
    MetaObject metaDataSource = SystemMetaObject.forObject(dataSource);
    for (Object key : properties.keySet()) {
      String propertyName = (String) key;
      if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) {//处理以driver.开头的属性名
        String value = properties.getProperty(propertyName);
        driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value);
      } else if (metaDataSource.hasSetter(propertyName)) {//元对象中拥有针对属性名的set设置方法
        String value = (String) properties.get(propertyName);
        Object convertedValue = convertValue(metaDataSource, propertyName, value);
        metaDataSource.setValue(propertyName, convertedValue);
      } else {
        throw new DataSourceException("Unknown DataSource property: " + propertyName);
      }
    }
    if (driverProperties.size() > 0) {
      metaDataSource.setValue("driverProperties", driverProperties);
    }
  }
  • getDataSource()方法
    该方法就实现了工厂接口中生成对象接口的方法,这里直接返回了由构造函数生成的UnpooledDataSource的实例对象dataSource。且该实例对象可以通过setProperties()设置其中的一些参数配置。
@Override
  public DataSource getDataSource() {
    return dataSource;
  }
  • convertValue()方法
    主要实现参数类型转换,供setProperties()方法使用。
private Object convertValue(MetaObject metaDataSource, String propertyName, String value) {
    Object convertedValue = value;
    Class<?> targetType = metaDataSource.getSetterType(propertyName);
    if (targetType == Integer.class || targetType == int.class) {
      convertedValue = Integer.valueOf(value);
    } else if (targetType == Long.class || targetType == long.class) {
      convertedValue = Long.valueOf(value);
    } else if (targetType == Boolean.class || targetType == boolean.class) {
      convertedValue = Boolean.valueOf(value);
    }
    return convertedValue;
  }
  1. 具体产品类 UnpooledDataSource
    在Mybatis的数据源模块中,工厂模式中对应的产品接口是javax.sql.DataSource,而UnpooledDataSource是具体的产品实现类。
public class UnpooledDataSource implements DataSource {
 //省略
}
  • 字段
    在UnpooledDataSource的字段中,分为了两类,其中一类是driverProperties,即这类属性在设置的时候键一般都会是以“driver.”为前缀,即在前面提到的,主要用来配置驱动器属性。剩下的就是另一类,每个字段的含有,在注解中已经表明就不在重复描述。
//这个是数据库的驱动类加载器,目的是从磁盘中加载数据库驱动类到内存
  private ClassLoader driverClassLoader;
  //驱动器属性,这个用于保存我们手动设置的数据库驱动器属性,如果我们不进行设置,则会使用默认值。这个属性设置的键一般都会是以“driver.”为前缀。
  private Properties driverProperties;
  //表示数据库驱动注册器,其内部保存着所有已注册的数据库驱动类实例,这个字段是static修饰的,表示在数据源类加载的时候就会创建,这个时候创建的其实是个空集合。再者使用ConcurrentHashMap集合,这是一个线程安全的键值对型集合,
  private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<String, Driver>();
  //数据库驱动类名
  private String driver;
  //数据库服务器URL地址
  private String url;
  //数据库服务器连接用户名
  private String username;
  //数据库服务器连接密码
  private String password;
  //是否自动提交,这是一个逻辑值,真表示使用自动提交,假表示关闭自动提交
  private Boolean autoCommit;
  //表示默认的事务级别
  private Integer defaultTransactionIsolationLevel;
  • 静态代码块,初始化驱动器。
    该静态代码块的作用就是为registeredDrivers集合赋值,保证在该类加载的时候就将默认的驱动器实例加载到静态集合中以备用
static {
	//DriverManager是JDK提供的驱动器管理类
	//这里调用DriverManager中getDrivers()方法,将会获取DriverManager中在集合registeredDrivers中的保存的驱动器实例,
	//在获取的时候会进行类加载验证,验证的目的是确保使用本类加载器获取的驱动器类与在registeredDrivers中保存的对应驱动类实例的类是
	//同一类型(==)。最后获取到的是驱动实例的枚举。
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) {
      Driver driver = drivers.nextElement();
      registeredDrivers.put(driver.getClass().getName(), driver);
    }
  }
  • 构造函数
    UnpooledDataSource提供了一系列的构造函数,主要用于实例化。
/**
   * 无参构造器,这个构造器在数据源工厂的无参构造器中被调用,用于创建一个空的UnpolledDataSource实例,
   * 然后使用工厂类中的setProperties()方法,为这个空实例中的各个字段进行赋值
   * 
   */
  public UnpooledDataSource() {
  }
  /**
   * 构造器
   */
  public UnpooledDataSource(String driver, String url, String username, String password) {
    this.driver = driver;
    this.url = url;
    this.username = username;
    this.password = password;
  }
  /**
   * 构造器
   */
  public UnpooledDataSource(String driver, String url, Properties driverProperties) {
    this.driver = driver;
    this.url = url;
    this.driverProperties = driverProperties;
  }
  /**
   * 构造器
   */
  public UnpooledDataSource(ClassLoader driverClassLoader, String driver, String url, String username, String password) {
    this.driverClassLoader = driverClassLoader;
    this.driver = driver;
    this.url = url;
    this.username = username;
    this.password = password;
  }
  /**
   * 构造器
   */
  public UnpooledDataSource(ClassLoader driverClassLoader, String driver, String url, Properties driverProperties) {
    this.driverClassLoader = driverClassLoader;
    this.driver = driver;
    this.url = url;
    this.driverProperties = driverProperties;
  }
  • 提供获取真正数据源的方法 getConnection()方法
    getConnection()方法是获取真正数据源的方法,其中是通过doGetConnection()方法实现真正数据源的实例化工作。下面分别介绍其中涉及到的方法。
/**
   * 获取数据源连接,通过调用方法doGetConnection()实现,该方法是真正执行数据库连接并获取这个连接的实现。
   */
  @Override
  public Connection getConnection() throws SQLException {
    return doGetConnection(username, password);
  }
  /**
   * 获取数据源连接,通过调用方法doGetConnection()实现,该方法是真正执行数据库连接并获取这个连接的实现。
   */
  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return doGetConnection(username, password);
  }

doGetConnection(String username, String password)方法主要是处理参数,真正的工作交给了doGetConnection(Properties properties)方法。

private Connection doGetConnection(String username, String password) throws SQLException {
    Properties props = new Properties();
    if (driverProperties != null) {
      props.putAll(driverProperties);
    }
    if (username != null) {
      props.setProperty("user", username);
    }
    if (password != null) {
      props.setProperty("password", password);
    }
    return doGetConnection(props);
  }

在doGetConnection(Properties properties)方法中,真正实现了java.sql.Connection对象的实例化工作。主要分为了三步:

  1. 第一步:通过 initializeDriver();完成了驱动器的初始化工作。
  2. 第二步:通过调用DriverManager的getConnection方法来获取数据库连接connection。其中参数包括了用户名、密码、数据库连接属性等。
  3. 调用configureConnection()方法进行数据库连接的配置,配置内容:自动提交(boolean值)与事务级别
 private Connection doGetConnection(Properties properties) throws SQLException {
	//驱动器初始化
    initializeDriver();
    //通过调用DriverManager的getConnection方法来获取数据库连接connection
    Connection connection = DriverManager.getConnection(url, properties);
    //调用configureConnection()方法进行数据库连接的配置,配置内容:自动提交(boolean值)与事务级别
    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);
        }
        // DriverManager requires the driver to be loaded via the system ClassLoader.
        // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html
        Driver driverInstance = (Driver)driverType.newInstance();
        //DriverProxy,是一个驱动代理,这个类是以静态代理类的方式定义的,其实现了Driver接口,实现了Driver接口中的所有抽象方法,
        //是一个真正的代理类,代理Driver真正的实现类,即真正起作用的驱动类实例,代理类将驱动类的所有方法全部保护起来
        DriverManager.registerDriver(new DriverProxy(driverInstance));
        registeredDrivers.put(driver, driverInstance);
      } catch (Exception e) {
        throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
      }
    }
  }

该方法主要进行数据库连接的配置,配置内容:自动提交(boolean值)与事务级别。

private void configureConnection(Connection conn) throws SQLException {
    if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
      conn.setAutoCommit(autoCommit);
    }
    if (defaultTransactionIsolationLevel != null) {
      conn.setTransactionIsolation(defaultTransactionIsolationLevel);
    }
  }
五、PooledDataSource 数据源

  数据库连接的创建过程是非常耗时的,数据库能够建立的连接数也非常有限,所以在大多数场景下,数据库连接是非常珍贵的资源,为了减少在创建数据库连接的过程中的消耗,就引入了数据库连接池的概念。PooledDataSource实现了简易数据库连接池的功能。其中为了实现数据库连接的池化,引入了两个两个对象,通过三个类实现了数据库连接的池化。分别是:

  • PooledConnection
    封装了真正的数据库连接对象( java.sql.Connection )以及其代理对象。
  • PoolState
    用于管理PooledConnection对象状态的。
  • PooledDataSource
    PooledDataSource 主要用来管理PooledConnection对象。
  1. PooledDataSourceFactory 具体工厂类
    PooledDataSourceFactory 具体 工厂类继承于UnpooledDataSourceFactory工厂类,两者基本没有区别,唯一的不同是在PooledDataSourceFactory 构造函数中生成的是PooledDataSource数据源实例。代码如下:
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
  /**
   * 构造器,初始化一个池化数据源的实例
   */
  public PooledDataSourceFactory() {
    this.dataSource = new PooledDataSource();
  }
}
  1. PooledConnection类
    封装了真正的数据库连接对象( java.sql.Connection )以及其代理对象。PooledConnection 继承了InvocationHandler接口,如下代码所示,即该类采用了JDK动态代理。
class PooledConnection implements InvocationHandler {
     //略
}
  • 字段
    其中,dataSource字段用来记录当前PooledConnection对象所在的PooledDataSource对象,该PooledConnection 是从该PooledDataSource中获取的。realConnection表示真正的数据库连接。proxyConnection表示使用JDK动态代理创建的代理真实连接。connectionTypeCode表示由数据库URL 、username、password计算出来的hash值,可用于标识该连接所在的连接池。其他字段,下面注释已经表明,不再分别描述。同时还提供了相应的setter/getter方法,比较简单,不再贴出代码了。
  //静态字符串常量,表示关闭
  private static final String CLOSE = "close";
  //静态数组常量,表示连接类型,这里使用到了多态的概念,Connection是所有连接的祖类,
  private static final Class<?>[] IFACES = new Class<?>[] { Connection.class };
  //数据库连接(真实连接)的HashCode值,默认为0,表示当真实连接不存在即为null时的值
  private final int hashCode;
  //池型数据源,为了方便调用其内部定义的部分方法来辅助完成池型连接的一些功能判断
  private final PooledDataSource dataSource;
  //真实的数据库连接,属于被代理的对象
  private final Connection realConnection;
  //使用JDK动态代理创建的代理真实连接的 
  private final Connection proxyConnection;
  //数据库连接被检出的时间戳,这个用于计算具体的检出时间
  private long checkoutTimestamp;
  //数据库连接被创建的时间戳,用于计算数据库连接被创建的时间
  private long createdTimestamp;
  //连接被最后使用的时间戳,用于计算数据库连接被最后使用的时间
  private long lastUsedTimestamp;
  //数据库连接的类型编码,格式为:url+username+password
  private int connectionTypeCode;
  //表示连接是否可用的逻辑值
  private boolean valid;
  • 构造函数
    有参构造函数,主要用来实例化对象,并初始化部分属性字段。
public PooledConnection(Connection connection, PooledDataSource dataSource) {
    this.hashCode = connection.hashCode();
    this.realConnection = connection;
    this.dataSource = dataSource;
    this.createdTimestamp = System.currentTimeMillis();
    this.lastUsedTimestamp = System.currentTimeMillis();
    this.valid = true;
    //创建代理连接实例
    this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
  }
  • hashCode()方法
    重写了hashCode()方法,主要是使用真正的数据库连接的hashCode作为该实例对象的hashCode,且该值已经在构造函数中进行了初始化。
 @Override
  public int hashCode() {
    return hashCode;
  }
  • equals()方法
    重写了equals()方法,主要用来把当前数据库连接与其他连接进行比较,验证是不是同一个连接,其实就是根据hashCode来判断是否是同一个连接。
@Override
  public boolean equals(Object obj) {
    if (obj instanceof PooledConnection) {//如果是PooledConnection类型,则比较真实连接是否一样
      return realConnection.hashCode() == ((PooledConnection) obj).realConnection.hashCode();
    } else if (obj instanceof Connection) {//如果是Connection,则比较连接的HashCode值
      return hashCode == obj.hashCode();
    } else {
      return false;
    }
  }
  • 是否失效invalidate()、checkConnection() 方法
    PooledConnection提供了一个设置失效的方法invalidate(),同时也提供了一个样式否是失效的方法checkConnection()。
public void invalidate() {
    valid = false;
  }
private void checkConnection() throws SQLException {
  if (!valid) {
    throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
  }
}`
  • invoke()方法
    InvocationHandler接口中定义的方法,是JDK动态代理中最核心的方法之一。
    这个方法主要做了两件事,
  1. 当对close()方法进行调用时,实际上是通过dataSource.pushConnection(this);方法,实现释放连接,把连接放回连接池中,而不是销毁连接。
  2. 当时不是close()方法的时候,交由真正的realConnection对象来执行该方法。
    其中,dataSource对象的pushConnection()方法会在后面详细介绍。
@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    //如果关闭当前连接,则当前连接会被重新添加到连接池中
    if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
      dataSource.pushConnection(this);
      return null;
    } else {//反之
      try {
        if (!Object.class.equals(method.getDeclaringClass())) {//判断这个方法是不是来自Object类
          // issue #579 toString() should never fail
          // throw an SQLException instead of a Runtime
          //除了toString()方法,其他方法调用之前要检查connection是否还是合法的,不合法要抛出SQLException
          checkConnection();
        }
        //其他的方法,则交给真正的connection去调用
        return method.invoke(realConnection, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }
  1. PoolState类
    用于管理PooledConnection对象状态的。该实例对象通过idleConnections(List<PooledConnection>)和activeConnections(List<PooledConnection>)两个集合,分别管理了空闲状态的链接和活动状态的链接。下面详细介绍该类。
  • 字段
    除了idleConnections和activeConnections两个字段,dataSource字段表示该状态管理的池化的数据源对象,其他还定义了一些系列用于统计的字段,各个字段的含有下面代码注释已经描述,不再重复。同时还提供了这些统计字段相应的setter/getter方法,比较简单,不再贴出代码了。
 //池化数据源
  protected PooledDataSource dataSource;
  //空闲的连接
  protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>();
  //活动的连接
  protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>();
  //请求次数
  protected long requestCount = 0;
  //总请求时间
  protected long accumulatedRequestTime = 0;
  //总检出时间
  protected long accumulatedCheckoutTime = 0;
  //过期连接数量
  protected long claimedOverdueConnectionCount = 0;
  //过期连接累计检出时间
  protected long accumulatedCheckoutTimeOfOverdueConnections = 0;
  //总等待时间
  protected long accumulatedWaitTime = 0;
  //要等待的次数
  protected long hadToWaitCount = 0;
  //坏的连接次数
  protected long badConnectionCount = 0;
  • 构造函数
    构造函数,主要是初始化实例对象,同时在构建实例对象的时候初始化了对应的池化数据源对象。
public PoolState(PooledDataSource dataSource) {
    this.dataSource = dataSource;
  }
  1. PooledDataSource类
    PooledDataSource主要用来管理PooledConnection对象。通过PoolState和UnpooledDataSource实例对象,实现了一个具有池化功能的数据源。其中,UnpooledDataSource非池化数据源,用来生成真实的数据库连接对象;PoolState用来管理数据池和池中连接的状态。下面分别介绍该类的组成部分:
  • 字段
    其中,通过PoolState和UnpooledDataSource实例对象,实现了一个具有池化功能的数据源。其他字段主要是用来定义池化数据源的一些默认配置信息或者数据源标识信息,下面代码注释已经很清楚了,不再重复描述。相关的getter/setter方法也比较简单,不再提出代码了。
  //连接池状态
  private final PoolState state = new PoolState(this);
  //非池化数据源,一个池型数据源就是一个非池型数据源加上一个连接池,也就是说,池型数据源是在非池型数据源基础上扩展而来
  private final UnpooledDataSource dataSource;

  // OPTIONAL CONFIGURATION FIELDS
  //连接池中最多可拥有的活动连接数
  protected int poolMaximumActiveConnections = 10;
  //连接池中最多可拥有的空闲连接数
  protected int poolMaximumIdleConnections = 5;
  //连接池最大检出时间,如果一个连接验证时间超过设定值,则将这个连接设置为过期(发生在推出连接时)
  protected int poolMaximumCheckoutTime = 20000;
  //池等待时间,当需要从池中获取一个连接时,如果空闲连接数量为0,而活动连接的数量也达到了最大值,
  //那么就针对那个最早取出的连接进行检查验证(checkout),如果验证成功(即在上面poolMaximumCheckoutTime限定的时间内验证通过),
  //说明这个连接还处于使用状态,这时取出操作暂停,线程等待限定时间,这个限定时间就是这个参数的使用位置。
  protected int poolTimeToWait = 20000;
  //这是一个关于坏连接容忍度的底层设置, 作用于每一个尝试从缓存池获取连接的线程. 如果这个线程获取到的是一个坏的连接,那么这个数据源允许这 
  //个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过 poolMaximumIdleConnections 与 poolMaximumLocalBadConnectionTolerance 之和。
  //默认值:3 (Since: 3.4.5)
  protected int poolMaximumLocalBadConnectionTolerance = 3;
  // 发送到数据库的侦测查询,用来检验连接是否处在正常工作秩序中并准备接受请求。默认是“NO PING QUERY SET”,
  //这会导致多数数据库驱动失败时带有一个恰当的错误消息。
  protected String poolPingQuery = "NO PING QUERY SET";
  //是否启用侦测查询。若开启,也必须使用一个可执行的 SQL 语句设置 poolPingQuery 属性(最好是一个非常快的 SQL),默认值:false。
  protected boolean poolPingEnabled;
  //配置 poolPingQuery 的使用频度。这可以被设置成匹配具体的数据库连接超时时间,来避免不必要的侦测,默认值:0
  //(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)。
  protected int poolPingConnectionsNotUsedFor;
  //连接的类型编码,这个类型编码在创建池型数据源实例的时候会被组装,他的组装需要从数据源中获取连接的url、username、password三个值,
  //将其按顺序组合在一起,这个类型编码可用于区别连接种类。
  private int expectedConnectionTypeCode;
  • 构造函数
    提供了多个构造函数,所有的构造函数都提供了构建对象、初始化dataSource、expectedConnectionTypeCode 等字段值的功能。其中expectedConnectionTypeCode 字段的赋值,是根据url、username、password三个参数,通过assembleConnectionTypeCode()方法,获取对应的hashCode值。
 public PooledDataSource() {
    dataSource = new UnpooledDataSource();
  }
 
  public PooledDataSource(UnpooledDataSource dataSource) {
    this.dataSource = dataSource;
  }

  public PooledDataSource(String driver, String url, String username, String password) {
    dataSource = new UnpooledDataSource(driver, url, username, password);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }

  public PooledDataSource(String driver, String url, Properties driverProperties) {
    dataSource = new UnpooledDataSource(driver, url, driverProperties);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }

  public PooledDataSource(ClassLoader driverClassLoader, String driver, String url, String username, String password) {
    dataSource = new UnpooledDataSource(driverClassLoader, driver, url, username, password);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }

  public PooledDataSource(ClassLoader driverClassLoader, String driver, String url, Properties driverProperties) {
    dataSource = new UnpooledDataSource(driverClassLoader, driver, url, driverProperties);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }
private int assembleConnectionTypeCode(String url, String username, String password) {
    return ("" + url + username + password).hashCode();
  }
  • getConnection()方法、popConnection()方法
    PooledDataSource类提供了两个getConnection()方法用于获取真正的数据源连接。底层其实是首先通过popConnection()方法实现获取取PooledConnection对象,然后通过PooledConnection.getProxyConnection ()方法获取数据库连接的代理对象proxyConnection。下面详细介绍popConnection()方法的实现:
  1. 这是个同步方法,线程安全。但是将synchronized锁同步放置到循环内部,而不是循环之外的原因是因为:如果将同步锁放置在循环之外,当多个线程执行到锁的位置,其中一个线程获得锁然后开始执行循环,如果发生问题导致无限循环,那么这个锁将是一直被这个线程所持有,导致其他线程永久处于等待锁的状态,程序无法执行下去。而将锁放置到循环内部,当多个线程来到锁之前,其中一个线程获得锁,执行循环内部代码,当执行完成一次循环,无论成功失败,都会释放锁,而其他线程就可以获得锁进而执行。
  2. 首先验证空闲连接集合是否为空(验证是否还有空闲连接备用),如果存在空闲连接,那么直接获取这个空闲连接,将这个连接从空闲连接集合中删除。
  3. 如果没有空闲连接,那么就验证活动连接集合中连接的数量是否达到最大值(poolMaximumActiveConnections),如果未达到最大值,这时,我们可以直接创建一个新的池型连接(需要一个真实连接于与一个池型数据源实例作为参数)
  4. 如果活动连接集合中的连接数目已经达到最大值(poolMaximumActiveConnections),那么就针对最早的那个活动连接(即在集合中排在0位的那个连接实例)进行验证。并获取其验证时间间隔值(该连接上一次记录验证时间戳到当前时间的间隔),将其与池连接的最大验证时限(poolMaximumCheckoutTime)进行比较,如果前者大,说明针对这个连接距上一次记录验证时间戳的时间超过了限定时限,这时将这个老连接从活动连接集合中删除,并新建一个池连接,还以老连接所代理的真实连接为真实连接(实际上就是创建一个新的代理),并将老的池连接设为无效。
  5. 如果验证时间与显示时间比较结果为验证时间小于限定时限(这个限定时限的设置需要根据项目实际情况来设置,或通过经验来设置,确保在这个时间之内连接的数据库操作执行完毕,不然贸然将连接关闭会导致原本的数据库操作失败),说明这个连接还可能处于使用状态,这时候只有等待一途,这里将线程设置等待限定秒数(poolTimeToWait),线程进入等待状态,那么就会释放同步锁,此时其他线程就能获得锁来进行执行。当前线程在等待N秒之后自动进入准备状态准备重新获得锁。
  6. 然后就获得的连接进行判断,如果连接不为空,那么验证连接是否可用(isValid),如果连接可用则设置连接类型编码,并记录验证时间戳(setCheckoutTimestamp)与最后一次使用时间戳(setLastUsedTimestamp),这两个时间戳可用于计算该连接的验证时间与最后一次使用时间,在前面会使用到这些值进行判断。再然后将该链接添加到活动连接集合中。
  7. 如果获取的连接为空,或者说没有获取到连接,则坏连接数加1,将连接置null,并验证坏连接数值,如果比当前空闲连接数量+3都大的话,那么就放弃获取连接,并抛出SqlException,(抛出异常也就意味着执行的终止,这个线程将不再执行循环操作)
@Override
  public Connection getConnection() throws SQLException {
    return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
  }
  
  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return popConnection(username, password).getProxyConnection();
  }
private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    int localBadConnectionCount = 0;
    //最外面是while死循环,如果一直拿不到connection,则不断尝试
    while (conn == null) {
      //这是个同步方法,线程安全。但是将synchronized锁同步放置到循环内部,而不是循环之外的原因是因为:
    	//如果将同步锁放置在循环之外,当多个线程执行到锁的位置,其中一个线程获得锁然后开始执行循环,如果发生问题导致无限循环,
    	//那么这个锁将是一直被这个线程所持有,导致其他线程永久处于等待锁的状态,程序无法执行下去。而将锁放置到循环内部,
    	//当多个线程来到锁之前,其中一个线程获得锁,
    	//执行循环内部代码,当执行完成一次循环,无论成功失败,都会释放锁,而其他线程就可以获得锁进而执行。
      synchronized (state) {
        if (!state.idleConnections.isEmpty()) {//如果有空闲的连接的话,删除空闲列表里第一个,并返回
          // Pool has available connection
          conn = state.idleConnections.remove(0);
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else {//如果没有空闲的连接
          // Pool does not have available connection
          //如果当前活动连接数小于最大活动连接,就创建一个池化连接
          if (state.activeConnections.size() < poolMaximumActiveConnections) {
            // Can create new connection
            conn = new PooledConnection(dataSource.getConnection(), this);
            if (log.isDebugEnabled()) {
              log.debug("Created connection " + conn.getRealHashCode() + ".");
            }
          } else {//如果当前活动连接数大于最大活动连接,暂时不能创建活动连接,需要等待
            // Cannot create new connection
        	//获取最早创建的活动连接
            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
            //获取该连接的检出时间
            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
            if (longestCheckoutTime > poolMaximumCheckoutTime) {//当该连接的检出时间大于设定的最大检出时间时
              // Can claim overdue connection
              //过期连接数加一
              state.claimedOverdueConnectionCount++;
              //累计过期连接检出时间添加当前连接的检出时间
              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
              //总检出时间添加当前连接的检出时间
              state.accumulatedCheckoutTime += longestCheckoutTime;
              //从活动连接队列中移除当前活动连接
              state.activeConnections.remove(oldestActiveConnection);
              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {//如果当前连接是非自动提交的配置,需要手动进行回滚。
                try {
                  oldestActiveConnection.getRealConnection().rollback();
                } catch (SQLException e) {
                  /*
                     Just log a message for debug and continue to execute the following
                     statement like nothing happend.
                     Wrap the bad connection with a new PooledConnection, this will help
                     to not intterupt current executing thread and give current thread a
                     chance to join the next competion for another valid/good database
                     connection. At the end of this loop, bad {@link @conn} will be set as null.
                   */
                  log.debug("Bad connection. Could not roll back");
                }  
              }
              //根据原有连接,创建一个新的连接
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
              //设置连接创建时间(使用原来连接的时间)
              conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
             //设置连接最后使用时间(使用原来连接的时间)
              conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
              //设置原来连接无效
              oldestActiveConnection.invalidate();
              if (log.isDebugEnabled()) {
                log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
              }
            } else {//当该连接的检出时间小于设定的最大检出时间时,就必须等待
              // Must wait
              try {
                if (!countedWait) {
                  //等待连接数加一
                  state.hadToWaitCount++;
                  //表示当前连接已经在等待中
                  countedWait = true;
                }
                if (log.isDebugEnabled()) {
                  log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
                }
                long wt = System.currentTimeMillis();
                state.wait(poolTimeToWait);
                //总等待时间累加
                state.accumulatedWaitTime += System.currentTimeMillis() - wt;
              } catch (InterruptedException e) {
                break;
              }
            }
          }
        }
        if (conn != null) {
          // ping to server and check the connection is valid or not
          if (conn.isValid()) {//连接有效时
            if (!conn.getRealConnection().getAutoCommit()) {
              //连接是非自动提交时,把连接进行回滚操作
              conn.getRealConnection().rollback();
            }
            //设置编码类型
            conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
            //设置连接检出时间
            conn.setCheckoutTimestamp(System.currentTimeMillis());
            //设置连接最后使用实际
            conn.setLastUsedTimestamp(System.currentTimeMillis());
            state.activeConnections.add(conn);
            //请求次数加一
            state.requestCount++;
            //请求总时间累加
            state.accumulatedRequestTime += System.currentTimeMillis() - t;
          } else {//连接无效时
            if (log.isDebugEnabled()) {
              log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
            }
            //坏连接数加一
            state.badConnectionCount++;
            localBadConnectionCount++;
            conn = null;
            //当坏连接次数大于 空闲连接数和坏连接容忍设置数时,抛出异常
            if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
              if (log.isDebugEnabled()) {
                log.debug("PooledDataSource: Could not get a good connection to the database.");
              }
              throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
            }
          }
        }
      }

    }

    if (conn == null) {//当获取不到连接时,抛出异常。
      if (log.isDebugEnabled()) {
        log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
      }
      throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    }

    return conn;
  }
  • forceCloseAll()方法
    关闭所有的activeConnections和idleConnections。 使用场景:连接是在数据源完全设置完整的情况下才生成的,数据源就是连接生成的基础,当我们要修改数据源的基础属性的时候,原有设置上产生的连接必定不再适合新的设置,需要全部推倒重来。
public void forceCloseAll() {
    synchronized (state) {
      expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
      for (int i = state.activeConnections.size(); i > 0; i--) {//关闭所有活动的连接
        try {
          PooledConnection conn = state.activeConnections.remove(i - 1);
          conn.invalidate();

          Connection realConn = conn.getRealConnection();
          if (!realConn.getAutoCommit()) {
            realConn.rollback();
          }
          realConn.close();
        } catch (Exception e) {
          // ignore
        }
      }
      for (int i = state.idleConnections.size(); i > 0; i--) {//关闭所有空闲的连接
        try {
          PooledConnection conn = state.idleConnections.remove(i - 1);
          conn.invalidate();

          Connection realConn = conn.getRealConnection();
          if (!realConn.getAutoCommit()) {
            realConn.rollback();
          }
          realConn.close();
        } catch (Exception e) {
          // ignore
        }
      }
    }
    if (log.isDebugEnabled()) {
      log.debug("PooledDataSource forcefully closed/removed all connections.");
    }
  }
  • pushConnection()方法
  1. 这个方法同样是一个同步方法,拥有同步锁,以池状态实例为锁。
  2. 首先我们将当前要推入的连接实例从活动连接中删除,表示其不再处于使用状态。
  3. 然后对连接额可用性进行(valid)判断,如果还处于可用状态,则验证空闲连接集合中的空闲连接数量是否小于设置的限定值(poolMaximumIdleConnections)和当前连接实例的类型编码是否与当前池型数据源中的连接类型编码一致,如果上面两点都满足,则进行下一步:
  4. 新建一个池型连接实例并将其添加到空闲连接集合中,这个池型连接实例是以之前要推入的连接为基础重新创建的,也就是说是针对那个要推入的池型连接的真实连接重新创建一个池型连接代理(只改变外包装,实质不改变),并将原池型连接的时间戳设置统统设置到新的连接中,保持连接的持续性,然后将原池型连接置为无效。
  5. 然后唤醒所有沉睡线程notifyAll()。
  6. 如果第(3)点中的判断中有一个不成立(空闲连接数量达到最大值或者连接的类型编码不一致)那么直接将该连接的真实连接关闭,池连接置为无效即可。
protected void pushConnection(PooledConnection conn) throws SQLException {

    synchronized (state) {
      //活动连接队列中移除该连接
      state.activeConnections.remove(conn);
      if (conn.isValid()) {//连接有效时
    	//当前空闲连接队列中连接数量小于最大空闲连接数且连接的类型编码符合要求
        if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
          //累计连接检出时间增加
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
          if (!conn.getRealConnection().getAutoCommit()) {//回滚连接
            conn.getRealConnection().rollback();
          }
          //重新欢迎该连接,并添加到空闲连接队列中
          PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
          state.idleConnections.add(newConn);
          newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
          newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
          conn.invalidate();
          if (log.isDebugEnabled()) {
            log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
          }
          state.notifyAll();
        } else {//其他情况,该连接需要真正的被关闭
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
          if (!conn.getRealConnection().getAutoCommit()) {//回滚
            conn.getRealConnection().rollback();
          }
          //真正的关闭连接
          conn.getRealConnection().close();
          if (log.isDebugEnabled()) {
            log.debug("Closed connection " + conn.getRealHashCode() + ".");
          }
          conn.invalidate();
        }
      } else {//无效连接时,抛出异常,坏连接数加一
        if (log.isDebugEnabled()) {
          log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
        }
        state.badConnectionCount++;
      }
    }
  }
  • pingConnection()方法
    检查连接是否可用。该方法会被池连接PooledConnection类中的isValid()方法调用,用于判断一个连接是否还可用。这个方法就是真正用于判断连接可用与否的功能性方法。
  1. 首先创建一个局部变量result用于保存判断结果,默认为true
  2. 然后将当前池型连接包裹的真实连接的开闭状态值的非值赋值给result(当真实连接处于关闭状态时,result值为false,当真实连接处于开启状态时,result值为true),如果赋值过程出现了异常,则直接将result置false
  3. 判断result的值,如果result值为true,则判断poolPingEnabled的值,这是侦测查询的开关,如果这个值为true,表示开启侦测查询,那么就可以执行以下内容。
  4. 判断poolPingConnectionsNotUsedFor的值是否大于等于0(这个判断的意思是判断是否设置了正确的poolPingConnectionsNotUsedFor值),并且判断该连接的自最后一次使用以来的时间间隔是否大于设定的poolPingConnectionsNotUsedFor值(验证该连接是否到了需要进行侦测查询的时间,如果小于设置时间则不进行侦测查询)
  5. 如果上述条件均满足,则进行一次侦测查询,这个侦测查询就是针对这个连接的一个测试查询,看看整个查询过程是否通畅,若通畅(没有任何异常出现),则将result置为true,一旦测试过程出现了异常,则将该连接的真实连接关闭,并将result置为false
protected boolean pingConnection(PooledConnection conn) {
	//连接是否可用的标识
    boolean result = true;

    try {
      //连接是否被关闭,关闭则表示连接不可用,否则,可用
      result = !conn.getRealConnection().isClosed();
    } catch (SQLException e) {//发送异常,说明不可用
      if (log.isDebugEnabled()) {
        log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
      }
      result = false;
    }

    if (result) {
      if (poolPingEnabled) {//如果开启连接可用侦查
    	//侦探检查评率在范围内时
        if (poolPingConnectionsNotUsedFor >= 0 && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
          try {
            if (log.isDebugEnabled()) {
              log.debug("Testing connection " + conn.getRealHashCode() + " ...");
            }
            //构建进行侦探的sql语句,如果测试正常,说明连接可用
            Connection realConn = conn.getRealConnection();
            Statement statement = realConn.createStatement();
            ResultSet rs = statement.executeQuery(poolPingQuery);
            rs.close();
            statement.close();
            if (!realConn.getAutoCommit()) {
              realConn.rollback();
            }
            result = true;
            if (log.isDebugEnabled()) {
              log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");
            }
          } catch (Exception e) {//测试连接异常时,说明连接不可用
            log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage());
            try {
              conn.getRealConnection().close();
            } catch (Exception e2) {
              //ignore
            }
            result = false;
            if (log.isDebugEnabled()) {
              log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
            }
          }
        }
      }
    }
    return result;
  }

六、JNDI 数据源

Jndi数据源和前面两个不太一样,JNDI数据源是托管型数据源,这个数据源的实现是为了使用如 Spring 或应用服务器这类的容器, 容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的引用。从代码结构上来说,没有单独的产品实现类。其实是因为,在Jndi数据源中,该数据源在JndiDataSourceFactory的setProperties()方法中,通过加载应用上下文,然后直接生产了对应数据源,不需要在单独定义数据源类。代码如下所示:

public class JndiDataSourceFactory implements DataSourceFactory {

  public static final String INITIAL_CONTEXT = "initial_context";
  public static final String DATA_SOURCE = "data_source";
  //和其他数据源配置相似, 它也可以通过名为 “env.” 的前缀直接向初始上下文发送属性。 比如:
  //env.encoding=UTF8
  public static final String ENV_PREFIX = "env.";

  private DataSource dataSource;

  @Override
  public void setProperties(Properties properties) {
    try {
      InitialContext initCtx;
      Properties env = getEnvProperties(properties);
      if (env == null) {
        initCtx = new InitialContext();
      } else {
        initCtx = new InitialContext(env);
      }

      if (properties.containsKey(INITIAL_CONTEXT)
          && properties.containsKey(DATA_SOURCE)) {
        Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT));
        dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE));
      } else if (properties.containsKey(DATA_SOURCE)) {
        dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
      }

    } catch (NamingException e) {
      throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e);
    }
  }

  @Override
  public DataSource getDataSource() {
    return dataSource;
  }

  private static Properties getEnvProperties(Properties allProps) {
    final String PREFIX = ENV_PREFIX;
    Properties contextProperties = null;
    for (Entry<Object, Object> entry : allProps.entrySet()) {
      String key = (String) entry.getKey();
      String value = (String) entry.getValue();
      if (key.startsWith(PREFIX)) {
        if (contextProperties == null) {
          contextProperties = new Properties();
        }
        contextProperties.put(key.substring(PREFIX.length()), value);
      }
    }
    return contextProperties;
  }

}

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68899.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!