mybatis自定义@{}符号

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

   工作中,mybatis的${}如果能切实避免sql注入,还是很好用的,比如动态字段、动态表名,但代码安全扫描可不管能不能避免,见到${}直接判定高风险,甚是恼人啊。我这几天就遇到了,动态表名功能可以用mybatis-plus里的DynamicTableNameParser类实现,但我用的是mybatis,框架不宜轻易更换,没办法只能研究DynamicTableNameParser,模仿着自己写一个动态表名的功能,然后赌气式的自定义了一个@{}替换符,我就不信你安全扫描还能扫出@{}

提示:以下是本篇文章正文内容,下面案例可供参考

一、研究一下DynamicTableNameParser的原理

1.mybatis的plugin功能

mybatis的plugin是针对于mybatis四大组件(Statementhandler、resultsethandler、parameterHandler、executor)做增强操作的,相当于spring中的AOP,平时用的最多的分页插件PageHelper就是通过plugin实现的,如以下mybatis-plus自带的分页插件代码

@Setter
@Deprecated
@Accessors(chain = true)
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class PaginationInterceptor extends AbstractSqlParserHandler implements Interceptor {

实现Interceptor 接口,加上@Intercepts注解即可实现plugin功能,@Signature配置拦截规则,可配置多个,type只能配四大上述四大对象中的一个,配其他无效,method配对应的对象里的方法,args配方法的所有参数(不要多配或者遗漏,否则报错,找不到拦截的方法)。当程序执行到StatementHandler的prepare方法时,就会被拦截,执行PaginationInterceptor的intercept方法

@Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        // SQL 解析
        this.sqlParser(metaObject);      

intercept方法的其他部分未粘贴进来,感兴趣的可以看下源码,继续跟sqlParser方法,调用AbstractSqlParserHandler里的sqlParser方法

public abstract class AbstractSqlParserHandler {
		//在应用程序里,传入DynamicTableNameParser
    private List<ISqlParser> sqlParserList;
    private ISqlParserFilter sqlParserFilter;

    /**
     * 拦截 SQL 解析执行
     */
    protected void sqlParser(MetaObject metaObject) {
        if (null != metaObject) {
            Object originalObject = metaObject.getOriginalObject();
            StatementHandler statementHandler = PluginUtils.realTarget(originalObject);
            metaObject = SystemMetaObject.forObject(statementHandler);

            if (null != this.sqlParserFilter && this.sqlParserFilter.doFilter(metaObject)) {
                return;
            }

            // @SqlParser(filter = true) 跳过该方法解析
            if (SqlParserHelper.getSqlParserInfo(metaObject)) {
                return;
            }

            // SQL 解析
            if (CollectionUtils.isNotEmpty(this.sqlParserList)) {
                // 好像不用判断也行,为了保险起见,还是加上吧.
                statementHandler = metaObject.hasGetter("delegate") ? (StatementHandler) metaObject.getValue("delegate") : statementHandler;
                if (!(statementHandler instanceof CallableStatementHandler)) {
                    // 标记是否修改过 SQL
                    boolean sqlChangedFlag = false;
                    String originalSql = (String) metaObject.getValue(PluginUtils.DELEGATE_BOUNDSQL_SQL);
                    for (ISqlParser sqlParser : this.sqlParserList) {
                    	//使用DynamicTableNameParser
                        if (sqlParser.doFilter(metaObject, originalSql)) {
                            SqlInfo sqlInfo = sqlParser.parser(metaObject, originalSql);
                            if (null != sqlInfo) {
                                originalSql = sqlInfo.getSql();
                                sqlChangedFlag = true;
                            }
                        }
                    }
                    if (sqlChangedFlag) {
                        metaObject.setValue(PluginUtils.DELEGATE_BOUNDSQL_SQL, originalSql);
                    }
                }
            }
        }
    }
}
        

重点看这里 for (ISqlParser sqlParser : this.sqlParserList) {,sqlParserList里装的是一系列的实现了ISqlParser接口的解析器,SqlInfo sqlInfo = sqlParser.parser(metaObject, originalSql);这一行实现了sql里表名的替换

public class DynamicTableNameParser implements ISqlParser {
		//String为需要替换的表名,ITableNameHandler为动态表名处理器,即替换规则
    private Map<String, ITableNameHandler> tableNameHandlerMap;

    @Override
    public SqlInfo parser(MetaObject metaObject, String sql) {
        Assert.isFalse(CollectionUtils.isEmpty(tableNameHandlerMap), "tableNameHandlerMap is empty.");
        if (allowProcess(metaObject)) {
        		//将sql语句按空格切开成一个一个单词,然后通过from join等关键字确定表名
            Collection<String> tables = new TableNameParser(sql).tables();
            if (CollectionUtils.isNotEmpty(tables)) {
                boolean sqlParsed = false;
                String parsedSql = sql;
                for (final String table : tables) {
                    ITableNameHandler tableNameHandler = tableNameHandlerMap.get(table);
                    if (null != tableNameHandler) {
                    		//找到了该表名的替换器,process方法里替换
                        parsedSql = tableNameHandler.process(metaObject, parsedSql, table);
                        sqlParsed = true;
                    }
                }
                if (sqlParsed) {
                    return SqlInfo.newInstance().setSql(parsedSql);
                }
            }
        }
        return null;
    }
    public boolean allowProcess(MetaObject metaObject) {
        return true;
    }
}

感兴趣的朋友可以网上搜一下mybaits-plus实现动态表名的方法

二、实现自己的插件和sql解析器

按照mybatis-plus的步骤指引,一步步实现即可,先定义plugin,然后定义sql解析器,把mybatis-plus能用的类copy过来,不报错就差不多了。接下来引入springboot,将plugin连带着sql解析器注入进spring容器,建mapper即可验证,上代码,gitee地址:https://gitee.com/wangchaoam/mybatisCustomLabel.git
代码结构图

总结

因看不惯银行对${}的一刀切,搞出了@{}来代替 ${},其实最简单的方法还是改mybatis的源码,不用写这么多代码,不用花时间搞懂mybatis-plus里大佬写的代码,只要将@{}加到解析器里,地位等同于 ${}即可,但maven库又被锁死,休想上传自己的jar包。如有问题,欢迎提出,人生中第一篇博文,分享一下这两天的成功
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值