2023-06-06  阅读(5)
原文作者:惑边 原文地址:https://blog.csdn.net/my_momo_csdn

Mybatis参数读取源码分析

  • Mybatis框架最底层需要访问数据库,本质是到数据库去执行sql语句,那么给sql语句传递参数是必不可少的一个环节,sql语句在预编译阶段会用占位符替换参数的位置,在需要执行的时候需要将参数放到对应的位置上,这个将参数替换占位符的工作就是ParameterHandler来完成的,本文我们来看看这个对象处理的相关知识。

一、ParameterHandler

  • ParameterHandler在Mybatis四大对象中负责将sql中的占位符替换为真正的参数,它是一个接口,有且只有一个实现类DefaultParameterHandler
    public interface ParameterHandler {
    
      Object getParameterObject();
    
      void setParameters(PreparedStatement ps)
          throws SQLException;
    
    }
  • setParameters是处理参数最核心的方法。

二、DefaultParameterHandler

  • DefaultParameterHandler是接口的唯一实现类,代码不多,主要是setParameters方法,通过注释我们可以看到其大体的功能主流程
    public class DefaultParameterHandler implements ParameterHandler {
    
      //1.类型处理器注册中心
      private final TypeHandlerRegistry typeHandlerRegistry;
    
      //2.MappedStatement是保存sql语句的数据结构
      private final MappedStatement mappedStatement;
      //3.参数对象
      private final Object parameterObject;
      //4.BoundSql对象是sql语句和相关信息的封装
      private BoundSql boundSql;
      //5.全局配置对象
      private Configuration configuration;
    
      //构造方法
      public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        this.mappedStatement = mappedStatement;
        this.configuration = mappedStatement.getConfiguration();
        this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
        this.parameterObject = parameterObject;
        this.boundSql = boundSql;
      }
    
      @Override
      public Object getParameterObject() {
        return parameterObject;
      }
    
      /**
       * 将占位符替换为参数值
       * */
      @Override
      public void setParameters(PreparedStatement ps) {
        ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
        //1.获取sql语句的参数,ParameterMapping里面包含参数的名称类型等详细信息,还包括类型处理器
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings != null) {
          //2.遍历依次处理
          for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            //3.OUT类型参数不处理
            if (parameterMapping.getMode() != ParameterMode.OUT) {
              Object value;
              //4.获取参数名称
              String propertyName = parameterMapping.getProperty();
              //5.如果propertyName是动态参数,就会从动态参数中取值。(当使用<foreach>的时候,MyBatis会自动生成额外的动态参数)
              if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                value = boundSql.getAdditionalParameter(propertyName);
              } else if (parameterObject == null) {
                  //6.如果参数是null,不管属性名是什么,都会返回null。
                value = null;
              } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                  //7.判断类型处理器是否有参数类型,如果参数是一个简单类型,或者是一个注册了typeHandler的对象类型,就会直接使用该参数作为返回值,和属性名无关。
                value = parameterObject;
              } else {
                //8.这种情况下是复杂对象或者Map类型,通过反射方便的取值。通过MetaObject操作
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
              }
              TypeHandler typeHandler = parameterMapping.getTypeHandler();
              //9.获取对应的数据库类型
              JdbcType jdbcType = parameterMapping.getJdbcType();
              //空类型
              if (value == null && jdbcType == null) {
                jdbcType = configuration.getJdbcTypeForNull();
              }
              //10.对PreparedStatement的占位符设置值(类型处理器可以给PreparedStatement设值)
              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);
              }
            }
          }
        }
      }
    }
  • 核心流程
    1.sql语句中的占位符?都对应了BoundSql#parameterMappings集合中的一个元素,在该对象中记录了对应的参数名称和参数的相关属性。
    在SimpleExecutor的doQuery方法中调用prepareStatement方法处理参数占位符,方法里面调用的就是PreparedStatementHandler的parameterize
    方法,最终调用的是DefaultParameterHandler#setParameters,在PreparedStatementHandler内部持有ParameterHandler对象。
    
    PS:简单来说就是SimpleExecutor#doQuery方法内部通过配置对象创建StatementHandler -> 调用PreparedStatementHandler#parameterize方
    法(内部持有ParameterHandler) ->DefaultParameterHandler#setParameters来完成sql语句执行之前的参数替换占位符

三、流程调试

  • 本节我们通过调试来看看前面说的核心流程,看看ParameterHandler参数处理在整个查询的哪一个步骤完成。断点在:SimpleExecutor#doQuery方法的prepareStatement(handler, ms.getStatementLog()),如下,
    然后执行一次查询操作:
    mapper.findMemberById(1);

3.1 SimpleExecutor#doQuery

  • SimpleExecutor#doQuery方法是一次查询的入口,查询处理和返回结果集都在这里。在第2步调用prepareStatement方法获取Statement,里面就包含参数的预处理。(这里还可以参考 14-Mybatis源码和设计模式-5(Executor组件与模板模式,装饰器模式))
    /**
       * 查询的实现
       * */
      @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();
          //1.创建StatementHandler
          StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
          //2.用StatementHandler对象创建stmt,并使用StatementHandler对占位符进行处理
          stmt = prepareStatement(handler, ms.getStatementLog());
          //3.通过statementHandler对象调用ResultSetHandler将结果集转化为指定对象返回
          return handler.<E>query(stmt, resultHandler);
        } finally {
          closeStatement(stmt);
        }
      }

202306062340475721.png

3.2 SimpleExecutor#prepareStatement

  • prepareStatement方法是获取Statement,默认是PrepareStatement。(其实是一个增强了的代理对象,具备日志能力的代理对象),其中第三步就是进行参数处理。
      /**
       * 创建Statement
       * */
      private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Statement stmt;
        //1.获取connection对象的动态代理,添加日志能力;(这里参考日志模块的代理模式)
        Connection connection = getConnection(statementLog);
        //2.使用StatementHandler,利用connection创建(prepare)Statement
        stmt = handler.prepare(connection, transaction.getTimeout());
        //3.使用StatementHandler处理占位符
        handler.parameterize(stmt);
        return stmt;
      }

202306062340484332.png

3.3 PreparedStatementHandler#parameterize

  • SimpleExecutor#prepareStatement 的方法首先会走到RoutingStatementHandler#parameterize,但是RoutingStatementHandler其实是一个静态代理,实际上走的是其内部的PreparedStatementHandler(参考:17-Mybatis源码分析(StatementHandler数据库访问))。
    后面就到了PreparedStatementHandler#parameterize,方法很简单,直接调用ParameterHandler的setParameters方法,直接走到DefaultParameterHandler#setParameters
      @Override
      public void parameterize(Statement statement) throws SQLException {
        parameterHandler.setParameters((PreparedStatement) statement);
      }

202306062340492303.png

3.4 DefaultParameterHandler#setParameters

  • 参考前面的注释和下面的调试信息,这里会直接使用typeHandler进行设值操作,调用之前实际上已经通过参数类型获取到了对应的typeHandler。(parameterMapping.getTypeHandler())

202306062340503524.png

3.5 BaseTypeHandler#setParameter

  • 这里实际上走的是BaseTypeHandler的子类IntegerTypeHandler,不过setParameter是BaseTypeHandler的一个模板方法,里面的一个流程方法setNonNullParameter会调用子类IntegerTypeHandler的,在IntegerTypeHandler的setNonNullParameter里面直接调用PreparedStatement的setInt方法。到这里其实就走到了PreparedStatement的代理类的invoke方法(我们前面提到过,其实四大对象运行时都是一个代理对象,是一个具备日志能力的代理对象,这里参考:11-Mybatis源码和设计模式-2(日志模块和适配器模式,代理模式)),然后就到了PreparedStatementLogger类。(下面文字写错了,因为参数是1,所以走下面一个逻辑)
    
      //BaseTypeHandler
      @Override
      public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
         //省略...
          try {
            setNonNullParameter(ps, i, parameter, jdbcType);
          //省略...
          }
      }
    
     //IntegerTypeHandler
      @Override
      public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
          throws SQLException {
        ps.setInt(i, parameter);
      }

202306062340512235.png

202306062340521186.png

3.6 PreparedStatementLogger#invoke

  • 最后通过method.invoke(statement, params),终于到了真实的PreparedStatement对象,调用的是PreparedStatement的setInt方法。我们发现绕了一圈已经到了JDBC的包下了,是的,Mybatis封装了很多最后底层参数设值还是JDBC那一套。

202306062340528197.png

202306062340536338.png

202306062340543829.png

四、小结

4.1 调试流程图

2023060623405503310.png

4.2 小结

  • Executor是sql语句的执行器,Executor通过配置对象创建StatementHandler,继而得到了StatementHandler,StatementHandler是整个数据库访问过程的控制关键,它的内部持有ParameterHandler,因此StatementHandler可以通过后者来处理参数。在StatementHandler处理参数的过程中会通过参数类型来找到对应的typeHandler来处理参数,整个过程中Statement对象都作为参数在传递,到了typeHandler他会调用Statement的setInt来设置值,其实整个过程中Statement对象都在传递,Mybatis通过封装,但是还是在使用JDBC的API。
  • 关于Mybatis中数据访问的整体流程,可以阅读参考文章[3]

五、参考


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文