一、解析配置文件
上一篇文章说到,SqlSessionFactory的配置都是委托给XMLConfigBuilder的parse方法完成的,本篇就来看看解析工程都做了哪些事情。
//解析配置
public Configuration parse() {
//如果已经解析过了,报错
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//根节点是configuration
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
首先检查下是否已经解析过了,如果直接抛异常,在这里贴一份常见的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">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
Mybatis解析xml用的是自己封装的一个名为XPathParser的工具类(使用的全是JDK的类包),至于怎么具体解析的,这里不做展开。如果是第一次解析,则从“/configuation”节点开始解析,代码如下:
private void parseConfiguration(XNode root) {
try {
//分步骤解析
//1.properties
propertiesElement(root.evalNode("properties"));
//2.类型别名
typeAliasesElement(root.evalNode("typeAliases"));
//3.插件
pluginElement(root.evalNode("plugins"));
//4.对象工厂
objectFactoryElement(root.evalNode("objectFactory"));
//5.对象包装工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//6.设置
settingsElement(root.evalNode("settings"));
// read it after objectFactory and objectWrapperFactory issue #631
//7.环境
environmentsElement(root.evalNode("environments"));
//8.databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//9.类型处理器
typeHandlerElement(root.evalNode("typeHandlers"));
//10.映射器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
二、十个解析项
上边的代码就是解析配置的核心,总共10个步骤,我们一一来看看:
2.1、properties元素
在properties下配置如下属性:
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>
可以在该配置文件上全局使用,用于替换其它配置的变量,比如:
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
另外,配置的Properties还可以传递到SqlSessionFactoryBuilder.build() 方法中使用,如下
SqlSessionFactory factory =
sqlSessionFactoryBuilder.build(reader, props);
// ... or ...
SqlSessionFactory factory =
new SqlSessionFactoryBuilder.build(reader, environment, props);
如果相同的配置项存在不同的位置,加载是怎样的?我们从代码里一看究竟,代码如下:
if (context != null) {
//properties元素下的属性会被首先加载
Properties defaults = context.getChildrenAsProperties();
//第二顺序被加载的是类路径下的resource或者url,如果与之前指定的属性重复,会覆盖掉之前加载的属性
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
if (resource != null && url != null) {
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);
}
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
看代码逻辑,属性加载的顺序如下:
- properties元素下的属性会被首先加载
- 第二顺序被加载的是类路径下的resource或者url,如果与之前指定的属性重复,会覆盖掉之前加载的属性。
- 通过方法参数被传递进来的最后被加载,它会覆盖掉前面两步所有与之重复的属性。
2.2、typeAliases元素
<typeAliases>
<typeAlias alias="Author" type="domain.blog.Author"/>
<typeAlias alias="Blog" type="domain.blog.Blog"/>
</typeAliases>
别名是为了简化java类型的实体全路径名过长的问题,如上,通过配置,Blog可以在任何地方代替domain.blog.Blog使用.
同理,也可以给J整个包起一个别名,方便使用,如果没有特殊说明使用全小写的类名作为别名:
<typeAliases>
<package name="domain.blog"/>
</typeAliases>
另外,也支持通过注解的形式指定别名
@Alias("author")
public class Author {
...
}
看一看解析别名的源码
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//如果是package
String typeAliasPackage = child.getStringAttribute("name");
//(一)调用TypeAliasRegistry.registerAliases,去包下找所有类,然后注册别名(有@Alias注解则用,没有则取类的simpleName)
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
//如果是typeAlias
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
//根据Class名字来注册类型别名
//(二)调用TypeAliasRegistry.registerAlias
if (alias == null) {
//alias可以省略
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
...
}
}
}
}
}
可以看出在解析别名过程中会从上至下遍历所有别名,登记到别名的map中。
如果我在配置别名时写成:
<typeAliases>
<typeAlias alias="goods_alias" type="org.apache.ibatis.wyjdemo.MbGoods"/>
<package name="org.apache.ibatis.wyjdemo"/>
</typeAliases>
即单独指定和通过包名同时配置,注册时会变成怎么样呢?打断点看下
可以看到两种方式都生效了,两个别名指向了一个bean.
2.3、pluginElement元素
Mybatis支持拓展插件实现丰富的功能,它允许你在映射语句执行过程汇中的某一点进行拦截调用,通常来说,Mybatis允许插件在以下几个方法调用拦截:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
这些类方法的详细介绍可以在发行的源码中方法签名查看更多细节。想要编写插件也非常简单,只需要实现Interceptor接口,并指定要拦截的方法签名即可。
我们自己实现一个非常简单的插件来感受下,插件的作用,首先要实现Interceptor接口,
@Intercepts({@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class }
)})
public class MySimplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 执行目标方法的前置处理
Object[] args = invocation.getArgs();
for (Object arg : args) {
System.out.println(arg);
}
Object returnObject = invocation.proceed();
// 执行目标方法的后置处理
return returnObject;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
签名这里我配置的是Excutor,并且拦截query方法多一些文章,实现比较简单就是打印出查询参数,然后再配置文件中加上,插件相关配置:
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.apache.ibatis.wyjdemo.MySimplePlugin">
</plugin>
</plugins>
执行输出如下
org.apache.ibatis.mapping.MappedStatement@38082d64
{id=12, param1=12}
org.apache.ibatis.session.RowBounds@dfd3711
null
这个四个值依次对应,@Signature注解中args属性配置的值。
说回正题,我们看下源码中是如何解析插件的配置的
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
//调用InterceptorChain.addInterceptor
configuration.addInterceptor(interceptorInstance);
}
}
}
源码很简单,总体来说就是从配置文件中解拿到插件配置的拦截器名称,然后实例化一个拦截器对象放到拦截器链中等待处理。核心就是调用resovleClass方法根据配置的拦截名获取一个该拦截的实例化对象。它又会调用resolveAlias实际去处理,resolveAlias源码如下:
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) {
return null;
}
String key = string.toLowerCase(Locale.ENGLISH);
Class<T> value;
//原理就很简单了,从HashMap里找对应的键值,找到则返回类型别名对应的Class
if (TYPE_ALIASES.containsKey(key)) {
value = (Class<T>) TYPE_ALIASES.get(key);
} else {
//找不到,再试着将String直接转成Class(这样怪不得我们也可以直接用java.lang.Integer的方式定义,也可以就int这么定义)
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
内容就是先从上一步维护的别名映射map中找寻是否已经拥有该实例(也就是拦截器中的配置也是支持别名的),没有找到的话用反射得到一个实例返回。
2.4、objectFactoryElement元素
对象工厂主要的作用就是生产封装结果集所需要的对象,Mybatis默认的对象工厂,不会对结果集做特殊的处理,仅是实例化一个目标对象,如果需要对结果对象进行定制化的处理,则要继承DefaultObjectFactory,重写父类方法,源码也非常简单,如下
private void objectFactoryElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
Properties properties = context.getChildrenAsProperties();
ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance();
factory.setProperties(properties);
configuration.setObjectFactory(factory);
}
}
2.5、objectFactoryElement元素
对象包装工程,官网文档上没有介绍,我自己也没有搞明白,先略过。。。
2.6、settingsElement元素
顾名思义是对Mybatis进行设置,这会影响Mybatis的运行方式,可以理解为是Mybatis的一些不同工能的开关,源码非常简单:
private void settingsElement(XNode context) throws Exception {
if (context != null) {
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
//检查下是否在Configuration类里都有相应的setter方法(没有拼写错误)
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")));
...
}
}
源码中可以看到,设置前先检查了setting节点中配置的属性key值的有效性,如果不是Configuration.class中的属性则在启动时就抛出异常,之后就是逐个读配置设置到configuration中了。(另:这个过程中用到的Reflector类,可以学习下)
2.7、environmentsElement元素
Mybatis支持多个环境配置,这允许我们写的SQL可以映射到多个数据库,例如,我们的开发、测试、生产会有不同环境等,在很多场景下都会有不同环境配置的需求,通过这个配置可以满足。
但是要注意,尽管我们可以配置多个环境,但是每个环境只能选择一个sqlSeesionFactory实例。如果我们需要连接两个数据库,就需要创建两个sqlSessionFactory,以此类推。很容易记着:每个数据都需要一个sqlSessionFactroy与之对应
看下配置文件:
<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>
源码部分:
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");
//循环比较id是否就是指定的environment
if (isSpecifiedEnvironment(id)) {
//7.1事务管理器
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
//7.2数据源
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());
}
}
}
}
首先判断是否指定了环境,未指定就使用默认环境,然后遍历environment节点进行配置,由上边代码可以看到配置的主要内容:加载事务管理器、数据源分别到TransactionFactory和DataSourceFactory,然后赋值到configuration中。
2.8、databaseIdProviderElement元素
Mybaits还支持根据不同数据库厂商执行不同的SQL语句,其实不是很实用,很少生产项目会用不同厂商的数据库,这里也不多做介绍。
2.9、typeHandlerElement元素
类型处理器,Mybatis无论是在设置参数或者从结果集获取数据都会用到类型处理器,它主要的功能是
获取到的值转换成对应的Java类型数据。
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
<typeHandlers>
<package name="org.mybatis.example"/>
</typeHandlers>
看配置文件就可以知道,这个元素的方式也是同时支持单个处理器配置以及扫描包下的处理器两种方式的,源码如下:
private void typeHandlerElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//如果是package
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
//(一)调用TypeHandlerRegistry.register,去包下找所有类
typeHandlerRegistry.register(typeHandlerPackage);
} else {
//如果是typeHandler
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);
//(二)调用TypeHandlerRegistry.register(以下是3种不同的参数形式)
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
循环遍历typeHandlers节点下元素,优先扫描包下处理器,不存在则去单配总获取。可自行实现定制化的typeHandler,参考 自定义typeHandler
2.10、mapperElement元素
既然上边几个步骤我们配置了这么多的元素,最后我们要配置SQL映射了,但是怎么找到配置的映射关系呢?
Mybatis提供四种方法,从配置文件可以看出
<!-- Using classpath relative resources -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- Using url fully qualified paths -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- Using mapper interface classes -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- Register all interfaces in a package as mappers -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
结合源码看:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//10.4自动扫描包下所有映射器
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) {
//10.1使用类路径
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//映射器比较复杂,调用XMLMapperBuilder
//注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
//10.2使用绝对url路径
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
//映射器比较复杂,调用XMLMapperBuilder
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
//10.3使用java类名
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.");
}
}
}
}
}
- 首先看是否用扫描包的方式配置。
- 如果是非包扫描方式,则会从resource、url、classs三种方式中按照顺序加载一种方式。
- 上述方式都没有采用的话,则会抛出异常。
总结
至此,XMLConfigbuilder初始化的过程我们已经过了一遍,这些步骤其实就是一个读Mybatis配置文件的过程,为后续的运行进行环境配置以及参数设置,到这一步,Configuration已经被“填充”完毕。