最近写业务代码时会遇到如下查询,若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。