【我要找工作_03】当我在学习MyBatis时,我到底在学习个啥?

478 阅读16分钟

为了不再四处寻找mybatis的八股、在我学习mybatismini版代码之后、基于自己的理解,写出了此文,如果你正好阅读到此文,恰好和我一样领着失业金,那就给我点个赞吧。行文如有错误,请指出。

核心:JDK的动态代理

mybatis 的核心是基于 jdk 动态代理实现,通过给 mapper 层接口生成代理对象、在执行接口中定义的方法时,委托给 sqlsession 接口,sqlsession将其交给执行器进行执行。 首先看一个 jdk 动态代理的代码示例:

// 定义接口以及实现
public interface UserService {
    void addUser(String username);
    void deleteUser(String username);
}

public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String username) {
        System.out.println("添加用户: " + username);
    }
    @Override
    public void deleteUser(String username) {
        System.out.println("删除用户: " + username);
    }
}

jdk 的动态代理通过实现 InvocationHandler接口实现。

public class UserServiceProxy implements InvocationHandler {
    // 目标对象(被代理的原始对象)
    private final Object target;
​
    public UserServiceProxy(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 前置增强
        System.out.println("[前置日志] 方法名: " + method.getName() + ", 参数: " + Arrays.toString(args));
        // 调用目标对象的方法
        Object result = method.invoke(target, args);
        // 后置增强
        System.out.println("[后置日志] 方法执行完成");
        return result;
    }
}

代码测试:

public static void main(String[] args) {
    // 1. 创建目标对象
    UserService target = new UserServiceImpl();
​
    // 2. 创建 InvocationHandler
    UserServiceProxy handler = new UserServiceProxy(target);
​
    // 3. 生成代理对象
    UserService proxy = (UserService) Proxy.newProxyInstance(
            target.getClass().getClassLoader(), // 类加载器
            target.getClass().getInterfaces(),  // 目标对象实现的接口
            handler                             // InvocationHandler
    );
​
    // 4. 通过代理对象调用方法
    proxy.addUser("张三");
    proxy.deleteUser("李四");
}
// output:
// [前置日志] 方法名: addUser, 参数: [张三]
// 添加用户: 张三
// [后置日志] 方法执行完成// [前置日志] 方法名: deleteUser, 参数: [李四]
// 删除用户: 李四
// [后置日志] 方法执行完成

这上面是一个最简单的 jdk 动态代理使用。到这里不知道你有没有如下疑问?

  • mybatis 的接口没有实现是如何实现代理的呢?
  • 明明 UserService 通过实现类进行实例化的,mybatis 的接口没有实现类怎么进行实例化呢?
  • UserServiceProxy明明返回的是一个代理对象,为什么可以强转为UserService接口对象呢?

如果有这些问题,那么我们就需要好好了解一下 Proxy.newProxyInstance() 这个方法,这个方法有三个参数,且返回的是一个代理对象。参数解释如下:

  • 第一个参数:传入类加载器:是为了和被代理的类处于同一个环境下。
  • 第二个参数:代理类要实现的接口,这就是能强制转为 UserService 的类型的原因。
  • 第三个参数:对代理类的逻辑增强,需要实现 InvocationHandler 接口。这样在调用接口的方法时,才能够执行到invoke方法。

综上所述,实际上和有没有接口的实现好像并没有什么关系!那么我们再来实现一个没有实现类的示例代码:

public interface DogMapper {
    String findDogName();
    Integer getDogAge();
}
public class DynmicHandler implements InvocationHandler {
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
​
        System.out.println(method.getName());
        if (method.getName().equals("getDogAge")){
          System.out.println("执行完毕");
        }
        return null;
    }
    public static void main(String[] args) {
        DynmicHandler handler = new DynmicHandler();
        // 3. 生成代理对象
        DogMapper proxy = (DogMapper) Proxy.newProxyInstance(
                DogMapper.class.getClassLoader(), // 类加载器
                new Class[]{DogMapper.class},  // 目标对象实现的接口
                handler                             // InvocationHandler
        );
        proxy.getDogAge();
    }
}
// output:
// getDogAge
// 执行完毕

可以看到即使接口没有实现类,我们依然可以代理拦截的接口的方法。但是由于没有方法的实现,我们肯定不能执行method.invoke()去调用原接口的方法。 是不是说如果我们在此通过设置一定的代码规范,就可以想干什么就做什么了?

那么 mybatis 是如何去实现 mapper 层方法调用的呢?回到通过给mapper层接口生成代理对象、在执行接口中定义的方法时,委托给sqlsession接口这句话。他会将其委托给 sqlSession 去执行。主要源代码如下:

// MapperProxy<T>.java
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      // toString方法直接调用原方法
      return method.invoke(this, args);
    }
    // 调用接口的方法
    return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

cachedInvoker() 会返回一个 MapperMethodInvoker 接口对象,其定义的方法如下:

interface MapperMethodInvoker {
  Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable;
}

他有两个实现类:

// 用于处理自定义的mapper接口和mapper.xml中的方法
private static class PlainMethodInvoker implements MapperMethodInvoker {
  private final MapperMethod mapperMethod;
  public PlainMethodInvoker(MapperMethod mapperMethod) {
    this.mapperMethod = mapperMethod;
  }
  @Override
  public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    return mapperMethod.execute(sqlSession, args);
  }
}
// 用于处理接口中默认的方法实现。
private static class DefaultMethodInvoker implements MapperMethodInvoker {
  private final MethodHandle methodHandle;
​
  public DefaultMethodInvoker(MethodHandle methodHandle) {
    this.methodHandle = methodHandle;
  }
​
  @Override
  public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    return methodHandle.bindTo(proxy).invokeWithArguments(args);
  }
}

我们只需要关注第一个类的实现通过mapperMethod.execute(sqlSession, args)去执行。此方法如下:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        // 其他代码省略
      }
      case DELETE: {
        // 其他代码省略
        break;
      }
      case SELECT:
        // 其他代码省略
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + "' attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

可以看到在这里就根据不同的insert | select | update | delete 标签,进行了不同的处理。

以插入方法为例,Object param = method.convertArgsToSqlCommandParam(args); 处理参数问题如参数解析、参数替换等。sqlSession.insert(command.getName(), param) 执行数据插入操作。可以看到这里就交给了接口 SqlSession,由于是接口需要交给实现类操作。在实现类DefalutSqlSession 中, 可以看到如下代码:

@Override
public int insert(String statement, Object parameter) {
  return update(statement, parameter);
}
@Override
public int update(String statement, Object parameter) {
  try {
    dirty = true;
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.update(ms, wrapCollection(parameter));
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

上面的代码可以看到,defaultSqlSession 会将其委托给 Executor 去调用,这里不继续追入了,后面还会讨论到这里。至此开头的这句话:

mybatis 的核心是基于动态代理实现,通过给 mapper 层接口生成代理对象、在执行接口中定义的方法时,委托给 Sqlsession 接口,Sqlsession 将其交给执行器进行执行。

我们就已经得到了验证。

本质:Mybatis的本质是对jdbc操作的封装

我们之所以会使用到 Mybatis 框架,最本质的原因还是基于 JDBC 对数据库的操作不太方便,回顾一下使用JDBC操作数据库的过程

  • 注册驱动
  • 获取链接
  • 拼接sql
  • 执行sql
  • 操作结果集返回结果
  • 关闭资源

每一次 jdbc 操作都需要重复如上过程,每一次的操作除了执行的 sql 不一样和处理返回结果不一样之外,其他的步骤没有任何不同。因此对于我们而言是可以抽象简化出来的。

在使用 mybatis 进行开发的时候,我们有感知的就是定义接口,编写 xml 文件。其余的事情是框架帮我们做了。因此简化了我们的开发流程。

注册驱动 / 获取链接

无论是只使用 mybatis 框架还是 Springboot 中使用mybatis,我们都需要编写数据库驱动账户密码等配置信息,以只使用 mybatis 框架为例,我们会在mybatis-config.xml中包含数据库相关信息如下:

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useSSL=true&amp;requireSSL=true"/>
            <property name="username" value="root"/>
            <property name="password" value="root123!"/>
        </dataSource>
    </environment>
</environments>

此时我们需要通过如下的代码去加载配置文件:

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml"));

mybatis 会通过如上代码去加载解析 xml 文件,获取到数据库相关信息。在拿到了账户、密码等信息后。就可以进行注册驱动、获取链接链接操作。在 mybatis 中,提供了对数据库连接到封装信息,其中包括池化和非池化的,池化是对非池化链接的扩展封装。在UnpooledDataSource.java中可以看到如下代码:

// 注册驱动
static {
  Enumeration<Driver> drivers = DriverManager.getDrivers();
  while (drivers.hasMoreElements()) {
    Driver driver = drivers.nextElement();
    registeredDrivers.put(driver.getClass().getName(), driver);
  }
}
​
// 获取链接
@Override
public Connection getConnection() throws SQLException {
  return doGetConnection(username, password);
}

数据库池化就是使用了一个list 和一些状态变量进行维护相关链接,只不过这里会使用代理链接进行处理,在关闭链接时,关闭的是代理对象,会将真实的链接方法list并通知其他获取链接的线程进行获取。

sql的解析

在使用 mybatis 时,我们可以通过注解和编写xml的方式,不同的使用方式有不同的解析方式,以 xml为例,在解析具体 sqlmapper.xml 中的标签时,提供了如下的解析类:

  • XMLConfigBuilder
  • XMLMapperBuilder
  • XMLStatementBuilder

这些类通过名称都可以发现具体的作用, 第一个类针对的 mybatis 的主配置文件,其中解析方法如下:

private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
                  configuration.getSqlFragments());
              mapperParser.parse();
            }
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            try (InputStream inputStream = Resources.getUrlAsStream(url)) {
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url,
                  configuration.getSqlFragments());
              mapperParser.parse();
            }
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException(
                "A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

可以看到最后是对mapper配置项目进行解析。在XMLMapperBuilder中:

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }
​

分别对 resultMap 和缓存以及sql语句进行解析。到这里就可以总结一下,mybatis在解析xml文件时,通过细化解析过程中,对不同的文件采用了不同的类去解析。在解析生成MappedStatement时,加载到configuration类中。

sql节点如 <select/update> 等标签的具体解析是在XMLStatementBuilder.parseStatementNode()中。

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
​
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
​
    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
​
    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());
​
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
​
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
              ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }
​
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType
        .valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    if (resultTypeClass == null && resultMap == null) {
      resultTypeClass = MapperAnnotationBuilder.getMethodReturnType(builderAssistant.getCurrentNamespace(), id);
    }
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");
    boolean dirtySelect = context.getBooleanAttribute("affectData", Boolean.FALSE);
​
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
        parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
  }

具体详细的代码解释就不在这里过多的叙述,解析完成后通过 builderAssistant.addMappedStatement装载在 configuration 对象中,

// configuration.java

  public void addMappedStatement(MappedStatement ms) {
    // namespce+"."+id
    mappedStatements.put(ms.getId(), ms);
  }

参数处理和sql执行

在使用 mybatis 的过程中,我们会常常使用到动态标签等,在执行sql查询等时候往往会对sql进行参数处理,在使用jdbc 的时候,我们会通过PrepareStatement进行处理,在mybatis中,我们最终执行的时候需要交给sqlsessionsqlsession会委托给executor,因此我们只需要在executor查找相关处理流程即可。

executor接口中,除了定义了事务相关的方法,还定义了queryupdate 方法用于数据库操作。在这里借助了模板模式对其实现,在 BaseExecutor 中实现了queryupdate 方法,调用了类中定义的抽象方法 doUpdatedoQuery

// baseExecutor.java
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  clearLocalCache();
  return doUpdate(ms, parameter);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,CacheKey key, BoundSql boundSql) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    clearLocalCache();
  }
  List<E> list;
  try {
    queryStack++;
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
  } finally {
    queryStack--;
  }
  if (queryStack == 0) {
    for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
    }
    // issue #601
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}
​
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;
}
​

可以看到,不管是在执行更新还是查询都是对doUpdate/Query方法的调用。都对缓存的做了处理,在query()中,还查询了缓存,再从数据库中获取。

不管是 query 还是 update 操作,在执行前都需要进行参数处理,以 doquery 为例,可以追踪到如下代码。

// SimpleExecutor
@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);
    // 参数处理
    stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }
// PrepareStatementHandler中
@Override
public void parameterize(Statement statement) throws SQLException {
  parameterHandler.setParameters((PreparedStatement) statement);
}
// DefaultParameterHandler.java
@Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }
​

到这一步就可以看到,我们完成了sql参数的设置和数据库操作。接下来就是对执行结果的包装。

结果集处理

jdbc操作的过程中我们是通过如下的方法进行结果包装,mybatis为我们屏蔽了这一步,让我们在使用的过程中无感。

private static void queryUsers(Connection conn) throws SQLException {
    String sql = "SELECT id, name, age FROM user";
    try (Statement stmt = conn.createStatement();
         ResultSet rs = stmt.executeQuery(sql)) {
        System.out.println("===== 用户列表 =====");
        while (rs.next()) {
            int id = rs.getInt("id");
            String name = rs.getString("name");
            int age = rs.getInt("age");
            System.out.printf("ID: %d, 姓名: %s, 年龄: %d\n", id, name, age);
          // new User(), setter....
        }
    }
}

MapperMethod这个类中,是将接口方法拦截交给sqlSession的地方,在执行查询方法时,会看到如下的代码

case SELECT:
  if (method.returnsVoid() && method.hasResultHandler()) {
    executeWithResultHandler(sqlSession, args);
    result = null;
  } else if (method.returnsMany()) {
    result = executeForMany(sqlSession, args);
  } else if (method.returnsMap()) {
    result = executeForMap(sqlSession, args);
  } else if (method.returnsCursor()) {
    result = executeForCursor(sqlSession, args);
  } else {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = sqlSession.selectOne(command.getName(), param);
    if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
      result = Optional.ofNullable(result);
    }
  }
  break;

可以看到根据不同的返回类型,执行了不同的查询方法,但他们最终都是会执行到executordoQuery方法,

selectOne为例,该方法最终会执行到如下方法:

@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);
    stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}  
// handler.query
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  String sql = boundSql.getSql();
  statement.execute(sql);
  return resultSetHandler.handleResultSets(statement);
}
// resultSetHandler.handleResultSets(statement);
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
  ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
​
  final List<Object> multipleResults = new ArrayList<>();
​
  int resultSetCount = 0;
  ResultSetWrapper rsw = getFirstResultSet(stmt);
​
  List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  int resultMapCount = resultMaps.size();
  validateResultMapsCount(rsw, resultMapCount);
  while (rsw != null && resultMapCount > resultSetCount) {
    ResultMap resultMap = resultMaps.get(resultSetCount);
    handleResultSet(rsw, resultMap, multipleResults, null);
    rsw = getNextResultSet(stmt);
    cleanUpAfterHandlingResultSet();
    resultSetCount++;
  }
  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);
}

可以看到如上的方法,mybatis具体怎么封装的,需要阅读源码,由于本文是为了理解 mybatis 整体的框架,因此不做过多的行文。

可以说到这里 mybatis 基本的工作流程就已经明白了。

通过对 xml 文件的解析,(不同的文件和标签有对应的builder负责),解析完成后会加载到configuration 对象中,在执行 mapper 层接口时,被动态代理拦截到委托给 sqlSessionsqlSession 交给 executor 执行,executor 有三个实现类分别为 SimpleExecutorBatchExecutorReuseExecutor, 对应不同的使用场景。执行完操作之后会将查询结果交给 ResultHandler 进行封装返回。

框架的扩展:插件机制

mybatis 的插件系统也是通过动态代理实现的。在研究插件的时候,我们可以回顾一下 mybatis 抽象那些操作出来:

  • 数据库驱动注册和获取链接
  • 解析xml时,生成的 StatementHandler 对象、封装了 jdbc 的操作。执行和参数处理。
  • 参数设置时,通过 ParameterHandler 进行参数设置。具体的参数替换过程
  • 执行 sql 操作时,委托给 Executor 执行操作,该接口定义了查询和更新
  • 封装结果时,ResultHandler 定义了具体的封装结果的方法。

因此假如我们要写一个插件、除了一个没办法修改外(毕竟要保证链接的正确性:不管如何去改装一辆车,至少车钥匙要能打开车),我们就可以通过插件拦截到上述的过程。进而对其进行修改。

public interface Interceptor {
    Object intercept(Invocation invocation) throws Throwable;
    // 代理
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    // 设置属性
    default void setProperties(Properties properties) {
        // NOP
    }
}
// 对拦截信息的封装
public class Invocation {
    // 调用的对象
    private Object target;
    // 调用的方法
    private Method method;
    // 调用的参数
    private Object[] args;
    // 放行;调用执行
    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(target, args);
    }
}

通过注解标识要拦截的类和方法,在执行方法时,通过判断该类上是否实现了被拦截的接口。判断是否执行代理。

public static Object wrap(Object target, Interceptor interceptor) {
    // 取得签名Map
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // 取得要改变行为的类(ParameterHandler|ResultSetHandler|StatementHandler|Executor),
    Class<?> type = target.getClass();
    // 取得接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 创建代理(StatementHandler)
    if (interfaces.length > 0) {
        // Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
        return Proxy.newProxyInstance(
                type.getClassLoader(),
                interfaces,
                new Plugin(target, interceptor, signatureMap));
    }
    return target;
}
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
  // 取 Intercepts 注解,
  Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
  // 必须得有 Intercepts 注解,没有报错
  if (interceptsAnnotation == null) {
      throw new RuntimeException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
  }
  // value是数组型,Signature的数组
  Signature[] sigs = interceptsAnnotation.value();
  // 每个 class 类有多个可能有多个 Method 需要被拦截
  Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
  // 一个拦截器可能拦截多个类。
  for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
      try {
          // 例如获取到方法;StatementHandler.prepare(Connection connection)、StatementHandler.parameterize(Statement statement)...
          Method method = sig.type().getMethod(sig.method(), sig.args());
          methods.add(method);
      } catch (NoSuchMethodException e) {
          throw new RuntimeException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
  }
  return signatureMap;
}
​
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    while (type != null) {
        for (Class<?> c : type.getInterfaces()) {
            // 拦截 ParameterHandler|ResultSetHandler|StatementHandler|Executor
            if (signatureMap.containsKey(c)) {
                interfaces.add(c);
            }
        }
        type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
}
​

这样在创建 statementHandler 或者其他可拦截的组件时,可以通过判断是否需要创建代理对象进行对方法进行拦截。

缓存

mybatis提供了缓存操作用于提高操作效率。

  • 一级缓存的作用范围以及可能存在的问题,不同的sqlsession之间的操作无法感知、造成脏读。【Statement Id + Offset + Limmit + Sql + Params】
  • 二级缓存是全局的,基于namespace范围的,不同的namespace之间还是会存在脏读的情况

到这里 mybatis 的整体架构就很清楚了,通过解析xml文件,加载使用信息,封装了jdbc的操作,提供的可扩展点和缓存功能。这就是mybatis!至于不同步骤的细化,需要通过研究源码细化我们对其的理解。到这里至少我们知道了在使用过程中对应的内容和模块该去哪一部分debug