Mybatis执行器

HeJin大约 7 分钟源码解析Mybatis源码解析

SQL执行流程

SqlSession

String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 先获取Mapper
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectById(2);
System.out.println(user);

// 直接调用sqlSession的方法进行查询
User user1 = sqlSession.selectOne("selectById", 2);
System.out.println(user1);
  • 在我们使用mybatis的过程中,我们是先使用SqlSessionFactoryBuilder根据mybatis核心配置文件构建一个SqlSessionFactory,然后通过SqlSessionFactory获取SqlSession

  • 对数据库的操作,对于使用方来说,直接使用SqlSession进行增删改查。我们可以直接调用SqlSession的方法,也可以获取到定义的Mapper对象之后,调用其方法。

  • 实际上 sqlSession.getMapper()的底层还是调用的 SqlSession的增删改查方法。

  • SqlSession是一个接口,具体的实现是在实现类中。对于一般的查询来说,是在DefaultSqlSession

    image-20221223175801356
    image-20221223175801356
    image-20221223175938917
    image-20221223175938917
    image-20221223180031500
    image-20221223180031500
    image-20221223180122876
    image-20221223180122876

SqlSession查询流程分析

public static void main(String[] args) throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();

    // 第一个参数是statement(Mapper接口里面定义的方法名称),第二个是parameter
    User user1 = sqlSession.selectOne("selectById", 2);
    System.out.println(user1);
}

1.程序进入到DefaultSqlSession里面的selectOne()方法

image-20221223180838728
image-20221223180838728

最终会调用DefaultSqlSession里面的selectList()方法:

private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 调用具体的执行器进行查询  
    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

2.进入到CachingExecutorquery()方法

image-20221223181345560
image-20221223181345560

进行一些处理之后,调用CachingExecutorquery()方法

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  // 获取二级缓存:如果mybatis没有开启二级缓存的话,cache就为null  
  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) {
        // 缓存没有命中,调用其他Executor查询  
        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);
}

3.进入到BaseExecutor的()方法

image-20221223182317522
image-20221223182317522

queryFromDatabase方法:

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 {
    // 执行查询
    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;
}

其中doQueryBaseExecutor内部定义的一个抽象方法,由子类进行实现:

protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
    throws SQLException;

mybatis默认的配置,实现是SimpleExecutor

4.进入到SimpleExecutordoQuery()方法

@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();
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    // 获取Statement 
    stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

prepareStatement()方法:

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  // 获取连接
  Connection connection = getConnection(statementLog);
  // 通过连接获取Statement
  stmt = handler.prepare(connection, transaction.getTimeout());
  handler.parameterize(stmt);
  return stmt;
}
image-20221223183601152
image-20221223183601152

我们都知道mybatis是对jdbc的封装,到这里我们可以看到jdbc的影子了。先拿到Connection(编写JDBC的时候,是通过驱动获取的),然后通过Connection获取PreparedStatement,通过PreparedStatement先进行sql和参数填充,然后执行查询,最后获取结果集。

// 1.注册驱动
Class<?> aClass = Class.forName("com.mysql.cj.jdbc.Driver");
Driver driver= (Driver) aClass.newInstance();
DriverManager.registerDriver(driver);

// 2.获取连接
String url = "jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai";
String username = "root";
String password = "root";
Connection connection = DriverManager.getConnection(url, username, password);

// 3.预处理sql
String sql = "select * from user where id = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1, 1);
// 4.执行查询
ResultSet resultSet = preparedStatement.executeQuery();

getMapper动态代理查询分析

public static void main(String[] args) throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 获取Mapper对象
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    User user = userMapper.selectById(2);
    System.out.println(user);
}
  • 在使用Mybatis的时候,我们只要定义一个接口,然后再对应的mapper文件中进行SQL的编写。然后就可以通过SqlSession获取的Mapper对象,执行查询。
  • 这里有个问题,我们定义的是接口,是不能直接调用的。而接口要使用,就必须要有实现类。我们没有定义实现类,那么肯定就是mybatis为我们提供了实现类。关键就在 sqlSession.getMapper这行代码了。通过debug去发现mybatis究竟是怎么为我们创建一个实现类的。
  • 这里还有一个问题,假设mybatis内部实现了我们定义的接口,就可以返回实现类。观察sqlSession.getMapper(UserMapper.class),发现传入参数是一个Mapper接口的Class对象。通过反射可以拿到UserMapper的接口信息。但是我们定义的接口很多,如果mybatis每个都去实现的话,比较麻烦且不显示。而且每个接口的操作都是类似的,根据参数查询数据库,也会有许多重复的逻辑。这时我们可能想到了动态代理,动态代理可以对接口进行拦截,并且可以对方法进行增强,返回数据等。既然Mapper接口需要一个实现类,那么我们就用动态代理生成一个实现类,并实现接口定义的查询逻辑,最后把查询结果返回。这样对于用户来说,只需要定义接口和编写mapper文件,就可实现与JDBC相同的功能。但是不需要去考虑SQL的拼接,参数的封装等等。这些重复且复杂的逻辑,mybatis使用动态代理来进行统一处理了。

获取Mapper对象

1.进入DefaultSqlSessiongetMapper()方法

image-20221223190941171
image-20221223190941171

2.进入ConfigurationgetMapper()方法

image-20221223191057061
image-20221223191057061

3.进入MapperRegistrygetMapper()方法

image-20221223191403138
image-20221223191403138

4.进入MapperProxyFactorynewInstance()方法

image-20221223191942896
image-20221223191942896

使用了JDK的动态代理,代理逻辑在最后一个参数,这个对象必须实现InvocationHandler接口,处理代理逻辑。

查看MapperProxy类,发现确实实现了InvocationHandler接口。

image-20221223192226670
image-20221223192226670

invoke()方法:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    } else {
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

由此可以知道的是,Mybatis底层使用了JDK的动态代理,代理的是我们定义的Mapper接口。Mybatis为我们的接口返回了一个代理对象。所以我们可以直接调用接口里面的方法。

执行查询

获取到了Mapper对象,接下来看看具体的查询逻辑是怎么实现的。

image-20221223193129685
image-20221223193129685

1.进入MapperProxy中。当我们调用Mapper代理接口的方法时,会进行代理类中的invoke()方法。这是动态代理的知识。

image-20221223193637825
image-20221223193637825

进入MapperProxy自定义的invoke()方法:

image-20221223193938782
image-20221223193938782

2.进入MapperMethodexecute()方法:

image-20221223194301438
image-20221223194301438

这里进行增删改查的判断,我们使用的是查询,会进行SELECT:

image-20221223194722213
image-20221223194722213

我们可以看一下,返回结果是多个的情况:

executeForMany()方法

image-20221223194901518
image-20221223194901518

executeForMap()方法

image-20221223195023111
image-20221223195023111

而且还可以发现,对于插入、修改、删除也是这样:

image-20221223200204746
image-20221223200204746

由此可以得出sqlSession.getMapper(Class对象)的执行逻辑:

  • 该方法通过JDK动态代理拦截传入的Mapper接口,返回一个Mapper代理对象。具体的代理逻辑在MapperProxy中,该类实现了InvocationHandler接口,代理逻辑在invoke方法中。
  • 当我们调用Mapper接口中的方法时,会被代理类MapperProxy拦截。最终会使用MapperMethod类的execute()方法进行查询。
  • MapperMethod类的execute()方法首先会进行增删改查的判断。对于增删改查,都是调用的SqlSession中的方法。

Mybatis执行器概览图

image-20221223190747526
image-20221223190747526