简述
在数据持久层中,数据源是一个非常重要的组件,其性能直接关系到整个数据持久层的性能。在实践中比较常见的第三方数据源组件有Apache Common DBCP、C3P0、Proxool等,MyBatis不仅可以集成第三方数据源组件,还提供了自己的数据源实现。
常见的数据源组件都实现了javax.sql.DataSource接口,MyBatis自身实现的数据源实现也不例外。MyBatis提供了两个javax.sql.DataSource接口实现,分别是PooledDataSource和UnpooledDataSource。Mybatis使用不同的DataSourceFactory接口实现创建不同类型的DataSource,
DataSource数据源分类
Mybatis源码在org.apache.ibatis.datasource包中:
mybatis配置文件中关于数据库的配置:
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
这里重点关注一下节点中的type属性,有三种取值,分别是:
UNPOOLED 不使用任何数据库连接池管理数据库连接
POOLED 使用Mybatis默认的数据库连接池管理数据库连接
JNDI 使用JNDI实现的数据源
MyBatis是通过工厂模式来创建数据源DataSource对象的,MyBatis定义了抽象的工厂接口:org.apache.ibatis.datasource.DataSourceFactory,通过其getDataSource()方法返回数据源DataSource
/**
* 数据源工厂接口
*
* @author kaifeng
* @author Clinton Begin
*/
public interface DataSourceFactory {
/**
* 设置数据源相关属性
*
* @param props 数据源属性
*/
void setProperties( Properties props );
/**
* 获取数据源对象
*/
DataSource getDataSource();
}
三种不同类型的type,分别有对应的dataSource工厂:
POOLED PooledDataSourceFactory
UNPOOLED UnpooledDataSourceFactory
JNDI JndiDataSourceFactory
UnpooledDataSourceFactory 和 JndiDataSourceFactory 实现了DataSourceFactory接口
PooledDataSourceFactory 继承了 UnpooledDataSourceFactory
DataSource实例化过程
1、解析配置文件并装配Cofiguration对象
Mybatis 解析配置文件并装配Cofiguration对象,同时Configuration中的属性
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
被实例化,并且在Configuration的无参构造函数中,向 TypeAliasRegistry的属性
private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();
中注册了许多常用的类,TypeAliasRegistry的构造函数中也注册了基本的java类型
2、根据type=POOLED属性查找工厂类
根据配置文件type=POOLED属性,从TypeAliasRegistry中查找对应的工厂类 PooledDataSourceFactory
3、实例化DataSource
由于PooledDataSourceFactory继承自UnpooledDataSourceFactory、所以UnpooledDataSourceFactory先被实例化、
UnpooledDataSourceFactory无参构造方法中实例化了其DataSource为UnpooledDataSource。
/**
* @author kaifeng
* @author Clinton Begin
*/
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();
}
}
接下来实例化 PooledDataSourceFactory 其无参构造方法体将父类UnpooledDataSourceFactory持有的DataSource实例化为PooledDataSource
/**
* 将父类UnpooledDataSourceFactory的dataSource实例化为PooledDataSource
*
* @author kaifeng
* @author Clinton Begin
*/
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
public PooledDataSourceFactory() {
this.dataSource = new PooledDataSource();
}
}
PooledDataSource实例化时初始化了一些关于数据库连接池的配置信息
PooledDataSource的无参构造方法中将其持有的UnpooledDataSource实例化。
/**
* Mybatis默认数据库连接池
*
* @author kaifeng
* @author Clinton Begin
*/
public class PooledDataSource implements DataSource {
private static final Log log = LogFactory.getLog(PooledDataSource.class);
private final PoolState state = new PoolState(this);
private final UnpooledDataSource dataSource;
/**
* 最大活动连接数(默认为10)
*/
protected int poolMaximumActiveConnections = 10;
/**
* 最大空闲连接数(默认为5)
*/
protected int poolMaximumIdleConnections = 5;
/**
* 最大可回收时间,即当达到最大活动链接数时,此时如果有程序获取连接,则检查最先使用的连接,看其是否超出了该时间,如果超出了该时间,则可以回收该连接。(默认20s)
*/
protected int poolMaximumCheckoutTime = 20000;
/**
* 没有连接时,尝试获取连接以及打印日志的时间间隔(默认20s)
*/
protected int poolTimeToWait = 20000;
/**
* 这是一个关于无效连接容忍度的底层设置, 作用于每一个尝试从缓存池获取连接的线程.
* 如果这个线程获取到的是一个无效的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,
* 但是这个重新尝试的次数不应该超过 poolMaximumIdleConnections 与 poolMaximumLocalBadConnectionTolerance 之和。
* 默认值:3 (新增于 3.4.5)
*/
protected int poolMaximumLocalBadConnectionTolerance = 3;
/**
* 检查数据源是否可访问的语句,默认为"NO PING QUERY SET",即没有,使用会导致抛异常
*/
protected String poolPingQuery = "NO PING QUERY SET";
/**
* 是否开启ping检测,(默认:false)
*/
protected boolean poolPingEnabled;
/**
* 设置ping检测时间间隔,通常用于检测超时连接(默认为0,即当开启检测后每次从连接词中获取连接以及放回连接池都需要检测)
*/
protected int poolPingConnectionsNotUsedFor;
private int expectedConnectionTypeCode;
public PooledDataSource() {
dataSource = new UnpooledDataSource();
}
}
UnpooledDataSource中关于数据库连接的属性值在实例化DataSourceFactory之后读取properties值设置到对应属性上。
/**
* 不使用连接池的数据源
*
* @author kaifeng
* @author Clinton Begin
* @author Eduardo Macarron
*/
public class UnpooledDataSource implements DataSource {
/**
* 数据源驱动类加载器
*/
private ClassLoader driverClassLoader;
/**
* 驱动连接属性
*/
private Properties driverProperties;
/**
* 已注册的驱动集合
*/
private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<String, Driver>();
/**
* 当前使用的驱动
*/
private String driver;
/**
* 数据源地址
*/
private String url;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 是否自动提交事务
*/
private Boolean autoCommit;
/**
* 默认事务隔离级别
*/
private Integer defaultTransactionIsolationLevel;
// 静态代码块,当类加载的时候,就从DriverManager中获取所有的驱动信息,放到当前维护的Map中
static {
Enumeration<Driver> drivers = DriverManager.getDrivers();
while (drivers.hasMoreElements()) {
Driver driver = drivers.nextElement();
registeredDrivers.put(driver.getClass().getName(), driver);
}
}
public UnpooledDataSource() {
}
}
如何从配置文件初始化DataSource
由上文得知Mybatis初始化过程是解析Mybatis配置文件并装配Configuration对象,从Mybatis配置文件中知道Mybatis数据库连接信息的配置是在environments标签中配置的:
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
所以我们可以从XMLConfigBuilder中的parse方法入手
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
我们看其中的关键代码
dataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
根据配置文件中dataSource标签中内容实例化DataSourceFactory,
通过DataSourceFactory获取DataSource
那么如何根据dataSource标签内容实例化DataSourceFactory的呢,我们看一下 dataSourceElement 方法:
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
//获取数据库连接池类型: POOLED-使用Mybatis自带数据库连接池。UNPOOL-不使用数据库连接池
String type = context.getStringAttribute("type");
Properties props = context.getChildrenAsProperties();
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
我们看关键代码 resolveClass(type)
经过一系列的方法调用、最终返回结果是:TypeAliasRegistry。
protected Class<?> resolveClass(String alias) {
if (alias == null) {
return null;
}
try {
return resolveAlias(alias);
} catch (Exception e) {
throw new BuilderException("Error resolving class. Cause: " + e, e);
}
}
protected Class<?> resolveAlias(String alias) {
return typeAliasRegistry.resolveAlias(alias);
}
上文中已经说明,在装配Configuration时,其无参构造函数已经向typeAliasRegistry注册了常用类,至此我们对数据源的初始化也就明了了。
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
typeAliasRegistry.registerAlias("LRU", LruCache.class);
typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
languageRegistry.register(RawLanguageDriver.class);
}
Connection对象创建时机
当我们需要执行SQL的时候,mybatis会调用DataSource对象创建java.sql.Connection对象,直到执行SQL时,Connection对象才会被创建,下面我们通过一个事例验证一下。
@Test
public void testGetSqlSession() throws Exception {
//解析mybatis.xml,获取SqlSession对象
1、SqlSession sqlSession = MybatisUtil.getSqlSession();
2、AuthorMapper authorMapper = sqlSession.getMapper(AuthorMapper.class);
3、System.err.println("即将创建Connection对象,执行SQL语句");
4、authorMapper.getAllAuthors();
}
MybatisUtil中的方法
/**
* @author kaifeng
*/
public class MybatisUtil {
private static SqlSessionFactory sqlSessionFactory;
/**
* 解析mybatis.xml文件创建SqlSessionFactory对象
*
* @return SqlSessionFactory instance
*/
public static SqlSessionFactory getSqlSessionFactory() {
String mybatisConfigPath = "config/mybatis.xml";
try {
InputStream inputStream = Resources.getResourceAsStream(mybatisConfigPath);
if (sqlSessionFactory == null) {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
} catch (IOException e) {
e.printStackTrace();
}
return sqlSessionFactory;
}
/**
* 通过SqlSessionFactory打开SqlSession与数据库的会话
*
* @return SqlSession 返回SqlSession对象
*/
public static SqlSession getSqlSession() {
return MybatisUtil.getSqlSessionFactory().openSession();
}
}
执行上边的单元测试代码,这里给出一部分关键的日志
2018-08-04 15:01:26 [org.apache.ibatis.parsing.GenericTokenParser.parse(GenericTokenParser.java:121)]-[DEBUG] [GenericTokenParser]-[parse]-待解析文本:DELETE FROM author
WHERE id = #{id},解析结果:DELETE FROM author
WHERE id = ?
2018-08-04 15:01:26 [org.apache.ibatis.parsing.GenericTokenParser.parse(GenericTokenParser.java:121)]-[DEBUG] [GenericTokenParser]-[parse]-待解析文本:UPDATE author
SET username = #{username},解析结果:UPDATE author
SET username = ?
2018-08-04 15:01:26 [org.apache.ibatis.parsing.GenericTokenParser.parse(GenericTokenParser.java:121)]-[DEBUG] [GenericTokenParser]-[parse]-待解析文本:SELECT
t1.id post_id,
t1.created_on createdTime,
t1.section,
t1.subject,
t1.draft,
t1.body,
t2.id post_comment_id,
t2.name post_comment_name,
t2.comment_text post_comment_text
FROM post t1 LEFT JOIN post_comment t2 ON t1.id = t2.post_id
WHERE t1.id = #{id},解析结果:SELECT
t1.id post_id,
t1.created_on createdTime,
t1.section,
t1.subject,
t1.draft,
t1.body,
t2.id post_comment_id,
t2.name post_comment_name,
t2.comment_text post_comment_text
FROM post t1 LEFT JOIN post_comment t2 ON t1.id = t2.post_id
WHERE t1.id = ?
即将创建Connection对象,执行SQL语句
2018-08-04 15:01:26 [org.apache.ibatis.transaction.jdbc.JdbcTransaction.openConnection(JdbcTransaction.java:137)]-[DEBUG] Opening JDBC Connection
2018-08-04 15:01:26 [org.apache.ibatis.datasource.pooled.PooledDataSource.popConnection(PooledDataSource.java:467)]-[DEBUG] Created connection 854487022.
2018-08-04 15:01:26 [org.apache.ibatis.transaction.jdbc.JdbcTransaction.setDesiredAutoCommit(JdbcTransaction.java:101)]-[DEBUG] Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@32ee6fee]
2018-08-04 15:01:26 [org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:159)]-[DEBUG] ==> Preparing: SELECT t.id, t.username, t.password, t.email, t.bio, t.favourite_section favouriteSection FROM author t
2018-08-04 15:01:26 [org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:159)]-[DEBUG] ==> Parameters:
Disconnected from the target VM, address: '127.0.0.1:50293', transport: 'socket'
2018-08-04 15:01:26 [org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:159)]-[DEBUG] <== Total: 1
从日志中我们可以看出直到执行第4句代码时,才会触发openConnection()方法创建java.sql.Connection对象
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
connection = dataSource.getConnection();
if (level != null) {
connection.setTransactionIsolation(level.getLevel());
}
setDesiredAutoCommit(autoCommmit);
}