关于java:阿里面试Mybatis中方法和SQL是怎么关联起来的呢

7次阅读

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

关注“Java 后端技术全栈”

回复“面试”获取全套面试材料

本文:3126 字 | 浏览时长:4 分 10 秒

明天是 Mybatis 源码剖析第四篇,也是最初一篇。

老规矩,先上案例代码:

public class MybatisApplication {
  public static final String URL = "jdbc:mysql://localhost:3306/mblog";
  public static final String USER = "root";
  public static final String PASSWORD = "123456";
    
  public static void main(String[] args) {
    String resource = "mybatis-config.xml";
    InputStream inputStream = null;
    SqlSession sqlSession = null;
    try {inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        // 上面这行代码就是明天的重点
        User user = userMapper.selectById(1));
        System.out.println(user);
    
    } catch (Exception e) {e.printStackTrace();
    } finally {
      try {inputStream.close();
      } catch (IOException e) {e.printStackTrace();
      }
      sqlSession.close();}
 }

曾经分享了三篇 Mybatis 源码剖析文章,从 Mybatis 的配置文件解析 到 获取 SqlSession,再到 获取 UserMapper 接口的代理对象。

明天咱们来剖析,userMapper 中的办法和 UserMapper.xml 中的 SQL 是怎么关联的,以及怎么执行 SQL 的。

咱们的故事从这一行代码开始:

User user = userMapper.selectById(1));

这一行代码背地源码搞完,也就代表着咱们 Mybatis 源码搞完(骨干局部)。

上面咱们持续开撸。

在上一篇中咱们晓得了 userMapper 是 JDK 动静代理对象,所以调用这个代理对象的任意办法都是执行触发治理类 MapperProxy 的 invoke() 办法。

因为篇幅较长,为了更好浏览,这里把文章分成两个局部:

  • 第一局部:MapperProxy.invoke() 到 Executor.query。
  • 第二局部:Executor.query 到 JDBC 中的 SQL 执行。

第一局部流程图:

MapperProxy.invoke()

开篇曾经说过了,调用 userMapper 的办法就是调用 MapperProxy 的 invoke() 办法,所以咱们就从这 invoke() 办法开始。

如果对于 Mybatis 源码不是很相熟的话,倡议先看看后面的文章。

//MapperProxy 类
@Override
public Object invoke(....) throws Throwable {
  try {
    // 首先判断是否为 Object 自身的办法,是则不须要去执行 SQL,// 比方:toString()、hashCode()、equals() 等办法。if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);
    } else if (method.isDefault()) {
      // 判断是否 JDK8 及当前的接口默认实现办法。return invokeDefaultMethod(proxy, method, args);
    }
  } catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);
  }
  //<3> 
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  //<4>
  return mapperMethod.execute(sqlSession, args);
}

<3> 处是从缓存获取 MapperMethod,这里退出了缓存次要是为了晋升 MapperMethod 的获取速度。缓存的应用在 Mybatis 中也是十分之多。

private final Map<Method, MapperMethod> methodCache;
private MapperMethod cachedMapperMethod(Method method) {return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}

Map 的 computeIfAbsent 办法:依据 key 获取值,如果值为 null,则把前面的 Object 的值付给 key。

持续看 MapperMethod 这个类,定义了两个属性 command 和 method,两个属性与之绝对应的两个动态外部类。

public class MapperMethod {
  private final SqlCommand command;
  private final MethodSignature method;
  public static class SqlCommand {
  private final String name;
  private final SqlCommandType type;
  public SqlCommand(...) {final String methodName = method.getName();
      final Class<?> declaringClass = method.getDeclaringClass();
      // 取得 MappedStatement 对象
      MappedStatement ms = resolveMappedStatement(...);
      // <2> 找不到 MappedStatement
      if (ms == null) {
        // 如果有 @Flush 注解,则标记为 FLUSH 类型
        if (method.getAnnotation(Flush.class) != null) {
            name = null;
            type = SqlCommandType.FLUSH;
        } else { 
          // 抛出 BindingException 异样,如果找不到 MappedStatement
          //(开发中容易见到的谬误)阐明该办法上,没有对应的 SQL 申明。throw new BindingException("Invalid bound statement (not found):"+ mapperInterface.getName() + "." + methodName);
       }
      } else {
      // 取得 name
      //id=com.tian.mybatis.mapper.UserMapper.selectById
      name = ms.getId();
      // 取得 type=SELECT
      type = ms.getSqlCommandType();
      // 如果 type=UNKNOWN
      // 抛出 BindingException 异样,如果是 UNKNOWN 类型
      if (type == SqlCommandType.UNKNOWN) {throw new BindingException("Unknown execution method for:" + name);
      }
      }
  }   
  private MappedStatement resolveMappedStatement(...) {
      // 取得编号
      //com.tian.mybatis.mapper.UserMapper.selectById
      String statementId = mapperInterface.getName() + "." + methodName;
      // 如果有,取得 MappedStatement 对象,并返回
      if (configuration.hasStatement(statementId)) {//mappedStatements.get(statementId); 
        // 解析配置文件时候创立并保留 Map<String, MappedStatement> mappedStatements 中
        return configuration.getMappedStatement(statementId);
        // 如果没有,并且以后办法就是 declaringClass 申明的,则阐明真的找不到
        } else if (mapperInterface.equals(declaringClass)) {return null;}
        // 遍历父接口,持续取得 MappedStatement 对象
        for (Class<?> superInterface : mapperInterface.getInterfaces()) {if (declaringClass.isAssignableFrom(superInterface)) {MappedStatement ms = resolveMappedStatement(...);
            if (ms != null) {return ms;}
        }
      }
      // 真的找不到,返回 null
      return null;
    } 
    //....
}
public static class MethodSignature {
    private final boolean returnsMap;
    private final Class<?> returnType;
    private final Integer rowBoundsIndex;
    //....
}

SqlCommand 封装了 statement ID,比如说:

com.tian.mybatis.mapper.UserMapper.selectById

和 SQL 类型。

public enum SqlCommandType {UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;}

另外,还有个属性 MethodSignature 办法签名,次要是封装的是返回值的类型和参数解决。这里咱们 debug 看看这个 MapperMethod 对象返回的内容和咱们案例中代码的关联。

妥妥的,故事持续,咱们接着看 MapperMethod 中 execute 办法。

MapperMethod.execute

下面代码中 <4> 处,先来看看这个办法的整体逻辑:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case SELECT:
         // 局部代码省略....
         Object param = method.convertArgsToSqlCommandParam(args);
          // 本次是 QUERY 类型,所以这里是重点 
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {result = Optional.ofNullable(result);
          }
        break;
      default:
        throw new BindingException("Unknown execution method for:" + command.getName());
    } 
    return result;
  }

这个办法中,依据咱们下面取得的不同的 type(INSERT、UPDATE、DELETE、SELECT) 和返回类型:

(本文是查问,所以这里的 type=SELECT)

  1. 调用 convertArgsToSqlCommandParam() 将办法参数转换为 SQL 的参数。

2. 调用 selectOne() 办法。这里的 sqlSession 就是 DefaultSqlSession,所以咱们持续回到 DefaultSqlSession 中 selectOne 办法中。

SqlSession.selectOne 办法

持续 DefaultSqlSession 中的 selectOne() 办法:

//DefaultSqlSession 中 
@Override
public <T> T selectOne(String statement, Object parameter) {
    // 这是一种好的设计办法
    // 不论是执行多条查问还是单条查问,都走 selectList 办法(重点)List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
      // 如果只有一条就返回第一条
      return list.get(0);
    } else if (list.size() > 1) {//( 开发中常见谬误) 办法定义的是返回一条数据,后果查出了多条数据,就会报这个异样
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found:" + list.size());
    } else {
      // 数据库中没有数据就返回 null
      return null;
    }
  }

这里调用的是本类中 selectList 办法。

@Override
public <E> List<E> selectList(String statement, Object parameter) {return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }
  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      // 从 configuration 获取 MappedStatement
      // 此时的 statement=com.tian.mybatis.mapper.UserMapper.selectById
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 调用执行器中的 query 办法
      return executor.query(...);
    } catch (Exception e) {//.....} finally {ErrorContext.instance().reset();}
  }

在这个办法里是依据 statement 从 configuration 对象中获取 MappedStatement 对象。

MappedStatement ms = configuration.getMappedStatement(statement);

在 configuration 中 getMappedStatement 办法:

// 寄存在一个 map 中的
//key 是 statement=com.tian.mybatis.mapper.UserMapper.selectById,value 是 MappedStatement
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>();  
  public MappedStatement getMappedStatement(String id) {return this.getMappedStatement(id, true);
  }
  public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {return mappedStatements.get(id);
  }

而 MappedStatement 外面有 xml 中增删改查标签配置的所有属性,包含 id、statementType、sqlSource、入参、返回值等。

到此,咱们曾经将 UserMapper 类中的办法和 UserMapper.xml 中的 sql 给彻底关联起来了。持续调用 executor 中 query() 办法:

executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

这里调用的是执行器 Executor 中的 query() 办法。

第二局部流程:

Executor.query() 办法

这里的 Executor 对象是在调用 openSession() 办法时创立的。对于这一点咱们在后面的文章曾经说过,这里就不再赘述了。

上面来看看调用执行器的 query() 放的整个流程:

咱们股市持续,看看具体源码是如何实现的。

CachingExecutor.query()

在 CachingExecutor 中

 @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

BoundSql 中次要是 SQL 和参数:

既然是缓存,咱们必定想到 key-value 数据结构。

上面来看看这个 key 生成规定:

这个二级缓存是怎么形成的呢?并且还要保障在查问的时候必须是惟一。

也就说,形成 key 次要有:

办法雷同、翻页偏移量雷同、SQL 雷同、参数雷同、数据源环境雷同才会被认为是同一个查问。

这里能说到这个层面就曾经阔以了。

如果向更深刻的搞,就得把 hashCode 这些扯进来了,请看下面这个张图里后面的几个属性就晓得和 hashCode 有关系了。

解决二级缓存

首先是从 ms 中取出 cache 对象,判断 cache 对象是否为 null,如果为 null,则没有查问二级缓存和写入二级缓存的流程。

有二级缓存,校验是否应用此二级缓存,再从事务管理器中获取二级缓存,存在缓存间接返回。不存在查数据库,写入二级缓存再返回。

@Override
public <E> List<E> query(....)
      throws SQLException {Cache cache = ms.getCache();
    // 判断是否有二级缓存
    if (cache != null) {flushCacheIfRequired(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;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
 }

那么这个 Cache 对象是什么创立的呢?

在解析 UserMapper.xml 时候,在 XMLMapperBuilder 类中的 cacheElement() 办法里。

对于二级缓存相干这一块在后面文章曾经说过,比方:

解析下面这些标签

创立 Cache 对象:

二级缓存解决完了,就来到 BaseExecutor 的 query 办法中。

BaseExecutor,query()

第一步,清空缓存

if (queryStack == 0 && ms.isFlushCacheRequired()) {clearLocalCache();
}

queryStack 用于记录查问栈,避免地柜查问反复解决缓存。

flushCache=true 的时候,会先清理本地缓存(一级缓存)。

如果没有缓存会从数据库中查问

list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

在看看这个办法的逻辑:

private <E> List<E> queryFromDatabase(...) throws SQLException {
    List<E> list;
    // 应用占位符的形式,先抢占一级缓存。localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      // 删除下面抢占的占位符 
      localCache.removeObject(key);
    }
    // 放入一级缓存中
    localCache.putObject(key, list);
    return list;
 }

先在缓存应用占位符占位,而后查问,移除占位符,将数据放入一级缓存中。

执行 Executor 的 doQuery() 办法,默认应用 SimpleExecutor。

list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

上面就来到了 SimpleExecutor 中的 doQuery 办法。

SimpleExecutor.doQuery

@Override
 public <E> List<E> doQuery(....) throws SQLException {
    Statement stmt = null;
    try {
      // 获取配置文件信息 
      Configuration configuration = ms.getConfiguration();
      // 获取 handler
      StatementHandler handler = configuration.newStatementHandler(....);
      // 获取 Statement
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 执行 RoutingStatementHandler 的 query 办法 
      return handler.query(stmt, resultHandler);
    } finally {closeStatement(stmt);
    }
 }

创立 StatementHandler

在 configuration 中 newStatementHandler() 里,创立了一个 StatementHandler 对象,先失去 RoutingStatementHandler(路由)。

public StatementHandler newStatementHandler() {StatementHandler statementHandler = new RoutingStatementHandler();
    // 执行 StatementHandler 类型的插件 
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

RoutingStatementHandler 创立的时候是就是创立根本的 StatementHandler 对象。

这里会依据 MapperStament 外面的 statementType 决定 StatementHandler 类型。 默认是 PREPARED

StatementHandler 外面蕴含了解决参数的 ParameterHandler 和处理结果集的 ResultHandler。

下面说的这几个对象正式被插件拦挡的四大对象,所以在创立的时都要用拦截器进行包装的办法。

对于插件相干的,请看之前已发的插件文章:插件原理剖析。

咱们故事持续:

创立 Statement

创建对象后就会执行 RoutingStatementHandler 的 query 办法。

//RoutingStatementHandler 中 
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    // 委派 delegate=PreparedStatementHandler
    return delegate.query(statement, resultHandler);
}

这里设计很有意思,所有的解决都要应用 RoutingStatementHandler 来路由,全副通过委托的形式进行调用。

执行 SQL

而后执行到 PreparedStatementHandler 中的 query 办法。

 @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;
    //JDBC 的流程了 
    ps.execute();
    // 处理结果集,如果有插件代理 ResultHandler,会先走到被拦挡的业务逻辑中
    return resultSetHandler.handleResultSets(ps);
  }

看到了 ps.execute(); 示意曾经到 JDBC 层面了,这时候 SQL 就曾经执行了。前面就是调用 DefaultResultSetHandler 类进行后果集解决。

到这里,SQL 语句就执行结束,并将后果集赋值并返回了。

总算搞完把 Mybatis 骨干源码掠了一遍,松口气~,能看到这里,证实小伙伴也是蛮用心的,辛苦了。越致力越幸福!

倡议抽时间,再次 debug,还有就是画画类之间的关系图,还有就是 Mybatis 中设计模式好好回味回味。

总结

从调用 userMapper 的 selectById() 办法开始,到办法和 SQL 关联起来,参数解决,再到 JDBC 中 SQL 执行。

残缺流程图:

感兴趣的小伙伴,能够对照着这张流程图就行一步一步的 debug。

记着:致力的人,世界不会亏待你的。

举荐浏览

把握 Mybatis 动静映射,我可是下了功夫的

图解多线程

搞定这 24 道 JVM 面试题,要价 30k 都有底气~

《写给大忙人看的 JAVA 核心技术》.pdf 下载

正文完
 0