mybatis使用高阶技巧

自定义结果集处理

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注入。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值