关于java:MyBatis-架构与原理深入解析面试随便问

1次阅读

共计 26289 个字符,预计需要花费 66 分钟才能阅读完成。

作者:七寸知架构 \
链接:https://www.jianshu.com/p/ec40a82cae28

1 引言

本文次要解说 JDBC 怎么演变到 Mybatis 的突变过程,重点解说了为什么要将 JDBC 封装成 Mybaits 这样一个长久层框架。再而阐述 Mybatis 作为一个数据长久层框架自身有待改良之处。

2 JDBC 实现查问剖析

咱们先看看咱们最相熟也是最根底的通过 JDBC 查询数据库数据,个别须要以下七个步骤:

  1. 加载 JDBC 驱动;
  2. 建设并获取数据库连贯;
  3. 创立 JDBC Statements 对象;
  4. 设置 SQL 语句的传入参数;
  5. 执行 SQL 语句并取得查问后果;
  6. 对查问后果进行转换解决并将处理结果返回;
  7. 开释相干资源(敞开 Connection,敞开 Statement,敞开 ResultSet);

举荐一个开源收费的 Spring Boot 实战我的项目:

https://github.com/javastacks/spring-boot-best-practice

以下是具体的实现代码:

public static List<Map<String,Object>> queryForList(){  
    Connection connection = null;  
    ResultSet rs = null;  
    PreparedStatement stmt = null;  
    List<Map<String,Object>> resultList = new ArrayList<Map<String,Object>>();  
          
    try {  
        // 加载 JDBC 驱动  
        Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();  
        String url = "jdbc:oracle:thin:@localhost:1521:ORACLEDB";  
              
        String user = "trainer";   
        String password = "trainer";   
              
        // 获取数据库连贯  
        connection = DriverManager.getConnection(url,user,password);   
              
        String sql = "select * from userinfo where user_id = ?";  
        // 创立 Statement 对象(每一个 Statement 为一次数据库执行申请)stmt = connection.prepareStatement(sql);  
              
        // 设置传入参数  
        stmt.setString(1, "zhangsan");  
              
        // 执行 SQL 语句  
        rs = stmt.executeQuery();  
              
        // 解决查问后果(将查问后果转换成 List<Map> 格局)ResultSetMetaData rsmd = rs.getMetaData();  
        int num = rsmd.getColumnCount();  
              
        while(rs.next()){Map map = new HashMap();  
            for(int i = 0;i < num;i++){String columnName = rsmd.getColumnName(i+1);  
                map.put(columnName,rs.getString(columnName));  
            }  
            resultList.add(map);  
        }  
              
    } catch (Exception e) {e.printStackTrace();  
    } finally {  
        try {  
            // 敞开后果集  
            if (rs != null) {rs.close();  
                rs = null;  
            }  
            // 敞开执行  
            if (stmt != null) {stmt.close();  
                stmt = null;  
            }  
            if (connection != null) {connection.close();  
                connection = null;  
            }  
        } catch (SQLException e) {e.printStackTrace();  
        }  
    }        
    return resultList;  
}

3 JDBC 演变到 Mybatis 过程

下面咱们看到了实现 JDBC 有七个步骤,哪些步骤是能够进一步封装的,缩小咱们开发的代码量。

3.1 第一步优化:连贯获取和开释

  1. 问题形容:

数据库连贯频繁的开启和敞开自身就造成了 资源的节约,影响零碎的性能

解决问题:

数据库连贯的获取和敞开咱们 能够应用数据库连接池来解决资源节约的问题。通过连接池就能够重复利用曾经建设的连贯去拜访数据库了。缩小连贯的开启和敞开的工夫。

  1. 问题形容:

然而当初 连接池多种多样,可能存在变动,有可能采纳 DBCP 的连接池,也有可能采纳容器自身的 JNDI 数据库连接池。

解决问题:

咱们能够 通过 DataSource 进行隔离解耦,咱们对立从 DataSource 外面获取数据库连贯,DataSource 具体由 DBCP 实现还是由容器的 JNDI 实现都能够,所以咱们将 DataSource 的具体实现通过让用户配置来应答变动。

3.2 第二步优化:SQL 对立存取

  1. 问题形容:

咱们应用 JDBC 进行操作数据库时,SQL 语句根本都散落在各个 JAVA 类中,这样有三个不足之处:

第一,可读性很差,不利于保护以及做性能调优。

第二,改变 Java 代码须要从新编译、打包部署。

第三,不利于取出 SQL 在数据库客户端执行(取出后还得删掉两头的 Java 代码,编写好的 SQL 语句写好后还得通过+号在 Java 进行拼凑)。

解决问题:

咱们能够思考不把 SQL 语句写到 Java 代码中,那么把 SQL 语句放到哪里呢?首先须要有一个对立寄存的中央,咱们能够将这些SQL 语句对立集中放到配置文件或者数据库外面(以 key-value 的格局寄存)。而后通过 SQL 语句的 key 值去获取对应的 SQL 语句。

既然咱们将 SQL 语句都对立放在配置文件或者数据库中,那么这里就波及一个 SQL 语句的加载问题

3.3 第三步优化:传入参数映射和动静 SQL

  1. 问题形容:

很多状况下,咱们都能够通过在 SQL 语句中设置占位符来达到应用传入参数的目标,这种形式自身就有肯定局限性,它是依照肯定程序传入参数的,要与占位符一一匹配。然而,如果咱们 传入的参数是不确定的 (比方列表查问,依据用户填写的查问条件不同,传入查问的参数也是不同的,有时是一个参数、有时可能是三个参数),那么咱们就得 在后盾代码中本人依据申请的传入参数去拼凑相应的 SQL 语句 ,这样的话还是 防止不了在 Java 代码外面写 SQL 语句的命运 。既然咱们曾经把 SQL 语句对立寄存在配置文件或者数据库中了, 怎么做到可能依据前台传入参数的不同,动静生成对应的 SQL 语句呢?

解决问题:

第一,咱们先解决这个动静问题,依照咱们失常的程序员思维是,通过 if 和 else 这类的判断来进行是最直观的,这个时候咱们想到了 JSTL 中的 <if test=””></if> 这样的标签,那么,能不能将这类的标签引入到 SQL 语句中呢?假如能够,那么咱们这里就须要一个专门的 SQL 解析器来解析这样的 SQL 语句,然而,if 判断的变量来自于哪里呢?传入的值自身是可变的,那么咱们得为这个值定义一个不变的变量名称,而且这个变量名称必须和对应的值要有对应关系,能够通过这个变量名称找到对应的值,这个时候咱们想到了 key-value 的 Map。解析的时候依据变量名的具体值来判断。

如果后面能够判断没有问题,那么如果判断的后果是 true,那么就须要输入的标签外面的 SQL 片段,然而怎么解决在标签外面应用变量名称的问题呢?这里咱们须要 应用一种有别于 SQL 的语法来嵌入变量(比方应用#变量名#)。这样,SQL 语句通过解析后就能够动静的生成合乎上下文的 SQL 语句。

还有,怎么辨别开占位符变量和非占位变量?有时候咱们单单应用占位符是满足不了的,占位符只能为查问条件占位,SQL 语句其余中央应用不了。这里咱们能够应用 #变量名# 示意占位符变量,应用变量名示意非占位符变量

3.4 第四步优化:后果映射和后果缓存

  1. 问题形容:

执行 SQL 语句、获取执行后果、对执行后果进行转换解决、开释相干资源是一整套下来的。如果是执行查问语句,那么执行 SQL 语句后,返回的是一个 ResultSet 后果集,这个时候咱们就须要将 ResultSet 对象的数据取出来,不然等到开释资源时就取不到这些后果信息了。咱们从后面的优化来看,以及将获取连贯、设置传入参数、执行 SQL 语句、开释资源这些都封装起来了,只剩下后果解决这块还没有进行封装,如果能封装起来,每个数据库操作都不必本人写那么一大堆 Java 代码,间接调用一个封装的办法就能够搞定了。

解决问题:

咱们剖析一下,个别对执行后果的有哪些解决,有可能将后果不做任何解决就间接返回,也有可能将后果转换成一个 JavaBean 对象返回、一个 Map 返回、一个 List 返回等 `,后果解决可能是多种多样的。从这里看,咱们必须通知 SQL 处理器两点: 第一,须要返回什么类型的对象;第二,须要返回的对象的数据结构怎么跟执行的后果映射,这样能力将具体的值 copy 到对应的数据结构上。

接下来,咱们能够进而思考对 SQL 执行后果的缓存来晋升性能 。缓存数据都是 key-value 的格局,那么这个 key 怎么来呢?怎么保障惟一呢?即便同一条 SQL 语句几次拜访的过程中因为传入参数的不同,失去的执行 SQL 语句也是不同的。那么缓存起来的时候是多对。 然而 SQL 语句和传入参数两局部合起来能够作为数据缓存的 key 值

3.5 第五步优化:解决反复 SQL 语句问题

  1. 问题形容:

因为咱们将所有 SQL 语句都放到配置文件中,这个时候会遇到一个 SQL 反复的问题,几个性能的 SQL 语句其实都差不多,有些可能是 SELECT 前面那段不同、有些可能是 WHERE 语句不同。有时候表构造改了,那么咱们就须要改多个中央,不利于保护。

解决问题:

当咱们的代码程序呈现反复代码时怎么办?将反复的代码抽离进去成为独立的一个类,而后在各个须要应用的中央进行援用 。对于 SQL 反复的问题,咱们也能够采纳这种形式,通过将 SQL 片段模块化, 将反复的 SQL 片段独立成一个 SQL 块,而后在各个 SQL 语句援用反复的 SQL 块,这样须要批改时只须要批改一处即可。

举荐一个开源收费的 Spring Boot 实战我的项目:

https://github.com/javastacks/spring-boot-best-practice

4 Mybaits 有待改良之处

  1. 问题形容:

Mybaits 所有的数据库操作都是基于 SQL 语句,导致什么样的数据库操作都要写 SQL 语句。一个利用零碎要写的 SQL 语句切实太多了。

改良办法:

咱们对数据库进行的操作大部分都是对表数据的增删改查,很多都是对单表的数据进行操作,由这点咱们能够想到一个问题:单表操作可不可以不写 SQL 语句,通过 JavaBean 的默认映射器生成对应的 SQL 语句 ,比方:一个类 UserInfo 对应于 USER_INFO 表,userId 属性对应于 USER_ID 字段。 这样咱们就能够通过反射能够获取到对应的表构造了,拼凑成对应的 SQL 语句显然不是问题

5 MyBatis 框架整体设计

5.1 接口层 - 和数据库交互的形式

MyBatis 和数据库的交互有两种形式:

  1. 应用传统的 MyBatis 提供的 API;
  2. 应用 Mapper 接口;

5.1.1 应用传统的 MyBatis 提供的 API

这是传统的传递 Statement Id 和查问参数给 SqlSession 对象,应用 SqlSession 对象实现和数据库的交互;MyBatis 提供了十分不便和简略的 API,供用户实现对数据库的增删改查数据操作,以及对数据库连贯信息和 MyBatis 本身配置信息的保护操作。

上述应用 MyBatis 的办法,是 创立一个和数据库打交道的 SqlSession 对象,而后依据 Statement Id 和参数来操作数据库 ,这种形式诚然很简略和实用,然而 它不合乎面向对象语言的概念和面向接口编程的编程习惯。因为面向接口的编程是面向对象的大趋势,MyBatis 为了适应这一趋势,减少了第二种应用 MyBatis 反对接口(Interface)调用形式。

5.1.2 应用 Mapper 接口

MyBatis 将配置文件中的每一个 <mapper> 节点形象为一个 Mapper 接口:

这个接口中申明的办法和 <mapper> 节点中的 <select|update|delete|insert> 节点项对应,即 <select|update|delete|insert> 节点的 id 值为 Mapper 接口中的办法名称,parameterType 值示意 Mapper 对应办法的入参类型,而resultMap 值则对应了 Mapper 接口示意的返回值类型或者返回后果集的元素类型

依据 MyBatis 的配置标准配置好后,通过 SqlSession.getMapper(XXXMapper.class)办法,MyBatis 会依据相应的接口申明的办法信息,通过动静代理机制生成一个 Mapper 实例,咱们应用 Mapper 接口的某一个办法时,MyBatis 会依据这个办法的办法名和参数类型,确定 Statement Id,底层还是通过 SqlSession.select(“statementId”,parameterObject); 或者 SqlSession.update(“statementId”,parameterObject); 等等来实现对数据库的操作,MyBatis 援用 Mapper 接口这种调用形式,纯正是为了满足面向接口编程的须要。(其实还有一个起因是在于,面向接口的编程,使得用户在接口上能够应用注解来配置 SQL 语句,这样就能够脱离 XML 配置文件,实现“0 配置”)。

5.2 数据处理层

数据处理层能够说是 MyBatis 的外围,从大的方面上讲,它要实现两个性能:

  1. 通过传入参数构建动静 SQL 语句;
  2. SQL 语句的执行以及封装查问后果集成 List<E>;

5.2.1 参数映射和动静 SQL 语句生成

动静语句生成能够说是 MyBatis 框架十分优雅的一个设计,MyBatis 通过传入的参数值,应用 Ognl 来动静地结构 SQL 语句,使得 MyBatis 有很强的灵活性和扩展性。

参数映射指的是对于 java 数据类型和 jdbc 数据类型之间的转换:这里有包含两个过程:查问阶段 ,咱们要将 java 类型的数据,转换成 jdbc 类型的数据,通过 preparedStatement.setXXX() 来设值;另一个就是 对 resultset 查问后果集的 jdbcType 数据转换成 java 数据类型

5.2.2 SQL 语句的执行以及封装查问后果集成 List<E>

动静 SQL 语句生成之后,MyBatis 将执行 SQL 语句,并将可能返回的后果集转换成 List<E> 列表。MyBatis 在对后果集的解决中,反对后果集关系一对多和多对一的转换 ,并且有两种反对形式, 一种为嵌套查问语句的查问,还有一种是嵌套后果集的查问

5.3 框架撑持层

1、事务管理机制

事务管理机制对于 ORM 框架而言是不可短少的一部分,事务管理机制的品质也是考量一个 ORM 框架是否优良的一个规范。

2、连接池管理机制

因为创立一个数据库连贯所占用的资源比拟大,对于数据吞吐量大和访问量十分大的利用而言,连接池的设计就显得十分重要

3、缓存机制

为了进步数据利用率和减小服务器和数据库的压力,MyBatis 会对于一些查问提供会话级别的数据缓存,会将对某一次查问,搁置到 SqlSession 中,在容许的工夫距离内,对于完全相同的查问,MyBatis 会间接将缓存后果返回给用户,而不必再到数据库中查找。

4、SQL 语句的配置形式

传统的 MyBatis 配置 SQL 语句形式就是应用 XML 文件进行配置的,然而这种形式不能很好地反对面向接口编程的理念,为了反对面向接口的编程,MyBatis 引入了 Mapper 接口的概念,面向接口的引入,对应用注解来配置 SQL 语句成为可能,用户只须要在接口上增加必要的注解即可,不必再去配置 XML 文件了,然而,目前的 MyBatis 只是对注解配置 SQL 语句提供了无限的反对,某些高级性能还是要依赖 XML 配置文件配置 SQL 语句。

5.4 疏导层

疏导层是配置和启动 MyBatis 配置信息的形式 。MyBatis 提供两种形式来疏导 MyBatis: 基于 XML 配置文件的形式和基于 Java API 的形式

5.5 次要构件及其互相关系

从 MyBatis 代码实现的角度来看,MyBatis 的次要的核心部件有以下几个:

SqlSession:作为 MyBatis 工作的次要顶层 API,示意和数据库交互的会话,实现必要数据库增删改查性能;

Executor:MyBatis 执行器,是 MyBatis 调度的外围,负责 SQL 语句的生成和查问缓存的保护;

StatementHandler:封装了 JDBC Statement 操作,负责对 JDBC statement 的操作,如设置参数、将 Statement 后果集转换成 List 汇合。

ParameterHandler:负责对用户传递的参数转换成 JDBC Statement 所须要的参数;

ResultSetHandler:负责将 JDBC 返回的 ResultSet 后果集对象转换成 List 类型的汇合;

TypeHandler:负责 java 数据类型和 jdbc 数据类型之间的映射和转换;

MappedStatement:MappedStatement 保护了一条 <select|update|delete|insert> 节点的封装;

SqlSource:负责依据用户传递的 parameterObject,动静地生成 SQL 语句,将信息封装到 BoundSql 对象中,并返回;

BoundSql:示意动静生成的 SQL 语句以及相应的参数信息;

Configuration:MyBatis 所有的配置信息都维持在 Configuration 对象之中;

它们的关系如下图所示:

6 SqlSession 工作过程剖析

  1. 开启一个数据库拜访会话 — 创立 SqlSession 对象
SqlSession sqlSession = factory.openSession(); 

MyBatis 封装了对数据库的拜访,把对数据库的会话和事务管制放到了 SqlSession 对象中

  1. 为 SqlSession 传递一个配置的 Sql 语句的 Statement Id 和参数,而后返回后果:
List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",params);

上述的 ”com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary”,是配置在 EmployeesMapper.xml 的 Statement ID,params 是传递的查问参数。

让咱们来看一下 sqlSession.selectList()办法的定义:

public <E> List<E> selectList(String statement, Object parameter) {return this.selectList(statement, parameter, RowBounds.DEFAULT);  
}  
 
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {  
    try {  
        //1. 依据 Statement Id,在 mybatis 配置对象 Configuration 中查找和配置文件绝对应的 MappedStatement      
        MappedStatement ms = configuration.getMappedStatement(statement);  
        //2. 将查问工作委托给 MyBatis 的执行器 Executor  
        List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);  
        return result;  
    } catch (Exception e) {throw ExceptionFactory.wrapException("Error querying database.  Cause:" + e, e);  
    } finally {ErrorContext.instance().reset();}  
} 

MyBatis 在初始化的时候,会将 MyBatis 的配置信息全副加载到内存中,应用 org.apache.ibatis.session.Configuration 实例来保护 。使用者能够应用 sqlSession.getConfiguration() 办法来获取。MyBatis 的配置文件中配置信息的组织格局和内存中对象的组织格局简直齐全对应的

上述例子中的:

<select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" >  
   select   
       EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY  
   from LOUIS.EMPLOYEES  
   <if test="min_salary != null">  
       where SALARY < #{min_salary,jdbcType=DECIMAL}  
   </if>  
</select>

加载到内存中会生成一个对应的 MappedStatement 对象,而后会以 key=”com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary”,value 为 MappedStatement 对象的模式保护到 Configuration 的一个 Map 中。当当前须要应用的时候,只须要通过 Id 值来获取就能够了。

从上述的代码中咱们能够看到 SqlSession 的职能是:SqlSession 依据 Statement ID, 在 mybatis 配置对象 Configuration 中获取到对应的 MappedStatement 对象,而后调用 mybatis 执行器来执行具体的操作

  1. MyBatis 执行器 Executor 依据 SqlSession 传递的参数执行 query()办法(因为代码过长,读者只需浏览我正文的中央即可):
/** 
   * BaseExecutor 类局部代码 
   * 
   */  
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
      // 1. 依据具体传入的参数,动静地生成须要执行的 SQL 语句,用 BoundSql 对象示意    
      BoundSql boundSql = ms.getBoundSql(parameter);  
      // 2. 为以后的查问创立一个缓存 Key  
      CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);  
      return query(ms, parameter, rowBounds, resultHandler, key, boundSql);  
}  
 
@SuppressWarnings("unchecked")  
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 {  
           queryStack++;  
           list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;  
           if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);  
           } else {  
               // 3. 缓存中没有值,间接从数据库中读取数据    
               list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);  
           }  
       } finally {queryStack--;}  
       if (queryStack == 0) {for (DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();  
           }  
           deferredLoads.clear(); // issue #601  
           if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {clearLocalCache(); // issue #482  
           }  
       }  
       return list;  
}

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {  
      List<E> list;  
      localCache.putObject(key, EXECUTION_PLACEHOLDER);  
      try {  
         
          //4. 执行查问,返回 List 后果,而后    将查问的后果放入缓存之中  
          list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);  
      } finally {localCache.removeObject(key);  
      }  
      localCache.putObject(key, list);  
      if (ms.getStatementType() == StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);  
      }  
      return list;  
} 
/** 
   * 
   * SimpleExecutor 类的 doQuery()办法实现 
   * 
   */  
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();  
          //5. 依据既有的参数,创立 StatementHandler 对象来执行查问操作  
          StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);  
          //6. 创立 java.Sql.Statement 对象,传递给 StatementHandler 对象  
          stmt = prepareStatement(handler, ms.getStatementLog());  
          //7. 调用 StatementHandler.query()办法,返回 List 后果集  
          return handler.<E>query(stmt, resultHandler);  
       } finally {closeStatement(stmt);  
       }  
}

上述的 Executor.query()办法几经转折,最初会创立一个 StatementHandler 对象,而后将必要的参数传递给 StatementHandler,应用 StatementHandler 来实现对数据库的查问,最终返回 List 后果集。

从下面的代码中咱们能够看出,Executor 的性能和作用是:

  1. 依据传递的参数,实现 SQL 语句的动静解析,生成 BoundSql 对象,供 StatementHandler 应用;
  2. 为查问创立缓存,以进步性能;
  3. 创立 JDBC 的 Statement 连贯对象,传递给 StatementHandler 对象,返回 List 查问后果;
  1. StatementHandler 对象负责设置 Statement 对象中的查问参数、解决 JDBC 返回的 resultSet,将 resultSet 加工为 List 汇合返回:

接着下面的 Executor 第六步,看一下:prepareStatement() 办法的实现:

/** 
   * 
   * SimpleExecutor 类的 doQuery()办法实现 
   * 
   */  
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 handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); 
          // 1. 筹备 Statement 对象,并设置 Statement 对象的参数 
          stmt = prepareStatement(handler, ms.getStatementLog()); 
          // 2. StatementHandler 执行 query()办法,返回 List 后果 
          return handler.<E>query(stmt, resultHandler); 
      } finally {closeStatement(stmt); 
      } 
}  
 
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
      Statement stmt;  
      Connection connection = getConnection(statementLog);  
      stmt = handler.prepare(connection);  
      // 对创立的 Statement 对象设置参数,即设置 SQL 语句中 ? 设置为指定的参数  
      handler.parameterize(stmt);  
      return stmt;  
}

以上咱们能够总结 StatementHandler 对象次要实现两个工作:

  1. 对于 JDBC 的 PreparedStatement 类型的对象,创立的过程中,咱们应用的是 SQL 语句字符串会蕴含 若干个? 占位符,咱们其后再对占位符进行设值。
    StatementHandler 通过 parameterize(statement)办法对 Statement 进行设值;
  2. StatementHandler 通过 List<E> query(Statement statement, ResultHandler resultHandler)办法来实现执行 Statement,和将 Statement 对象返回的 resultSet 封装成 List;
  1. StatementHandler 的 parameterize(statement) 办法的实现:
/** 
   * StatementHandler 类的 parameterize(statement) 办法实现  
   */  
public void parameterize(Statement statement) throws SQLException {  
      // 应用 ParameterHandler 对象来实现对 Statement 的设值    
      parameterHandler.setParameters((PreparedStatement) statement);  
}  
/** 
   *  
   * ParameterHandler 类的 setParameters(PreparedStatement ps) 实现 
   * 对某一个 Statement 进行设置参数 
   */  
public void setParameters(PreparedStatement ps) throws SQLException {ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());  
      List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();  
      if (parameterMappings != null) {for (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping parameterMapping = parameterMappings.get(i);  
              if (parameterMapping.getMode() != ParameterMode.OUT) {  
                  Object value;  
                  String propertyName = parameterMapping.getProperty();  
                  if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params  
                      value = boundSql.getAdditionalParameter(propertyName);  
                  } else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {value = parameterObject;} else {MetaObject metaObject = configuration.newMetaObject(parameterObject);  
                      value = metaObject.getValue(propertyName);  
                  }  
         
                  // 每一个 Mapping 都有一个 TypeHandler,依据 TypeHandler 来对 preparedStatement 进行设置参数  
                  TypeHandler typeHandler = parameterMapping.getTypeHandler();  
                  JdbcType jdbcType = parameterMapping.getJdbcType();  
                  if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();  
                  // 设置参数  
                  typeHandler.setParameter(ps, i + 1, value, jdbcType);  
              }  
          }  
      }  
}

从上述的代码能够看到,StatementHandler 的 parameterize(Statement) 办法调用了 ParameterHandler 的 setParameters(statement) 办法,
ParameterHandler 的 setParameters(Statement)办法负责 依据咱们输出的参数,对 statement 对象的 ? 占位符处进行赋值。

  1. StatementHandler 的 List<E> query(Statement statement, ResultHandler resultHandler)办法的实现:
 /** 
    * PreParedStatement 类的 query 办法实现 
    */  
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {//1. 调用 preparedStatemnt。execute()办法,而后将 resultSet 交给 ResultSetHandler 解决    
      PreparedStatement ps = (PreparedStatement) statement;  
      ps.execute();  
      //2. 应用 ResultHandler 来解决 ResultSet  
      return resultSetHandler.<E> handleResultSets(ps);  
}  

从上述代码咱们能够看出,StatementHandler 的 List<E> query(Statement statement, ResultHandler resultHandler)办法的实现,是调用了 ResultSetHandler 的 handleResultSets(Statement) 办法。ResultSetHandler 的 handleResultSets(Statement) 办法会将 Statement 语句执行后生成的 resultSet 后果集转换成 List<E> 后果集

/**   
   * ResultSetHandler 类的 handleResultSets()办法实现 
   *  
   */  
public List<Object> handleResultSets(Statement stmt) throws SQLException {final List<Object> multipleResults = new ArrayList<Object>();  
 
      int resultSetCount = 0;  
      ResultSetWrapper rsw = getFirstResultSet(stmt);  
 
      List<ResultMap> resultMaps = mappedStatement.getResultMaps();  
      int resultMapCount = resultMaps.size();  
      validateResultMapsCount(rsw, resultMapCount);  
     
      while (rsw != null && resultMapCount > resultSetCount) {ResultMap resultMap = resultMaps.get(resultSetCount);  
       
          // 将 resultSet  
          handleResultSet(rsw, resultMap, multipleResults, null);  
          rsw = getNextResultSet(stmt);  
          cleanUpAfterHandlingResultSet();  
          resultSetCount++;  
      }
 
      String[] resultSets = mappedStatement.getResulSets();  
      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);  
}  

7 MyBatis 初始化机制

7.1 MyBatis 的初始化做了什么

任何框架的初始化,无非是加载本人运行时所须要的配置信息。MyBatis 的配置信息,大略蕴含以下信息,其高层级构造如下:

MyBatis 配置信息结构图

MyBatis 的上述配置信息会配置在 XML 配置文件中,那么,这些信息被加载进入 MyBatis 外部,MyBatis 是怎么保护的呢?

MyBatis 采纳了一个十分直白和简略的形式 —应用 org.apache.ibatis.session.Configuration 对象作为一个所有配置信息的容器,Configuration 对象的组织构造和 XML 配置文件的组织构造简直齐全一样(当然,Configuration 对象的性能并不限于此,它还负责创立一些 MyBatis 外部应用的对象,如 Executor 等,这将在后续的文章中探讨)。如下图所示:

Configuration 对象的组织构造和 XML 配置文件的组织构造简直齐全一样

MyBatis 依据初始化好 Configuration 信息,这时候用户就能够应用 MyBatis 进行数据库操作了。能够这么说,MyBatis 初始化的过程,就是创立 Configuration 对象的过程

MyBatis 的初始化能够有两种形式:

基于 XML 配置文件:基于 XML 配置文件的形式是将 MyBatis 的所有配置信息放在 XML 文件中,MyBatis 通过加载并 XML 配置文件,将配置文信息组装成外部的 Configuration 对象。

基于 Java API:这种形式不应用 XML 配置文件,须要 MyBatis 使用者在 Java 代码中,手动创立 Configuration 对象,而后将配置参数 set 进入 Configuration 对象中。

接下来咱们将通过 基于 XML 配置文件形式的 MyBatis 初始化,深入探讨 MyBatis 是如何通过配置文件构建 Configuration 对象,并应用它。

7.2 基于 XML 配置文件创立 Configuration 对象

当初就从应用 MyBatis 的简略例子动手,深入分析一下 MyBatis 是怎么实现初始化的,都初始化了什么。看以下代码:

String resource = "mybatis-config.xml";  
InputStream inputStream = Resources.getResourceAsStream(resource);  
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);  
SqlSession sqlSession = sqlSessionFactory.openSession();  
List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo");

有过 MyBatis 应用教训的读者会晓得,上述语句的作用是执行 com.foo.bean.BlogMapper.queryAllBlogInfo 定义的 SQL 语句,返回一个 List 后果集。总的来说,上述代码经验了 mybatis 初始化 –> 创立 SqlSession –> 执行 SQL 语句 返回后果三个过程。

上述代码的性能是依据配置文件 mybatis-config.xml 配置文件,创立 SqlSessionFactory 对象,而后产生 SqlSession,执行 SQL 语句。而 mybatis 的初始化就产生在第三句:SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 当初就让咱们看看第三句到底产生了什么。

  1. MyBatis 初始化根本过程:

SqlSessionFactoryBuilder 依据传入的数据流生成 Configuration 对象,而后依据 Configuration 对象创立默认的 SqlSessionFactory 实例。

初始化的根本过程如下序列图所示:

MyBatis 初始化序列图

由上图所示,mybatis 初始化要通过简略的以下几步:

  1. 调用 SqlSessionFactoryBuilder 对象的 build(inputStream)办法;
  2. SqlSessionFactoryBuilder 会依据输出流 inputStream 等信息创立 XMLConfigBuilder 对象;
  3. SqlSessionFactoryBuilder 调用 XMLConfigBuilder 对象的 parse()办法;
  4. XMLConfigBuilder 对象返回 Configuration 对象;
  5. SqlSessionFactoryBuilder 依据 Configuration 对象创立一个 DefaultSessionFactory 对象;
  6. SqlSessionFactoryBuilder 返回 DefaultSessionFactory 对象给 Client,供 Client 应用。

SqlSessionFactoryBuilder 相干的代码如下所示:

public SqlSessionFactory build(InputStream inputStream)  {return build(inputStream, null, null);  
}  

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties)  {  
      try  {  
          //2. 创立 XMLConfigBuilder 对象用来解析 XML 配置文件,生成 Configuration 对象  
          XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);  
          //3. 将 XML 配置文件内的信息解析成 Java 对象 Configuration 对象  
          Configuration config = parser.parse();  
          //4. 依据 Configuration 对象创立出 SqlSessionFactory 对象  
          return build(config);  
      } 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.}  
      }
}

// 从此处能够看出,MyBatis 外部通过 Configuration 对象来创立 SqlSessionFactory, 用户也能够本人通过 API 结构好 Configuration 对象,调用此办法创 SqlSessionFactory  
public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);  
}  

上述的初始化过程中,波及到了以下几个对象:

SqlSessionFactoryBuilder:SqlSessionFactory 的结构器,用于创立 SqlSessionFactory,采纳了 Builder 设计模式

Configuration:该对象是 mybatis-config.xml 文件中所有 mybatis 配置信息

SqlSessionFactory:SqlSession 工厂类,以工厂模式创立 SqlSession 对象,采纳了 Factory 工厂设计模式

XMLConfigBuilder:负责将 mybatis-config.xml 配置文件解析成 Configuration 对象,共 SqlSessonFactoryBuilder 应用,创立 SqlSessionFactory

  1. 创立 Configuration 对象的过程:
    接着上述的 MyBatis 初始化根本过程探讨,当 SqlSessionFactoryBuilder 执行 build()办法,调用了 XMLConfigBuilder 的 parse()办法,而后返回了 Configuration 对象 。那么 parse() 办法是如何解决 XML 文件,生成 Configuration 对象的呢?
  • (1)XMLConfigBuilder 会 将 XML 配置文件的信息转换为 Document 对象 ,而 XML 配置定义文件DTD 转换成 XMLMapperEntityResolver 对象,而后 将二者封装到 XpathParser 对象中,XpathParser 的作用是提供依据 Xpath 表达式获取根本的 DOM 节点 Node 信息的操作

如下图所示:

XpathParser 组成结构图和生成图

  • (2)之后 XMLConfigBuilder 调用 parse()办法:会从 XPathParser 中取出 <configuration> 节点对应的 Node 对象,而后解析此 Node 节点的子 Node:properties, settings, typeAliases,typeHandlers, objectFactory, objectWrapperFactory, plugins, environments,databaseIdProvider, mappers:
public Configuration parse() {if (parsed) {throw new BuilderException("Each XMLConfigBuilder can only be used once.");  
     }  
     parsed = true;  
     // 源码中没有这一句,只有 parseConfiguration(parser.evalNode("/configuration"));  
     // 为了让读者看得更清晰,源码拆分为以下两句  
     XNode configurationNode = parser.evalNode("/configuration");  
     parseConfiguration(configurationNode);  
     return configuration;
}
/** 
  * 解析 "/configuration" 节点下的子节点信息,而后将解析的后果设置到 Configuration 对象中 
  */  
private void parseConfiguration(XNode root) {  
     try {  
         //1. 首先解决 properties 节点     
         propertiesElement(root.evalNode("properties")); //issue #117 read properties first  
         //2. 解决 typeAliases  
         typeAliasesElement(root.evalNode("typeAliases"));  
         //3. 解决插件  
         pluginElement(root.evalNode("plugins"));  
         //4. 解决 objectFactory  
         objectFactoryElement(root.evalNode("objectFactory"));  
         //5.objectWrapperFactory  
         objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));  
         //6.settings  
         settingsElement(root.evalNode("settings"));  
         //7. 解决 environments  
         environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631  
         //8.database  
         databaseIdProviderElement(root.evalNode("databaseIdProvider"));  
         //9.typeHandlers  
         typeHandlerElement(root.evalNode("typeHandlers"));  
         //10.mappers  
         mapperElement(root.evalNode("mappers"));  
     } catch (Exception e) {throw new BuilderException("Error parsing SQL Mapper Configuration. Cause:" + e, e);  
     }  
 }  

留神:在上述代码中,还有一个十分重要的中央,就是解析 XML 配置文件子节点 <mappers> 的办法 mapperElements(root.evalNode(“mappers”)), 它将解析咱们配置的 Mapper.xml 配置文件,Mapper 配置文件能够说是 MyBatis 的外围,MyBatis 的个性和理念都体现在此 Mapper 的配置和设计上。

  • (3)而后将这些值解析进去设置到 Configuration 对象中:

    解析子节点的过程这里就不一一介绍了,用户能够参照 MyBatis 源码认真琢磨,咱们就看上述的 environmentsElement(root.evalNode(“environments”)); 办法是如何将 environments 的信息解析进去,设置到 Configuration 对象中的:

/** 
  * 解析 environments 节点,并将后果设置到 Configuration 对象中 
  * 留神:创立 envronment 时,如果 SqlSessionFactoryBuilder 指定了特定的环境(即数据源);*      则返回指定环境(数据源)的 Environment 对象,否则返回默认的 Environment 对象;*      这种形式实现了 MyBatis 能够连贯多数据源 
  */  
private void environmentsElement(XNode context) throws Exception {if (context != null)  
    {if (environment == null)  
         {environment = context.getStringAttribute("default");  
         }  
         for (XNode child : context.getChildren())  
         {String id = child.getStringAttribute("id");  
              if (isSpecifiedEnvironment(id))  
              {  
                  //1. 创立事务工厂 TransactionFactory  
                  TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));  
                  DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));  
                  //2. 创立数据源 DataSource  
                  DataSource dataSource = dsFactory.getDataSource();  
                  //3. 结构 Environment 对象  
                  Environment.Builder environmentBuilder = new Environment.Builder(id)  
             .transactionFactory(txFactory)  
             .dataSource(dataSource);  
                  //4. 将创立的 Envronment 对象设置到 configuration 对象中  
                  configuration.setEnvironment(environmentBuilder.build());  
             }  
         }  
    }  
}
private boolean isSpecifiedEnvironment(String id)  
{if (environment == null)  
      {throw new BuilderException("No environment specified.");  
      }  
      else if (id == null)  
      {throw new BuilderException("Environment requires an id attribute.");  
      }  
      else if (environment.equals(id))  
      {return true;}  
      return false;  
 }  
  • (4)返回 Configuration 对象:

    将上述的 MyBatis 初始化根本过程的序列图细化:

    基于 XML 配置创立 Configuration 对象的过程

7.3 基于 Java API 手动加载 XML 配置文件创立 Configuration 对象,并应用 SqlSessionFactory 对象

咱们能够应用 XMLConfigBuilder 手动解析 XML 配置文件来创立 Configuration 对象,代码如下:

String resource = "mybatis-config.xml";  
InputStream inputStream = Resources.getResourceAsStream(resource);  
// 手动创立 XMLConfigBuilder,并解析创立 Configuration 对象  
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, null,null);  
Configuration configuration=parse();  
// 应用 Configuration 对象创立 SqlSessionFactory  
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);  
// 应用 MyBatis  
SqlSession sqlSession = sqlSessionFactory.openSession();  
List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo");  

7.4 波及到的设计模式

初始化的过程波及到创立各种对象,所以会应用一些创立型的设计模式。在初始化的过程中,Builder 模式使用的比拟多

7.4.1 Builder 模式利用 1:SqlSessionFactory 的创立

对于创立 SqlSessionFactory 时,会 依据状况提供不同的参数,其参数组合能够有以下几种

依据状况提供不同的参数,创立 SqlSessionFactory

因为结构时参数不定,能够为其创立一个结构器 Builder,将 SqlSessionFactory 的构建过程和示意离开

MyBatis 将 SqlSessionFactoryBuilder 和 SqlSessionFactory 互相独立

7.4.2 Builder 模式利用 2:数据库连贯环境 Environment 对象的创立

在构建 Configuration 对象的过程中,XMLConfigBuilder 解析 mybatis XML 配置文件节点 <environment> 节点时,会有以下相应的代码:

private void environmentsElement(XNode context) throws Exception {if (context != null) {if (environment == null) {environment = context.getStringAttribute("default");  
        }  
        for (XNode child : context.getChildren()) {String id = child.getStringAttribute("id");  
            // 是和默认的环境雷同时,解析之  
            if (isSpecifiedEnvironment(id)) {TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));  
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));  
                DataSource dataSource = dsFactory.getDataSource();  
  
                // 应用了 Environment 内置的结构器 Builder,传递 id 事务工厂和数据源  
                Environment.Builder environmentBuilder = new Environment.Builder(id)  
                .transactionFactory(txFactory)  
                .dataSource(dataSource);  
                configuration.setEnvironment(environmentBuilder.build());  
            }  
        }  
    }  
}  

在 Environment 外部,定义了动态外部 Builder 类:

public final class Environment {  
    private final String id;  
    private final TransactionFactory transactionFactory;  
    private final DataSource dataSource;  
  
    public Environment(String id, TransactionFactory transactionFactory, DataSource dataSource) {if (id == null) {throw new IllegalArgumentException("Parameter'id'must not be null");  
        }  
        if (transactionFactory == null) {throw new IllegalArgumentException("Parameter'transactionFactory'must not be null");  
        }  
        this.id = id;  
        if (dataSource == null) {throw new IllegalArgumentException("Parameter'dataSource'must not be null");  
        }  
        this.transactionFactory = transactionFactory;  
        this.dataSource = dataSource;  
    }  
  
    public static class Builder {  
        private String id;  
        private TransactionFactory transactionFactory;  
        private DataSource dataSource;  
  
        public Builder(String id) {this.id = id;}  
  
        public Builder transactionFactory(TransactionFactory transactionFactory) {  
            this.transactionFactory = transactionFactory;  
            return this;  
        }  
  
        public Builder dataSource(DataSource dataSource) {  
            this.dataSource = dataSource;  
            return this;  
        }  
  
        public String id() {return this.id;}  
  
        public Environment build() {return new Environment(this.id, this.transactionFactory, this.dataSource);  
        }  
    }  
  
    public String getId() {return this.id;}  
  
    public TransactionFactory getTransactionFactory() {return this.transactionFactory;}  
  
    public DataSource getDataSource() {return this.dataSource;}
}

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿(2022 最新版)

2. 劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4. 别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0