关注“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类@Overridepublic 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中 @Overridepublic <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办法。

@Overridepublic <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是MappedStatementprotected 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,则没有查问二级缓存和写入二级缓存的流程。

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

@Overridepublic <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中 @Overridepublic <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下载