【Mybatis】【数据源】【一】Mybatis源码解析-内置数据源

1  前言

这节我们要看一下数据源的东西了。比如我们以前在XML配置的什么驱动、url、账号密码啥的以及现在我们在SpringBoot下配置的其实都是为了创建我们的数据源,那么这节我们来看下数据源的基础知识和创建过程。

官方关于数据源的讲解:https://mybatis.org/mybatis-3/zh/configuration.html

2  数据源

Mybatis 有三种内建的数据源类型(就是 type=[UNPOOLED|POOLED|JNDI]),我们大概了解下每种数据源概念以及常用的配置。

2.1  UNPOOLED

这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。 性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形。UNPOOLED 类型的数据源仅仅需要配置以下属性:

  • driver – 这是 JDBC 驱动的 Java 类全限定名(并不是 JDBC 驱动中可能包含的数据源类)。
  • url – 这是数据库的 JDBC URL 地址。
  • username – 登录数据库的用户名。
  • password – 登录数据库的密码。
  • defaultTransactionIsolationLevel – 默认的连接事务隔离级别。
  • defaultNetworkTimeout – 等待数据库操作完成的默认网络超时时间(单位:毫秒)。查看 java.sql.Connection#setNetworkTimeout() 的 API 文档以获取更多信息。

作为可选项,你也可以传递属性给数据库驱动。只需在属性名加上“driver.”前缀即可,例如:

  • driver.encoding=UTF8

这将通过 DriverManager.getConnection(url, driverProperties) 方法传递值为 UTF8 的 encoding 属性给数据库驱动。

2.2  POOLED

这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求。

除了上述提到 UNPOOLED 下的属性外,还有更多属性用来配置 POOLED 的数据源:

  • poolMaximumActiveConnections – 在任意时间可存在的活动(正在使用)连接数量,默认值:10
  • poolMaximumIdleConnections – 任意时间可能存在的空闲连接数。
  • poolMaximumCheckoutTime – 在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000 毫秒(即 20 秒)
  • poolTimeToWait – 这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)。
  • poolMaximumLocalBadConnectionTolerance – 这是一个关于坏连接容忍度的底层设置, 作用于每一个尝试从缓存池获取连接的线程。 如果这个线程获取到的是一个坏的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过 poolMaximumIdleConnections 与 poolMaximumLocalBadConnectionTolerance 之和。 默认值:3(新增于 3.4.5)
  • poolPingQuery – 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求。默认是“NO PING QUERY SET”,这会导致多数数据库驱动出错时返回恰当的错误消息。
  • poolPingEnabled – 是否启用侦测查询。若开启,需要设置 poolPingQuery 属性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句),默认值:false。
  • poolPingConnectionsNotUsedFor – 配置 poolPingQuery 的频率。可以被设置为和数据库连接超时时间一样,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)。

2.3  JNDI

这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。这种数据源配置只需要两个属性:

  • initial_context – 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
  • data_source – 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。

和其他数据源配置类似,可以通过添加前缀“env.”直接把属性传递给 InitialContext。比如:

  • env.encoding=UTF8

这就会在 InitialContext 实例化时往它的构造方法传递值为 UTF8 的 encoding 属性。

你可以通过实现接口 org.apache.ibatis.datasource.DataSourceFactory 来使用第三方数据源实现:

public interface DataSourceFactory {
  void setProperties(Properties props);
  DataSource getDataSource();
}

org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory 可被用作父类来构建新的数据源适配器,比如下面这段插入 C3P0 数据源所必需的代码:

import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;
import com.mchange.v2.c3p0.ComboPooledDataSource;

public class C3P0DataSourceFactory extends UnpooledDataSourceFactory {

  public C3P0DataSourceFactory() {
    this.dataSource = new ComboPooledDataSource();
  }
}

为了令其工作,记得在配置文件中为每个希望 MyBatis 调用的 setter 方法增加对应的属性。 下面是一个可以连接至 PostgreSQL 数据库的例子:

<dataSource type="org.myproject.C3P0DataSourceFactory">
  <property name="driver" value="org.postgresql.Driver"/>
  <property name="url" value="jdbc:postgresql:mydb"/>
  <property name="username" value="postgres"/>
  <property name="password" value="root"/>
</dataSource>

对于三种内置的数据源简单总结下:

  • UNPOOLED,就是不用池子,跟我们以前 JDBC 操作数据库一样,从驱动上获取连接执行语句;
  • POOLED,就是使用连接池,复用连接;
  • JNDI,适配外部数据源比如C3P0、DPCP、Druid都是走的这个途径。

3  源码分析

首先我们来看下关于三个数据源的代码结构:

那我们数据源的创建入口在哪里呢,我们的数据源是配置在环境变量里的,在解析环境变量配置的时候,就会根据配置的不通类型来创建对应的数据源工厂,继而获取数据源,我们来看下入口:

private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    if (environment == null) {
      // 获取环境变量上default属性值
      environment = context.getStringAttribute("default");
    }
    for (XNode child : context.getChildren()) {
      // 获取 id 属性
      String id = child.getStringAttribute("id");
      /*
       * 检测当前 environment 节点的 id 与其父节点 environments 的属性 default
       * 内容是否一致,一致则返回 true,否则返回 false甚至报错
       */
      if (isSpecifiedEnvironment(id)) {
        // 解析 transactionManager 节点
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
        // 解析 dataSource 节点
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        // 创建 DataSource 对象 !!!
        DataSource dataSource = dsFactory.getDataSource();
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        // 构建 Environment 对象,并设置到 configuration 中
        configuration.setEnvironment(environmentBuilder.build());
      }
    }
  }
}
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
  if (context != null) {
    // 获取我们数据源上的 type 类型也就是 UNPOOLED|POOLED|JNDI
    String type = context.getStringAttribute("type");
    // 获取配置值
    Properties props = context.getChildrenAsProperties();
    // 创建数据源工厂
    DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
    factory.setProperties(props);
    return factory;
  }
  throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}

那我们来看下每种数据源的工厂类。

3.1  UnpooledDataSourceFactory

非池化数据源工厂,我们的数据源工厂都是实现了 DataSourceFactory,该接口其实就两个方法,设置属性和获取数据源,我们看下非池化的工厂创建:

public class UnpooledDataSourceFactory implements DataSourceFactory {

  private static final String DRIVER_PROPERTY_PREFIX = "driver.";
  private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length();

  protected DataSource dataSource;

  public UnpooledDataSourceFactory() {
    this.dataSource = new UnpooledDataSource();
  }

  @Override
  public void setProperties(Properties properties) {
    Properties driverProperties = new Properties();
    // 给数据源对象创建元对象信息
    MetaObject metaDataSource = SystemMetaObject.forObject(dataSource);
    // 遍历属性
    for (Object key : properties.keySet()) {
      String propertyName = (String) key;
      // 如果属性的名字是以 .driver开头的  这个属性还真没用过不知道干啥的哈
      if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) {
        String value = properties.getProperty(propertyName);
        // 就往 driverProperties 放进去
        driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value);
      } else if (metaDataSource.hasSetter(propertyName)) {
        // 如果有 setter 方法,设置值
        String value = (String) properties.get(propertyName);
        // 这个就是转换值的类型  比如连接数超时时间都是整数,转换类型
        Object convertedValue = convertValue(metaDataSource, propertyName, value);
        // 给数据源对象设置值
        metaDataSource.setValue(propertyName, convertedValue);
      } else {
        throw new DataSourceException("Unknown DataSource property: " + propertyName);
      }
    }
    // 也是设置进去 driverProperties
    if (driverProperties.size() > 0) {
      metaDataSource.setValue("driverProperties", driverProperties);
    }
  }

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

  // 转换值类型
  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;
  }

}

内容想必大家也都能看懂,主要就是解析我们的配置并进行类型转换以及基础创建。

3.2  PooledDataSourceFactory

池化数据源工厂,就是继承了我们上边的非池化工厂,所做的操作都是类似的。

// 继承了 UnpooledDataSourceFactory 设置属性和 getDataSource 内容一致
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
  public PooledDataSourceFactory() {
    this.dataSource = new PooledDataSource();
  }
}

3.3  JndiDataSourceFactory

public class JndiDataSourceFactory implements DataSourceFactory {

  public static final String INITIAL_CONTEXT = "initial_context";
  public static final String DATA_SOURCE = "data_source";
  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;
  }

}

针对外部数据源的示例引用,我会单独拿Druid来做示例,讲解如何加载对应的数据源,并将两者结合到一块的哈。

4  小结

那么关于内置数据源的一个概念了解和三种数据源工厂的基本初始化过程,那么下节我们着重看下连接池方式的管理,有理解不对的地方欢迎指正哈。

posted @ 2023-03-01 22:51  酷酷-  阅读(81)  评论(0编辑  收藏  举报