文章目录
- 前言
- 一、SqlSessionFactoryBuilder
- 1.1、XMLConfigBuilder
- 1.2、parse
- 二、mappers标签的解析
- 2.1、cacheElement
- 2.1.1、缓存策略
- 2.2、buildStatementFromContext
- 2.2.1、sql的解析
前言
本篇主要介绍MyBatis源码中的配置文件解析
部分。MyBatis是对于传统JDBC的封装,屏蔽了传统JDBC与数据库进行交互,组装参数,获取查询结果并自己封装成对象的繁琐过程。
原生MyBatis首先需要配置mybatis-config.xml
:
<?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>
<properties resource="jdbc.properties"/>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<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>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
并且指定数据源jdbc.properties
:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456
创建数据库访问层接口:
public interface UserMapper {
List<User> selectAll();
User selectById(int id);
void insert(User user);
void update(User user);
void delete(int id);
}
以及对应的xml文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mybatis.mapper.UserMapper">
<cache/>
<resultMap id="userResultMap" type="com.example.mybatis.entity.User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="age" column="age"/>
</resultMap>
<select id="selectAll" resultMap="userResultMap">
SELECT * FROM users
</select>
<select id="selectById" resultMap="userResultMap" parameterType="int">
SELECT * FROM users WHERE id = #{id}
</select>
<insert id="insert" parameterType="com.example.mybatis.entity.User">
INSERT INTO users (name, age) VALUES (#{name}, #{age})
</insert>
<update id="update" parameterType="com.example.mybatis.entity.User">
UPDATE users SET name = #{name}, age = #{age} WHERE id = #{id}
</update>
<delete id="delete" parameterType="int">
DELETE FROM users WHERE id = #{id}
</delete>
</mapper>
mybatis-config.xml
常见的标签:
标签 | 作用 |
---|---|
<settings> | 控制 MyBatis 全局行为(缓存、懒加载、日志等) |
<typeAliases> | 设置类型别名,简化 Mapper XML 中类名书写 |
<typeHandlers> | 自定义类型转换器(Java类型 ↔ JDBC类型) |
<plugins> | 注册插件(如分页插件、SQL打印等) |
<objectFactory> | 自定义对象创建逻辑 |
<environments> | 配置数据库环境及事务管理 |
<mappers> | 注册 Mapper 映射文件或 Mapper 接口 |
原生MyBatis的使用,其中读取配置文件并进行解析,主要体现在SqlSessionFactoryBuilder
的build
方法中:
public class Main {
public static void main(String[] args) throws Exception {
//将xml构筑成configuration配置类
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
//解析xml,注册成SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
try (SqlSession session = sqlSessionFactory.openSession()) {
User user = session.selectOne("com.example.mybatis.mapper.UserMapper.selectById", 1);
System.out.println(user);
}
}
}
一、SqlSessionFactoryBuilder
1.1、XMLConfigBuilder
在调用SqlSessionFactoryBuilder
的build
方法时,首先会去创建一个XMLConfigBuilder
,目的是构建一个XML配置文件解析器对象。
其中的核心代码,这段代码的作用是注册别名,将配置文件中的 “JDBC”、"POOLED"等关键词和实际的类型进行绑定。
别名 | 实际类 | 用途 |
---|---|---|
"JDBC" | JdbcTransactionFactory | JDBC事务管理器(默认事务方式) |
"MANAGED" | ManagedTransactionFactory | 受容器管理的事务(如 Spring) |
"JNDI" | JndiDataSourceFactory | 从 JNDI 获取数据源 |
"POOLED" | PooledDataSourceFactory | 数据库连接池(MyBatis 内置) |
"UNPOOLED" | UnpooledDataSourceFactory | 不使用连接池的数据源 |
"PERPETUAL" | PerpetualCache | 永久缓存 |
"FIFO" | FifoCache | 先进先出缓存 |
"LRU" | LruCache | 最近最少使用缓存 |
"SOFT" | SoftCache | 基于 SoftReference 的缓存 |
"WEAK" | WeakCache | 基于 WeakReference 的缓存 |
"DB_VENDOR" | VendorDatabaseIdProvider | 根据数据库类型自动切换 SQL |
"XML" | XMLLanguageDriver | MyBatis 默认的 XML SQL 语言驱动器 |
"RAW" | RawLanguageDriver | 原生 SQL 写法语言驱动器 |
"SLF4J" | Slf4jImpl | 使用 SLF4J 的日志输出 |
"COMMONS_LOGGING" | JakartaCommonsLoggingImpl | 使用 Commons Logging 日志 |
"LOG4J" | Log4jImpl | 使用 Log4j 日志 |
"LOG4J2" | Log4j2Impl | 使用 Log4j2 日志 |
"JDK_LOGGING" | Jdk14LoggingImpl | 使用 JDK 内建日志 |
"STDOUT_LOGGING" | StdOutImpl | 输出日志到控制台 |
"NO_LOGGING" | NoLoggingImpl | 不输出日志 |
"CGLIB" | CglibProxyFactory | 使用 CGLIB 动态代理 |
"JAVASSIST" | JavassistProxyFactory | 使用 Javassist 动态代理 |
1.2、parse
真正解析配置文件的是利用上一步构造出的XMLConfigBuilder
的parse
方法,首先会进行判断,如果已经解析过,则抛出异常,不会重复解析:
否则就将标记设置为true。并且执行
parseConfiguration
方法,从根节点进行解析:
每一行都对应了一个 <mybatis-config.xml> 中的标签,逐步填充 Configuration 对象内容:
/**
* 解析 <configuration> 根节点的各个子标签,并将配置信息填充到 Configuration 对象中
*/
private void parseConfiguration(XNode root) {
try {
// 【1】先解析 <properties> 标签(必须最优先解析),以便后续标签中的占位符 ${} 能被正确替换
propertiesElement(root.evalNode("properties"));
// 【2】解析 <settings> 标签,将其内容转换为 Properties 对象
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 【3】解析 settings 中的 vfsImpl 属性(如果配置了自定义 VFS 实现类)
loadCustomVfs(settings);
// 【4】解析 settings 中的 logImpl 属性(设置日志实现类,如 LOG4J、STDOUT_LOGGING 等)
loadCustomLogImpl(settings);
// 【5】解析 <typeAliases> 标签,注册用户自定义的别名或包扫描别名
typeAliasesElement(root.evalNode("typeAliases"));
// 【6】解析 <plugins> 标签,注册 MyBatis 插件(如分页插件、SQL 拦截器等)
pluginElement(root.evalNode("plugins"));
// 【7】解析 <objectFactory> 标签,设置自定义对象工厂(用于实例化结果对象)
objectFactoryElement(root.evalNode("objectFactory"));
// 【8】解析 <objectWrapperFactory> 标签,自定义对象包装器(封装结果对象属性访问行为)
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 【9】解析 <reflectorFactory> 标签,自定义反射器工厂(高级反射行为控制)
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 【10】将 <settings> 中的配置项应用到 Configuration 对象中
settingsElement(settings);
// 【11】解析 <environments> 标签,注册事务管理器和数据源配置(必须在 objectFactory 之后执行)
environmentsElement(root.evalNode("environments"));
// 【12】解析 <databaseIdProvider> 标签,支持数据库厂商识别(如区分 MySQL、Oracle)
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 【13】解析 <typeHandlers> 标签,注册自定义类型处理器(TypeHandler)
typeHandlerElement(root.evalNode("typeHandlers"));
// 【14】解析 <mappers> 标签,加载 Mapper 映射器(包括 XML 和接口方式)
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
// 如果解析过程中发生异常,则封装为 BuilderException 抛出
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
当解析完成后,会得到一个configuration
对象,其中就包含了配置文件中的各种值。相当于此时的xml配置文件已经转化为了configuration
对象。最后还会将其再次包装成SqlSessionFactory
,后续会利用SqlSessionFactory
进行sql相关逻辑的执行。
其中最关键的是mappers标签的解析。
二、mappers标签的解析
mapperElement
方法,首先会拿到mappers
根标签,然后进行解析。
/**
* 解析 <mappers> 标签,支持三种加载方式:package、resource/url、class
*/
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 遍历 <mappers> 下的所有子节点(可能是 <package> 或 <mapper>)
for (XNode child : parent.getChildren()) {
// 情况1:<package name="com.xxx.mapper"/>,批量注册包下所有 Mapper 接口
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
// 自动扫描指定包下的所有接口,并注册到 Configuration 中
configuration.addMappers(mapperPackage);
} else {
// 情况2~4:单个 <mapper> 节点,通过 resource/url/class 指定加载方式
String resource = child.getStringAttribute("resource"); // 从 classpath 中加载 Mapper XML
String url = child.getStringAttribute("url"); // 从网络路径加载 Mapper XML
String mapperClass = child.getStringAttribute("class"); // 直接加载 Mapper 接口类
// 情况2:只指定 resource,加载 Mapper XML 文件
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource); // 设置错误上下文信息
try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(
inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse(); // 解析 Mapper XML,注册语句映射
}
// 情况3:只指定 url,加载远程 Mapper XML 文件
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try (InputStream inputStream = Resources.getUrlAsStream(url)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(
inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse(); // 同样调用解析逻辑
}
// 情况4:只指定 class,注册 Mapper 接口类(无 XML 时适用)
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface); // 注册接口类到 MapperRegistry
// 情况5:配置冲突,三种方式只能选一种,否则抛异常
} else {
throw new BuilderException(
"A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
案例中对应的是情况2
,首先会注册一个mapper解析器,然后调用其parse方法对案例中UserMapper.xml
进行解析,在该方法中,首先会进行判断,如果已经进行过解析,则不会重复解析。
解析的核心方法在于
configurationElement
,同样是对于xml中的各种标签再次分类解析:
这里重点看一下
cacheElement
以及buildStatementFromContext
。
2.1、cacheElement
cacheElement
和Mybatis的二级缓存有关。简单的说,Mybatis有两级缓存:
- 一级缓存是SqlSession 级别的,并且默认开启。
- 二级缓存是Mapper 映射级别,默认不开启,如果需要,应该在某个mapper.xml中使用cache标签开启。
cacheElement
方法正是解析mapper.xml中的cache标签:
/**
* 解析 <cache> 标签,构建二级缓存对象并注册到 Configuration 中。
*/
private void cacheElement(XNode context) {
// 1. 判断 <cache> 标签是否存在
if (context != null) {
// 2. 解析缓存类型(默认是 PERPETUAL,即 PerpetualCache)
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 3. 解析缓存淘汰策略(默认是 LRU,即最近最少使用)
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 4. 缓存刷新间隔(可选):指定自动清空缓存的时间(毫秒)
Long flushInterval = context.getLongAttribute("flushInterval");
// 5. 缓存大小(可选):最大缓存对象个数
Integer size = context.getIntAttribute("size");
// 6. 是否为读写缓存(readOnly=false 表示使用序列化;true 表示共享引用)
// readWrite = true 表示开启对象副本,确保线程安全
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
// 7. 是否阻塞:当缓存正在被其他线程刷新时,是否阻塞等待
boolean blocking = context.getBooleanAttribute("blocking", false);
// 8. 获取 <cache> 中配置的其他 <property> 子节点
Properties props = context.getChildrenAsProperties();
// 9. 构建缓存并注册到 Configuration,封装为 MapperBuilderAssistant.useNewCache()
builderAssistant.useNewCache(
typeClass, // 缓存类型类(如 PerpetualCache)
evictionClass, // 淘汰策略类(如 LruCache)
flushInterval, // 缓存刷新间隔
size, // 缓存容量
readWrite, // 是否使用读写模式
blocking, // 是否阻塞模式
props // 自定义属性
);
}
}
在useNewCache
中,最终会调用CacheBuilder
的build
方法:
build
方法中运用到了装饰器模式
,所有的Cache都实现了一个共同的父类Cache。
在**cache = newCacheDecoratorInstance(decorator, cache);这一行代码中,传入LruCache
和当前的Cache
实例(PERPETUAL),将PERPETUAL
包装到LRU
中:(LruCache的delegate属性,指向的是传入的PerpetualCache实例)
然后继续执行到cache = setStandardDecorators(cache);**这一行代码,会继续进行装饰器的包装:
setStandardDecorators
方法,对于Cache
实例层层包装,赋值给各自的delegate
属性:
包装完成的层次:SynchronizedCache线程同步缓存区->LoggingCache统计命中率以及打印日志->SerializedCache序列化->LruCache最少使用->PerpetualCache默认。
2.1.1、缓存策略
默认的PerpetualCache
,使用的是HashMap
进行存储。
而LruCache
,为了实现最近最少使用的机制,使用了LinkedHashMap
的数据结构,并且重写了它的removeEldestEntry
方法,关键在于,LinkedHashMap
构造时第三个参数为 true 表示按访问顺序排列:
LruCache cache = new LruCache(new PerpetualCache("myCache"));
cache.setSize(3);
cache.put("A", 1); // A
cache.put("B", 2); // A B
cache.put("C", 3); // A B C
cache.get("A"); // B C A (A 被访问过,移到尾部)
cache.put("D", 4); // C A D(B 被淘汰)
SynchronizedCache
,每个方法上通过加synchronized
保证线程安全:
LoggingCache
,会记录日志,以及统计缓存命中次数:
2.2、buildStatementFromContext
buildStatementFromContext
是用来解析 select、insert、update、delete 标签中sql语句的方法,首先会解析出这些节点,然后进行循环,获取到XMLStatementBuilder
后,执行parseStatementNode
方法:
在
parseStatementNode
方法中有几个关键点,这一段代码会判断当前的标签是否为select,如果是select标签,则不会清除一级缓存(增删改会清除),以及判断是否使用二级缓存(默认 select 使用)
2.2.1、sql的解析
真正执行解析sql的是下图中的代码:
同样地会先去构建一个
XMLScriptBuilder
,然后调用其parseScriptNode
方法进行解析:
在
parseScriptNode
方法中,首先会解析 SQL 标签中的所有子标签,然后去进行判断:
- 包含动态 SQL(即是否包含 if、choose、${} 等动态节点)构建 DynamicSqlSource(运行时动态拼接 SQL)
- 不包含动态 SQL(即是否包含 if、choose、${} 等动态节点)构建 RawSqlSource(直接编译成静态 SQL,提升效率)
MixedSqlNode
对象,实现了SqlNode
接口,SqlNode是所有动态 SQL节点的统一接口,而MixedSqlNode
代表了 一整个 SQL 脚本块,比如select标签中所有内容就会变成一个 MixedSqlNode。
SqlNode 接口
│
├── MixedSqlNode // 组合节点
├── StaticTextSqlNode // 静态文本节点:普通 SQL 字符串
├── TextSqlNode // 动态文本节点:包含 ${}
├── IfSqlNode // if 标签
├── ChooseSqlNode // choose/when/otherwise
├── ForEachSqlNode // foreach
├── WhereSqlNode // where
├── TrimSqlNode // trim
├── SetSqlNode // set
└── BindSqlNode // bind
用一个案例说明,假如我在mapper.xml中定义了如下的sql语句:
<select id="findUser" parameterType="map" resultType="User">
SELECT * FROM user
<where>
<if test="name != null">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
则生成的结构如下:
MixedSqlNode
├── StaticTextSqlNode(“SELECT * FROM user”)
└── WhereSqlNode
└── MixedSqlNode
├── IfSqlNode(test=“name != null”) → TextSqlNode(“AND name = #{name}”)
└── IfSqlNode(test=“age != null”) → TextSqlNode(“AND age = #{age}”)