Mybatis-源码分析之配置解析

复习总结:

  1. mybatis通过sqlSession调用方法来执行sql查询,sqlSession是由单例对象sqlSessionFactory创建的
  2. 单例对象sqlSessionFactory的创建需要解析全局配置文件,根据配置对象创建出DefaultSqlSessionFactory
  3. 全局配置的解析重点在于mapper的解析,根据mapper配置方式不同分为xml解析和接口解析,每一个解析完成的mapper最终都会调用addMapper方法存储到Configuration.MapperRegistry对象中,该对象维护了每个mapper接口到代理生成对象的映射
  4. mapper中的每一个statement最终都会被解析成一个MappedStatement对象,解析的重点在于SqlSource的创建,动态的、静态的有所区别,最终MappedStatement对象会被添加到Configuration.mappedStatements对象中,这是另一个map,用于维护statementId到MappedStatement对象的映射。
  5. statementId 就是mapper接口对应的查询方法的全限定名,也是mapper映射器中namespace+”.”+sqlId,二者保持绝对的一致。

之前的文章提到使用mybatis的两种方式:

  • 使用statementId硬编码方式调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void testStatement() throws IOException {
    SqlSession session = sqlSessionFactory.openSession();
    try {
    Blog blog = (Blog) session.selectOne("com.panda.mybatis.mapper.BlogMapper.selectBlogById", 1);
    System.out.println(blog);
    } finally {
    session.close();
    }
    }
  • 使用mapper接口调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public void testSelect() throws IOException {
    SqlSession session = sqlSessionFactory.openSession(); // ExecutorType.BATCH
    try {
    BlogMapper mapper = session.getMapper(BlogMapper.class);
    Blog blog = mapper.selectBlogById(1);
    System.out.println(blog);
    } finally {
    session.close();
    }
    }

这两种方式只是在确定statementId的方式上有些不同,最终都是通过SqlSession来执行sql查询的,所以sqlSession就是本文的分析入口。

mybatis中每个SqlSession都会持有一个jdbc连接,SqlSession的接口方法经过层层调用最终都会调用到jdbc的方法,那么我们怎么获取一个SqlSession呢?mybatis为我们提供了一个SqlSessionFactory来获取SqlSession,下面的讲述从SqlSessionFactory的创建开始。

sqlSessionFactory创建代码如下,该对象在应用中以单例形式存在,生命周期与应用相同,一旦创建,就会一直存在。

1
2
3
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

将全局配置文件的文件流传入build方法,创建出一个SqlSessionFactory对象。

1、全局配置的解析

进入SqlSessionFactoryBuilder.build方法(贴出核心代码,忽略部分):

1
2
3
4
5
6
7
8
9
10
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 用于解析 mybatis-config.xml,同时创建了 Configuration 对象 >>
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 解析XML,最终返回一个 DefaultSqlSessionFactory >>
return build(parser.parse());
} catch (Exception e) {
...
}
}

主要流程分为:

  1. 创建XMLConfigBuilder对象作为全局配置解析器
  2. 解析全局配置文件,解析完成获得全局Configuration对象(核心)
  3. 使用Configuration对象创建一个SqlSessionFactory

具体xml文件的解析过程不做展开,主要关心解析出了哪些东西,解析器XMLConfigBuilder继承自BaseConfigBuilder,父类中声明了一个Configuration成员对象,进入parse()方法:

1
2
3
4
5
6
7
8
// XMLConfigBuilder
public Configuration parse() {
...
parsed = true;
// XPathParser,dom 和 SAX 都有用到 >>
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

首先找到”/configuration”标签,继续跟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void parseConfiguration(XNode root) {
try {
// 对于全局配置文件各种标签的解析
propertiesElement(root.evalNode("properties"));
// 解析 settings 标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 类型别名
typeAliasesElement(root.evalNode("typeAliases"));
// 插件
pluginElement(root.evalNode("plugins"));
// 用于创建对象
objectFactoryElement(root.evalNode("objectFactory"));
// 用于对对象进行加工
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 反射工具箱
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// settings 子标签赋值,默认值就是在这里提供的 >>
settingsElement(settings);
// 创建了数据源 >>
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析引用的Mapper映射器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}

解析各种子标签:

1.1 properties

用来配置参数信息,比如最常见的数据库连接信息,为了避免直接把参数写死在xml中,我们可以把这些参数单独放在properties文件中,用标签引用进来,在xml配置中就可以使用”${}”的形式进行引用。

1
2
3
4
5
6
7
8
9
10
11
12
<properties resource="db.properties"></properties>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>

可以使用resource引用应用里的相对路劲,也可以是用url指定本地服务器或者网络的绝对路径

1.2 settings

里面是mybatis内部的一些核心配置

1.3 typeAliases

该标签配置的是类型别名,主要用来简化配置中的全类名。如果每个地方都配置全类名的话,内容会比较多,所以我们可以为自己的Bean创建别名,既可以指定单个类,也可以指定一个package,自动转换

1
2
3
4
<typeAliases>
<typeAlias alias="blog" type="com.panda.domain.Blog" />
<package name="com.panda.domain"/>
</typeAliases>

配置别名之后,在映射器配置文件中只需要写别名就行了,例如:

1
2
3
<select id="selectBlogByBean"  parameterType="blog" resultType="blog" >
select bid, name, author_id authorId from blog where name = '${name}'
</select>
1.4 typeHandlers

配置java类型与jdbc类型相互转换的处理器,mybatis对于常见的类型已经注册好了一批Handler,对于用户自定义类型,需要自定义编写handler进行转换,例如json对象转换成varchar

1.5 objectFactory

数据库返回结果集之后需要转换成java对象,此时需要创建对象实例,由于我们不知道需要处理的类型是什么,有哪些属性,所以不能使用new的方式去创建,只能通过反射。

在mybatis中提供了一个工厂类的接口,叫ObjectFactory专门用来创建对象的实例,里面定义了4个方法:

1
2
3
4
5
6
7
public interface ObjectFactory {
default void setProperties(Properties properties) {// NOP
}
<T> T create(Class<T> type);
<T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs);
<T> boolean isCollection(Class<T> type);
}

有一个默认实现类DefaultObjectFactory,创建对象始终都调用了instantiateClass方法,方法中使用反射。默认情况下所有对象都是由该对象创建。

扩展:如果想要修改对象工厂在初始化实体类时候的行为,可以通过继承DefaultObjectFactory自定义对象工厂,并在objectFactory注册,创建对象时会调用到自定义的对象工厂

1.6 plugin

插件是Mybatis很强大的机制,更很多其他框架一样,mybatis预留了插件的接口,让mybatis更容易扩展,后面专门分析

1.7 environments

此标签用来管理数据库环境,比如我们可以有开发环境测试环境生产环境的数据库,可以再不同环境中使用不同的数据源

一个标签代表一个数据源,这里面有两个关键的子标签,一个是事务管理器标签,一个是数据源标签

如果配置的是“JDBC” 则会使用Connection对象的commit、rollback、close 来管理事务

如果配置的是“MANAGED”,mybatis会把事务交给容器管理,比如Jboss、Weblogic

1.8 mapper

mappers标签配置的是mybatis中可能会执行到的sql语句半成品,全局配置中的mappers有两种配置方式:

1
2
3
4
5
6
7
8
<mappers> 
<package name="com.panda.mybatis.mapper"/>
</mappers>
<mappers>
<mapper resource="BlogMapper.xml"/>
<mapper url="e://....//BlogMapperExt.xml"/>
<mapper mapperClass="com.panda.mybatis.mapper.BlogMapper"/>
</mappers>

2、Mapper映射器的解析

1
mapperElement(root.evalNode("mappers"));

跟进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//XMLConfigBuilder.class
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 不同的mapper配置方式解析方式也有点区别,但最终都是调用 addMapper()方法(添加到 MapperRegistry)。这个方法和 getMapper() 对应
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) {
// resource 相对路径
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 解析 Mapper.xml,总体上做了两件事情 >>
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
// url 绝对路径, 与相对路径流程大差不差
...
} else if (resource == null && url == null && mapperClass != null) {
// class 单个接口
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
...
}
}
}
}
}

对mapper的解析主要分为两种:

  • mapper.xml的解析—使用XMLMapperBuilder类进行解析
  • mapper接口的解析—调用addMapper(...)方法将mapper添加到MapperRegistry中。
2.1 MapperRegistry

这个类用于存放被解析出来的mapper接口

1
2
3
4
5
public class MapperRegistry {
private final Configuration config; // 全局配置对象
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
...
}

可以看到该类中维护了一个map,存放mapper接口到代理工厂类的映射,此处设计动态代理将在分析执行阶段时进行详细的分析。目前只需要了解被解析过的mapper会被添加到这个类的实例中。

2.2 XMLMapperBuilder

XMLMapperBuilder用于对xml映射器进行解析,跟进XMLMapperBuilder的parse方法,它解析的就是xml映射器文件了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//XMLMapperBuilder.class
public void parse() {
// 总体上做了两件事情,对于语句的注册和接口的注册
if (!configuration.isResourceLoaded(resource)) {
// 1、具体增删改查标签的解析。 一个查询标签被解析成一个MappedStatement。 >>
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
// 2、把namespace(接口类型)映射到一个工厂类MapperProxyFactory >>
bindMapperForNamespace();
}

parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}

解析mapper映射器主要做两件事:

  • 解析具体的增删改查标签,每个标签被解析成一个MappedStatement
  • 吧namespace标签映射到一个工厂类MapperProxyFactory,就是MapperRegistry中的map中
2.2.1 configurationElement
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//XMLMapperBuilder.class
private void configurationElement(XNode context) {
// 添加缓存对象
cacheRefElement(context.evalNode("cache-ref"));
// 解析 cache 属性,添加缓存对象
cacheElement(context.evalNode("cache"));
// 创建 ParameterMapping 对象
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 创建 List<ResultMapping>
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析可以复用的SQL
sqlElement(context.evalNodes("/mapper/sql"));
// 解析增删改查标签,得到 MappedStatement >>
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
...
}

private void buildStatementFromContext(List<XNode> list) {
...
// 解析 Statement >>
buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// 用来解析增删改查标签的 XMLStatementBuilder
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
// 解析 Statement,添加 MappedStatement 对象 >>
statementParser.parseStatementNode();
}
}
1
2
3
4
5
6
//XMLStatementBuilder.class
public void parseStatementNode() {
...
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
...
}

这里就开始将xml中的sql语句进行解析,一条查询语句解析成一个MappedStatement对象,我们主要关注MS的成员变量sqlSourcesqlSource是对xml映射文件中的配置查询的抽象,每条查询配置对应一个sqlSource

跟进createSqlSource

1
2
3
4
5
6
7
//XMLLanguageDriver.class
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
//创建sql脚本解析器
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
//解析脚本 >>
return builder.parseScriptNode();
}

XMLScriptBuilder是用于解析单条sql配置脚本的类

1
2
3
4
5
6
7
8
9
10
11
//XMLScriptBuilder.class
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}

通常我们写的sql配置解析成的类型有StaticSqlSourceDynamicSqlSource两种,很明显这两种类型的对应了静态sql和动态sql,其实对于静态sql,mybatis更常用的是RawSqlSource,因为它在启动的时候就已经计算好了mapping,性能好一些。

parseDynamicTags()

首先创建MixedSqlNode作为root节点,然后遍历所有子标签

  • 如果子标签是静态文本的话,查看文本中有没有“${}”占位符,有的话用文本创建子节点TextSqlNode,并将当前的 isDynamic= true,这种情况属于sql拼接,有sql注入的风险
  • 若没有该占位符,则用静态文本创建子节点StaticTextSqlNode
  • 如果子标签是动态标签(9个动态标签之一),使用指定标签的Handler创建相应类型的子节点,并将isDynamic= true
  • 创建的所有子节点将add到root节点的contents中

然后XMLScriptBuilder根据isDynamic的值来创建sqlSource对象:如果isDynamic= true则创建DynamicSqlSource,否则创建RawSqlSource

Raw是生的,未加工的意思,RawSqlSource是还没加工好的StaticSqlSource,它持有一个SqlSource引用,实例化的时候会创建一个StaticSqlSource并赋值给该引用,创建StaticSqlSource时已经将#{}占位符替换成了?占位,替换#{}时,每处理一个占位符就会生成一个ParameterMapping,最终得到一个名为`ParameterMappings的List

这样设计的原因:静态sql语句不受运行时参数影响,因此在解析阶段就可以将sql、parameterMapping等创建出来,提高静态查询的性能。

让我们回到XMLStatementBuilder代码

1
2
3
4
5
6
7
8
9
10
11
//XMLStatementBuilder.class
public void parseStatementNode() {
...
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
...
// >> 关键的一步: MappedStatement 的创建
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

addMappedStatement方法参数非常多,导致方法体很长,但是逻辑确很清晰,这里不贴代码,简单描述一下流程:

  • 首先使用传入的众多参数,初始化了一个MappedStatement.Builder,显然是建造者模式;
  • 然后使用Builder建造一个MappedStatement,并将这个MS加入到Configuration对象的mappedStatements

Map<String, MappedStatement> mappedStatements

这是Configuration对象用于存储MappedStatement的地方,key是ms的id,就是sql配置文件的 namespace + “.” + sqlid

ms解析完成之后我们再回到XMLMapperBuilder.parse()方法,继续看第二步。

1
2
3
4
5
6
7
8
9
10
11
12
//XMLMapperBuilder.class
public void parse() {
// 总体上做了两件事情,对于语句的注册和接口的注册
if (!configuration.isResourceLoaded(resource)) {
// 1、具体增删改查标签的解析。 一个查询标签被解析成一个MappedStatement。 >>
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
// 2、把namespace(接口类型)映射到一个工厂类MapperProxyFactory >>
bindMapperForNamespace();
}
...
}
2.2.2 bindMapperForNamespace()

该方法主要负责把namespace(接口类型)映射到一个工厂类MapperProxyFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//XMLMapperBuilder.class
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
...
boundType = Resources.classForName(namespace);
...
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
configuration.addLoadedResource("namespace:" + namespace);
// 添加到 MapperRegistry,本质是一个 map,里面也有 Configuration >>
configuration.addMapper(boundType);
}
}
}
}

判断configuration中有没有注册该接口,没有的添加到

1
2
3
4
//Configuration.class
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type); // >>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//MapperRegistry.class
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
...
boolean loadCompleted = false;
try {
// !Map<Class<?>, MapperProxyFactory<?>> 存放的是接口类型,和对应的工厂类的关系
knownMappers.put(type, new MapperProxyFactory<>(type));
// 注册了接口之后,根据接口,开始解析所有方法上的注解,例如 @Select >>
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}

configuration持有的MapperRegistry对象,用来管理所有的mapper接口与其代理类工厂的映射,从这段代码我们了解到,代理类工厂的类型是MapperProxyFactory,这是一个很重要的类,类中提供了创建mapper代理对象的方法

MapperProxyFactory

本文先简单的介绍一下MapperProxyFactory,后续在分析执行阶段在详细分析动态代理在这里的执行过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MapperProxyFactory<T> {

private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}

public Class<T> getMapperInterface() {
return mapperInterface;
}

public Map<Method, MapperMethodInvoker> getMethodCache() {
return methodCache;
}

@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
// 1:类加载器:2:被代理类实现的接口、3:实现了 InvocationHandler 的触发管理类
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}

因为这里使用的是JDK动态代理,所以创建代理对象需要一个实现了InvocationHandler接口的触发管理类,调用代理对象的方法都会进入到该管理类的invoke方法。

sql的执行是有sqlSession发起的,所以触发管理类需要获取执行当前sql的sqlsession对象,这样执行代理对象的查询方法才会进入到sqlSession中。

解析阶段的主要流程大致如上,很多细节没有扣,留待以后慢慢学习

总结:

  1. 解析主要是全局解析和映射器解析两块,全局解析主要是一些关键配置,映射器解析主要是解析得到MS对象
  2. Ms对象是mybatis中很重要的对象,尤其是其成员sqlSource
  3. 解析阶段还为每个mapper接口生成了代理类工厂,这块是mybatis的重要设计