当前位置: 首页 > article >正文

第 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 接口操纵整个树形结构

优点:

  1. 帮助调用者屏蔽对象的复杂性
  2. 通过增加树中节点的方式,添加新的 Component 对象,从而实现扩展,符合开闭原则
3.2.2 OGNL 表达式简介

MyBatis 中涉及的 OGNL 表达式的功能主要是:存取 Java 对象树中的属性、调用 Java 对象树中的方法

OGNL 表达式有三个重要概念:

  1. 表达式

    OGNL 表达式执行的所有操作都是根据表达式解析得到的。例如:“对象名.方法名”表示调用指定对象的指定方法。

    深入学习请参考:(OGNL - Apache Commons OGNL - Language Guide)

  2. root 对象

    OGNL 表达式指定了具体的操作,而 root 对象指定了需要操作的对象

  3. 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 语句。


http://www.kler.cn/a/511744.html

相关文章:

  • Restormer: Efficient Transformer for High-Resolution Image Restoration解读
  • Ubuntu 24.04 LTS 空闲硬盘挂载到 文件管理器的 other locations
  • HarmonyOS应用开发-低代码开发登录页面(超详细)
  • 昇腾环境ppstreuct部署问题记录
  • 三电平空间矢量详解
  • Java 对象池管理的高性能工具库 Apache Commons Pool 2
  • Bash 中 nohup 与 的区别及用法解析
  • leetcode416.分割等和子集
  • nginx作为下载服务器配置
  • Python人脸识别库DeepFace使用教程及源码解析
  • imbinarize函数用法详解与示例
  • python 基础类json和csv
  • 深入剖析iOS网络优化策略,提升App性能
  • 【LC】2239. 找到最接近 0 的数字
  • Node.js 写一个登录中间件
  • 排序算法学习小结
  • 如何确保Python爬虫不违反微店规定
  • Elixir语言的软件开发工具
  • 切面Aop的了解和使用
  • 【优选算法篇】2----复写零
  • 打游戏黑屏了但是有游戏声音 原因分析
  • 口令攻击和钓鱼攻击
  • nvm的各种命令及其用途
  • spring那些事
  • 2021最新中高阶Android面试题总结,已整理成文档_android面试题2021中高级
  • Springboot项目启动优化详解