美文网首页Mybatis
Mybatis 拦截器实现 Like 通配符转义

Mybatis 拦截器实现 Like 通配符转义

作者: htger | 来源:发表于2020-08-03 22:35 被阅读0次

    缘起

    在 Mybatis 执行 like 查询时,如果查询字符包含通配符‘%’或‘_’则查询结果并不是我们预期结果。比如,一张文件表里有字段 file_name 记录文件名,而我们需要找到以‘_’开头的文件。查询语句是这样: select * from file where file_name like '_%' 。如果你了解mysql 就知道执行该语句将返回表中所有的文件名不为空的记录。这实际是与我们目标相悖。正确的语句应该是 select * from file where file_name like "\\_%"(默认转义字符是反斜杠)。

    代码

    不止一次遇到该问题了,之前的解决办法,是在 mapper 的参数中做转义。比如下面这段代码(是一个帖子搜索参数的定义,我们重点关注 titlePrefix 这代表标题是一个前缀匹配)

    
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class PostSearchParam implements Serializable {
    
        private Integer type;
        /**
         * 标题前缀
         */
        private String titlePrefix;
        /**
         * 学院名称
         */
        private String groupName;
        /**
         * userId
         */
        private Integer userId;
    }
    
    

    为了实现转义,添加一个新的getter方法来获取转义后的标题

    ....
    public String getEscapeTitlePrefix(){
        return likeEscape(this.titlePrefix);
    }
    
    String escapeLike(String value) {
            if (value != null) {
                return value
                    .replaceAll("_", "\\\\_")
                    .replaceAll("%", "\\\\%");
            }
            return null;
        }
    ...
    

    虽然能够实现预期,但是你想想,如果有很多这样的类,那我们需要敲很多遍一样的代码。为了“永绝后患”,可以通过mybatis拦截器去除冗余代码。

    拦截器的主要内容:1. 识别 mapper 中的sql语句中包含like查询(正则);2. 找到like后的占位符,将该占位的字符串转义。

    /**
     * Like 转义
     * <p>
     * 使用方法:
     * <ol>
     * <li> 添加插件 </li>
     * </ol>
     * </p>
     */
    @Intercepts(
            {
                    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
                            Object.class, RowBounds.class, ResultHandler.class}),
                    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
                            Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
            }
    )
    public class LikeStringEscapeInterceptor implements Interceptor {
    
        /**
         * 检查sql中,是否含有like查询
         */
        private static Pattern LIKE_PARAM_PATTERN = Pattern
                .compile("like\\s+['\"%_]*\\?", Pattern.CASE_INSENSITIVE);
    
        @Override
        public Object plugin(Object target) {
            return Plugin.wrap(target, this);
        }
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            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) {
                //4 个参数时
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 个参数时
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
    
            SqlCommandType sqlCommandType = ms.getSqlCommandType();
            StatementType statementType = ms.getStatementType();
            // 只处理 有参数的查询语句
            if (sqlCommandType == SqlCommandType.SELECT
                    && statementType == StatementType.PREPARED) {
                escapeParameterIfContainingLike(boundSql);
                return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return invocation.proceed();
        }
    
        void escapeParameterIfContainingLike(BoundSql boundSql) {
            if (boundSql == null) {
                return;
            }
            String prepareSql = boundSql.getSql();
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    
            // 找到 like 后面的参数
            List<Integer> position = findLikeParam(prepareSql);
            if (position == null || position.size() == 0) {
                return;
            }
    
            List<ParameterMapping> likeParameterMappings = new ArrayList<>();
    
           // 复制
            MetaObject metaObject = MetaObjectUtil.forObject(boundSql.getParameterObject());
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping pm = parameterMappings.get(i);
                String property = pm.getProperty();
                // 忽略无法处理的属性。例如 __frch_
                if (metaObject.hasGetter(property)) {
                    boundSql.setAdditionalParameter(property, metaObject.getValue(property));
                    if (position.contains(i)) {
                        likeParameterMappings.add(pm);
                    }
                }
            }
    
            // 覆盖 转义字符
            delegateMetaParameterForEscape(boundSql, likeParameterMappings);
        }
    
    
        /**
         * @param boundSql              原 boundSql
         * @param likeParameterMappings 需要转义的参数
         * @return 支持转义的 boundSql
         */
        void delegateMetaParameterForEscape(BoundSql boundSql, List<ParameterMapping> likeParameterMappings) {
    
            for (ParameterMapping mapping : likeParameterMappings) {
                String property = mapping.getProperty();
    
                MetaObject metaObject = MetaObjectUtil.forObject(boundSql.getParameterObject());
                Object value = metaObject.getValue(property);
                if (value instanceof String) {
                    boundSql.setAdditionalParameter(property, escapeLike((String) value));
                }
            }
        }
    
        String escapeLike(String value) {
            if (value != null) {
                return value
                        .replaceAll("_", "\\\\_")
                        .replaceAll("%", "\\\\%");
            }
            return null;
        }
    
    
        List<Integer> findLikeParam(String prepareSql) {
            Matcher matcher = LIKE_PARAM_PATTERN.matcher(prepareSql);
    
            int pos = 0;
            List<Integer> indexes = new ArrayList<>();
    
            while (matcher.find(pos)) {
                int start = matcher.start();
                int index = StringUtils.countMatches(prepareSql.substring(0, start), "?");
                indexes.add(index);
                pos = matcher.end();
            }
            return indexes;
        }
    }
    
    

    使用

    如果需要使用,请添加到mybatis的插件中

    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor"/>
        <plugin
          interceptor="xxxxx.LikeStringEscapeInterceptor"/>
      </plugins>
    

    如有错误,欢迎大家指正。

    相关文章

      网友评论

        本文标题:Mybatis 拦截器实现 Like 通配符转义

        本文链接:https://www.haomeiwen.com/subject/zkosrktx.html