IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> MyBatis源码分析 -> 正文阅读

[Java知识库]MyBatis源码分析

一、前言

MyBatis官方文档:https://mybatis.org/mybatis-3/zh/

1、介绍

对于MyBatis,其工作流程实际上分为两部分:第一,构建,也就是解析我们写的xml配置,将其变成它所需要的对象。第二,执行,在构建完成的基础上,去执行我们的SQL,完成与Jdbc的交互

2、快速上手

数据库配置如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 ="db.properties"></properties>
    <settings>
        <!-- 使用驼峰命名法 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 开启日志-->
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>
    <typeAliases>
        <!--别名设置,默认是包名,当然也可以每个路径名做一个别名映射 -->
        <package name="org.demo.po"/>
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <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>

    <!-- 将我们写好的sql映射文件(EmployeeMapper.xml)一定要注册到全局配置文件(mybatis-config.xml)中 -->
    <mappers>
        <!--这里填写resource下的路径-->
        <mapper resource="mappers/EmployeeMapper.xml" />
    </mappers>
</configuration>

创建db.properties外部文件

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/learnmybatis?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=root

创建EmployeeMapper.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 -->
<mapper namespace="test">
    <!--resultType别名大小写都可,因为最后会统一变成小写,同时源码已经内置了很多别名 -->
    <select id="getEmp" resultType="Employee">
        select * from employee
    </select>
</mapper>

创建测试文件

public static void main(String[] args) throws IOException {
    // 加载mybatis框架主配置文件
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    // 读取解析配置文件内容,创建SqlSessionFacory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    // 获取sqlSession对象
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 这种操作不推荐,推荐使用mapper代理方式,后面有提到
    // 执行数据库操作
    List<Employee> list = sqlSession.selectList("test.getEmp");
    System.out.println(list);
    // 释放资源
    sqlSession.close();
    // sqlSession.commit();
}

二、Mybatis的构建

1、核心流程

1.1 介绍

Configuration 是整个MyBatis的配置体系集中管理中心,前面所学Executor、StatementHandler、Cache、MappedStatement…等绝大部分组件都是由它直接或间接的创建和管理。其主要作用如下

  • 存储全局配置信息,其来源于settings(设置)
  • 初始化并维护全局基础组件
    • typeAliases(类型别名)
    • typeHandlers(类型处理器)
    • plugins(插件)
    • environments(环境配置)
    • cache(二级缓存空间)
  • 初始化并维护MappedStatement
  • 组件构造器,并基于插件进行增强
    • newExecutor(执行器)
    • newStatementHandler(JDBC处理器)
    • newResultSetHandler(结果集处理器)
    • newParameterHandler(参数处理器)

Configuration 配置信息来源于xml和注解,每个文件和注解都是由若干个配置元素组成,并呈现嵌套关系,总体关系如下图所示,关于各配置的使用请参见官网给出文档:https://mybatis.org/mybatis-3/zh/configuration.html#properties

在这里插入图片描述

无论是xml 注解这些配置元素最弱都要被转换成JAVA配置属性或对象组件来承载。其对应关系如下:

  • 全配置(config.xml) 由Configuration对像属性承载
  • sql映射<select|insert…> 或@Select 等由MappedStatement对象承载
  • 缓存<cache…> 或@CacheNamespace 由Cache对象承载
  • 结果集映射 由ResultMap 对象承载

在这里插入图片描述

1.2 配置文件解析

  • XMLConfigBuilder :解析config.xml文件,会直接创建一个configuration对象,用于解析全局配置
  • XMLMapperBuilder :解析Mapper.xml文件,内容包含等
  • MapperBuilderAssistant:Mapper.xml解析辅助,在一个Mapper.xml中Cache是对Statement(sql声明)共享的,共享组件的分配即由该解析实现
  • XMLStatementBuilder:SQL映射解析 即<select|update|insert|delete> 元素解析成MapperStatement
  • SqlSourceBuilder:Sql数据源解析,将声明的SQL解析可执行的SQL
  • XMLScriptBuilder:解析动态SQL数据源当中所设置 SqlNode脚本集

XML文件解析流程

整体解析流程是从XmlConfigBuilder 开始,然后逐步向内解析,直到解析完所有节点。我们通过一个MappedStatement 解析过程即可了解到期整体解析流程
在这里插入图片描述
注解配置解析

注解解析底层实现是通过反射获取Mapper接口当中注解元素实现。有两种方式一种是直接指定接口名,一种是指定包名然后自动扫描包下所有的接口类。这些逻辑均由Mapper注册器(MapperRegistry)实现。其接收一个接口类参数,并基于该参数创建针对该接口的动态代理工厂,然后解析内部方法注解生成每个MapperStatement 最后添加至Configuration 完成解析。
在这里插入图片描述

1.3 源码分析

在这里插入图片描述
进入build方法,可以看见代码将xml文件传入并返回了一个SqlSessionFactory对象,而这个对象是使用构造者模式创建的。

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    // 开始解析配置文件,这里先生产一个解析对象
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}

在进入build对象的parse()方法,这个方法初始化Configuration对象,并且解析xml文件,把解析内容放入到Configuration对象中。其中就包括别名的映射,在初始化阶段别名映射会自动注册一些常用的别名,如果我们自己也配置也自动注册到Configuration对象的TypeAliasRegistry的TYPE_ALIASES的map中,并且把数据源和事务解析以后放入到Environment,给后续的执行提供数据链接和事务管理。

public Configuration parse() {
  //查看该文件是否已经解析过
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  //如果没有解析过,则继续往下解析,并且将标识符置为true
  parsed = true;
  //解析<configuration>节点,即从根节点开始解析,名字必须是configuration
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}

在进入parseConfiguration()方法,可以看到这个方法已经在解析<configuration>下的节点了,例如<settings>,<typeAliases>,<environments><mappers>等,同时返回了Configuration对象

// root即是完整的xml内容
private void parseConfiguration(XNode root) {
  try {
    //解析<Configuration>下的节点
    propertiesElement(root.evalNode("properties"));
    //<settings>
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    loadCustomLogImpl(settings);
    // 别名<typeAliases>解析
    // 所谓别名其实就是把你指定的别名对应的class存储在一个Map当中
    typeAliasesElement(root.evalNode("typeAliases"));
    //插件 <plugins>
    pluginElement(root.evalNode("plugins"));
    //自定义实例化对象的行为<objectFactory>
    objectFactoryElement(root.evalNode("objectFactory"));
    //MateObject   方便反射操作实体类的对象
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    // read it after objectFactory and objectWrapperFactory issue #631
    //<environments>
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    // typeHandlers
    typeHandlerElement(root.evalNode("typeHandlers"));
    //主要 <mappers> 指向我们存放SQL的xxxxMapper.xml文件
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

2、Configuration对象详解

2.1 配置文件dataSource 内容替换

对于db.properties替代,在parseConfiguration()方法中的propertiesElement(root.evalNode("properties"));就是对外部配置文件的替换修改,它首先形成Properties对象对其替换
在这里插入图片描述

2.2 typeAliasesElement别名设置

Mybatis别名设置若存在标签,则mapper.xml别名默认是类名(忽略大小写),同时将其存放于TYPE_ALIASES这个HashMap中,同时里面已经存在很多内置别名,可以直接使用
在这里插入图片描述
在这里插入图片描述

2.3 数据库相关内容载入

environmentsElement(root.evalNode("environments"));方法将数据库相关信息配置(例如事务,数据库账号密码等)存入enviroment对象,最终和configuration相关联存入其对象中
在这里插入图片描述

2.4 mapper解析(重要?)

mybatis-config.xml文件中我们一定会写一个叫做的标签,这个标签中的<mapper>节点存放了我们对数据库进行操作的SQL语句,这里就详细详解一下mapper的执行过程

<mappers>
    <!-- 通过包名,这里要求xml和mapoper包在同一包下 -->
    <package name="org.demo.po"/>
    <!-- 通过配置文件路径,多个文件可以通过*Mapper.xml通配符 -->
    <mapper resource="mappers/EmployeeMapper.xml" />
    <!-- 通过Java全限定类名 -->
    <mapper class="org.demo.mapper.EmployeeMapper.java"/>
    <!-- 通过url 通常是mapper不在本地时用 -->
    <mapper url=""/>
</mappers>

这是<mappers>标签的几种配置方式,通过这几种配置方式,可以帮助我们更容易理解mappers的解析

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
      //遍历解析mappers下的节点
      for (XNode child : parent.getChildren()) {
      //首先解析package节点
      if ("package".equals(child.getName())) {
        //获取包名
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        //如果不存在package节点,那么扫描mapper节点
        //resource/url/mapperClass三个值只能有一个值是有值的
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        //优先级 resource>url>mapperClass
        if (resource != null && url == null && mapperClass == null) {
            //如果mapper节点中的resource不为空
          ErrorContext.instance().resource(resource);
           //那么直接加载resource指向的XXXMapper.xml文件为字节流
          InputStream inputStream = Resources.getResourceAsStream(resource);
          //通过XMLMapperBuilder解析XXXMapper.xml,可以看到这里构建的XMLMapperBuilde还传入了configuration,所以之后肯定是会将mapper封装到configuration对象中去的。
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          //解析
          mapperParser.parse();
        } else if (resource == null && url != null && mapperClass == null) {
          //如果url!=null,那么通过url解析
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url == null && mapperClass != null) {
            //如果mapperClass!=null,那么通过加载类构造Configuration
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          configuration.addMapper(mapperInterface);
      } else {
            //如果都不满足  则直接抛异常  如果配置了两个或三个  直接抛异常
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

在这里插入图片描述

我们的配置文件中写的是通过resource来加载mapper.xml的,所以会通过XMLMapperBuilder来进行解析,我们在进去它的parse()方法。在这个parse()方法中,调用了一个configuationElement代码,用于解析XXXMapper.xml文件中的各种节点,包括<cache><cache-ref><paramaterMap>(已过时)、<resultMap><sql>、还有增删改查节点,和上面相同的是,我们也挑一个主要的来说,因为解析过程都大同小异。

public void parse() {
  //判断文件是否之前解析过
  if (!configuration.isResourceLoaded(resource)) {
      //解析mapper文件节点(主要)(下面贴了代码)
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    //绑定Namespace里面的Class对象
    bindMapperForNamespace();
  }
  //重新解析之前解析不了的节点,先不看,最后填坑。
  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}


//解析mapper文件里面的节点
// 拿到里面配置的配置项 最终封装成一个MapperedStatemanet
private void configurationElement(XNode context) {
  try {
      //获取命名空间 namespace,这个很重要,后期mybatis会通过这个动态代理我们的Mapper接口
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
        //如果namespace为空则抛一个异常
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    //解析缓存节点
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));

    //解析parameterMap(过时)和resultMap  <resultMap></resultMap>
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    //解析<sql>节点 
    //<sql id="staticSql">select * from test</sql> (可重用的代码段)
    //<select> <include refid="staticSql"></select>
    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);
  }
}

这里解析其中一项举例,解析增删改查节点<select> <insert> <update> <delete>,进入buildStatementFromContext(context.evalNodes("select|insert|update|delete"))方法

private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    //解析xml
    buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      //解析xml节点
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      //xml语句有问题时 存储到集合中 等解析完能解析的再重新解析
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

进入statementParser.parseStatementNode();方法,解析里面的xml节点

public void parseStatementNode() {
    //获取<select id="xxx">中的id
    String id = context.getStringAttribute("id");
    //获取databaseId 用于多数据库,这里为null
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
    }
    //获取节点名  select update delete insert
    String nodeName = context.getNode().getNodeName();
    //根据节点名,得到SQL操作的类型
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    //判断是否是查询
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //是否刷新缓存 默认:增删改刷新 查询不刷新
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //是否使用二级缓存 默认值:查询使用 增删改不使用
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    //是否需要处理嵌套查询结果 group by

    // 三组数据 分成一个嵌套的查询结果
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    //替换Includes标签为对应的sql标签里面的值
    includeParser.applyIncludes(context.getNode());

    //获取parameterType名
    String parameterType = context.getStringAttribute("parameterType");
    //获取parameterType的Class
    Class<?> parameterTypeClass = resolveClass(parameterType);

    //解析配置的自定义脚本语言驱动 这里为null
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    //解析selectKey
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    //设置主键自增规则
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
        keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
        keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
                configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
                ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }
    /************************************************************************************/
    //解析Sql(重要)  根据sql文本来判断是否需要动态解析 如果没有动态sql语句且 只有#{}的时候 直接静态解析使用?占位 当有 ${} 不解析
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //获取StatementType,可以理解为Statement和PreparedStatement
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    //没用过
    Integer fetchSize = context.getIntAttribute("fetchSize");
    //超时时间
    Integer timeout = context.getIntAttribute("timeout");
    //已过时
    String parameterMap = context.getStringAttribute("parameterMap");
    //获取返回值类型名
    String resultType = context.getStringAttribute("resultType");
    //获取返回值烈性的Class
    Class<?> resultTypeClass = resolveClass(resultType);
    //获取resultMap的id
    String resultMap = context.getStringAttribute("resultMap");
    //获取结果集类型
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
        resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    //将刚才获取到的属性,封装成MappedStatement对象(代码贴在下面)
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered,
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

具体的MappedStatement对象,这里每一个方法id对应存储一个MappedStatement对象,这样在执行的时候就可以直接通过id获得映射的MappedStatement对象了,即可以直接执行获取mysql结果了

//将刚才获取到的属性,封装成MappedStatement对象
public MappedStatement addMappedStatement(
        String id,
        SqlSource sqlSource,
        StatementType statementType,
        SqlCommandType sqlCommandType,
        Integer fetchSize,
        Integer timeout,
        String parameterMap,
        Class<?> parameterType,
        String resultMap,
        Class<?> resultType,
        ResultSetType resultSetType,
        boolean flushCache,
        boolean useCache,
        boolean resultOrdered,
        KeyGenerator keyGenerator,
        String keyProperty,
        String keyColumn,
        String databaseId,
        LanguageDriver lang,
        String resultSets) {

    if (unresolvedCacheRef) {
        throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    //id = namespace
    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    //通过构造者模式+链式变成,构造一个MappedStatement的构造者
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
            .resource(resource)
            .fetchSize(fetchSize)
            .timeout(timeout)
            .statementType(statementType)
            .keyGenerator(keyGenerator)
            .keyProperty(keyProperty)
            .keyColumn(keyColumn)
            .databaseId(databaseId)
            .lang(lang)
            .resultOrdered(resultOrdered)
            .resultSets(resultSets)
            .resultMaps(getStatementResultMaps(resultMap, resultType, id))
            .resultSetType(resultSetType)
            .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
            .useCache(valueOrDefault(useCache, isSelect))
            .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
        statementBuilder.parameterMap(statementParameterMap);
    }

    //通过构造者构造MappedStatement
    MappedStatement statement = statementBuilder.build();
    //将MappedStatement对象封装到Configuration对象中
    configuration.addMappedStatement(statement);
    return statement;
}

在这里插入图片描述

3、动态SQL构建

3.1 动态SQL解析

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

OGNL表达示

OGNL全称是对象导航图语言(Object Graph Navigation Language)是一种JAVA表达示语言,可以方便的存取对象属和方法,已用于逻辑判断。其支持以下特性:获取属性属性值,以及子属性值进行逻辑计;表达示中可直接调用方法(如果是无参方法,可以省略括号);通过下标访问数组或集合;遍历集合

3.2 动态SQL脚本

每个动态元素都会有一个与之对应的脚本类,即会产生许多SqlNode脚本如if 对应ifSqlNodeforEarch对应ForEachSqlNode 以此类推下去。同时脚本之间是呈现嵌套关系的,比如if元素中会包含一个MixedSqlNode ,而MixedSqlNode下又会包含1至1至多个其它节点,最后组成一课脚本语法树。最后SqlNode的接口非常简单,就只有一个apply方法,方法的作用就是执行当前脚本节点逻辑,并把结果应用到DynamicContext当中去。

这里要注意下面三个脚本

  • StaticTextSqlNode 表示一段纯静态文本如: select * from user
  • TextSqlNode 表示一个通过参数拼装的文本如:select * from ${user}
  • MixedSqlNode 表示多个节点的集合

在这里插入图片描述

3.3 SqlSource(SQL数据源)

SqlSource 是基于XML解析而来,解析的底层是使用Dom4j 把XML解析成一个个子节点,在通过 XMLScriptBuilder 遍历这些子节点最后生成对应的Sql源。在上层定义上每个Sql映射(MappedStatement)中都会包含一个SqlSource 用来获取可执行Sql(BoundSql)。SqlSource又分为原生SQL源与动态SQL源,以及第三方源
在这里插入图片描述

  • roviderSqlSource :第三方法SQL源,每次获取SQL都会基于参数动态创建静态数据源,然后在创建BoundSql
  • DynamicSqlSource:动态SQL源包含了SQL脚本,每次获取SQL都会基于参数又及脚本,动态创建创建BoundSql
  • RawSqlSource:不包含任何动态元素,原生文本的SQL。但这个SQL是不能直接执行的,需要转换成BoundSql
  • StaticSqlSource:包含可执行的SQL,以及参数映射,可直接生成BoundSql。前面三个数据源都要先创建StaticSqlSource然后才创建BoundSql

3.4 源码流程

生成SQL语句代码,首先这里会通过<select>节点获取到我们的SQL语句,假设SQL语句中只有${},那么直接就什么都不做,在运行的时候直接进行赋值。而如果扫描到了#{}字符串之后,会进行替换,将#{}替换为 ?

这里会生成一个GenericTokenParser,这个对象可以传入一个openToken和closeToken,如果是#{},那么openToken就是#{,closeToken就是 },然后通过parse方法中的handler.handleToken()方法进行替换。在这之前由于已经进行过SQL是否含有#{}的判断了,所以在这里如果是只有${},那么handler就是BindingTokenParser的实例化对象,如果存在#{},那么handler就是ParameterMappingTokenHandler的实例化对象。

mapperElement() > mapperParser.parse() > 进入XMLMapperBuilder类 configurationElement()> buildStatementFromContext() > buildStatementFromContext() > statementParser.parseStatementNode();

//XMLStatementBuilder类parseStatementNode方法
//解析Sql(重要)根据sql文本来判断是否需要动态解析 如果没有动态sql语句且 只有#{}的时候 直接静态解析使用?占位 当有 ${} 不解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

/*进入createSqlSource方法*/
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    //进入这个构造
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    //进入parseScriptNode
    return builder.parseScriptNode();
}
/**
进入这个方法
*/
public SqlSource parseScriptNode() {
    //#
    //会先解析一遍
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      //如果是${}会直接不解析,等待执行的时候直接赋值
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      //用占位符方式来解析  #{} --> ?
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    //获取select标签下的子标签
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
          //如果是查询
        //获取原生SQL语句 这里是 select * from test where id = #{id}
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        //检查sql是否是${}
        if (textSqlNode.isDynamic()) {
            //如果是${}那么直接不解析
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
            //如果不是,则直接生成静态SQL
            //#{} -> ?
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
          //如果是增删改
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }

进入sqlSource = new RawSqlSource()>sqlSourceParser.parse()

/*从上面的代码段到这一段中间需要经过很多代码,就不一段一段贴了*/
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    //这里会生成一个GenericTokenParser,传入#{}作为开始和结束,然后调用其parse方法,即可将#{}换为 ?
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    //这里可以解析#{} 将其替换为?
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

//经过一段复杂的解析过程
public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    //遍历里面所有的#{} select ?  ,#{id1} ${}
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
            //使用占位符 ?
            //注意handler.handleToken()方法,这个方法是核心
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
}

//BindingTokenParser 的handleToken
//当扫描到${}的时候调用此方法  其实就是不解析 在运行时候在替换成具体的值
@Override
public String handleToken(String content) {
  this.isDynamic = true;
  return null;
}
//ParameterMappingTokenHandler的handleToken
//全局扫描#{id} 字符串之后  会把里面所有 #{} 调用handleToken 替换为?
@Override
public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
}

4、构建总结

4.1 总结

MyBatis需要做的就是,先判断这个节点是用来干什么的,然后再获取这个节点的id、parameterType、resultType等属性,封装成一个MappedStatement对象,由于这个对象很复杂,所以MyBatis使用了构造者模式来构造这个对象,最后当MappedStatement对象构造完成后,将其封装到Configuration对象中。

MyBatis需要对配置文件进行解析,最终会解析成一个Configuration对象

  • Configuration对象,保存了mybatis-config.xml的配置信息。
  • MappedStatement,保存了XXXMapper.xml的配置信息。

但是最终MappedStatement对象会封装到Configuration对象中,合二为一,成为一个单独的对象,也就是Configuration

在这里插入图片描述

4.2 面试回答

我在开发xxxx项目的时候、使用Mybatis开发项目,我对Mybatis的认识是:它其实是一个orm持久层框架,其实就对jdbc一个封装而得的框架,使用好处其实就可以把jdbc从连接开辟事务管理以及连接关闭和sql执行,对数据的映射pojo整个过程进行一个封装而已。它的整个执行的过程:

  • 首先会引入mybatis依赖,然后会定义个xml核心配置文件放入类路径resouces,这个文件里面就描述了数据源、mapper映射、别名的映射、数据类型转换、插件、属性配置等。定义好以后,那么接下就是创建一个SqlSessionFactory对象,但是在创建这个对象之前,我们会进行xml文件的解析,解析过程中会使用SqlSessionFacotoryBuilder里面提供了一个build方法。这个方法的做了一个非常核心的事情:初始化Configuration对象,并且把对应类的属性的对象全部初始化,并且解析核心xml文件
  • 把解析核心的xml配置文件的内容放入到Configuration对象中属性中,其中就包括别名的映射,在初始化阶段别名映射会自动注册一些常用的别名。如果我们自己也配置也会自动注册到
    Configuration对象的TypeAliasRegistry的map中
  • 并且把在配置文件中的数据源和事务解析以后放入到Environment,给后续的执行,提供数据链接和事务管理
  • 然后在解析xxxMapper.xml配置文件,根据配置文件解析的规则,会解析里面对应的节点。比如:<select<update<insert <delete<dql <cache<cache-ref <resultMap等,然后把每个解析的节点放入到一个叫MapperStament对象,sql语句就放入到这个对象SqlSource中
  • 并且把解析的每一个节点对应的MapperStatment同时放入到Configuration全局的
    Map (mapperedStatments)中,以节点的id和命名空间+id做为key,以MapperStatement对象做value,给后续执行提供一个参考和方向

三、Mybatis的执行

1、SqlSession对象生成

1.1 Xml对象直接生成

核心

将SqlSessionFactoryBuilder中通过build方法创建和装配好Configuration对象通过构造函数进行下传,传递到SqlSession中,最后开辟SqlSession会话对象

源码分析

public static void main(String[] args) throws IOException {
    // 加载mybatis框架主配置文件
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    // 读取解析配置文件内容,创建SqlSessionFacory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    /*---------开始注入执行------------*/
    // 获取sqlSession对象
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 执行数据库操作
    List<Employee> list = sqlSession.selectList("test.getEmp");
    System.out.println(list);
    // 释放资源
    sqlSession.close();
}

这里进入SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);方法后,Configuration是DefaultSqlSessionFactory的一个属性。而SqlSessionFactoryBuilderbuild方法中实际上就是调用XMLConfigBuilder对xml文件进行解析生成Configuration对象,然后注入到SqlSessionFactory中

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      //解析config.xml(mybatis解析xml是用的  java dom)     dom4j sax...
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      //parse(): 解析config.xml里面的节点
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
}

public SqlSessionFactory build(Configuration config) {
      //注入到SqlSessionFactory
    return new DefaultSqlSessionFactory(config);
}
public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
}

通过调用sqlSessionFactory.openSession();方法来获取SqlSession对象,而openSession中实际上就是对SqlSession做了进一步的加工封装,包括增加了事务、执行器等

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        //对SqlSession对象进行进一步加工封装
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        final Executor executor = configuration.newExecutor(tx, execType);
        //构建SqlSession对象
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        closeTransaction(tx); // may have fetched a connection so lets call close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

1.2 mapper 代理对象的生成

首先需要修改部分文件内容

// 创建EmployeeMapper、代理mapper 
package org.demo.mapper;
import org.demo.po.Employee;
import java.util.List;
public interface EmployeeMapper {

    List<Employee> getEmp();
}

//同时修改EmployeeMapper.xml的命名空间namespace为
//<mapper namespace="org.demo.mapper.EmployeeMapper">


//最后修改测试类
public static void main(String[] args) throws IOException {
    // 加载mybatis框架主配置文件
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    // 读取解析配置文件内容,创建SqlSessionFacory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    // 获取sqlSession对象
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //获取Mapper
    EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
    // 执行数据库操作
    List<Employee> list = mapper.getEmp();
    System.out.println(list);
    // 释放资源
    sqlSession.close();
}

源码分析

从SqlSession的getMapper()方法进入,可以看到这里mapperProxyFactory对象会从一个叫做knownMappers的对象中以type为key取出值,这个knownMappers是一个HashMap,存放了我们的EmployeeMapper对象,而这里的type,就是我们上面写的Mapper接口

//getMapper方法最终会调用到这里,这个是MapperRegistry的getMapper方法
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
      //MapperProxyFactory  在解析的时候会生成一个map  map中会有我们的DemoMapper的Class
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
}

对于knownMappers生成,在configuration对象在解析的时候,会调用parse()方法,这个方法内部有一个bindMapperForNamespace方法,而就是这个方法帮我们完成了knownMappers的生成,并且将我们的Mapper接口put进去

public void parse() {
      //判断文件是否之前解析过
    if (!configuration.isResourceLoaded(resource)) {
        //解析mapper文件
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      //这里:绑定Namespace里面的Class对象*
      bindMapperForNamespace();
    }

    //重新解析之前解析不了的节点
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }
private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          configuration.addLoadedResource("namespace:" + namespace);
            //这里将接口class传入
          configuration.addMapper(boundType);
        }
      }
    }
  }
public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
          //这里将接口信息put进konwMappers。
        knownMappers.put(type, new MapperProxyFactory<>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
}

我们在getMapper之后,获取到的是一个Class,之后的代码就简单了,就是生成标准的代理类了,调用newInstance()方法。到这里,就完成了代理对象MapperProxy)的创建,很明显的,MyBatis的底层就是对我们的接口进行代理类的实例化,从而操作数据库。

public T newInstance(SqlSession sqlSession) {
    //首先会调用这个newInstance方法
    //动态代理逻辑在MapperProxy里面
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    //通过这里调用下面的newInstance方法
    return newInstance(mapperProxy);
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
     //jdk自带的动态代理
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

查看动态代理调用的方法逻辑,进入MapperProxy类,发现实现了InvocationHandler接口。

在方法开始代理之前,首先会先判断是否调用了Object类的方法,如果是,那么MyBatis不会去改变其行为,直接返回,如果是默认方法,则绑定到代理对象中然后调用,如果都不是,那么就是我们定义的mapper接口方法了,那么就开始执行。执行方法需要一个MapperMethod对象,这个对象是MyBatis执行方法逻辑使用的,MyBatis这里获取MapperMethod对象的方式是,首先去方法缓存中看看是否已经存在了,如果不存在则new一个然后存入缓存中,因为创建代理对象是十分消耗资源的操作。总而言之,这里会得到一个MapperMethod对象,然后通过MapperMethod的excute()方法,来真正地执行逻辑。

public class MapperProxy<T> implements InvocationHandler, Serializable {

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
      //构造
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //这就是一个很标准的JDK动态代理了
    //执行的时候会调用invoke方法
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
          //判断方法所属的类
          //是不是调用的Object默认的方法
          //如果是  则不代理,不改变原先方法的行为
        return method.invoke(this, args);
      } else if (method.isDefault()) {
          //对于默认方法的处理
          //判断是否为default方法,即接口中定义的默认方法。
          //如果是接口中的默认方法则把方法绑定到代理对象中然后调用。
          //这里不详细说
        if (privateLookupInMethod == null) {
          return invokeDefaultMethodJava8(proxy, method, args);
        } else {
          return invokeDefaultMethodJava9(proxy, method, args);
        }
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //如果不是默认方法,则真正开始执行MyBatis代理逻辑。
    //获取MapperMethod代理对象
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //执行
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
      //动态代理会有缓存,computeIfAbsent 如果缓存中有则直接从缓存中拿
      //如果缓存中没有,则new一个然后放入缓存中
      //因为动态代理是很耗资源的
    return methodCache.computeIfAbsent(method,
        k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
  }
}

最后执行逻辑,这里首先会判断SQL的类型:SELECT|DELETE|UPDATE|INSERT,判断SQL类型为SELECT之后,就开始判断返回值类型,根据不同的情况做不同的操作。然后开始获取参数>执行SQL

//execute() 这里是真正执行SQL的地方
public Object execute(SqlSession sqlSession, Object[] args) {
    //判断是哪一种SQL语句
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
          //我们的例子是查询

          //判断是否有返回值
        if (method.returnsVoid() && method.hasResultHandler()) {
            //无返回值
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
            //返回值多行 这里调用这个方法
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
            //返回Map
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
            //返回Cursor
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
}

//返回值多行 这里调用这个方法
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    //返回值多行时执行的方法
    List<E> result;
    //param是我们传入的参数,如果传入的是Map,那么这个实际上就是Map对象
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
        //如果有分页
      RowBounds rowBounds = method.extractRowBounds(args);
        //执行SQL的位置
      result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
        //如果没有
        //执行SQL的位置
      result = sqlSession.selectList(command.getName(), param);
    }
    // issue #510 Collections & arrays support
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      if (method.getReturnType().isArray()) {
        return convertToArray(result);
      } else {
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
}

/**
*  获取参数名的方法
*/
public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
        //如果传过来的参数是空
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
        //如果参数上没有加注解例如@Param,且参数只有一个,则直接返回参数
      return args[names.firstKey()];
    } else {
        //如果参数上加了注解,或者参数有多个。
          //那么MyBatis会封装参数为一个Map,但是要注意,由于jdk的原因,我们只能获取到参数下标和参数名,但是参数名会变成arg0,arg1.
        //所以传入多个参数的时候,最好加@Param,否则假设传入多个String,会造成#{}获取不到值的情况
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
          //entry.getValue 就是参数名称
        param.put(entry.getValue(), args[entry.getKey()]);
        //如果传很多个String,也可以使用param1,param2.。。
        // add generic param names (param1, param2, ...)
        final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
        // ensure not to overwrite parameter named with @Param
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }

2、执行SQL前述

进入sqlSession.selectList("test.getEmp");方法,可以发现在调用sqlsession执行的selectList、insert、update、delete的时候,其实就是根据执行的statement名字,到Configuration的mapperStatements对应的map中去找到有没有一个对应的 MapperStatement对象,如果找到就返回这个对象,然后给后续执行一个依据和参考

在这里插入图片描述

3、执行SQL语句——执行器

3.1 执行架构

https://zhuanlan.zhihu.com/p/299020451
在这里插入图片描述

  • 执行器:Executor, 处理流程的头部,主要负责缓存、事务、批处理。一个执行可用于执行多条SQL。它和SQL处理器是1对N的关系
  • Sql处理器:StatementHandler 用于和JDBC打道,比如基于SQL声明Statement、设置参数、然后就是调用Statement来执行。它只能用于一次SQL的执行
  • 参数处理器:ParameterHandler,用于解析SQL参数,并基于参数映射,填充至PrepareStatement。同样它只能用于一次SQL的执行
  • 结果集处理器:ResultSetHandler,用于读取ResultSet 结果集,并基于结果集映射,封装成JAVA对象。他也只用用于一次SQL的执行

3.2 执行器Executor

执行器的实现有三种:SimpleExecute、ReuseExecute和BatchExecute,这三种执行器有个抽象的基础执行器BaseExecutor,用于维护缓存和事务;此外通过装饰器形式添加了一个缓存执行器CachingExecutor,用于处理二级缓存

  • SimpleExecute 简单执行器(默认)
    SimpleExecutor是执行器的默认实现,主要完成了“执行”功能,在利用StatementHandler 完成。每次调用执行方法 都会构建一个StatementHandler,并预行参数,然后执行
    默认情况是executor是CachingExecutor。这个执行器是二级缓存的执行器,如果在配置文件xxxxMapper.xml文件中申明了<cache/>节点的话,就是使用CachingExecutor;如果没有,就会委托SimpleExecutor(默认类型是simple,在configuration创建的时候指定)执行器去执行你的SQL语句,然后这里会执行的结果放入loaclCache一级缓存中。
    在这里插入图片描述

  • ReuseExecute 可重用执行器
    ReuseExecutor 区别在于他会将在会话期间内的Statement进行缓存,并使用SQL语句作为Key。所以当执行下一请求的时候,不在重复构建Statement,而是从缓存中取出并设置参数,然后执行(参数不同也可以重用)

  • BatchExecute 批处理执行器
    BatchExecutor 顾名思议,它就是用来作批处理的。但会将所 有SQL请求集中起来,最后调用Executor.flushStatements() 方法时一次性将所有请求发送至数据库

3.3 **SimpleExecute **简单执行器源码分析(重点?)

执行SQL的核心方法就是selectList,即使是selectOne,底层实际上也是调用了selectList方法,然后取第一个而已

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      //MappedStatement:解析XML时生成的对象, 解析某一个SQL  会封装成MappedStatement,里面存放了我们所有执行SQL所需要的信息
      MappedStatement ms = configuration.getMappedStatement(statement);
      //查询,通过executor
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
}

selectList内部调用了Executor对象执行SQL语句,首先进入的是CachingExecutor执行器,若没有开启二级缓存,那么委托简单执行器

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //获取sql语句
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    //生成一个缓存的key  
    //key = ms.id + rowBounds.getOffset()+rowBounds.getOffset()+sql+参数+上下文坏境的id
    //这里是-1954235241:110303602:test.getEmp:0:2147483647:select * from employee:development
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

@Override
//二级缓存查询
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
      //二级缓存的Cache
    Cache cache = ms.getCache();
    if (cache != null) {
      //如果Cache不为空则进入
      //如果有需要的话,就刷新缓存(有些缓存是定时刷新的,需要用到这个)
      flushCacheIfRequired(ms);
      //如果这个statement用到了缓存(二级缓存的作用域是namespace,也可以理解为这里的ms)
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        //先从缓存拿
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
            //如果缓存的数据等于空,那么查询数据库
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //查询完毕后将数据放入二级缓存
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        //返回
        return list;
      }
    }
    //如果cache根本就不存在,那么直接查询一级缓存。并委托delegate(默认简单执行器)查询
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

首先MyBatis在查询时,不会直接查询数据库,而是会进行二级缓存的查询,由于二级缓存的作用域是namespace,也可以理解为一个mapper,所以还会判断一下这个mapper是否开启了二级缓存,如果没有开启,则进入一级缓存继续查询。

//一级缓存查询
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
       //查询栈+1
      queryStack++;
      //一级缓存
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
          //对于存储过程有输出资源的处理
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
          //如果缓存为空,则从数据库拿
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
       //查询栈-1
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    //结果返回
    return list;
}

如果一级缓存localCache里查到了,那么直接就返回结果了,如果一级缓存没有查到结果,那么最终会进入数据库进行查询

//数据库查询
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    //先往一级缓存中put一个占位符
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      //调用doQuery方法查询数据库
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    //往缓存中put真实数据
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}
//SimpleExecutor类
//真实数据库查询
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      //封装,StatementHandler也是MyBatis四大对象之一
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      //#{} -> ? 的SQL在这里初始化
      stmt = prepareStatement(handler, ms.getStatementLog());
      //参数赋值完毕之后,才会真正地查询。
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
}

总结,一级缓存和二级缓存的key是一样的,一级缓存默认开启,二级缓存需要设置开启。这里CacheExecutor使用的是装饰者模式,即在不改变原有类结构和继承的情况下,通过包装原对象去扩展一个新功能。
在这里插入图片描述

4、执行SQL语句——数据库查询

执行流程
在这里插入图片描述

4.1 StatementHandler介绍

StatementHandler即为JDBC处理器,基于JDBC构建JDBC Statement并设置参数,然后执行Sql。每调用会话当中一次SQl,都会有与之相对应的且唯一的Statement实例,一个SQL请求会经过会话,然后是执行器,最由StatementHandler执行jdbc最终到达数据库,这三者之间比例是1:1:n

StatementHandler接口定义了JDBC操作的相关方法如下,

// 基于JDBC 声明Statement
Statement prepare(Connection connection, Integer transactionTimeout)
    throws SQLException;
// 为Statement 设置方法
void parameterize(Statement statement)
    throws SQLException;
// 添加批处理(并非执行)
void batch(Statement statement)
    throws SQLException;
// 执行update操作
int update(Statement statement)
    throws SQLException;
// 执行query操作
<E> List<E> query(Statement statement, ResultHandler resultHandler)
    throws SQLException;

StatementHandler有三个子类SimpleStatementHandlerPreparedStatementHandlerCallableStatementHandler,分别对应JDBC中的StatementPreparedStatementCallableStatement

4.2 参数处理和转换

参数处理即将Java Bean转换成数据类型。总共要经历过三个步骤,ParamNameResolver(参数转换)、ParameterHandler(参数映射)、TypeHandler(参数赋值)

参数转换

所有转换逻辑均在ParamNameResolver中实现

@Select("select * from employee where id = #{id}")
@Options
Employee getEmpById(@Param("id") Integer id);
  • 单个参数的情况下且没有设置@param注解会直接转换,勿略SQL中的引用名称
  • 多个参数情况:优先采用@Param中设置的名称,如果没有则用参数序号代替 即"param1、parm2等"
    在这里插入图片描述

参数映射

映射是指Map中的key如何与SQL中绑定的参数相对应。以下这几种情况

  • 单个原始类型:直接映射,勿略SQL中引用名称
  • Map类型:基于Map key映射
  • Object:基于属性名称映射,支持嵌套对象属性访问

参数赋值

通过TypeHandlerPrepareStatement设置值,通常情况下一般的数据类型MyBatis都有与之相对应的TypeHandler

4.3 结果集封装

MetaObject相当于一个工具类,里面还包括分词器等,可以参考MetaObject详解

在这里插入图片描述

读取ResultSet数据,并将每一行转换成相对应的对象。用户可在转换的过程当中可以通过ResultContext来控制是否要继续转换,转换后的对象都会暂存在ResultHandler中最后统一封装成list返回给调用方,结果集转换中99%的逻辑DefaultResultSetHandler中实现。整个流程可大致分为以下阶段:

  • 读取结果集
  • 遍历结果集当中的行
  • 创建对象
  • 填充属性
//PreparedStatementHandler,这里是真正查询
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
  ps.execute();
  return resultSetHandler.<E> handleResultSets(ps);
}

在SQL执行阶段,MyBatis已经完成了对数据的查询,那么现在还存在最后一个问题,那就是结果集处理,换句话来说,就是将结果集封装成对象,这里会创建一个处理结果集的对象

//DefaultResultSetHandler
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
    //resultMap可以通过多个标签指定多个值,所以存在多个结果集
    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    //拿到当前第一个结果集
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    //拿到所有的resultMap
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    //resultMap的数量
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    //循环处理每一个结果集
    while (rsw != null && resultMapCount > resultSetCount) {
        //开始封装结果集 list.get(index) 获取结果集
      ResultMap resultMap = resultMaps.get(resultSetCount);
      //传入resultMap处理结果集 rsw 当前结果集(主线)
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }
    //如果只有一个结果集,那么从多结果集中取出第一个
    return collapseSingleResultList(multipleResults);
}
//处理结果集
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
   //处理结果集
      try {
      if (parentMapping != null) {
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        if (resultHandler == null) {
          //判断resultHandler是否为空,如果为空建立一个默认的。
          //结果集处理器
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          //处理行数据
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      // issue #228 (close resultsets)
      //关闭结果集
      closeResultSet(rsw.getResultSet());
    }
}

调用handleRwoValues()方法进行行数据的处理

//处理行数据
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    //是否存在内嵌的结果集
    if (resultMap.hasNestedResultMaps()) {
      ensureNoRowBounds();
      checkResultHandler();
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
        //不存在内嵌的结果集
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
}
//没有内嵌结果集时调用
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    //获取当前结果集
    ResultSet resultSet = rsw.getResultSet();
    skipRows(resultSet, rowBounds);
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
        //遍历结果集
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
      //拿到行数据,将行数据包装成一个Object
      Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
      storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
}
//通过每行的结果集,然后将其直接封装成一个Object对象
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    //创建一个空的Map存值
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    //创建一个空对象装行数据
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())){
        //通过反射操作返回值
        //此时metaObject.originalObject = rowValue
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      boolean foundValues = this.useConstructorMappings;
      if (shouldApplyAutomaticMappings(resultMap, false)) {
    //判断是否需要自动映射,默认自动映射,也可以通过resultMap节点上的autoMapping配置是否自动映射
          //这里是自动映射的操作。
        foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
      }
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
      foundValues = lazyLoader.size() > 0 || foundValues;
      rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
}
//在getRowValue中会判断是否是自动映射的,我们这里没有使用ResultMap,所以是自动映射(默认),那么就进入applyAutomaticMappings()方法,而这个方法就会完成对象的封装。
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
      //自动映射参数列表
    List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
      //是否找到了该列
    boolean foundValues = false;
    if (!autoMapping.isEmpty()) {
        //遍历
      for (UnMappedColumnAutoMapping mapping : autoMapping) {
          //通过列名获取值
        final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
        if (value != null) {
            //如果值不为空,说明找到了该列
          foundValues = true;
        }
        if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
          // gcode issue #377, call setter on nulls (value is not 'found')
            //在这里赋值
          metaObject.setValue(mapping.property, value);
        }
      }
    }
    return foundValues;
}

我们可以看到这个方法会通过遍历参数列表从而通过metaObject.setValue(mapping.property, value);对返回对象进行赋值,所有的赋值操作在内部都是通过一个叫ObjectWrapper的对象完成的,先看看中代码的metaObject.setValue()方法

//MetaObject类,工具类
public void setValue(String name, Object value) {
    PropertyTokenizer prop = new PropertyTokenizer(name);
    if (prop.hasNext()) {
      MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
      if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
        if (value == null) {
          // don't instantiate child path if value is null
          return;
        } else {
          metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
        }
      }
      metaValue.setValue(prop.getChildren(), value);
    } else {
        //这个方法最终会调用objectWrapper.set()对结果进行赋值
      objectWrapper.set(prop, value);
    }
}

objectWrapper有两个实现:BeanWrapperMapWrapper,如果是自定义类型,那么就会调用BeanWrapper的set方法。MapWrapper的set方法实际上就是将属性名和属性值放到map的key和value中,而BeanWrapper则是使用了反射,调用了Bean的set方法,将值注入。

//MapWrapper的set方法
public void set(PropertyTokenizer prop, Object value) {
    if (prop.getIndex() != null) {
      Object collection = resolveCollection(prop, map);
      setCollectionValue(prop, collection, value);
    } else {
      //实际上就是调用了Map的put方法将属性名和属性值放入map中
      map.put(prop.getName(), value);
    }
}

//BeanWrapper的set方法
public void set(PropertyTokenizer prop, Object value) {
    if (prop.getIndex() != null) {
      Object collection = resolveCollection(prop, object);
      setCollectionValue(prop, collection, value);
    } else {
      //在这里赋值,通过反射赋值,调用setXX()方法赋值
      setBeanProperty(prop, object, value);
    }
}
private void setBeanProperty(PropertyTokenizer prop, Object object, Object value) {
    try {
      Invoker method = metaClass.getSetInvoker(prop.getName());
      Object[] params = {value};
      try {
        method.invoke(object, params);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    } catch (Throwable t) {
      throw new ReflectionException("Could not set property '" + prop.getName() + "' of '" + object.getClass() + "' with value '" + value + "' Cause: " + t.toString(), t);
    }
}

4.4 结果集映射

映射是指返回的ResultSet列与Java Bean 属性之间的对应关系。通过ResultMapping进行映射描述,在用ResultMap封装成一个整体,包括手动映射和自动映射

property属性名(必填)
column列名(必填)
jdbcTypejdbc类型(可自动推导)
javaTypejava类型(可自动推导)
typeHandler类型处理器(可自动推导)

在这里插入图片描述
在这里插入图片描述

4.5 懒加载

懒加载的参考文章之一

懒加载是为了改善在映射结果集解析对象属性时,大量的嵌套子查询的并发效率问题,当设置懒加载后,只有在使用指定属性时才会触发子查询,从而实现分散SQL请求的目的

配置方式

在mybais主配置文件中配置开启侵入式加载深度加载,也可以在xml映射文件中配置fetchType,有效值为 lazyeager。 指定属性后,将在映射中忽略全局配置参数 lazyLoadingEnabled,使用属性的值

<!--配置直接延迟加载,默认是false-->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 侵入式延迟加载开关,默认是true -->
<setting name="aggressiveLazyLoading" value="true"/>

内部原理

代理过程发生在结果集解析创建对象之后(DefaultResultSetHandler.createResultObject),如果对应的属性设置了懒加载,则会通过**ProxyFactory **创建代理对象,该对象继承自原对象,然后将对象的值全部拷贝到代理对象,并设置相应MethodHandler(原对象直接抛弃)

通过对Bean的动态代理,重写所有属性的getXxx方法,代理之后Bean会包含一个MethodHandler,内部在包含一个Map用于存放待执行懒加载,执行前懒加载前会移除。LoadPair用于针对反序列化的Bean准备执行环境。ResultLoader用于执行加载操作,执行前如果原执行器关闭会创建一个新的。
在这里插入图片描述

4.6 嵌套映射

映射是指返回的ResultSet列与Java Bean 属性之间的对应关系。通过ResultMapping进行映射描述,在用ResultMap封装成一个整体。映射分为简单映射与复合嵌套映射,联合查询分为一对一查询和一对多查询
在这里插入图片描述
流程说明

所有映射流程的解析都是在DefaultResultSetHandler当中完成。主要方法如下:

  • handleRowValuesForNestedResultMap()

嵌套结果集解析入口,在这里会遍历结果集中所有行。并为每一行创建一个RowKey对象。然后调用getRowValue()获取解析结果对象。最后保存至ResultHandler中(注:调用getRowValue前会基于RowKey获取已解析的对象,然后作为partialObject参数发给getRowValue)

  • getRowValue()

该方法最终会基于当前行生成一个解析好对象。具体职责包括,1.创建对象、2.填充普通属性和3.填充嵌套属性。在解析嵌套属性时会以递归的方式在调用getRowValue获取子对象。最后一步4.基于RowKey 暂存当前解析对象(如果partialObject参数不为空 只会执行 第3步。因为1、2已经执行过了)

  • applyNestedResultMappings()

解析并填充嵌套结果集映射,遍历所有嵌套映射,然后获取其嵌套ResultMap。接着创建RowKey 去获取暂存区的值。然后调用getRowValue 获取属性对象。最后填充至父对象(如果通过RowKey能获取到属性对象,它还是会去调用getRowsValue,因为有可能属下还存在未解析的属性)

MyBatis循环依赖问题

mybatis解决循环依赖主要是利用一级缓存和内置的queryStack标识。mybatis中BaseExecutor执行器对一级缓存进行管控,利用queryStack标识对最终结果进行处理,一级缓存对没有操作的查询缓存key进行空参填充,在嵌套子查询中会判断是否命中一级缓存,然后将其添加到延迟队列(非懒加载),直到整个查询结束再对其进行延迟队列的加载,填充所有数据
在这里插入图片描述

其源码主要在DefaultResultSetHandler类中,方法调用手动映射,具体为applyPropertyMappings>getPropertyMappingValue>getNestedQueryMappingValue>ResultLoader- 结果集加载器>再次进入BaseExecutor的query中,对queryStack进行累加,直到跳出整个查询

四、Mybatis的缓存

1、缓存概述

myBatis中存在两个缓存,一级缓存和二级缓存

  • 一级缓存:也叫做会话级缓存,生命周期仅存在于当前会话,不可以直接关关闭。但可以通过flushCachelocalCacheScope对其做相应控制。
  • 二级缓存:也叫应用级缓存,缓存对象存在于整个应用周期,而且可以跨线程使用。

2、一级缓存

2.1 缓存命中与清空

缓存命中参数

  • SQL与参数相同
  • 同一个会话
  • 相同的MapperStatement ID
  • RowBounds行范围相同

触发清空缓存

  • 手动调用clearCache,注意clearLocalCache 不是清空某条具体数据,而是清当前会话下所有一级缓存数据
  • 执行提交回滚(commit、Rolback)
  • 执行任意增删改update
  • 配置flushCache=true
  • 缓存作用域为Statement(即子查询,子查询依赖一级缓存)

2.2 集成Spring一级缓存失效

因为Spring 对SqlSession进行了封装,通过SqlSessionTemplae ,使得每次调用Sql,都会重新构建一个SqlSession,解决方法是

  • 开启事务,因为一旦开启事务,Spring就不会在执行完SQL之后就销毁SqlSession,因为SqlSession一旦关闭,事务就没了,一旦我们开启事务,在事务期间内,缓存会一直存在
  • 使用二级缓存

在这里插入图片描述

3、二级缓存

3.1 简介

二级缓存也称作是应用级缓存,与一级缓存不同的,是它的作用范围是整个应用,而且可以跨线程使用。所以二级缓存有更高的命中率,适合缓存一些修改较少的数据,在流程上是先访问二级缓存,在访问一级缓存。二级缓存的更新,必须是在会话提交之后,同时要提交之后才能命中缓存

3.2 二级缓存使用

缓存空间声明

二级默认缓存默认是不开启的,需要为其声明缓存空间才可以使用,通过@CacheNamespace 或 在xml配置<Cache/>。声明之后该缓存为该Mapper所独有,其它Mapper不能访问。如需要多个Mapper共享一个缓存空间可通过@CacheNamespaceRef<cache-ref namespace=""/>进行引用同一个缓存空间。@CacheNamespace 详细配置见下表:

配置说明
implementation指定缓存的存储实现类,默认是用HashMap存储在内存当中
eviction指定缓存溢出淘汰实现类,默认LRU ,清除最少使用
flushInterval设置缓存定时全部清空时间,默认不清空。
size指定缓存容量,超出后就会按eviction指定算法进行淘汰
readWritetrue即通过序列化复制,来保证缓存对象是可读写的,默认true
blocking为每个Key的访问添加阻塞锁,防止缓存击穿
properties为上述组件,配置额外参数,key对应组件中的字段名。

缓存其它配置

除@CacheNamespace 还可以通过其它参数来控制二级缓存

字段配置域说明
cacheEnabled在mybatis设置二级缓存全局开关,默认开启
useCache<selectupdate
flushCache<selectupdate

注意:若*Mapper.xmlmapper接口同时设置SQL查询,并同时配置了缓存,那么两个缓存空间是不一致,需要用缓存引用ref使用同一个缓存空间

3.3 责任链设计

这里MyBatis抽像出Cache接口,其只定义了缓存中最基本的功能方法:

  • 设置缓存
  • 获取缓存
  • 清除缓存
  • 获取缓存数量

然后上述中每一个功能都会对应一个组件类,并基于装饰者加责任链的模式,将各个组件进行串联。在执行缓存的基本功能时,其它的缓存逻辑会沿着这个责任链依次往下传递。
在这里插入图片描述

3.4 执行流程

原本会话是通过Executor实现SQL调用,这里基于装饰器模式使用CachingExecutor对SQL调用逻辑进行拦截,以嵌入二级缓存相关逻辑。这里SqlSession会话可以对应多个暂存区,而多个暂存区对应一个缓存空间
在这里插入图片描述
查询操作query

当会话调用query() 时,会基于查询语句、参数等数据组成缓存Key,然后尝试从二级缓存中读取数据。读到就直接返回,没有就调用被装饰的Executor去查询数据库,然后在填充至对应的暂存区。

请注意,这里的查询是实时从缓存空间读取的,而变更,只会记录在暂存区

更新操作update

当执行update操作时,同样会基于查询的语句和参数组成缓存KEY,然后在执行update之前清空缓存。这里清空只针对暂存区,同时记录清空的标记,以便当会话提交之时,依据该标记去清空二级缓存空间。

如果在查询操作中配置了flushCache=true ,也会执行相同的操作。

提交操作commit

当会话执行commit操作后,会将该会话下所有暂存区的变更,更新到对应二级缓存空间去。

3.5 缓存源码事务分析

这个类是MyBatis用于缓存事务管理的类

public class TransactionalCacheManager {
 //事务缓存
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }
  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }
  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }
  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }
  private TransactionalCache getTransactionalCache(Cache cache) {
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
  }
}

TransactionalCacheManager中封装了一个Map,用于将事务缓存对象缓存起来,这个Map的Key是我们的二级缓存对象,而Value是一个叫做TransactionalCache。

  • 其中在getObject()方法中存在两个分支:如果发现缓存中取出的数据为null,那么会把这个key放到entriesMissedInCache中,这个对象的主要作用就是将我们未命中的key全都保存下来,防止缓存被击穿,并且当我们在缓存中无法查询到数据,那么就有可能到一级缓存和数据库中查询,那么查询过后会调用putObject()方法,这个方法本应该将我们查询到的数据put到真实缓存中,但是现在由于存在事务,所以暂时先放到entriesToAddOnCommit中;如果发现缓存中取出的数据不为null,那么会查看事务提交标识(clearOnCommit)是否为true,如果为true,代表事务已经提交了,之后缓存会被清空,所以返回null,如果为false,那么由于事务还没有被提交,所以返回当前缓存中存的数据
  • 事务提交成功时有以下几步:清空真实缓存、将本地缓存(未提交的事务缓存 entriesToAddOnCommit)刷新到真实缓存、将所有值复位
  • 回滚步骤:清空真实缓存中未命中的缓存、将所有值复位
public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);

  //真实缓存对象
  private final Cache delegate;
  //是否需要清空提交空间的标识
  private boolean clearOnCommit;
  //所有待提交的缓存
  private final Map<Object, Object> entriesToAddOnCommit;
  //未命中的缓存集合,防止击穿缓存,并且如果查询到的数据为null,说明要通过数据库查询,有可能存在数据不一致,都记录到这个地方
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<>();
    this.entriesMissedInCache = new HashSet<>();
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  @Override
  public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
        //如果取出的是空,那么放到未命中缓存,并且在查询数据库之后putObject中将本应该放到真实缓存中的键值对放到待提交事务缓存
      entriesMissedInCache.add(key);
    }
    //如果不为空
    // issue #146
    //查看缓存清空标识是否为false,如果事务提交了就为true,事务提交了会更新缓存,所以返回null。
    if (clearOnCommit) {
      return null;
    } else {
        //如果事务没有提交,那么返回原先缓存中的数据,
      return object;
    }
  }

  @Override
  public void putObject(Object key, Object object) {
      //如果返回的数据为null,那么有可能到数据库查询,查询到的数据先放置到待提交事务的缓存中
      //本来应该put到缓存中,现在put到待提交事务的缓存中去。
    entriesToAddOnCommit.put(key, object);
  }

  @Override
  public Object removeObject(Object key) {
    return null;
  }

  @Override
  public void clear() {
      //如果事务提交了,那么将清空缓存提交标识设置为true
    clearOnCommit = true;
    //清空entriesToAddOnCommit
    entriesToAddOnCommit.clear();
  }

  public void commit() {
    if (clearOnCommit) {
        //如果为true,那么就清空缓存。
      delegate.clear();
    }
    //把本地缓存刷新到真实缓存。
    flushPendingEntries();
    //然后将所有值复位。
    reset();
  }

  public void rollback() {
      //事务回滚
    unlockMissedEntries();
    reset();
  }

  private void reset() {
      //复位操作。
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
  }

  private void flushPendingEntries() {
      //遍历事务管理器中待提交的缓存
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
        //写入到真实的缓存中。
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
        //把未命中的一起put
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

  private void unlockMissedEntries() {
    for (Object entry : entriesMissedInCache) {
        //清空真实缓存区中未命中的缓存。
    try {
        delegate.removeObject(entry);
      } catch (Exception e) {
        log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
            + "Consider upgrading your cache adapter to the latest version.  Cause: " + e);
      }
    }
  }

}

3.6 使用经验

二级缓存不能存在一直增多的数据

由于二级缓存的影响范围不是SqlSession而是namespace,所以二级缓存会在你的应用启动时一直存在直到应用关闭,所以二级缓存中不能存在随着时间数据量越来越大的数据,这样有可能会造成内存空间被占满。

二级缓存有可能存在脏读的问题(可避免)

由于二级缓存的作用域为namespace,那么就可以假设这么一个场景,有两个namespace操作一张表,第一个namespace查询该表并回写到内存中,第二个namespace往表中插一条数据,那么第一个namespace的二级缓存是不会清空这个缓存的内容的,在下一次查询中,还会通过缓存去查询,这样会造成数据的不一致。所以当项目里有多个命名空间操作同一张表的时候,最好不要用二级缓存,或者使用二级缓存时避免用两个namespace操作一张表。

五、Mybatis插件

1、核心原理

插件机制是为了对MyBatis现有体系进行扩展而提供的入口。底层通过动责任链模式+ JDK动态代理实现。插件的核心是拦截四个接口的子对象,拦截以后会进入到intercept方法中进行业务的处理,而Invocation对象可以获取到四个接口的具体

  • Executor:执行器
  • StatementHandler:JDBC处理器
  • ParameterHandler:参数处理器
  • ResultSetHandler:结果集处理器
//注意interceptorChain.pluginAll()方法
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
        executor = new CachingExecutor(executor, autoCommit);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}


Interceptor类核心方法代码

public interface Interceptor {
  //intercept方法:如果自定插件实现Interceptor覆盖intercept方法,
  //这个方法是一个核心方法,里面参数Invocation对象,这个对象可以通过反射调度原来的对象的方法。
  Object intercept(Invocation invocation) throws Throwable;
  //target被拦截的对象,它的作用把拦截的target对象变成一个代理对象
  Object plugin(Object target);
  //允许plugin在注册的时候,配置插件需要的参数,这个参数可以在mybatsi的核心配置文件中注册插件的时候,一起配置到文件中
  void setProperties(Properties properties);
}

2、源码分析

2.1 插件类创建

首先创建自定义插件

@Intercepts({@Signature(
        type= Executor.class,
        method = "update",
        args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
    //自定义属性
    private int number;

    // 当执行目标方法时会被方法拦截
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget(); //被代理对象
        Method method = invocation.getMethod(); //代理方法
        Object[] args = invocation.getArgs(); //方法参数
        // do something ...... 方法拦截前执行代码块
        Object result = invocation.proceed();
        // do something .......方法拦截后执行代码块
        return result;
    }
    // 生成代理对象,可自定义生成代理对象,这样就无需配置@Intercepts注解。另外需要自行判断是否为拦截目标接口。
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);// 调用通用插件代理生成机器
    }

    //配置属性
    public void setProperties(Properties properties) {
        this.number = Integer.parseInt(properties.getProperty("number", String.valueOf(100)));
    }
}

在config.xml 中添加插件配置,注意配置顺序

<plugins>
    <plugin interceptor="org.demo.plugin.ExamplePlugin">
        <property name="number" value="1"/>
    </plugin>
</plugins>

2.2 源码分析

构建图
在这里插入图片描述

核心代码

插件对象的创建InterceptorChain

//SqlSessionFactoryBuilder类的build方法
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

XMLConfigBuilder会实例化一个Configuration对象,在创建Configuration对象,会调用构造函数,InterceptorChain对象的创建,就是在Configuration的构造函数中进行了初始化,如InterceptorChain interceptorChain = new InterceptorChain();

进入InterceptorChain 代码

public class InterceptorChain {
  //这个集合很重要
  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

  public Object pluginAll(Object target) {
    //pluginAll方法是把具体的四大接口的具体实现类,生成动态代理的方法。
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }
}

在parse()方法解析的时候,parseConfiguration()中解析插件pluginElement(root.evalNode("plugins"))

//XMLConfigBuilder类
private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    //这里循环解析配置文件中的所有定义的插件
    for (XNode child : parent.getChildren()) {
      String interceptor = child.getStringAttribute("interceptor");
      //如果插件有配置属性。获取到配置的属性,然后把属性的值,注册到Properties对象中
      Properties properties = child.getChildrenAsProperties();
      //同时获取到具体的注册的插件对象
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      //调用插件,并赋值插件中的属性
      interceptorInstance.setProperties(properties);
      //注册到interceptorChain中
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

// Configuration类
//这个集合就是把下面解析的插件,进行注册和收集的容器
public void addInterceptor(Interceptor interceptor) {
  interceptorChain.addInterceptor(interceptor);
}

插件的执行流程
在这里插入图片描述

2.3 源码总结

Mybatis插件使用机制就是:** jdk动态代理+责任链的设计模式**。插件的运行和注册会分为几个阶段:

1、定义阶段,定义插件类然后实现Interceptor接口,覆盖这个接口三个方法,分别是:plugin方法,interceptor方法,setProperties方法

  • intercept方法:如果自定插件实现Interceptor覆盖intercept方法,这个方法是一个核心方法,里面参数Invocation对象,这个对象可以通过反射调度原来的对象的方法。
  • plugin方法:target被拦截的对象。它的作用:把拦截的target对象变成一个代理对象
  • setProperties方法:允许plugin在注册的时候,配置插件需要的参数,这个参数可以在mybats的核心配置文件中注册插件的时候,一起配置到文件中

2、注册阶段,写入到mybatis配置文件中,如果是spring整合myabtis化,就使用配置类来进行插件的注册

3、同时在定义的时候,会通过@Intercepts注解和签名,来告诉插件具体要拦截那些类执行的方法,mybatis对四个接口实现类都会进行拦截

4、运行和执行阶段,定义了执行器的插件后,在初始化sqlsession的时候会确定一个执行器,而执行器在创建的时候,会调用executor = (Executor)interceptorChain.pluginAll(executor)。这个方法的作用就是把执行器对象变成一个代理对象,而代理对象的生成,是通过插件的的plugin方法进行生成和创建,具体的话是通过代理类Plugin中的wrap方法创建而生成,生成executor代理对象之后,当代理执行器执行方法的时候,就进入Plugin代理类中invoke方法中进行业务处理

3、分页插件举例

3.1 分页插件原理

首先设定一个Page类,其包含total、size、index 3个属性,在Mapper接口中声明该参数即表示需要执行自动分页逻辑。总体实现步骤包含3个:

  1. 检测是否满足分页条件
  2. 自动求出当前查询的总行数
  3. 修改原有的SQL语句 ,添加 limit offset 关键字。

3.2 检测是否满足分页条件

分页条件是 1.是否为查询方法,2.查询参数中是否带上Page参数。在intercept 方法中可直接获得拦截目标StatementHandler ,通过它又可以获得BoundSql 里面就包含了SQL 和参数。遍历参数即可获得Page。

// 带上分页参数
StatementHandler target = (StatementHandler) invocation.getTarget();
// SQL包 sql、参数、参数映射
BoundSql boundSql = target.getBoundSql();
Object parameterObject = boundSql.getParameterObject();
Page page = null;
if (parameterObject instanceof Page) {
    page = (Page) parameterObject;
} else if (parameterObject instanceof Map) {
    page = (Page) ((Map) parameterObject).values().stream().filter(v -> v instanceof Page).findFirst().orElse(null);
}

3.3 查询总行数

实现逻辑是 将原查询SQL作为子查询进行包装成子查询,然后用原有参数,还是能过原来的参数处理器进行赋值。关于执行是采用JDBC 原生API实现。MyBatis执行器,从而绕开了一二级缓存。

private int selectCount(Invocation invocation) throws SQLException {
    int count = 0;
    StatementHandler target = (StatementHandler) invocation.getTarget();
    // SQL包 sql、参数、参数映射
    String countSql = String.format("select count(*) from (%s) as _page", target.getBoundSql().getSql());
    // JDBC
    Connection connection = (Connection) invocation.getArgs()[0];
    PreparedStatement preparedStatement = connection.prepareStatement(countSql);
    target.getParameterHandler().setParameters(preparedStatement);
    ResultSet resultSet = preparedStatement.executeQuery();
    if (resultSet.next()) {
        count = resultSet.getInt(1);
    }
    resultSet.close();
    preparedStatement.close();

    return count;
}

3.4 修改原SQL

最后一项就是修改原来的SQL,前面我是可以拿到BoundSql 的,但它没有提供修改SQL的方法,这里可以采用反射强行为SQL属性赋值。也可以采用MyBatis提供的工具类SystemMetaObject来赋值

String newSql= String.format("%s limit %s offset %s", boundSql.getSql(),page.getSize(),page.getOffset());
SystemMetaObject.forObject(boundSql).setValue("sql",newSql);

参考文章:

手把手带你阅读Mybatis源码(一)构造篇

手把手带你阅读Mybatis源码(二)执行篇

手把手带你阅读Mybatis源码(三)缓存篇

源码阅读网

MyBatis源码解析大合集

[学相伴飞哥]Mybatis的源码分析-执行过程(一)

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-03-03 15:57:54  更:2022-03-03 16:01:39 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 11:46:34-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码