MyBatis源码分析三MyBatis的核心对象及其作用

Posted 结构化思维wz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis源码分析三MyBatis的核心对象及其作用相关的知识,希望对你有一定的参考价值。

三、MyBatis的核心对象及其作用

本文2.3w字,详细介绍了MyBatis的核心对象和作用以及MyBatis运行流程,是如何通过动态代理创建实现类的。

文章目录


我们知道,JDBC有四个核心对象:
(1)DriverManager,用于注册数据库连接
(2)Connection,与数据库连接对象
(3)Statement/PrepareStatement,操作数据库SQL语句的对象
(4)ResultSet,结果集或一张虚拟表。

MyBatis的底层操作封装了JDBC的API,MyBatis的工作原理以及核心流程与JDBC的使用步骤一脉相承,而MyBatis也有几大核心对象,将在下文中具体介绍。

数据存储类对象

概念:在Java中(JVM)对Mybatis相关的配置信息进行封装

Configuration

mybatis-config.xml ----> Configuration
   Configuration 
      1. 封装了mybatis-config.xml ---> Configuration属性
      2. 封装了mapper 文件 -->  Configuration属性 + MappedStatement(封装了Mapper中的一个方法)
      3. 创建Mybatis其他相关的对象  --> new Excutor....
  1. 封装了mybatis-config.xml

2.封装了mapper 文件相关内容,在Configuration对象中进行了汇总。

3.创建Mybatis其他相关的对象

MappedStatement

每个MappedStatement对应了我们自定义Mapper接口中的一个方法,它保存了开发人员编写的SQL语句、参数结构、返回值结构、Mybatis对它的处理方式的配置等细节要素,是对一个SQL命令是什么、执行方式的完整定义。可以说,有了它Mybatis就知道如何去调度四大组件顺利的完成用户请求。

  XXXDAOMapper.xml ----> MappedStatement
 	 对应的就是 Mapper文件中的一个一个的 配置标签 
 	 <select id. -----> MappedStatement
     <insert id. -----> MappedStatement 
  还封装了sql语句--> BoundSql

一个Mybatis应用中 N 个 MappedStament 对象 ,通过namespace+id来保证唯一性。 Statement 有类型:

public enum StatementType 
  STATEMENT, PREPARED, CALLABLE

- Statement:可以发送字符串类型的sql,不支持传递参数。适用于静态sql语句。
- PreparedStatement: 预编译的sql语句,接受参数传入,并且可以防止sql注入,提高安全性。Sql语句会编译在数据库系统,适用于多次执行相同的sql,因此性能高于Statement- CallableStatement:在执行存储过程时使用,并且可以接受参数传入。

默认使用PREPARED类型:

在使用中,我们可以通过标签中的属性修改:

BoundSql

问题SQL语句是如何封装的呢?封装的对象与MappedStatement有什么关系呢?

mappedStatement.getBoundSql():

  public BoundSql getBoundSql(Object parameterObject) 
    // 表示从XML文件或批注读取的映射语句的内容
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) 
      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
    

    // check for nested result maps in parameter mappings (issue #30)
    for (ParameterMapping pm : boundSql.getParameterMappings()) 
      String rmId = pm.getResultMapId();
      if (rmId != null) 
        ResultMap rm = configuration.getResultMap(rmId);
        if (rm != null) 
          hasNestedResultMaps |= rm.hasNestedResultMaps();
        
      
    

    return boundSql;
  

debug图:

下面再看看BoundSql这个类的详细信息:成员变量具体存些什么

/*
在处理任何动态内容后,从SqlSource获取的实际SQL字符串。SQL可能有SQL占位符“?”以及具有每个参数的附加信息(至少是要从中读取值的输入对象的属性名称)的参数映射的列表(有序)。 还可以具有由动态语言创建的其他参数(用于循环、绑定…)。
*/
public class BoundSql 

  private final String sql; //Sql语句
  private final List<ParameterMapping> parameterMappings; //参数映射
  private final Object parameterObject; //参数
  private final Map<String, Object> additionalParameters; //其它参数
  private final MetaObject metaParameters; //Meta参数

  public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) 
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.parameterObject = parameterObject;
    this.additionalParameters = new HashMap<>();
    this.metaParameters = configuration.newMetaObject(additionalParameters);
  

  public String getSql() 
    return sql;
  

  public List<ParameterMapping> getParameterMappings() 
    return parameterMappings;
  

  public Object getParameterObject() 
    return parameterObject;
  

  public boolean hasAdditionalParameter(String name) 
    String paramName = new PropertyTokenizer(name).getName();
    return additionalParameters.containsKey(paramName);
  

  public void setAdditionalParameter(String name, Object value) 
    metaParameters.setValue(name, value);
  

  public Object getAdditionalParameter(String name) 
    return metaParameters.getValue(name);
  

操作类对象

MyBatis如何操作数据库?我们都知道MyBatis的门面–>SqlSession。SqlSeesion内部有封装了那些操作?

Excutor

SqlSession只是一个前台客服,真正发挥作用的是Executor,对SqlSession方法的访问最终都会落到Executor的相应方法上去。Alt+7 来看看这个接口里面都有什么?

拓展:Excutor为什么设计成接口呢?

一般操作行的功能都会设计成接口,方便以后增加其它实现方式,提高拓展性。

再看看看Excutor的实现类:

Executor分成两大类:一类是CachingExecutor,另一类是普通的Executor。

(1)CachingExecutor有一个重要属性delegate,它保存的是某类普通的Executor,在构造函数时候传入。执行数据库update操作时,它直接调用delegate的update方法,执行query方法时先尝试从cache中取值,取不到再调用delegate的查询方法,并将查询结果存入cache中。(缓存章节细讲)

(2)普通Executor分三类:SimpleExecutor、ReuseExecutor和BatchExecutor。它们都继承于BaseExecutor,BatchExecutor专门用于执行批量sql操作,ReuseExecutor会重用Statement执行sql操作,SimpleExecutor只是简单执行sql。

  • BatchExcutor:JDBC中批处理的操作

  • ReuseExcutor:可重用执行器,将Statement存入map中,操作map中的Statement而不会重复创建Statement

    # 这两个sql语句不能一模一样,所以不能复用,参数也要一样才能复用,所以这个ReuseExcutor并不常用
    insert into t_user(ID,name)values1,‘小明’);
    insert into t_user(ID,name)values2,‘小刘’);
    
  • SimpleExcutor: 是一种常规执行器,每次执行都会创建一个Statement,用完后关闭。常用Excutor Mybatis推荐 默认

     Configuration protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
    

StatementHandler

Excutor如何与JDBC建立联系呢?–>StatementHandler是Mybatis封装了JDBC Statement,真正Mybatis进行数据库访问操作的核心。

MyBatis为什么要包装一层StatementHandler对象呢?而不用JDBC Statement呢?

  • 因为StatementHandler的功能更细致,仅仅是负责增删改查的功能。

看看源码中具体的操作:例如这个查询方法

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException 
  //拿到sql语句
  String sql = boundSql.getSql();
  //JDBC操作
  statement.execute(sql);
  return resultSetHandler.handleResultSets(statement);

StatementHandler接口的实现类:

分别对应之前的几种MappedStament类型。

ParamenterHandler

那么如何将MyBatis中的参数替换成JDBC中的参数呢? —> ParamenterHandler

主要作用:负责对用户传递的参数转换成JDBC Statement 所需要的参数,这个接口很简单只有两个方法。

  • getParameterObject: 用于读取参数
  • setParameters: 用于对 PreparedStatement 的参数赋值

public void setParameters(PreparedStatement ps) 
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
  // parameterMappings 就是对 # 或者 $ 里面参数的封装
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings != null) 
    // 如果是参数化的SQL,便需要循环取出并设置参数的值
    for (int i = 0; i < parameterMappings.size(); i++) 
      ParameterMapping parameterMapping = parameterMappings.get(i);
      // 如果参数类型不是 OUT ,这个类型与 CallableStatementHandler 有关
      // 因为存储过程不存在输出参数,所以参数不是输出参数的时候,就需要设置。
      if (parameterMapping.getMode() != ParameterMode.OUT) 
        Object value;
        // 得到#  中的属性名
        String propertyName = parameterMapping.getProperty();
        // 如果 propertyName 是 Map 中的key
        if (boundSql.hasAdditionalParameter(propertyName))  // issue #448 ask first for additional params
          // 通过key 来得到 additionalParameter 中的value值
          value = boundSql.getAdditionalParameter(propertyName);
        
        // 如果不是 additionalParameters 中的key,而且传入参数是 null, 则value 就是null
        else if (parameterObject == null) 
          value = null;
        
        // 如果 typeHandlerRegistry 中已经注册了这个参数的 Class对象,即它是Primitive 或者是String 的话
        else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) 
          value = parameterObject;
         else 
          // 否则就是 Map
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        
        // 在通过SqlSource 的parse 方法得到parameterMappings 的具体实现中,我们会得到parameterMappings的typeHandler
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        // 获取typeHandler 的jdbc type
        JdbcType jdbcType = parameterMapping.getJdbcType();
        if (value == null && jdbcType == null) 
          jdbcType = configuration.getJdbcTypeForNull();
        
        try 
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
         catch (TypeException e) 
          throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
         catch (SQLException e) 
          throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
        
      
    
  

ResultSetHandler

目的:对JDBC中查询结果集 ResultSet 进行封装 ,把查询结果与实体类进行绑定

要了解 ResultSetHandler 之前,首先需要了解 ResultSetHandler的继承关系以及基本方法

public interface ResultSetHandler 

  // 处理结果集
  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  // 批量处理结果集
  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  // 处理存储过程的结果集
  void handleOutputParameters(CallableStatement cs) throws SQLException;


DefaultResultSetHandler是ResultSetHandler默认的实现类,ResultSetHandler 主要负责处理两件事

  1. 处理 Statement 执行后产生的结果集,生成结果列表
  2. 处理存储过程执行后的输出参数

按照 Mapper 文件中配置的 ResultType 或 ResultMap 来封装成对应的对象,最后将封装的对象返回即可。

核心源码如下:

@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException 
  ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

  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);
  // 如果ResultSet 包装器不是null, 并且 resultmap 的数量  >  resultSet 的数量的话
  // 因为 resultSetCount 第一次肯定是0,所以直接判断 ResultSetWrapper 是否为 0 即可
  while (rsw != null && resultMapCount > resultSetCount) 
    // 从 resultMap 中取出 resultSet 数量
    ResultMap resultMap = resultMaps.get(resultSetCount);
    // 处理结果集, 关闭结果集
    handleResultSet(rsw, resultMap, multipleResults, null);
    rsw = getNextResultSet(stmt);
    cleanUpAfterHandlingResultSet();
    resultSetCount++;
  

  // 从 mappedStatement 取出结果集
  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);

TypeHandler

类型的处理,用于Java类型和jdbc类型之间的转换

无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成 Java 类型。Mybatis默认为我们实现了许多TypeHandler, 当我们没有配置指定TypeHandler时,Mybatis会根据参数或者返回结果的不同,默认为我们选择合适的TypeHandler处理。

下面是解析XML配置的核心代码:(可以先看下一章XML解析再看这些源码)

  private void typeHandlerElement(XNode parent) 
    if (parent != null) 
      for (XNode child : parent.getChildren()) 
        //子节点为package时,获取其name属性的值,然后自动扫描package下的自定义typeHandler
        if ("package".equals(child.getName())) 
          String typeHandlerPackage = child.getStringAttribute("name");
          typeHandlerRegistry.register(typeHandlerPackage);
         else 
          String javaTypeName = child.getStringAttribute("javaType");
          String jdbcTypeName = child.getStringAttribute("jdbcType");
          String handlerTypeName = child.getStringAttribute("handler");
          Class<?> javaTypeClass = resolveClass(javaTypeName);
          JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
          Class<?> typeHandlerClass = resolveClass(handlerTypeName);
          if (javaTypeClass != null) 
            if (jdbcType == null) 
              //注册typeHandler, typeHandler通过TypeHandlerRegistry这个类管理
              typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
             else 
              typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
            
           else 
            typeHandlerRegistry.register(typeHandlerClass);
          
        
      
    
  

MyBatis层次结构

MyBatis的核心对象如何与SqlSession建立联系?

MyBatis源码中的核心对象是在SqlSession调用对应功能的时候建立联系。

我们使用MyBatis是这样:

//写法1:可读性更好,能更好的表示操作类型。是写法2的封装。(代理设计模式)
UserDao userDao = sqlSession.getMapper(UserDao.class);
User user = userDao.queryById(params);
//写法2:
//List<User> list = sqlSession.selectList("xxx.xxx.xxx.mapper.UserDao.queryById", params);

下面从SqlSession逐步分析:(以update为例)

  /**
   * update
   * @param statement 与要执行的语句匹配的唯一标识符。 namespace+id
   * @param parameter 要传递给语句的参数对象。
   * @return int
   */
  @Override
  public int update(String statement, Object parameter) 
    try 
      dirty = true;
      //获取标签内容
      MappedStatement ms = configuration.getMappedStatement(statement);
      //调用Executor来执行
      return executor.update(ms, wrapCollection(parameter));
     catch (Exception e) 
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
     finally 
      ErrorContext.instance().reset();
    
  

调用Executor

  MyBatis源码分析四XML解析与核心对象的构建

MyBatis源码分析四XML解析与核心对象的构建

mybatis源码分析-----核心调度对象StatmentHandler

MyBatis核心源码深度剖析SQL执行过程

mybatis拦截器源码分析

Mybatis和mybatis-spring一级缓存