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 语句。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |