mybatis数据源的创建过程稍微有些曲折。
1. 数据源的创建过程;
2. mybatis支持哪些数据源,也就是dataSource标签的type属性可以写哪些合法的参数?
弄清楚这些问题,对mybatis的整个解析流程就清楚了,同理可以应用于任何一个配置上的解析上。
从SqlSessionFactoryBuilder开始追溯DataSource的创建。SqlSessionFactoryBuilder中9个构造方法,其中字符流4个构造方法一一对应字节流4个构造方法,都是将mybatis-config.xml配置文件解析成Configuration对象,最终导向build(Configuration configuration)进行SqlSessionFactory的构造。
配置文件的在build(InputStream, env, Properties)构造方法中进行解析,InputStream和Reader方式除了流不一样之外均相同,本处以InputStream为例,追踪一下源码。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// mybatis-config.xml文件的解析对象
// 在XMLConfigBuilder中封装了Configuration对象
// 此时还未真正发生解析,但是将解析的必备条件都准备好了
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// parser.parse()的调用标志着解析的开始
// mybatis-config.xml中的配置将会被解析成运行时对象封装到Configuration中
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
在XMLConfigBuilder进一步追踪,疑问最终保留在其父类BaseBuilder的resolveClass方法上,该方法对数据源工厂的字节码进行查找。
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// mybatis-config.xml的根节点就是configuration
// 配置文件的解析入口
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties")); //issue #117 read properties first
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
settingsElement(root.evalNode("settings"));
// environment节点包含了事务和连接池节点
environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// 如果调用的build没有传入environment的id
// 那么就采用默认的environment,即environments标签配置的default="environment_id"
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());
}
}
}
}
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
// dataSource标签的属性type
String type = context.getStringAttribute("type");
// 解析dataSource标签下的子标签
// 实际上就是数据源的配置信息,url、driver、username、password等
Properties props = context.getChildrenAsProperties();
// resolveClass:到XMLConfigBuilder的父类BaseBuilder中进行工厂Class对象的查找
// 这里是重点
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
在父类中并没有窥探到重点,转到其实例属性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) {
// BaseBuilder中的实例属性
// 实例属性:protected final TypeAliasRegistry typeAliasRegistry;
return typeAliasRegistry.resolveAlias(alias);
}
typeAliasRegistry中实际上是在一个Map中进行KV的匹配。
public Class resolveAlias(String string) {
try {
if (string == null) return null;
String key = string.toLowerCase(Locale.ENGLISH); // issue #748
Class value;
if (TYPE_ALIASES.containsKey(key)) {
// private final Map> TYPE_ALIASES = new HashMap>();
// TYPE_ALIASES是一个实例属性,类型是一个Map
value = (Class) TYPE_ALIASES.get(key);
} else {
value = (Class) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
那么问题就来了,工厂类什么时候被注册到这个map中的?
实际上在SqlSessionFactoryBuilder的build(InputStream, env, Propeerties)方法中调用parse解析配置文件之前,我们忽略了一段重要的代码。
查看创建XMLConfigBuilder的过程,根据继承中初始化的规则,将会在父类BaseBuilder构造方法中创建Configuration对象,而Configuration对象的构造方法中将会注册框架中的一些重要参数。
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
// 转
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
// 转调父类构造方法
// 同时最终要的是直接new Configuration()传入父类
// Configuration中的属性TypeAliasRegistry将会注册数据源工厂
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
public abstract class BaseBuilder {
protected final Configuration configuration;
protected final TypeAliasRegistry typeAliasRegistry;
protected final TypeHandlerRegistry typeHandlerRegistry;
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
// typeAliasRegistry来自于Configuration
// 也就是合理解释了刚才通过typeAliasRegistry来找数据源工厂
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
至此,数据源创建结束。接下来就看看怎么用。
mybatis JNDI之前已经剖析过源码,此处不再进行剖析,原文链接:点击打开链接
mybatis UNPOOLED数据源创建的思想,先通过默认构造方法创建数据源工厂(此时UNPOOLED dataSource随之创建),将mybatis-config.xml中数据源的配置信息通过setProperties传给工厂,然后通过工厂getDataSource。回顾一下这一段源码。
最终是利用简单的反射通过默认无参的构造方法实例化了数据源工厂,此时在数据源工厂中也实例化了UNPOOLED数据源对象。
resolveClass(type)这句话,从configuration中拿到UNPOOLED对应的value,即org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory.class,然后通过无参构造器实例化工厂对象,在工厂的无参构造中也直接实例化了dataSource对象,即org.apache.ibatis.datasource.unpooled.UnpooledDataSource,然后调用setProperties方法,把配置文件中配置的参数(SqlSessionFactoryBuilder.builder中如果传入Properties也会被putAll,同key则覆盖value)进行dataSource设置。
配置数据源,最重要的是connection的获取和管理,通过UNPOOLED方式来配置数据源,实际上和直接是用JDBC没有太多区别,操作的都是原生的、没有任何修饰的connection。
POOLED工厂直接继承UNPOOLED工厂,只是在POOLED工厂的默认构造中实例化org.apache.ibatis.datasource.pooled.PooledDataSource覆盖了UNPOOLED中实例化的dataSource对象。其他的一模一样,紧接着调用setProperties方法等。
直接关注连接对象,通过POOLED,因为连接池需要存放连接对象,因此连接对象的close方法需要进行改写,连接池的状态也需要进行管理(PoolState封装了连接池的状态)。
至于获取连接,会care PoolState中空闲链表中是否还有可用的connection,有则直接返回,没有则看是否已经到达配置的最大连接数,没有到达则new一个新的,这部分源码较长但是逻辑简单,就不贴出来了。直接看connection的代理部分吧。PooledConnection直接实现了JDK动态代理中的InvocationHandler,其invoke方法中重写了close方法,将连接对象还回池中,当非close方法的时候,会检查一下connection的状态是否正常,正常则直接调用原逻辑。
class PooledConnection implements InvocationHandler {
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())) {
// issue #579 toString() should never fail
// throw an SQLException instead of a Runtime
checkConnection();
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
}
参看:点击打开链接
其实和第一节息息相关,直接使用mybatis的时候,在配置标签
源码思想很简单,这里就不再贴出来了,因此要在纯mybatis环境下要继承其他数据源,例如C3P0或DBCP,只需要将其datasource继承org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory即可,因为setProperties方法在UnpooledDataSourceFactory中定义,并且默认POOLED也是继承于此。
Mybatis是否支持自定义的数据源,关键在于两点,一是该数据源是否有默认构造函数(无参构造函数),二是可以通过get/set方式来进行数据源配置。满足以上两点,就足够了。mybatis内置的数据源实在是弱得看不下去,所以集成其他数据源的时候,无论是传统的JNDI/DBCP/C3P0还是当下牛逼闪闪的HikariCP也罢,都可以集成到mybatis中使用!当然,如果你使用Spring来处理数据源,那么这里就可以不用考虑了,Spring-mybatis的jar包已经帮你处理好了···
其实整个逻辑还是很简单的,源码揭露一切。
附注:
本文如有错漏,烦请不吝指正,谢谢!