第 3 章 核心处理层(上)
3.1 MyBatis初始化
MyBatis 初始化过程中,除了会读取 mybatis-config.xml 配置文件以及映射配置文件,还会加载配置文件指定的类,处理类中的注解,创建一些配置对象,最终完成框架中各个模块的初始化。
3.1.1 建造者模式
建造者模式(也被称为“生成器模式”)将一个复杂对象的构建过程与它的表示分离,从而使得同样的构建过程可以创建不同的表示。用户只需要了解复杂对象的类型和内容,而无须关注复杂对象的具体构造过程。
主要角色
- 建造者(Builder)接口,定义建造者构建产品对象的各部分的行为
- 具体建造者(ConcreteBuilder),直接创建产品对象。必须实现两类方法,即建造方法和获取方法
- 导演(Director),调用具体建造者创建需要的产品对象
- 产品(Product),用户需要使用的复杂对象
优点:
- 导演角色不需要知晓产品类的内部细节
- 将复杂产品的创建过程分散到了不同的构造步骤中
- 每个具体建造者都可以创建出完整的产品对象,建造都之间相互独立,新产品出现时,只需添加新的具体建造者即可,符合开闭原则
3.1.2 BaseBuilder
MyBatis 初始化的主要工作是加载并解析 mybatis-config.xml 配置文件,映射文件以及相关的注解信息。其入口是 SqlSessionFactoryBuilder.build() 方法
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
// 读取配置文件
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// 解析配置文件得到 Configuration 对象,创建 DefaultSqlSessionFactory 对象
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
其中用来解析 XML 的 XMLConfigBuilder,就是继承自 BaseBuilder,其子类如图
public abstract class BaseBuilder {
// Configuration 是 MyBatis 初始化过程的核心对象,MyBatis 中几乎全部的配置信息都会保存到 Configuration 对象中
protected final Configuration configuration;
// mybatis-config.xml 配置文件中的别名标签
protected final TypeAliasRegistry typeAliasRegistry;
// mybatis-config.xml 配置文件中使用 <typeHandler> 标签添加的 TypeHandler
protected final TypeHandlerRegistry typeHandlerRegistry;
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
}
3.1.3 XMLConfigBuilder
XMLConfigBuilder 主要负责解析 mybatis-config.xml 配置文件
public class XMLConfigBuilder extends BaseBuilder {
// 标识是否已经解析过 mybatis-config.xml 配置文件
private boolean parsed;
// 用于解析 mybatis-config.xml 配置文件的 XPathParser 对象
private final XPathParser parser;
// 标识 <environment> 配置的名称,默认读取 <environment> 标签的 default 属性
private String environment;
// ReflectorFactory 负责创建和缓存 Reflector 对象
private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
}
XMLConfigBuilder 通过调用 parseConfiguration 方法实现解析功能,具体实现如下
private void parseConfiguration(XNode root) {
try {
// 解析 <properties> 标签
propertiesElement(root.evalNode("properties"));
// 解析 <settings> 标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 设置 vfsImpl 字段
loadCustomVfs(settings);
// 加载日志实现类
loadCustomLogImpl(settings);
// 解析 <typeAliases> 标签
typeAliasesElement(root.evalNode("typeAliases"));
// 解析 <plugins> 标签
pluginElement(root.evalNode("plugins"));
// 解析 <objectFactory> 标签
objectFactoryElement(root.evalNode("objectFactory"));
// 解析 <objectWrapperFactory> 标签
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析 <reflectorFactory> 标签
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 设置 settings 字段
settingsElement(settings);
// 解析 <environments> 标签
environmentsElement(root.evalNode("environments"));
// 解析 <databaseIdProvider> 标签
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析 <typeHandlers> 标签
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析 <mappers> 标签
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
3.1.4 XMLMapperBuilder
XMLMapperBuilder 负责解析映射配置文件,parse 方法是解析入口
public class XMLMapperBuilder extends BaseBuilder {
private final XPathParser parser;
private final MapperBuilderAssistant builderAssistant;
private final Map<String, XNode> sqlFragments;
private final String resource;
}
parse
public void parse() {
// 判断是否已经加载过该映射文件
if (!configuration.isResourceLoaded(resource)) {
// 处理<mapper>节点
configurationElement(parser.evalNode("/mapper"));
// 将resource添加到Configuration.loadedResources集合中,它是一个 HashSet<String> 集合,其中存储了已经加载过的映射文件的路径
configuration.addLoadedResource(resource);
// 注册 Mapper 接口
bindMapperForNamespace();
}
// 解析 configurationElement 方法中解析失败的<resultMap>节点
parsePendingResultMaps();
// 解析 configurationElement 方法中解析失败的<cache-ref>节点
parsePendingCacheRefs();
// 解析 configurationElement 方法中解析失败的 SQL 语句节点
parsePendingStatements();
}
configurationElement
private void configurationElement(XNode context) {
try {
// 获取 namespace 属性
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 解析 <cache-ref> 节点
cacheRefElement(context.evalNode("cache-ref"));
// 解析 <cache> 节点
cacheElement(context.evalNode("cache"));
// 解析 <parameterMap> 节点,已废弃,不推荐使用
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析 <resultMap> 节点
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析 <sql> 节点
sqlElement(context.evalNodes("/mapper/sql"));
// 解析 <select>、<insert>、<update>、<delete> 节点
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
3.1.5 XMLStatementBuilder
XMLStatementBuilder 负责解析 SQL 节点定义的 SQL 语句。也就是上述 configurationElement 方法的最后一步 buildStatementFromContext 实际上是调用 XMLStatementBuilder 实现的。
public class XMLStatementBuilder extends BaseBuilder {
private final MapperBuilderAssistant builderAssistant;
private final XNode context;
private final String requiredDatabaseId;
}
MyBatis 使用 SqlSource 接口表示映射文件或注解中定义的 SQL 语句
public interface SqlSource {
// 根据映射文件或注解描述的 SQL 语句,以及传入的参数,返回可执行的 SQL
BoundSql getBoundSql(Object parameterObject);
}
MyBatis 使用 MappedStatement 表示映射配置文件中定义的 SQL 节点
public final class MappedStatement {
// 节点中的 id 属性(包括命名空间前缀)
private String resource;
// 对应一条 SQL 语句
private SqlSource sqlSource;
// SQL 的类型,INSERT、UPDATE、DELETE、SELECT
private SqlCommandType sqlCommandType;
// ……
}
XMLStatementBuilder 的 parseStatementNode 方法是解析 SQL 节点的入口函数
public void parseStatementNode() {
// 获取 SQL 节点的 id 和 databaseId 属性,若 databaseId 属性值与当前使用的数据库不匹配,则不加载该 SQL 节点;
// 若存在相同 id 且 databaseId 属性值不为空的 SQL 节点,则不加载该 SQL 节点;
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 获取 SQL 节点的 nodeName 属性值,即 SQL 类型,如 SELECT、INSERT、UPDATE、DELETE 等;
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 在解析 SQL 节点之前,先解析 <include> 节点,将 <include> 节点中的 SQL 片段合并到当前 SQL 节点中;
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 处理 <selectKey> 节点,该节点用于生成主键;
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 完成 SQL 节点的解析,后面单独分析
}
3.1.6 绑定Mapper接口
每个映射配置文件的命名空间可以绑定一个 Mapper 接口,并注册到 MapperRegistry 中。通过 XMLMapperBuilder.bindMapperForNamespace() 实现映射配置文件与对应 Mapper 接口的绑定。
private void bindMapperForNamespace() {
// 获取映射配置文件的命名空间
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
// 解析命名空间对应的类型
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// ignore, bound type is not required
}
// 是否已经加载了 boundType 接口
if (boundType != null && !configuration.hasMapper(boundType)) {
// 追加 namespace 前缀,并添加到 configuration.loadedResources 集合中
configuration.addLoadedResource("namespace:" + namespace);
// 调用 MapperRegistry.addMapper() 方法,注册 boundType 接口
configuration.addMapper(boundType);
}
}
}
3.1.7 处理incomplete*集合
XMLMapperBuilder.configurationElement() 方法在解析映射配置文件前面的节点时,可能会引用后面还未解析的节点,导致解析失败并抛出 IncompleteElementException。
MyBatis 会捕获这些异常并做针对性处理,前面提到的 parsePendingResultMaps()、parsePendingCacheRefs()、parsePendingStatements() 三个方法就是为此而产生的。
private void parsePendingStatements() {
// 获取 Configuration.incompleteStatements 集合
Collection<XMLStatementBuilder> incompleteStatements = configuration.getIncompleteStatements();
// 加锁,避免并发问题
synchronized (incompleteStatements) {
// 遍历 incompleteStatements 集合
Iterator<XMLStatementBuilder> iter = incompleteStatements.iterator();
while (iter.hasNext()) {
try {
// 重新解析 Statement 节点
iter.next().parseStatementNode();
iter.remove();
} catch (IncompleteElementException e) {
// Statement is still missing a resource...
}
}
}
}
3.2 SqlNode&SqlSource
映射配置文件中定义的 SQL 会被解析成 SqlSource 对象,SQL 语句中定义的动态 SQL 节点、文本节点等,则由 SqlNode 接口的相应实现表示。
public interface SqlSource {
// 通过解析得到 BondSql 对象,BondSql 对象封装了包含“?”占位符的 SQL 语句,以及绑定的实参
BoundSql getBoundSql(Object parameterObject);
}
- DynamicSqlSource,负责处理动态 SQL 语句
- RawSqlSource,负责处理静态语句
- StaticSqlSource,以上二者处理后的 SQL 会封装成 StaticSqlSource 返回
3.2.1 组合模式
组合模式是将对象组合成树形结构,以表示“部分——整体”的层次结构(一般是树形结构),用户可以像处理一个简单对象一样来处理一个复杂对象。
- 抽象组件(Component),定义了树形结构中所有类的公共行为(operation),以及一些用于管理子组件的方法(add,remove,getChild)
- 树叶(Leaf),表示叶子节点,叶子节点没有子节点
- 树枝(Composite),有子组件的组件
- 调用者(Client),通过 Component 接口操纵整个树形结构
优点:
- 帮助调用者屏蔽对象的复杂性
- 通过增加树中节点的方式,添加新的 Component 对象,从而实现扩展,符合开闭原则
3.2.2 OGNL 表达式简介
MyBatis 中涉及的 OGNL 表达式的功能主要是:存取 Java 对象树中的属性、调用 Java 对象树中的方法
OGNL 表达式有三个重要概念:
-
表达式
OGNL 表达式执行的所有操作都是根据表达式解析得到的。例如:“对象名.方法名”表示调用指定对象的指定方法。
深入学习请参考:(OGNL - Apache Commons OGNL - Language Guide)
-
root 对象
OGNL 表达式指定了具体的操作,而 root 对象指定了需要操作的对象
-
OgnlContext(上下文对象)
OgnlContext 类继承了 Map 接口,可以存放除 root 对象之外的其他对象。在使用 OGNL 表达式操作非 root 对象时,需要使用 #前缀,而操作 root 对象则不需要使用 #前缀。
示例代码:
MyBatis/MyBatis技术内幕/MyBatis-Tec-Inside/src/test/java/com/example/chapter3/section2/OgnlTest.java · cuizhigang/notes - 码云 - 开源中国 (gitee.com)
在 MyBatis 中,使用 OgnlCache 对原生的 OGNL 进行了封装。OGNL 表达式的解析过程是比较耗时的,为了提高效率,OgnlCache 使用 expressionCache 字段(ConcurrentHashMap<String, Object>)对解析后的 OGNL 表达式进行缓存。
private static final Map<String, Object> expressionCache = new ConcurrentHashMap<>();
public static Object getValue(String expression, Object root) {
try {
// 创建一个 OGNL 上下文对象,OgnlClassResolver 替代了 OGNL 中原有的 DefaultClassResolver
// 其主要功能是使用前面介绍的 Resource 工具类定位资源
Map<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
private static Object parseExpression(String expression) throws OgnlException {
// 从缓存中获取 OGNL 表达式对应的节点对象
Object node = expressionCache.get(expression);
if (node == null) {
// 如果缓存中不存在,则解析 OGNL 表达式
node = Ognl.parseExpression(expression);
// 将解析结果放入缓存
expressionCache.put(expression, node);
}
return node;
}
3.2.3 DynamicContext
DynamicContext 是用于记录解析动态 SQL 语句之后产生的 SQL 语句片段的容器。
public class DynamicContext {
// 参数上下文
private final ContextMap bindings;
// 在 SqlNode 解析动态 SQL 时,会将解析后的 SQL 语句片段添加到该属性中保存,最终拼凑出一条完整的 SQL
private final StringJoiner sqlBuilder = new StringJoiner(" ");
// 追加 SQL 片段
public void appendSql(String sql) {
this.sqlBuilder.add(sql);
}
// 获取解析后的、完整的 SQL 语句
public String getSql() {
return this.sqlBuilder.toString().trim();
}
}
3.2.4 SqlNode
public interface SqlNode {
// 根据用户传入的实参,解析该 SqlNode 所记录的动态 SQL 节点,并调用 DynamicContext.appendSql() 方法将解析后的 SQL 片段追加到 DynamicContext.sqlBuilder 中保存
// 当 SQL 节点下所有 SqlNode 完成解析后,我们就可以从 DynamicContext 中获取一条动态生成的、完整的 SQL 语句
boolean apply(DynamicContext context);
}
SqlNode 接口有多个实现类,每个实现类对应一个动态 SQL 节点。
StaticTextSqlNode&MixedSqlNode
StaticTextSqlNode 中使用 text 字段(String类型)记录了对应的非动态 SQL 语句节点。
MixedSqlNode 中使用 contents 字段(List类型)记录其子节点对应的 SqlNode 对象集合。
TextSqlNode
TextSqlNode 表示的是包含“${}”占位符的动态 SQL 节点。
IfSqlNode
IfSqlNode 对应的动态 SQL 节点是 节点。使用 OGNL 检测表达式是否成立,并根据结构决定是否执行 apply() 方法。
TrimSqlNode&WhereSqlNode&SetSqlNode
TrimSqlNode 对应节点,会根据子节点的解析结果,添加或删除相应的前缀或后缀。
WhereSqlNode 是 TrimSqlNode 的子类,对应节点
SetSqlNode 是 TrimSqlNode 的子类,对应节点
ForEachSqlNode
ForEachSqlNode 对应节点。
ChooseSqlNode
ChooseSqlNode,对应节点。
VarDeclSqlNode
VarDeclSqlNode 表示动态 SQL 语句中的 节点,该节点可以从 OGNL 表达式中创建一个变量并将其记录到上下文中。
<select id="getUserInfo" parameterType="com.example.User" resultType="com.example.UserInfo">
<bind name="name" value="'%' + name + '%'"/>
<bind name="age" value="age * 2"/>
SELECT * FROM user_info WHERE 1=1
<if test="name != null and name != ''">
AND name LIKE #{name}
</if>
<if test="age != null">
AND age >= #{age}
</if>
</select>
3.2.5 SqlSourceBuilder
SqlSourceBuilder 主要完成了两方面的操作,一方面是解析 SQL 语句中的“#{}”占位符中定义的属性,另一方面是将 SQL 语句中的“#{}”占位符替换成“?”占位符。
3.2.6 DynamicSqlSource
DynamicSqlSource 负责解析动态 SQL 语句。
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 创建 DynamicContext 对象,parameterObject 为传入的参数
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 通过 rootSqlNode.apply 方法调用整个树形结构中全部 SqlNode.apply 方法,
// 每个 SqlNode.apply 方法会将解析后的 SQL 语句片段追加到 context 中,最终通过 context.getSql() 获取完整的 SQL 语句
rootSqlNode.apply(context);
// 创建 SqlSourceBuilder 对象,解析 SQL 语句中的 #{} 占位符,将 SQL 语句中的 #{} 占位符替换成 ? 占位符
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 创建 BoundSql 对象,并将DynamicContext.bindings 中的参数添加到 BoundSql.additionalParameters 集合中
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
3.2.7 RawSqlSource
RawSqlSource 负责处理静态 SQL 语句。