💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
日常开发使用mybatis进行CURD操作十分简便,我们只需要在Mapper接口定义好方法,然后在mapper.xml中写好SQL语句,就能在业务代码中使用了。这简单到令人发指步骤,让我们麻痹到以为这个过程是非常简单的,时间久了脑子都笨了。今天,接着上一篇文章中的例子,结合源码学习一下mybatis中一条SQL的执行过程,了解下背后发生的故事。 # 从Demo开始 还是之前的例子,代码如下。 ## 示例代码 ~~~xml <!--CompanyDAO.xml--> <select id="selectById" resultMap="baseResultMap" > select <include refid="BaseColumns"></include> from company where id= #{id} </select> 复制代码 ~~~ CompanyDAO.xml,按照id查询数据表company的记录,id为主键,selectById为statementId。 ~~~java public interface CompanyDao { /** * 根据id查询CompanyDO * * @param id 主键id * @return 查询结果 */ CompanyDO selectById(@Param("id") Integer id); } 复制代码 ~~~ CompanyDao.java,Mapper接口,仅包含一个方法selectById,按照mybatis要求,与mapper的xml配置文件一一对应。 ~~~java public class Program { public static void main(String[] args) throws IOException { // mybatis配置文件 String path = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(path); // 获取 SqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 创建 SqlSession try (SqlSession sqlSession = sqlSessionFactory.openSession()) { //获取Mapper:CompanyDao CompanyDao dao = sqlSession.getMapper(CompanyDao.class); //调用selectById方法 CompanyDO companyDO = dao.selectById(1); System.out.println(companyDO); } } } 复制代码 ~~~ Program.java业务代码,调用逻辑比较简单,再啰嗦一下: * 通过mybatis-config.xml创建SqlSessionFactory; * 使用SqlSessionFactory创建并打开SqlSession; * 通过sqlSession获取CompanyDao的Mapper实例对象; * 调用selectById方法获取查询结果; ## 示例分析 由以上示例代码可知,CompanyDao是一个接口,我们并没有编写它的实现类,那为啥可以创建它的对象呢?我们现在得到的到底是哪个类的实例? 打个断点!下面调试界面截图,我们发现dao是org.apache.ibatis.binding.MapperProxy的实例。 ![image.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/63b05a91c0bb4218b0d76bb7be8bdc55~tplv-k3u1fbpfcp-zoom-1.image) 熟悉动态代理的同学,看一眼就知道MapperProxy是一个代理类。我们就从这里入手,顺藤摸瓜。 # 流程分析 ## Mapper对象怎么来的? MapperProxy对象是通过SqlSession#getMapper方法创建的,这里的sqlSession是DefaultSqlSession的实例,跟着源码走一下。 ~~~java //org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper @Override public <T> T getMapper(Class<T> type) { //这里调用了configuration的方法,继续走 return configuration.<T>getMapper(type, this); } //org.apache.ibatis.session.Configuration#getMapper public <T> T getMapper(Class<T> type, SqlSession sqlSession) { //所有的Mapper都注册在Configuration#mapperRegistry(不过是存储的MapperProxyFactory),所以是通过它来获取Mapper对象 //下一步进入MapperRegistry#getMapper return mapperRegistry.getMapper(type, sqlSession); } //org.apache.ibatis.binding.MapperRegistry#getMapper public <T> T getMapper(Class<T> type, SqlSession sqlSession) { //通过type从map中获取MapperProxyFactory实例 final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); //如果mapperProxyFactory,则抛出异常 if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { //通过mapperProxyFactory的newInstance创建实例,继续走 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } } //org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession) public T newInstance(SqlSession sqlSession) { //先创建了MapperProxy对象 final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); //调用了newInstance方法 return newInstance(mapperProxy); } //org.apache.ibatis.binding.MapperProxyFactory#newInstance protected T newInstance(MapperProxy<T> mapperProxy) { //到这里就揭开了代理对象创建的面纱 //使用Proxy创建了代理对象 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } 复制代码 ~~~ 为了方便梳理,上面把Mapper创建过程的代码放在一起了,我们可以发现整个过程是通过Configuration串在一起的(Configuration是mybatis运行时的基础)。简单描述一下过程: * 入口在DefaultSqlSession#getMapper,整个方式只是一个中继,需要通过Configuration#getMapper来创建; * 在解析mybatis配置时,所有的Mapper注册信息由Configuration#mapperRegistry存储,但是存储的其实是一个与Mapper类型相关的MapperProxyFactory,既然是Factory,那就要生产点啥了。 * MapperProxyFactory创建了MapperProxy对象,然后通过Proxy创建了代理对象,由MapperProxy伪装了一个Mapper实例。 所以,Mapper对象(示例中的dao)其实一个“披着Mapper皮的MapperProxy对象”。上述过程虽然跳转的类比较多,一层一层走下来最主要的就是Java动态代理相关的内容。通过下面的时序图再来看下Mapper对象的创建过程吧。 ![image.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/74bb6ee5cec841f6b20e4cdc220cca83~tplv-k3u1fbpfcp-zoom-1.image) ## 代理对象invoke方法执行 创建Mapper对象后,接下来调用Mapper方法,从下面的代码开始: ~~~java CompanyDO companyDO = dao.selectById(1); 复制代码 ~~~ dao为MapperProxy对象,当调用selectById时,会先调用org.apache.ibatis.binding.MapperProxy#invoke方法(这里可以看下Java动态代理相关的文章,保存出代理类的动态实现,有机会写篇文章)。 ~~~java @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { //判断是否为Object类 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } //判断是否为默认方法 else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } //走到这里说明是被代理接口的方法,获取或设置MapperMethod缓存,得到MapperMethod对象 final MapperMethod mapperMethod = cachedMapperMethod(method); //方法执行 return mapperMethod.execute(sqlSession, args); } private MapperMethod cachedMapperMethod(Method method) { //如果缓存中不存在method,就创建一个对象添加到缓存。 return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); } 复制代码 ~~~ 到这里,我们可以知道方法invoke最终执行的是org.apache.ibatis.binding.MapperMethod#execute,接着看MapperMethod这个类。 ~~~java public class MapperMethod { private final SqlCommand command; private final MethodSignature method; public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { //sql命令,包含mapper接口、mapper方法和configuration this.command = new SqlCommand(config, mapperInterface, method); //方法签名 this.method = new MethodSignature(config, mapperInterface, method); } } 复制代码 ~~~ MapperMethod构造方法内步创建了SqlCommand和MethodSignature,这两个类都是MapperMethod的内部类。等下看org.apache.ibatis.binding.MapperMethod#execute时会发现,它的执行与这两个类有很大关系,我们先来看下这两个做了什么。 ~~~java public static class SqlCommand { private final String name; //sql命令类型枚举:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH; private final SqlCommandType type; //构造方法:主要逻辑 public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { //获取方法名称:示例中就是:selectById final String methodName = method.getName(); //方法所在类的全路径限定名称:com.raysonxin.dao.CompanyDao final Class<?> declaringClass = method.getDeclaringClass(); //解析获取MappedStatement:看下面方法的注释。 //这样就可以拿到MappedStatement对象了 MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration); if (ms == null) { if(method.getAnnotation(Flush.class) != null){ name = null; type = SqlCommandType.FLUSH; } else { throw new BindingException("Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName); } } else { //获取id name = ms.getId(); //获取命令类型 type = ms.getSqlCommandType(); if (type == SqlCommandType.UNKNOWN) { throw new BindingException("Unknown execution method for: " + name); } } } public String getName() { return name; } public SqlCommandType getType() { return type; } //解析获取MappedStatement private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, Class<?> declaringClass, Configuration configuration) { //拼接获取statement,示例中是:com.raysonxin.dao.CompanyDao.selectById //这其实就是Configuration中MappedStatement注册的key String statementId = mapperInterface.getName() + "." + methodName; //判断configuration是否存在这个statementId if (configuration.hasStatement(statementId)) { //如果存在,获取并返回 return configuration.getMappedStatement(statementId); } else if (mapperInterface.equals(declaringClass)) { return null; } //这里是从父接口中获取的逻辑 for (Class<?> superInterface : mapperInterface.getInterfaces()) { if (declaringClass.isAssignableFrom(superInterface)) { MappedStatement ms = resolveMappedStatement(superInterface, methodName, declaringClass, configuration); if (ms != null) { return ms; } } } return null; } } 复制代码 ~~~ SqlCommand就是根据接口和方法的签名信息,从Configuration中获取对应的MappedStatement,MappedStatement是在解析mybatis-config.xml是添加的,大家可以翻阅Configuration解析代码,或者查看上一篇文章《Mybatis源码之Configuration》。SqlCommand创建后,我们就可以清楚知道了其对应的MappedStatement,也就知道了它是查询、更新、删除还是添加这样的命令类型。 ~~~java public static class MethodSignature { //返回结果是否为多个值,即列表 private final boolean returnsMany; //返回结果是否为Map类型 private final boolean returnsMap; //返回结果是否为void private final boolean returnsVoid; //返回结果是否为游标 private final boolean returnsCursor; //返回结果是否为Optioanl类型 private final boolean returnsOptional; //返回值类型 private final Class<?> returnType; private final String mapKey; // private final Integer resultHandlerIndex; //查询数据范围参数索引 private final Integer rowBoundsIndex; private final ParamNameResolver paramNameResolver; public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) { Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface); if (resolvedReturnType instanceof Class<?>) { this.returnType = (Class<?>) resolvedReturnType; } else if (resolvedReturnType instanceof ParameterizedType) { this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType(); } else { this.returnType = method.getReturnType(); } this.returnsVoid = void.class.equals(this.returnType); this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray(); this.returnsCursor = Cursor.class.equals(this.returnType); this.returnsOptional = Optional.class.equals(this.returnType); this.mapKey = getMapKey(method); this.returnsMap = this.mapKey != null; this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); this.paramNameResolver = new ParamNameResolver(configuration, method); } } 复制代码 ~~~ MethodSignature是通过反射获取其方法描述信息,也就是签名,代码太多,仅通过其字段和构造方法看下吧。MethodSignature主要的逻辑是获取其返回值类型,比如单个、多个、Map等。 对SqlCommand和MethodSignature两个内部类有一定的认识后,我们继续看下MapperMethod#execute方法。execute方法会根据SqlCommandType处理不同的命令: * INSERT、UPDATE、DELETE的处理方式应该说是基本相同,参数转换后调用对应的SqlSession方法去执行; * 对于SELECT命令,需要根据返回值特点,转换参数、处理查询范围等,然后调用SqlSession不同的查询方法; ~~~java public class MapperMethod { //... //方法执行逻辑 public Object execute(SqlSession sqlSession, Object[] args) { Object result; //判断命令类型:间接从MappedStatement获取的 switch (command.getType()) { //insert数据 //INSERT、UPDATE、DELETE的处理方式一毛一样的; //1、参数处理:把参数列表转为Map结构,同时会复制一份参数按照param0、param1这样put到Map //2、调用SqlSession对应的方法执行命令,command.getName()就是我们示例中的com.raysonxin.dao.CompanyDao.selectById case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } //更新数据 case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } //删除数据 case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } //查询命令:会根据返回值的类型调用不同的处理方法,进而路由到SqlSession不同的查询方法,在此之前需要做一些准备工作: //selectOne、selectList、selectCursor等 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; case FLUSH: result = sqlSession.flushStatements(); break; 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; } //... } 复制代码 ~~~ MapperMethod#execute最终调用了SqlSession接口的方法,在我们的示例中会调用SqlSession#selectOne。 ![image.png](data:image/svg+xml;utf8,) 走到这里是不是有点懵了,怎么调来调去,绕了一圈又回到SqlSession了?为啥不直接调用SqlSession接口呢?这里插一句:*直接使用SqlSession确实是可以,而且也是早起Mybatis所支持的方式;但是它不符合面向对象语言的概念和面向接口编程的编程习惯。由于面向接口的编程是面向对象的大趋势,MyBatis 为了适应这一趋势,增加了第二种使用MyBatis 支持接口(Interface)调用方式,即Mapper接口。* * 这一段过程的主要工作就是:**采用动态代理方法,根据我们所调用的接口和方法,结合Configuration所加载的配置信息,动态的把请求路由到SqlSession对应的接口上来。** *![image.png](data:image/svg+xml;utf8,)\_ ## Sql命令执行 绕了一圈回到SqlSession后,才算是真正开始sql命令语句的执行。这个过程也是很复杂的,需要依次经过SqlSession、Executor、StatementHandler、ParamterHandler、ResultSetHandler等,流程较长,不能面面俱到,我们随着代码走一遍流程,然后通过后续的文章逐个补齐细节。先看个大体的流程图: ![image.png](data:image/svg+xml;utf8,) ### SqlSession 按照示例,DefaultSqlSession为具体实现,从DefaultSqlSession#selectOne()开始。 ~~~java @Override public <T> T selectOne(String statement, Object parameter) { // Popular vote was to return null on 0 results and throw exception on too many. //调用selectList方法,获取返回结果list List<T> list = this.selectList(statement, parameter); //如果结果刚好是1个,返回;若为多个,抛出异常;若为空,返回null; 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 { return null; } } @Override public <E> List<E> selectList(String statement, Object parameter) { //中转一下,RowBounds给了默认值,不设限。 return this.selectList(statement, parameter, RowBounds.DEFAULT); } @Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { //从configuration获取MappedStatement, //示例中statement=com.raysonxin.dao.CompanyDao.selectById MappedStatement ms = configuration.getMappedStatement(statement); //通过执行器Executor执行查询 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } 复制代码 ~~~ 根据DefaultSqlSession代码走下来,从selectOne走到selectList,在selectList中调用了Executor#query方法。根据Executor的原理,当前executor为CachingExecutor实例,并且SimpleExecutor为其代理执行器(如下图),所以接下来进入Executor执行器的流程。 ![image.png](data:image/svg+xml;utf8,) ### Executor CachingExecutor是Executor的缓存设计,若缓存中存在所要查询的内容会直接返回,否则会交给其代理执行器通过数据库进行查询。从CachingExecutor#query(MappedStatement, Object, RowBounds, ResultHandler)开始: ~~~java //org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler) @Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { //获取BoundSql BoundSql boundSql = ms.getBoundSql(parameterObject); //获取缓存key CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); //调用重载方法 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } //org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql) @Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) 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); //数据为空还是要调用委托执行器的query if (list == null) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } //缓存不存在时,调用委托执行器的query return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } 复制代码 ~~~ 我们看到如果未启用或者未命中缓存的情况,CachingExecutor会通过其委托执行器继续查询,也就是SimpleExecutor#query。SimpleExecutor继承自BaseExecutor,在BaseExecutor也同样具有缓存的处理逻辑,我们暂时跳过,仅贴出主要代码: ~~~java public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { //省略。。。 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--; } //.... } 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 { //执行doQuery,这个是抽象方法,具体实现在SimpleExecutor 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; } @Override public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { //获取configuration Configuration configuration = ms.getConfiguration(); //创建StatementHandler,默认为PreparedStatementHandler StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); //创建并初始化statement stmt = prepareStatement(handler, ms.getStatementLog()); //由StatementHandler执行查询,使用ResultSetHandler处理查询结果 return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } } private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; Connection connection = getConnection(statementLog); //创建Statement,BaseStatementHandler#prepare stmt = handler.prepare(connection, transaction.getTimeout()); //设置参数,使用ParameterHandler handler.parameterize(stmt); return stmt; } 复制代码 ~~~ Executor过程的执行就到这里了,CachingExecutor->BaseExecutor->SimpleExecutor,并且完成了StatementHandler执行的准备工作。CachingExecutor充当了缓存一层,当未命中缓存时会执行数据库查询,此时交给BaseExecutor及其子类执行。我们通过doQuery方法看到了StatementHandler创建、参数设置、执行与关闭整个生命周期,可以说Executor是StatementHandler的调度者。 从SimpleExecutor#doQuery、SimpleExecutor#prepareStatement方法看,可以把StatementHandler的工作分为几个过程:创建Statement对象、设置Statement参数、执行Statement查询并返回结果、关闭Statement对象。其中“设置Statement参数”需要使用ParameterHandler进行参数设置、“执行Statement查询并返回结果”需要使用ResultSetHandler处理查询结果。接下来重点看下参数设置、查询、结果处理三个过程。 ### ParameterHandler prepareStatement方法中,调用了StatementHandler#parameterize进行参数设置,我们看下PreparedStatementHandler的实现。 ~~~java @Override public void parameterize(Statement statement) throws SQLException { parameterHandler.setParameters((PreparedStatement) statement); } 复制代码 ~~~ 这里的parameterHandler是ParameterHandler实例。在mybatis中,ParameterHandler仅有一个默认实现,即:DefaultParameterHandler。所以,直接看DefaultParameterHandler#setParameters方法: ~~~java @Override public void setParameters(PreparedStatement ps) { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); //获取参数映射列表,它来自SqlSource的解析过程,目前了解即可。 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { //遍历参数映射 for (int i = 0; i < parameterMappings.size(); i++) { //获取当前参数映射对象 ParameterMapping parameterMapping = parameterMappings.get(i); //如果参数模式不是OUT,即IN、INOUT时,继续执行 if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; //获取参数名称:示例中为:id 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; } //类型处理器是否存在当前参数类型。如果只有一个参数,这里一般会命中。 //我们编写sql语句时,没有标明入参类型,mybatis会把参数类型解析为Object else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } //多个参数时,会走到这里。 else { //转为MetaObject类似于一个map 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 { //通过类型处理器设置Statement参数。 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); } } } } } 复制代码 ~~~ setParameters设置参数的过程比较简单:获取参数映射列表后,依次遍历参数映射对象,逐个参数类型获取类型处理器,然后使用类型处理器设置Statement参数。当然,这里面会设置到类型处理器的查询中转操作(UnKnown->Integer),大家可以调试看下过程。 ### StatementHandler 参数设置完成后,就到了执行操作了。示例中将会走到PreparedStatementHandler#query,如下: ~~~java @Override public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { //转为PreparedStatement类型 PreparedStatement ps = (PreparedStatement) statement; //执行 ps.execute(); //处理查询结果 return resultSetHandler.handleResultSets(ps); } 复制代码 ~~~ 呃,这个太简洁了。把statement类型转为PreparedStatement,执行execute,处理查询结果并返回。Statement相关的内容属于传统JDBC范畴了,大家比较熟悉,不再赘述。 ### ResultSetHandler 这应该是查询过程中的最后一步了!由PreparedStatementHandler#query方法可知,执行java.sql.PreparedStatement#execute后,mybatis使用ResultSetHandler#handleResultSets处理查询结果。mybatis对ResultSetHandler也仅有一个默认实现DefaultResultSetHandler,直接看代码吧: ~~~java @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(); //如果rsw为空,或者resultMapCount<1,会抛出异常 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); } 复制代码 ~~~ 总结一下这个阶段的处理过程,看图吧,一图胜千言! ![image.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/19e503e00a0e45708a6d576cb40ac4c2~tplv-k3u1fbpfcp-zoom-1.image) # 总结 本文从一个简答的Mapper查询方法开始,详细分析了其执行过程。按照我的个人理解,把执行过程分为了三个阶段: * Mapper接口实例的动态实现阶段:Mybatis基于动态代理,动态实现Mapper接口,其实一个“披着Mapper皮的MapperProxy对象”; * Mapper接口命令解析阶段:这个过程中基于接口、方法签名,结合反射技术、Configuration配置信息,确认了对应的SqlSession方法入口。 * SQL命令执行阶段:从SqlSession入口开始,依次经过Executor、StatementHandler、ParameterHandler、ResultSetHandler四大组件的层层处理,最终完成查询过程。 作者:码路印记 链接:https://juejin.cn/post/6879710920534884360 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。