自定义结果集处理
mybatis的结果集处理在DefaultResultSetHandler.handleResultSet里:
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
this.handleRowValues(rsw, resultMap, (ResultHandler)null, RowBounds.DEFAULT, parentMapping);
} else if (this.resultHandler == null) {
// 如果不指定自己的resultHandler,就走这里。所以,一般情况下都会走这条分支
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(this.objectFactory);
this.handleRowValues(rsw, resultMap, defaultResultHandler, this.rowBounds, (ResultMapping)null);
multipleResults.add(defaultResultHandler.getResultList());
} else {
// 如果指定了自己的resultHandler,走这里
this.handleRowValues(rsw, resultMap, this.resultHandler, this.rowBounds, (ResultMapping)null);
}
} finally {
this.closeResultSet(rsw.getResultSet());
}
}
DefaultResultHandler的处理逻辑就是拿到ResultSet里的所有行记录,以List方式返回:
public class DefaultResultHandler implements ResultHandler<Object> {
private final List<Object> list;
public DefaultResultHandler() {
this.list = new ArrayList();
}
public DefaultResultHandler(ObjectFactory objectFactory) {
this.list = (List)objectFactory.create(List.class);
}
public void handleResult(ResultContext<?> context) {
this.list.add(context.getResultObject());
}
public List<Object> getResultList() {
return this.list;
}
}
如果行记录数很大,比如几百万条,使用DefaultResultHandler就会占用大量的内存空间。这时,我们可能需要指定自己的resultHandler来做分批处理。
如何指定自己的resultHandler呢?很简单,重载mapper里已有的接口即可:
// 使用DefaultResultHandler
List<Group> selectXXX(@Param("ids") List<Long> ids);
// 使用自定义的resultHandler
void selectXXX(@Param("ids") List<Long> ids, ResultHandler resultHandler);
xml文件也不用改,因为我们就使用selectXXX的配置,只不过拿到List后,走我们自己的resultHandler而已。
如何获得mybatis的执行sql
参考下面代码:
// 先拿到SqlSessionFactory
SqlSessionFactory sqlSessionFactory = (SqlSessionFactory) springContext().getBean("sqlSessionFactory", SqlSessionFactory.class);
// 根据sql id拿到MappedStatement对象
MappedStatement ms = sqlSessionFactory.getConfiguration().getMappedStatement("com.lee.selectXXX");
// 这里要根据dao接口里的@Param注解,将参数逐个加入ParamMap
MapperMethod.ParamMap paramMap = new MapperMethod.ParamMap();
paramMap.put("ids", Arrays.asList(0L, 1L));
String sql = ms.getBoundSql(paramMap).getSql();
最后拿到的bound sql形如:
select GROUP_ID,
......
from tbl_t
where GROUP_ID in
(
?
,
?
)
将#{}换成?占位符,相当于把mybatis的语法替换成了PrepareStatement可以接受的语法。
要最终执行上述boundsql,还得把参数flatten,传给PrepareStatement执行,因为@Param描述的一个参数有可能对应PrepareStatement的多个占位符。这个flatten的动作在DefaultParameterHandler.setParameters里完成。
PageInterceptor原理
使用样例代码:
@Named
@Slf4j
public class RepairService implements IRepairService {
@Inject
private IRepairDao iRepairDao;
@Override
public PagedResult<RepairPlanVo> listRepairPlans(PageVO pageVO) throws ApplicationException {
IPageQueryHandler<RepairPlanVo> iPageQueryHandler = () -> iRepairDao.listRepairPlans();
PagedResult<RepairPlanVo> pagedResult = iPageQueryHandler.page(pageVO);
return pagedResult;
}
iPageQueryHandler.page逻辑为:
public interface IPageQueryHandler<R> {
default PagedResult<R> page(PageVO pageVO) {
PagedResult var4;
try {
PageHelper.startPage(pageVO);
//我们会发现,到了这一步,this.list()返回的是已经分页了的结果列表,但list函数的实现() -> iRepairDao.listRepairPlans()明明是查了所有的记录啊,why?奥秘就在PageInterceptor,它拦截了mybatis的sql查询动作,篡改了iRepairDao.listRepairPlans()的行为
PageInfo<R> pageInfo = new PageInfo(this.list());
PagedResult<R> pagedResult = new PagedResult();
pageVO.setTotalRows((int)pageInfo.getTotal());
pagedResult.setPageVO(pageVO);
pagedResult.setResult(pageInfo.getList());
var4 = pagedResult;
} finally {
PageHelper.clearPage();
}
return var4;
}
List<R> list();
}
PageInterceptor的intercept方法:
public Object intercept(Invocation invocation) throws Throwable {
try {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement)args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds)args[2];
ResultHandler resultHandler = (ResultHandler)args[3];
Executor executor = (Executor)invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
if (args.length == 4) {
//boundSql就是原始sql:select * from t_repair_plan
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
cacheKey = (CacheKey)args[4];
boundSql = (BoundSql)args[5];
}
this.checkDialectExists();
if (this.dialect instanceof Chain) {
boundSql = ((Chain)this.dialect).doBoundSql(Type.ORIGINAL, boundSql, cacheKey);
}
List resultList;
//skip表示是否跳过分页,无分页信息就会跳过
if (!this.dialect.skip(ms, parameter, rowBounds)) {
if (this.dialect.beforeCount(ms, parameter, rowBounds)) {
//这里会将原始sql转成count sql并执行:select COUNT(0) from t_repair_plan
Long count = this.count(executor, ms, parameter, rowBounds, (ResultHandler)null, boundSql);
if (!this.dialect.afterCount(count, parameter, rowBounds)) {
Object var12 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
return var12;
}
}
//这里执行分页查询sql:select * from t_repair_plan limit ? offset ?
resultList = ExecutorUtil.pageQuery(this.dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
Object var16 = this.dialect.afterPage(resultList, parameter, rowBounds);
return var16;
} finally {
if (this.dialect != null) {
this.dialect.afterAll();
}
}
}
动态sql的OGNL语法说明
mybatis里的动态sql使用的是OGNL表达式语法,类似于SPEL。
语法如下(参考这里):
e1 or e2 或
e1 and e2 与
e1 == e2,e1 eq e2 相等
e1 != e2,e1 neq e2 不等
e1 lt e2, e1 < e2:小于
e1 lte e2, e1 <= e2:小于等于
e1 gt e2, e1 > e2:大于
e1 gte e2, e1 >= e2:大于等于
e1 in e2
e1 not in e2
e1 + e2,e1 * e2,e1/e2,e1 - e2,e1%e2 四则运算
!e,not e 非
e.method(args) 调用对象方法
e.property 对象属性值
e1[e2] 按索引取值,如数组和字典
@class@method(args) 调用类的静态方法
@class@field 调用类的静态字段值
OGNL里的变量
变量全都是java对象,该对象的public方法可以直接在OGNL里调用,getXXX方法可以简写为obj.XXX属性。这给了mybatis非常强大的动态能力。
如何打印sql
在log4j2.xml里配置日志级别为debug:
<Logger name="org.apache.ibatis" level="debug" additivity="true">
<AppenderRef ref="sql"/>
</Logger>
动态代理的原理
可以参考下面文章,写得很详细了:
https://blog.csdn.net/m0_71777195/article/details/128426110
我们总结一下,调用链是:
sqlSession(一般是sqlSessionTemplate ) -> dao bean -> MapperProxy -> MapperMethod -> sqlSession的增删改查
其中,dao bean就是动态代理机制创建出来的。
这里有个问题,一个dao bean是怎么注册到spring框架里的?
这需要mybatis-spring包里的MapperFactoryBean做一个桥接,FactoryBean顾名思义,就是专用来动态生成其它bean的bean。一般情况下,spring框架autowire一个接口时,会去bean定义中找有没有该接口的实现类,有,直接把这个实现类做成bean;但对于mybatis的dao接口,其实现类是没有的,需要运行时动态生成,这就必须借助FactoryBean了。
每个FactoryBean必须实现getObject和getObjectType接口,MapperFactoryBean的实现如下:
public T getObject() throws Exception {
return this.getSqlSession().getMapper(this.mapperInterface);
}
// getObjectType用于向spring框架声明:我可以动态生成某个interface接口的bean,如果找不到该interface的实现类,就用我吧
public Class<T> getObjectType() {
return this.mapperInterface;
}
我们看到,getObject实际上就是从sqlSessionTemplate 里拿到特定mapperInterface的dao bean,具体实现是在MapperRegistry里:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
} else {
try {
// 这里会创建一个动态代理对象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}
mapperProxyFactory.newInstance实现如下:
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
...
protected T newInstance(MapperProxy<T> mapperProxy) {
// 这里创建动态代理对象O,O会把mapperInterface接口的调用转到mapperProxy里。
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
return this.newInstance(mapperProxy);
}
}
防范SQL注入
我认为,mybatis相比于原生SQL API或JdbcTemplate更安全的地方在于,它在提供足够的动态SQL能力的同时,能将SQL注入的风险降到最低:我们只需避免在xml里配置${}变量即可,即便有某些例外,我们也可以快速排查,做好分析备案。
相比之下,原生SQL API或JdbcTemplate都支持在代码里手写SQL,这就给SQL注入以可趁之机,对新手来讲尤其如此,即便是老手,在拼接一些复杂SQL时也会疏忽。
这就是框架限制的威力。
另一个框架JPA里的@Query注解也有同样的效果,能有效防范SQL注入。