探索Mybatis中Mapper默认返回值

文章目录

背景

​ 最近写业务代码时会遇到如下查询,若StudentMapper中的listByName方法返回null而非空的list,则在该业务代码的第二行就会容易抛出NPE,为了消除代码的副作用,需要结合Mybatis源码来分析该次查询若没有数据是返回null还是空的list。

业务代码:

List<Student> students = studentMapper.listByName("zqh");
students.stream().map(Student::getId()).collect(Collectors.toList());

StudentMapper.java

public interface StudentMapper {
    List<Student> listByName(@Param("name") String name);
}

StudentMapper.xml

<select id="listByName" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List"/>
    from student
    where name like CONCAT('%',#{name},'%')
</select>

源码分析

Mybatis的Mapper是通过Java动态代理实现的,而返回StudentMapper的代理是通过MapperProxyFactory动态生成的,执行如listByName方法是在生成的代理MapperProxy中,该类负责执行代理类的方法,MapperProxy源码如下:

public class MapperProxy<T> implements InvocationHandler, Serializable {

  //....
    
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

}

内部逻辑比较清楚,这里直接看MapperMethod类的execute方法,如下:

public class MapperMethod {
    // 省略......
    
    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        switch (command.getType()) {
            case INSERT: {
                //......
            }
            case UPDATE: {
                //......
            }
            case DELETE: {
                //......
            }
            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()) {
                    //.....
                }
                //.....
        }
        //......
    }
                
    private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
        List<E> result;
        Object param = method.convertArgsToSqlCommandParam(args);
        if (method.hasRowBounds()) {
            // 分页
        } else {
            // 查询
            result = sqlSession.<E>selectList(command.getName(), param);
        }
        
        if (!method.getReturnType().isAssignableFrom(result.getClass())) {
            if (method.getReturnType().isArray()) {
                // 转换一
                return convertToArray(result);
            } else {
                // 转换二
                return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
            }
        }
        return result;
    }
    
    // 转换一
    private <E> E[] convertToArray(List<E> list) {
        E[] array = (E[]) Array.newInstance(method.getReturnType().getComponentType(), list.size());
        array = list.toArray(array);
        return array;
    }
    
    // 转换二
    private <E> Object convertToDeclaredCollection(Configuration config, List<E> list) {
        Object collection = config.getObjectFactory().create(method.getReturnType());
        MetaObject metaObject = config.newMetaObject(collection);
        metaObject.addAll(list);
        return collection;
    }
  }

通过debug模式发现,listByName方法在获取StudentMapper动态代理时会把该方法封装在MapperMethod中静态内部类MethodSignature中(returnsMany=true,returnType=interface java.util.List),所以会走到executeForMany方法中,该方法仍然在MapperMethod中。

上述代码中,转换一入参不可能是null,转换二处若list为null就会根据方法返回类型创建一个空list,在这里已经得出了结论:在listByName方法查询中,若查找不到所取的数据,会返回一个空的list。要想从根本上查明在上面的查询中(即sqlSession.selectList)会不会返回null,所以需要具体查看sqlSession.selectList方法,Debug如下,最终会执行到DefaultSqlSession的selectList方法:

在这里插入图片描述

DefaultSqlSession.java

public class DefaultSqlSession implements SqlSession {
    
  @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) {
      //......
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
      //......
  }
  //......
}

由上看,最终会执行到BaseExecutor的query方法(SqlSession有四大对象,即Executor、StatementHandler、ParameterHandler和ResultHandler),如下:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    //......
    List<E> list;
    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);
    }
    
    //......
    return list;
}

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    //......
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    //......
    return list;
}


query函数仍然看不出list返回是否为null,接着看(若工程配置的defaultExecutorType是REUSE)ReuseExecutor的doQuery方法:

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    //.....
    return handler.<E>query(stmt, resultHandler);
}

这里会交给PreparedStatementHandler的query方法,如下:

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.<E> handleResultSets(ps);
}

会交给DefaultResultSetHandler的handleResultSets来处理,如下:

public List<Object> handleResultSets(Statement stmt) throws SQLException {
    //....
    // 创建代码如下-----重点
    final List<Object> multipleResults = new ArrayList<Object>();
    
    //......
    while (rsw != null && resultMapCount > resultSetCount) {
        //......
        handleResultSet(rsw, resultMap, multipleResults, null);
        //......
    }
    //
    return collapseSingleResultList(multipleResults);
}

终于揭开庐山真面目了!即在mapper中的类似listByName方法的查询,若查询不到数据,会返回一个空的list

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bboyzqh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值