Mybatis源码学习记录(数据源篇)

前言

我们知道数据源是一个非常重要的基础组件,它的性能直接关系到数据持久层的性能,尽管市面上有很多第三方数据源组件,比如阿里的druid,Apache的DBCPc3p0,不管是哪种数据源,最终都需要实现javax.sql.DataSource接口

Mybatis框架自身也提供了数据源的实现,分别是PooledDataSourceUnpooledDataSource

学习Mybatis提供的数据源实现之前我们先看下javax.sql.DataSource接口,通过它来直观的了解作为一个数据源应该具有哪些功能?

DataSource接口

public interface DataSource extends CommonDataSource, Wrapper {
    
    // 尝试通过数据源建立一个连接
    Connection getConnection() throws SQLException;
    
    // 重载的方法,传入用户名以及密码
    Connection getConnection(String username, String password) throws SQLException;
}

通过DataSource接口我们可以知道,数据源本身就是就算是一个连接工厂,当你需要连接时,就问工厂要(调用getConnection方法)一个就行了

一般DataSource接口是由数据库驱动商实现,且基本上会有三种实现形式

  • 基础实现:每次需要连接对象都是单纯的生产一个标准的新连接对象返回

  • 连接池实现:生产的连接对象自动加入连接池中以便复用连接对象,该实现方式需要与一个中间层连接管理器合作

  • 分布式事务方式实现:此种实现较为复杂,本文不会涉及

Mybaits中的数据源实现就是针对以上前两点实现的,UnpooledDataSource对应基础实现方式,而PooledDataSource针对连接池方式的实现,下面我们直接看源码

UnpooledDataSource

非池化数据源,即每次都是创建一个新的数据库连接对象返回

public class UnpooledDataSource implements DataSource {
  
  private ClassLoader driverClassLoader;
  // 驱动相关属性配置,在下面的工厂方法模式的setProperties方法中会将该属性赋值上(如果有驱动相关属性配置的化)
  private Properties driverProperties;
  // 已注册到驱动管理器的驱动集合,不同的数据库对应不同的驱动,比如mysql驱动,oracle驱动等...
  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;

  static {
    // 利用static块在UnpooledDataSource类初始化阶段,将已经注册到驱动管理器中的驱动考一份存入registeredDrivers集合中,以驱动的className为key保存
    // 实际上基于SPI,只要classpath下有驱动的jar包,这里通过DriverManager.getDrivers()方法就可以直接获取到注册号的驱动了,因为这里会触发DriverManager类加载,并执行static块的loadInitialDrivers方法,根据ServiceLoader去加载数据库驱动jar中META-INF/services/下指定的文件(java.sql.Driver)
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) {
      Driver driver = drivers.nextElement();
      registeredDrivers.put(driver.getClass().getName(), driver);
    }
  }
  
  // 省略各种重载的构造函数...
  
  // 省略各种属性的getter/setter方法...
  
  @Override
  public Connection getConnection() throws SQLException {
    return doGetConnection(username, password);
  }

  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return doGetConnection(username, password);
  }
  
  // getConnection方法都会转到doGetConnection方法上
  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);
  }

  private Connection doGetConnection(Properties properties) throws SQLException {
    // 1. 初始化驱动
    initializeDriver();
    // 2. 通过驱动管理器获取连接,注意如果1.中没有合适的驱动注册到驱动管理器中,这里的getConnection方法内,根据指定的url前缀(如:`jdbc:mysql:xxx`)就找不到合适的JDBC驱动,也就获取不到连接对象
    Connection connection = DriverManager.getConnection(url, properties);
    // 3. 配置连接对象
    configureConnection(connection);
    // 4. 返回连接对象
    return connection;
  }
  
  // 只有不符合JDBC Driver SPI的驱动才可能会进入if内
  private synchronized void initializeDriver() throws SQLException {
    // 假设这里传入的driver为某种不符号SPI的驱动商驱动,第一次会进入if内
    if (!registeredDrivers.containsKey(driver)) {
      Class<?> driverType;
      try {
        if (driverClassLoader != null) {
          // 如果driverClassLoader在配置文件中配置了,就进入这里
          driverType = Class.forName(driver, true, driverClassLoader);
        } else {
          // 没有配置单独的driverClassLoader,则执行这里,加载driver驱动类
          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();
        // 注册驱动类,注意这里是一个静态代理类
        DriverManager.registerDriver(new DriverProxy(driverInstance));
        // 存入已注册驱动集合中
        registeredDrivers.put(driver, driverInstance);
      } catch (Exception e) {
        throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
      }
    }
  }
  
  private void configureConnection(Connection conn) throws SQLException {
    // 配置连接对象,就是在已经获取到的连接上配置自动提交和默认事务的隔离级别
    if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
      conn.setAutoCommit(autoCommit);
    }
    if (defaultTransactionIsolationLevel != null) {
      conn.setTransactionIsolation(defaultTransactionIsolationLevel);
    }
  }
  
  // 静态内部类,Driver的静态代理类
  private static class DriverProxy implements Driver {
    private Driver driver;

    DriverProxy(Driver d) {
      this.driver = d;
    }

    @Override
    public boolean acceptsURL(String u) throws SQLException {
      return this.driver.acceptsURL(u);
    }

    @Override
    public Connection connect(String u, Properties p) throws SQLException {
      return this.driver.connect(u, p);
    }

    @Override
    public int getMajorVersion() {
      return this.driver.getMajorVersion();
    }

    @Override
    public int getMinorVersion() {
      return this.driver.getMinorVersion();
    }

    @Override
    public DriverPropertyInfo[] getPropertyInfo(String u, Properties p) throws SQLException {
      return this.driver.getPropertyInfo(u, p);
    }

    @Override
    public boolean jdbcCompliant() {
      return this.driver.jdbcCompliant();
    }

    // @Override only valid jdk7+
    public Logger getParentLogger() {
      return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
    }
  }
image.png
image.png
image.png

以上就是非池化数据源的全部源码,代码比较简单,对于熟悉JDBC编程的同学几乎没有难度,此外我们额外提一下关于该非池化数据源合的创建过程,以上源码我们省略了其构造函数,实际上Mybatis采用了工厂方法模式来创建非池化以及池化数据源

工厂方法模式

Mybatis定义了一个DataSourceFactory接口来作为工厂方法模式中的工厂接口

public interface DataSourceFactory {

  // 为DataSource设置相关属性
  void setProperties(Properties props);
  // 获取数据源对象
  DataSource getDataSource();
}

那既然采用了工厂方法模式来创建不同的数据源实例,那么自然针对不同的产品(数据源)就会存在对应的工厂实现类,针对UnpooledDataSource产品的工厂类实现就是UnpooledDataSourceFactory

利用工厂方法模式,Mybatis就可以直接面向工厂接口以及产品接口编程,而不用去管具体的工厂类和具体的产品类,与之带来的优点就是开闭原则对扩展开放对修改关闭

如果我们需要增加一种数据源(产品,比如增加一种第三方数据源),Mybatis只要再额外增加一种对应的工厂类就可以了

UnpooledDataSourceFactory

public class UnpooledDataSourceFactory implements DataSourceFactory {
  // 以“driver”开头的属性
  private static final String DRIVER_PROPERTY_PREFIX = "driver.";
  private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length();

  protected DataSource dataSource;

  // 利用工厂的构造函数直接new一个数据源对象出来
  public UnpooledDataSourceFactory() {
    this.dataSource = new UnpooledDataSource();
  }

  @Override
  public void setProperties(Properties properties) {
    // 抽出属性配置中的驱动相关配置,并保存到driverProperties中
    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)) {
        String value = (String) properties.get(propertyName);
        Object convertedValue = convertValue(metaDataSource, propertyName, value);
        // dataSource相关的配置属性,直接set进去
        metaDataSource.setValue(propertyName, convertedValue);
      } else {
        throw new DataSourceException("Unknown DataSource property: " + propertyName);
      }
    }
    if (driverProperties.size() > 0) {
      // 最后,如果有驱动相关的属性配置的化,将其设置到dataSource对象的driverProperties属性上去
      metaDataSource.setValue("driverProperties", driverProperties);
    }
  }

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

至此我们解析了整个Mybatis提供的非池化数据源的创建过程,可见底层还是调用了Java JDBC相关的代码的

总结

  1. 调用非池化数据源工厂类UnpooledDataSourceFactory的构造函数,创建工厂对象
  2. 工厂对象的构造函数中调用非池化数据源类UnpooledDataSource的构造函数,创建非池化数据源对象
  3. 调用非池化数据源工厂对象setProperties(...)方法,根据入参Properties,设置非池化数据源对象的基本属性(如urlusernamepassword等)以及赋值driverProperties属性
  4. 最后就可以调用非池化数据源工厂对象的getDataSource方法获取数据源对象实例啦

下文会分析Mybatis提供的另一种数据源实现,即池化数据源PooledDataSource的源码

最后附上Mybatis针对数据源的配置文件写法,帮助同学回忆一下哈

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE configuration  
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  
  "http://mybatis.org/dtd/mybatis-3-config.dtd">  
<configuration>  
    <!-- 对事务的管理和连接池的配置 -->  
    <environments default="development">  
        <environment id="development">  
            <transactionManager type="JDBC" />  
            <dataSource type="POOLED"> 
                <!-- oracle驱动 -->
                <property name="driver" value="oracle.jdbc.driver.OracleDriver" />  
                <property name="url" value="jdbc:oracle:thin:@localhost:1521:orcl" />  
                <property name="username" value="ibatis" />  
                <property name="password" value="ibatis" />  
            </dataSource>  
        </environment>  
    </environments>  
      
    <!-- mapping 文件路径配置 -->  
    <mappers>  
        <mapper resource="com/yu/res/UserMapper.xml" />  
    </mappers>  
</configuration>

如果是mysql驱动的化上面的value值就是com.mysql.jdbc.Driver,另外url属性值就是类似这样的jdbc:mysql://xxx:3306/wilson?serverTimezone=UTC,另外一点,以上配置没有单独配置driverClassLoader驱动类加载器

最后关于JDBC的Driver驱动类加载可以看看xwiz大神的这篇文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342