记录是一种精神,是加深理解最好的方式之一。
最近看了下Mybatis的源码,了解了下Mybatis对配置文件的解析过程,在这里把他记下来。虽然这不复杂,对这方面的博客也有很多,写的也很好。但我坚信看懂了是其一,能够教别人或者描述清楚记下来才能真正的掌握。
曹金桂 [email protected] (如有欠缺还请指教)
时间:2016年10月9日16:00
Mybatis的核心对象就是SqlSession,它封装了框架方法数据库的所有操作。使用Mybatis框架第一件事就是获取SqlSession对象。那SqlSession对象从何而来呢?Mybatis提供了工厂对象(SqlSessionFactory)来构建SqlSession。那么我们下面来看下Mybatis是怎么通过读取XML配置文件来构建SqlSessionFactory工厂对象的。
以下代码是我们使用Mybatis的代码示例:
InputStream stream = SqlSessionFactory.class.getClassLoader().getResourceAsStream("Mybatis-conf.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(stream);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 调用Mybatis进行数据库操作代码 ......
sqlSession.close();
我们知道,SqlSessionFactory是通过SqlSessionFactoryBuilder来构建的。我们继续看下SqlSessionFacotyBuilder的build方法。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
Configuration config = parser.parse(); //Mybatis框架配置对象
return new DefaultSqlSessionFactory(config);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
try {
inputStream.close();
} catch (IOException e) {}
}
}
以上代码可以发现,Mybatis是通过XMLConfigBuilder对象来解析配置文件,生成Mybatis的配置对象Configuration。Configuration对象是Mybatis的基础,包含了框架所有的配置。Mybatis在运行时,都是通过此对象的属性来构建JDBC操作的封装。此篇文章就是详细说明XMLConfigBuilder是怎么来解析我们的Mybatis的XMl配置文件,生成Configuration对象的。知道了框架怎么解析配置,那自然懂得怎么去有效配置Mybatis框架,也即能够熟练使用Mybatis。
知道了Mybatis框架是通过XmlConfigBuilder来解析配置文件生成需要的Configuation配置对象的。我们自己看下XmlConfigBuilder的parse()解析方法:
// 解析mybatis的配置文件,返回Configuration对象
public Configuration parse() {
if (parsed) { //重复解析控制
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
XNode root = parser.evalNode("/configuration");
try {
propertiesElement(root.evalNode("properties"));
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
settingsElement(root.evalNode("settings"));
environmentsElement(root.evalNode("environments"));
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);
}
return configuration;
}
源码中我们看到有个XNode对象,这个对象是Mybatis框架中解析XML文件的对象。这个对象封装了很多XML解析中的通用方法,在平常开发中,如果需要对XML解析,可以直接使用,非常方便(大家有兴趣可以看下它的源码)。另外,对于XML配置文件的解析,我们应该同其约束文件一起来分析其解析过程(对于其他框架亦是如此)。
typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, plugins?, environments?, databaseIdProvider?, mappers?)>
在约束文件中看到,Mybatis配置文件根节点只能包含properties, typeAliases, plugins等几个节点。那对应Mybatis的配置文件解析,当然也是对这几个节点进行解析了。那下面我们就对各个节点的配置进行详细说明。
如果用过Spring就知道,Spring的配置文件中可以配置PropertyPlaceholderConfigurer对象,用于其XML配置中的变量配置。而Mybatis的properties标签于其作用一样。Mybatis配置文件的properteis节点DTD约束如下:
property*)>
#IMPLIED
url CDATA #IMPLIED
>
我们可以配置resource和url属性,也可以有property子标签。那我们看下Mybatis是怎么解析的。
// 解析properties配置
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
Properties defaults = context.getChildrenAsProperties();
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
if (resource != null && url != null) { // resource和url只能同时配置一个
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
configuration.setVariables(defaults); // 将属性设置到Configuration对象
parser.setVariables(defaults);
}
}
我们可以看出,properties标签是不能同时配置resource和url属性的。并且,properties下的property标签的值会覆盖resource或者url外部的联接文件。properties标签就是这么简单,解析完生成java.util.Properties对象设置到Configuration对象属性中,供其他配置使用。当然,其他配置项必须是property标签, 如:
Mybatis中的别名就是用来简化配置的。如果有一对象com.tianba.mybatis.domain.User,我们要配置SQL查询的返回结果是User对象,那么我们需要在Select标签中的resultType属性值为com.tianba.mybatis.domain.User。使用别名之后,我们给这个对象定义别名为user,那么我们在用到这个对象的配置时候,只要写它的别名即可,无需写类的全名。先看Mybatis怎么配置别名。
<typeAliases>
<typeAlias type="com.tianba.mybatis.domain.User" alias="user"/>
"com.tianba.mybatis.domain"/>
typeAliases>
当然,如果项目中有很多个POJO对象,也不需要我们一个一个的配置,只需要指定package即可。
private void typeAliasesElement(XNode parent) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// package 配置
String typeAliasPackage = child.getStringAttribute("name");
typeAliasRegistry.registerAliases(typeAliasPackage);
} else {
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
我们发现,Mybatis的别名是通过TypeAliasRegistry对象来注册的。TypeAliasRegistry其实很简单,使用map来保存Class的别名。它有多个registerAlias方法,可以提供package,这样的话他默认使用类名来做别名(com.tianba.mybatis.domain.User -> User),另外也可以使用@Alias注解在类上标记别名。具体可以看它的源码。PS:TypeAliasRegistry中使用ResolverUtil来获取package下所有的Class。这个工具类也很实用哦。
另外,Mybatis在创建Configuration对象的时候就已经添加了不少别名,相关别名可以参考Configuration的构造函数和TypeAliasRegistry的构造函数。
MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能。我们这里只介绍拦截器怎么配置,及Mybatis怎么解析拦截器的配置(拦截器的原理详解请关注我的文集,后续会继续推出相关文章)。配置示例:
<plugins>
<plugin interceptor="com.tianba.mybatis.interceptor.PageHelper">
<property name="defaultPageSize" value="20" />
plugin>
<plugin interceptor="com.tianba.mybatis.interceptor.CloseLocalCacheInterceptor" />
plugins>
继续看下Mybatis怎么解析
private void pluginElement(XNode parent) throws Exception {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
// configuration.addInterceptor
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor); //拦截器执行链
}
Mybatis的配置对象中维护着一个拦截器执行链对象InterceptorChain,解析plugins也就是简单的网执行链中添加一个拦截器对象。当然,拦截器类的@Intercepts注解是必须的。
Mybatis中的setting配置和properties配置一样,也是配置参数名和参数值。但到底和properteis有什么区别呢?这个也是我当时使用Mybatis中困恼的一个问题。但看了源码之后就很清晰了。我们知道properties的配置参数是为其他的配置服务的,配置项不是不定的。而settings的配置项是配置Configuration对象的属性的,配置项定死就那么几个,不配的话框架有默认值。当然setting的范围没有在XML中约束,个人觉得Mybatis团队应该把这个约束加上。所以,没看源码是不清晰settings配置的。我们先看源码:
// 解析settings配置,对应Configuration对象属性的set方法
private void settingsElement(XNode context) throws Exception {
if (context != null) {
Properties props = context.getChildrenAsProperties();
// 检查所有配置的key在Configuration中是否有对应Set方法
MetaClass metaConfig = MetaClass.forClass(Configuration.class);
for (Object key : props.keySet()) {
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), true));
configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
configuration.setLogPrefix(props.getProperty("logPrefix"));
configuration.setLogImpl(resolveClass(props.getProperty("logImpl")));
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
}
}
一看源码就清晰了,settings配置能配哪些属性都在这里了。那具体这些属性该配置什么呢?从官方文档截图如下:
如果在项目中Mybatis和Spring配合使用,那environments的配置是省略的。此配置项是配置数据库连接池和事物管理的。若何Spring配合使用,则事务的管理和数据库连接池一般都是交给Spring控制。先看配置的示例:
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${user}"/>
<property name="password" value="${passwd}"/>
dataSource>
environment>
<environment id="product">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${product.driver}"/>
<property name="url" value="${product.url}"/>
<property name="username" value="${product.user}"/>
<property name="password" value="${product.passwd}"/>
dataSource>
environment>
environments>
在environments标签下可以配置多个environment标签。这个怎么理解呢?在我们开发的时候有关数据库的配置肯定都是生产和测试分开的。那我们这里可以配置两个environment节点,product对应生产,development对应测试开发。当我们环境切换的时候,只要把environments的default属性改成要使用的environment的id属性即可。
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// 解析environments节点的default属性的值 例如:
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) { //isSpecial就是根据由environments的default属性去选择对应的enviroment
// 事务, mybatis有两种:JDBC 和 MANAGED, 配置为JDBC则直接使用JDBC的事务,配置为MANAGED则是将事务托管给容器,
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// enviroment节点下面就是dataSource节点了,解析dataSource节点
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
configuration.setEnvironment(new Environment(id, txFactory, dataSource));
}
}
}
}
从源码看出来,我们的transactionManager中的type属性即配置了事物工厂的实现类,当然,这里使用的就是别名。dataSource中的type也是配置了DataSourceFactory的实现类。在Configuration初始化时候,已经添加了这两个工厂接口对应的实现类别名了。
public Configuration() {
//TransactionFactory
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); // Mybatis内部的JDBC事务管理器
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); // 外部容器事务管理器
//DataSourceFactory
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); // Jndi数据源
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
// other alias
.......
}
那这两个type的配置和其含义已经很清晰了。具体区别可以看其对应实现类的源码,很简单明了。如果这些都不能满足需求,那我可以还自定义自己的实现,只有实现相应的接口即可,然后在type属性配置上我们自己的实现类(package.className)即可。从这里看出,Mybatis的扩展还是做得很好的。
Java有java的数据类型,数据库有数据库的数据类型,那么Mybatis在往数据库中插入数据的时候是如何把java类型转换成数据库类型插入数据库,在从数据库读取数据的时候又是如何把数据库类型转换成java类型来处理呢?这中间必然要经过一个类型转换。Mybatis中提供一个叫做TypeHandler类型处理器的东西,通过它可以实现Java类型跟数据库类型的相互转换。我们先看配置
<typeHandlers>
<typeHandler handler="com.tianba.mybatis.typehandlers.CustomTimeStampHandler" javaType="java.util.Date" jdbcType="VARCHAR" />
<package name="com.tianba.mybatis.typehandlers" />
typeHandlers>
看配置,和typeAliases的配置项很相似。
private void typeHandlerElement(XNode parent) throws Exception {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
String javaTypeName = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
Class> javaTypeClass = resolveClass(javaTypeName);
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
Class> typeHandlerClass = resolveClass(handlerTypeName);
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
是的,和typeAliases类似,typeHandler也是通过typehandlerRegistry来注册的,只是在注册的时候要确定javaType和对应的jdbcType。如果是通过package扫描注册的,则必须在类上通过注解标记。大部分情况下,Mybatis提供的类型转换已经足够我们使用了,在TypeHandlerRegistry类构造时候,已经为我们提供了很多的类型转换器了。
public TypeHandlerRegistry() {
register(Boolean.class, new BooleanTypeHandler());
register(boolean.class, new BooleanTypeHandler());
register(JdbcType.BOOLEAN, new BooleanTypeHandler());
register(JdbcType.BIT, new BooleanTypeHandler());
register(Byte.class, new ByteTypeHandler());
register(byte.class, new ByteTypeHandler());
......
}
要是有需要自定义类型转换器,也很简单。参考Mybatis内部类型转换实现类,依葫芦画瓢。
在Mybatis配置文件解析中,对Mapper的解析是最复杂的。Mapper的解析是由XMLMapperBuilder对象来进行的。先看配置
<mappers>
<mapper resource="MybatisConfig/UserMapper.xml"/>
<mapper class="com.tianba.mybatis.persistence.UserMapper"/>
<mapper url="http://www.52tianba.com/xxx/xxx"/>
mappers>
我们看到,可以配置Mapper.xml,还可以配置接口。配置接口的话那必须保证所对应的xml文件名和文件路径都和类保持一致。
private void mapperElement(XNode parent) throws Exception {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
这里我不打算详细分析Mybatis怎么对Mapper.xml进行解析的,后面我会出一篇文章专门分析Mybatis怎么解析Mapper.xml文件的(期待吧…)。
Mybatis配置文件解析过程其实就是把XML文件的配置解析成Configuration对象。当然,如果你熟悉的话完全可以不需要使用他的配置,直接使用代码初始化Configuration对象也是一样的。我相信只要你理解了这篇文章,你完全可以做到。