一、程序员的可能都是不靠谱的
请不要惹程序员生气,后果很严重!
程序员都是不靠谱的,但是我很靠谱,请找我,不谢!
二、再NB也抵挡不住犯错
今天再次撸一下mybatis数据源的源码,发现粗大事啦!
Apache的程序员也有屌丝啊,想想还是很激动,原来不止我一个人是屌丝程序员啊。但也可能是我理解不够深入,所以写出来,让大家瞅瞅,群撸一下。好,回正题。
1. Mysql驱动包
用JDBC第一步,就是导入数据库驱动jar包,安装mysql目录下就有,直接粘过来,build path一下就好了。
写JDBC第一步,就是注册驱动。注册驱动有两种方式,一种是通过DriverManager来进行注册,需要传入驱动包中对java.sql.Driver的实现类的实例对象;另一种是利用类加载原理来做,只需要把驱动字节码加载到内存生成对应的Class对象即可(请参看另一篇博文:《类加载机制》),一般就是Class.forName或通过Class的class属性来获取。两种方式,那么到底孰优孰劣?
因为mysql驱动包的缘故,第二种方式性能占优。mysql对java.sql.Driver的实现类中,通过静态代码块已经加载过一次数据库驱动了,因此通过第一种方式会导致驱动的重复加载,从而产生覆盖(产生覆盖,JVM判断两个对象是否相等通过equals方法,但是前提是两个对象有相同的类加载器,从而判定mysql驱动中通过静态代码块加载的驱动会被覆盖),而通过类加载的方式是不会导致重复加载驱动的,只是获得了mysql实现的驱动类的Class对象。mysql驱动包中源码如下所示,重点查看静态代码块。
package com.mysql.jdbc;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
// ~ Static fields/initializers
// ---------------------------------------------
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
// ~ Constructors
// -----------------------------------------------------------
/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
实际上,根据类加载原理,如果使用的是mysql数据库,那么驱动是不需要显式加载的(如下图例证)。已经在驱动包的实现类中加载过了,直接通过DriverManager获取就可了,但是为了保证程序的通用性,所以还是显式加载一下最好,因此在写JDBC代码的时候,建议使用第二种方式来进行加载。
那么就总结一下吧,mysql环境下使用JDBC进行数据库操作:
1. JDBC加载数据库驱动建议使用类加载方式,这种方式不会导致驱动的重复加载,消耗性能;
2. 如果使用DriverManager进行驱动加载,则会出现驱动重复加载,导致mysql驱动包中静态代码块加载的驱动被覆盖,空耗性能;
2. Mybatis数据源驱动
mybatis自己做了数据源,分别是JNDI、POOLED、UNPOOLED,一般情况下,生产环境使用JNDI(请参看博文:《MyBatis Tomcat JNDI原理及源码分析》),开发环境使用POOLED,不使用数据源则使用UNPOOLED,三种方方式的细节不是本次讨论的重点因此略去。除了JNDI方式,最终获取数据库连接的都是通过org.apache.ibatis.datasource.unpooled.UnpooledDataSource类的doGetConnection方法,但是在获取连接之前都是需要加载驱动的,而mybatis则通过方法initializeDriver来进行驱动的加载,代码如下。
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();
DriverManager.registerDriver(new DriverProxy(driverInstance));
registeredDrivers.put(driver, driverInstance);
} catch (Exception e) {
throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
}
}
}
问题也就出现在这里,源码不好看,请看截图。
mybatis是通过DriverManager进行驱动加载的,也就是说,该方式是会导致驱动重复加载空耗性能的。有人会质疑注册驱动的方法传入的是一个驱动的代理对象,但实际上这个方法只是对原始驱动包装了一下,就算采用代理,静态代码块也不会消失,因此重复加载驱动空耗性能的问题依旧存在。
补充:文章写的比较早,那时候还比较嫩,分析是有纰漏的。首先,mybatis是不存在驱动重复加载的,在initializeDriver()方法的第一行,就是通过mybatis-config.xml中数据源配置属性“driver"来进行判断在ConcurrentHashMap是否已经存在相同KEY的驱动,以mysql为例,"driver"="com.mysql.jdbc.Driver",两者都是字符串,而ConcurrentHashMap是线程安全的(分段锁),因此同一数据库类型是不会导致驱动重复加载的。
也就是说,如果mybatis同时使用了tomcat和oracle,那么只会存在两个驱动实例,一开始的if判断过不了。
至于java.sql.DriverManager.registerDriver(Driver driver)是否会导致驱动重复注册的问题,答案是肯定的,确实是会造成驱动重复注册。DriverManager中通过CopyOnWriteArrayList线程安全的数据结构来存放驱动,只要传入的driver不为null,就会被放到ArrayList中。更严重的是,从DriverManager中获取驱动使用的时候,是通过遍历CopyOnWriteArrayList,只要拿到匹配数据库的第一个驱动就返回,以mysql为例,因此如果注册多个mysql驱动,那么每次都是第一个驱动被使用,而后面的驱动则一直闲着。不同数据库的驱动都存放在该结构中,但是无论哪一种数据库,都只有第一个驱动会被使用,其余的都会闲着浪费资源。
java.sql.DriverManager源码分析如下。
再回顾一下DriverManager.getConnection方法,遍历ArrayList,匹配立即返回,因此同一数据库注册多个驱动只会有第一个被使用,其余空闲,千万注意,传入的username, password, url是在连接数据库的时候才使用,而连接数据库则是通过Driver对象(实际上是driver.connec方法)。
三、真的是犯错吗
上图中,画红线的下一句,将驱动代理和原始驱动放入到map中,也就意味着mabatis是允许多个驱动存在,实际上在mybatis-config.xml中可以配置多个environment(注意:environments标签是需要配置default属性的),不同的环境可能使用不同数据库的数据源,因此mybatis是需要存放每一个环境的数据库库驱动对象,为此,只有付出重复加载驱动空耗性能的代价换取设计上的实现,事实上,几乎只有在mysql环境下才会产生空耗问题。所以,这是一种折中选择吧,并不是真正的犯错!
mybatis的结构是一个SqlSessionFactory中一个Configuration(mybatis的配置中心),一个Configuration一个Enviroment,每个Environment中有一个DataSource对象和一个TransactionFactory;对于数据源而言,UnpoolDataSource是直接通过DriverManager.getConnection来获取连接,而PoolDataSource的设计有一个池的概念(资源复用,就是java.sql.Connection对象复用),而获取资源一样需要通过JDBC的DriverManager.getConnection,然而这一步已经让UnpoolDataSource做了,所以包装一下,PoolDataSource只需要对UnPoolDataSOurce应用装饰模式即可。
上面扯一堆啊,还是讲重点吧。
实际上对于不同的enviroment,在创建SqlSessionFactory的时候是要指定的,所以不同的SqlSessionFactory中拥有各自的Configuration,因此不同的SqlSessionFactory对象中的数据源工厂也是不同的对象,根本谈不上复用二字。更通俗的说,environment中配置mysql和配置oracle可以,但是是不同的environment,所以啊对于UnpoolDataSource而言,不同实例不相往来,没什么浪费不浪费可谈。
讲到这里,实际上还是要批评一下mybatis数据源的设计,不是说全不全面(要想全面获得db server的特性,直接轮JDBC得了),关于驱动这里啊,实际上可以设计成单例,在所有的mybatis datasource之间共享(人家也能共享,只需要Driver.connect方法),何必为了孤单单的一个Driver实例而配置一个大map,实际上终其一生啊,就放一个Driver,放个大map干嘛呀这是,您说对吧。
至此,讨论结束。
附注:
本文如有错漏,烦请不吝指正,谢谢!