3.1 MyBatis初始化
MyBatis 初始化过程中,除了会读取 mybatis-config.xml 配置文件以及映射配置文件,还会加载配置文件指定的类,处理类中的注解,创建一些配置对象,最终完成框架中各个模块的初始化。
3.1.1 建造者模式
建造者模式(也被称为“生成器模式”)将一个复杂对象的构建过程与它的表示分离,从而使得同样的构建过程可以创建不同的表示。用户只需要了解复杂对象的类型和内容,而无须关注复杂对象的具体构造过程。
主要角色
- 建造者(Builder)接口,定义建造者构建产品对象的各部分的行为
- 具体建造者(ConcreteBuilder),直接创建产品对象。必须实现两类方法,即建造方法和获取方法
- 导演(Director),调用具体建造者创建需要的产品对象
- 产品(Product),用户需要使用的复杂对象
优点:
- 导演角色不需要知晓产品类的内部细节
- 将复杂产品的创建过程分散到了不同的构造步骤中
- 每个具体建造者都可以创建出完整的产品对象,建造都之间相互独立,新产品出现时,只需添加新的具体建造者即可,符合开闭原则
3.1.2 BaseBuilder
MyBatis 初始化的主要工作是加载并解析 mybatis-config.xml 配置文件,映射文件以及相关的注解信息。其入口是 SqlSessionFactoryBuilder.build() 方法
java">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,其子类如图
java">public abstract class BaseBuilder {// Configuration 是 MyBatis 初始化过程的核心对象,MyBatis 中几乎全部的配置信息都会保存到 Configuration 对象中protected final Configuration configuration;// mybatis-config.xml 配置文件中的别名标签protected final TypeAliasRegistry typeAliasRegistry;// mybatis-config.xml 配置文件中使用 <typeHandler> 标签添加的 TypeHandlerprotected 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 配置文件
java">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 方法实现解析功能,具体实现如下
java">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 方法是解析入口
java">public class XMLMapperBuilder extends BaseBuilder {private final XPathParser parser;private final MapperBuilderAssistant builderAssistant;private final Map<String, XNode> sqlFragments;private final String resource;}
parse
java">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
java">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 实现的。
java">public class XMLStatementBuilder extends BaseBuilder {private final MapperBuilderAssistant builderAssistant;private final XNode context;private final String requiredDatabaseId;
}
MyBatis 使用 SqlSource 接口表示映射文件或注解中定义的 SQL 语句
java">public interface SqlSource {// 根据映射文件或注解描述的 SQL 语句,以及传入的参数,返回可执行的 SQLBoundSql getBoundSql(Object parameterObject);
}
MyBatis 使用 MappedStatement 表示映射配置文件中定义的 SQL 节点
java">public final class MappedStatement {// 节点中的 id 属性(包括命名空间前缀)private String resource;// 对应一条 SQL 语句private SqlSource sqlSource;// SQL 的类型,INSERT、UPDATE、DELETE、SELECTprivate SqlCommandType sqlCommandType;// ……}
XMLStatementBuilder 的 parseStatementNode 方法是解析 SQL 节点的入口函数
java">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 接口的绑定。
java">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() 三个方法就是为此而产生的。
java">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 表达式进行缓存。
java">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 语句片段的容器。
java">public class DynamicContext {// 参数上下文private final ContextMap bindings;// 在 SqlNode 解析动态 SQL 时,会将解析后的 SQL 语句片段添加到该属性中保存,最终拼凑出一条完整的 SQLprivate 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
java">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 语句。
java">@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 语句。