作者:七寸知架构 \
链接: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开发手册(嵩山版)》最新公布,速速下载!

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