mybatis/mybatis-plus模糊查询语句特殊字符转义拦截器

在最近的公司开发中,遇到了这样的情况。在列表查询时录入了‘%,_’等特殊字符,而在查询时无法精确匹配‘%, _’。究其原因,‘%, _’是MySQL的关键字,如果我们想要精确匹配‘%, _’等特殊字符,那么需要对其进行转义。

有两种方式,

第一种

在具体的方法中针对特殊字段调用以下方法转义一次,优点:编码简单,缺点:机械,代码重复不灵活。

/**
 * 转义通配符
 *
 * @param before 待转义字符串
 * @return 转义后字符串
 */
String escapeChar(String before) {
    if (StringUtils.isNotBlank(before)) {
        before = before.replaceAll("\\\\", "\\\\\\\\");
        before = before.replaceAll("_", "\\\\_");
        before = before.replaceAll("%", "\\\\%");
    }
    return before;
}

第二种

配置mybatis的特殊字符转义拦截器。这个就可以做到全局配置,且不影响现有代码。

具体步骤:

1.使用mybatis提供的拦截器拦截所有的查询请求。

具体实现在代码中均有注释

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
​
import java.util.*;
​
/**
 * mybatis/mybatis-plus模糊查询语句特殊字符转义拦截器
 *
 * @author lxw
 */
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
@Slf4j
public class MybatisLikeSqlInterceptor implements Interceptor {
​
    /**
     * SQL语句like
     */
    private final static String SQL_LIKE = " like ";
​
    /**
     * SQL语句占位符
     */
    private final static String SQL_PLACEHOLDER = "?";
​
    /**
     * SQL语句占位符分隔
     */
    private final static String SQL_PLACEHOLDER_REGEX = "\\?";
​
    /**
     * 所有的转义器
     */
    private static Map<Class, AbstractLikeSqlConverter> converterMap = new HashMap<>(4);
​
    static {
        converterMap.put(Map.class, new MapLikeSqlConverter());
        converterMap.put(Object.class, new ObjectLikeSqlConverter());
    }
​
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement statement = (MappedStatement) args[0];
        Object parameterObject = args[1];
        BoundSql boundSql = statement.getBoundSql(parameterObject);
        String sql = boundSql.getSql();
        this.transferLikeSql(sql, parameterObject, boundSql);
        return invocation.proceed();
    }
​
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
​
    @Override
    public void setProperties(Properties arg0) {
​
    }
​
    /**
     * 修改包含like的SQL语句
     *
     * @param sql             SQL语句
     * @param parameterObject 参数对象
     * @param boundSql        绑定SQL对象
     */
    private void transferLikeSql(String sql, Object parameterObject, BoundSql boundSql) {
        if (!isEscape(sql)) {
            return;
        }
        sql = sql.replaceAll(" {2}", " ");
        // 获取关键字的个数(去重)
        Set<String> fields = this.getKeyFields(sql, boundSql);
        if (fields == null) {
            return;
        }
        // 此处可以增强,不止是支持Map对象,Map对象仅用于传入的条件为Map或者使用@Param传入的对象被Mybatis转为的Map
        AbstractLikeSqlConverter converter;
        // 对关键字进行特殊字符“清洗”,如果有特殊字符的,在特殊字符前添加转义字符(\)
        if (parameterObject instanceof Map) {
            converter = converterMap.get(Map.class);
        } else {
            converter = converterMap.get(Object.class);
        }
        converter.convert(sql, fields, parameterObject);
    }
​
    /**
     * 是否需要转义
     *
     * @param sql SQL语句
     * @return true/false
     */
    private boolean isEscape(String sql) {
        return this.hasLike(sql) && this.hasPlaceholder(sql);
    }
​
    /**
     * 判断SQL语句中是否含有like关键字
     *
     * @param str SQL语句
     * @return true/false
     */
    private boolean hasLike(String str) {
        if (StringUtils.isBlank(str)) {
            return false;
        }
        return str.toLowerCase().contains(SQL_LIKE);
    }
​
    /**
     * 判断SQL语句中是否包含SQL占位符
     *
     * @param str SQL语句
     * @return true/false
     */
    private boolean hasPlaceholder(String str) {
        if (StringUtils.isBlank(str)) {
            return false;
        }
        return str.toLowerCase().contains(SQL_PLACEHOLDER);
    }
​
    /**
     * 获取需要替换的所有字段集合
     *
     * @param sql      完整SQL语句
     * @param boundSql 绑定的SQL对象
     * @return 字段集合列表
     */
    private Set<String> getKeyFields(String sql, BoundSql boundSql) {
        String[] params = sql.split(SQL_PLACEHOLDER_REGEX);
        Set<String> fields = new HashSet<>();
        for (int i = 0; i < params.length; i++) {
            if (this.hasLike(params[i])) {
                String field = boundSql.getParameterMappings().get(i).getProperty();
                fields.add(field);
            }
        }
        return fields;
    }
​
}

2.定义SQL语句转义模板,分别对Map和Object对象进行处理

2.1 定义AbstractLikeSqlConverter

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
​
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Set;
​
/**
 * 包含like的SQL语句转义模板
 *
 * @author lxw
 */
@Slf4j
public abstract class AbstractLikeSqlConverter<T> {
​
    /**
     * SQL语句like使用关键字%
     */
    private final static String LIKE_SQL_KEY = "%";
​
    /**
     * SQL语句需要转义的关键字
     */
    private final static String[] ESCAPE_CHAR = new String[]{LIKE_SQL_KEY, "_", "\\"};
​
    /**
     * mybatis-plus中like的SQL语句样式
     */
    private final static String MYBATIS_PLUS_LIKE_SQL = " like ?";
​
    /**
     * mybatis-plus中参数前缀
     */
    private final static String MYBATIS_PLUS_WRAPPER_PREFIX = "ew.paramNameValuePairs.";
​
    /**
     * mybatis-plus中参数键
     */
    final static String MYBATIS_PLUS_WRAPPER_KEY = "ew";
​
    /**
     * mybatis-plus中参数分隔符
     */
    final static String MYBATIS_PLUS_WRAPPER_SEPARATOR = ".";
​
    /**
     * mybatis-plus中参数分隔符替换器
     */
    final static String MYBATIS_PLUS_WRAPPER_SEPARATOR_REGEX = "\\.";
​
    /**
     * 已经替换过的标记
     */
    final static String REPLACED_LIKE_KEYWORD_MARK = "replaced.keyword";
​
    /**
     * 转义特殊字符
     *
     * @param sql       SQL语句
     * @param fields    字段列表
     * @param parameter 参数对象
     */
    public void convert(String sql, Set<String> fields, T parameter) {
        for (String field : fields) {
            if (this.hasMybatisPlusLikeSql(sql)) {
                if (this.hasWrapper(field)) {
                    // 第一种情况:在业务层进行条件构造产生的模糊查询关键字,使用QueryWrapper,LambdaQueryWrapper
                    this.transferWrapper(field, parameter);
                } else {
                    // 第二种情况:未使用条件构造器,但是在service层进行了查询关键字与模糊查询符`%`手动拼接
                    this.transferSelf(field, parameter);
                }
            } else {
                // 第三种情况:在Mapper类的注解SQL中进行了模糊查询的拼接
                this.transferSplice(field, parameter);
            }
        }
    }
​
    /**
     * 转义条件构造的特殊字符
     * 在业务层进行条件构造产生的模糊查询关键字,使用QueryWrapper,LambdaQueryWrapper
     *
     * @param field     字段名称
     * @param parameter 参数对象
     */
    public abstract void transferWrapper(String field, T parameter);
​
    /**
     * 转义自定义条件拼接的特殊字符
     * 未使用条件构造器,但是在service层进行了查询关键字与模糊查询符`%`手动拼接
     *
     * @param field     字段名称
     * @param parameter 参数对象
     */
    public abstract void transferSelf(String field, T parameter);
​
    /**
     * 转义自定义条件拼接的特殊字符
     * 在Mapper类的注解SQL中进行了模糊查询的拼接
     *
     * @param field     字段名称
     * @param parameter 参数对象
     */
    public abstract void transferSplice(String field, T parameter);
​
    /**
     * 转义通配符
     *
     * @param before 待转义字符串
     * @return 转义后字符串
     */
    String escapeChar(String before) {
        if (StringUtils.isNotBlank(before)) {
            before = before.replaceAll("\\\\", "\\\\\\\\");
            before = before.replaceAll("_", "\\\\_");
            before = before.replaceAll("%", "\\\\%");
        }
        return before;
    }
​
    /**
     * 是否包含需要转义的字符
     *
     * @param obj 待判断的对象
     * @return true/false
     */
    boolean hasEscapeChar(Object obj) {
        if (!(obj instanceof String)) {
            return false;
        }
        return this.hasEscapeChar((String) obj);
    }
​
    /**
     * 处理对象like问题
     *
     * @param field     对象字段
     * @param parameter 对象
     */
    void resolveObj(String field, Object parameter) {
        if (parameter == null || StringUtils.isBlank(field)) {
            return;
        }
        try {
            PropertyDescriptor descriptor = new PropertyDescriptor(field, parameter.getClass());
            Method readMethod = descriptor.getReadMethod();
            Object param = readMethod.invoke(parameter);
            if (this.hasEscapeChar(param)) {
                Method setMethod = descriptor.getWriteMethod();
                setMethod.invoke(parameter, this.escapeChar(param.toString()));
            } else if (this.cascade(field)) {
                int index = field.indexOf(MYBATIS_PLUS_WRAPPER_SEPARATOR) + 1;
                this.resolveObj(field.substring(index), param);
            }
        } catch (IntrospectionException | IllegalAccessException | InvocationTargetException e) {
            log.error("反射 {} 的 {} get/set方法出现异常", parameter, field, e);
        }
    }
​
    /**
     * 判断是否是级联属性
     *
     * @param field 字段名
     * @return true/false
     */
    boolean cascade(String field) {
        if (StringUtils.isBlank(field)) {
            return false;
        }
        return field.contains(MYBATIS_PLUS_WRAPPER_SEPARATOR) && !this.hasWrapper(field);
    }
​
    /**
     * 是否包含mybatis-plus的包含like的SQL语句格式
     *
     * @param sql 完整SQL语句
     * @return true/false
     */
    private boolean hasMybatisPlusLikeSql(String sql) {
        if (StringUtils.isBlank(sql)) {
            return false;
        }
        return sql.toLowerCase().contains(MYBATIS_PLUS_LIKE_SQL);
    }
​
    /**
     * 判断是否使用mybatis-plus条件构造器
     *
     * @param field 字段
     * @return true/false
     */
    private boolean hasWrapper(String field) {
        if (StringUtils.isBlank(field)) {
            return false;
        }
        return field.contains(MYBATIS_PLUS_WRAPPER_PREFIX);
    }
​
    /**
     * 判断字符串是否含有需要转义的字符
     *
     * @param str 待判断的字符串
     * @return true/false
     */
    private boolean hasEscapeChar(String str) {
        if (StringUtils.isBlank(str)) {
            return false;
        }
        for (String s : ESCAPE_CHAR) {
            if (str.contains(s)) {
                return true;
            }
        }
        return false;
    }
​
}

2.2 定义MapLikeSqlConverter处理Map类型参数的情况

import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
​
import java.util.Map;
import java.util.Objects;
​
/**
 * 参数对象为Map的转换器
 *
 * @author lxw
 * @create_date 2021/11/11 14:28
 */
public class MapLikeSqlConverter extends AbstractLikeSqlConverter<Map> {
​
    @Override
    public void transferWrapper(String field, Map parameter) {
        AbstractWrapper wrapper = (AbstractWrapper) parameter.get(MYBATIS_PLUS_WRAPPER_KEY);
        parameter = wrapper.getParamNameValuePairs();
        String[] keys = field.split(MYBATIS_PLUS_WRAPPER_SEPARATOR_REGEX);
        // ew.paramNameValuePairs.param1,截取字符串之后,获取第三个,即为参数名
        String paramName = keys[2];
        String mapKey = String.format("%s.%s", REPLACED_LIKE_KEYWORD_MARK, paramName);
        if (parameter.containsKey(mapKey) && Objects.equals(parameter.get(mapKey), true)) {
            return;
        }
        if (this.cascade(field)) {
            this.resolveCascadeObj(field, parameter);
        } else {
            Object param = parameter.get(paramName);
            if (this.hasEscapeChar(param)) {
                String paramStr = param.toString();
                parameter.put(keys[2], String.format("%%%s%%", this.escapeChar(paramStr.substring(1, paramStr.length() - 1))));
            }
        }
        parameter.put(mapKey, true);
    }
​
    @Override
    public void transferSelf(String field, Map parameter) {
        if (this.cascade(field)) {
            this.resolveCascadeObj(field, parameter);
            return;
        }
        Object param = parameter.get(field);
        if (this.hasEscapeChar(param)) {
            String paramStr = param.toString();
            parameter.put(field, String.format("%%%s%%", this.escapeChar(paramStr.substring(1, paramStr.length() - 1))));
        }
    }
​
    @Override
    public void transferSplice(String field, Map parameter) {
        if (this.cascade(field)) {
            this.resolveCascadeObj(field, parameter);
            return;
        }
        Object param = parameter.get(field);
        if (this.hasEscapeChar(param)) {
            parameter.put(field, this.escapeChar(param.toString()));
        }
    }
​
    /**
     * 处理级联属性
     *
     * @param field     级联字段名
     * @param parameter 参数Map对象
     */
    private void resolveCascadeObj(String field, Map parameter) {
        int index = field.indexOf(MYBATIS_PLUS_WRAPPER_SEPARATOR);
        Object param = parameter.get(field.substring(0, index));
        if (param == null) {
            return;
        }
        this.resolveObj(field.substring(index + 1), param);
    }
​
}

2.3 定义ObjectLikeSqlConverter处理Object类型参数的情况

import lombok.extern.slf4j.Slf4j;
​
/**
 * 通用参数的转换器
 *
 * @author lxw
 */
@Slf4j
public class ObjectLikeSqlConverter extends AbstractLikeSqlConverter<Object> {
​
    @Override
    public void transferWrapper(String field, Object parameter) {
        // 尚未发现这种情况
    }
​
    @Override
    public void transferSelf(String field, Object parameter) {
        // 尚未发现这种情况
    }
​
    @Override
    public void transferSplice(String field, Object parameter) {
        this.resolveObj(field, parameter);
    }
​
}

3. 注册到Spring中

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
/**
 * mybatis/mybatis-plus模糊查询语句特殊字符转义配置
 *
 * @author lxw
 */
@Configuration
public class MybatisLikeSqlConfig {
​
    @Bean
    public MybatisLikeSqlInterceptor mybatisSqlInterceptor() {
        return new MybatisLikeSqlInterceptor();
    }
​
}

这样就可以实现全局模糊查询特殊字符转义的拦截器配置了。

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Mybatis-Plus中的拦截器用于在执行SQL语句前后进行一些额外的操作,比如分页、加密等。在Mybatis-Plus中,可以通过MybatisPlusInterceptor来管理和配置拦截器链。通过addInnerInterceptor方法,可以向MybatisPlusInterceptor中添加自定义的拦截器。\[1\]\[2\]\[3\]这些拦截器可以在配置文件中进行添加,并且可以按照添加的顺序执行。在配置文件中,可以使用@Configuration注解来标识配置类,使用@Bean注解来创建MybatisPlusInterceptor实例,并通过addInnerInterceptor方法添加自定义拦截器。同时,可以使用MybatisPlusInterceptor的addInnerInterceptor方法添加内置的拦截器,比如分页拦截器。\[2\]\[3\]这样,就可以实现对Mybatis-Plus拦截器进行配置和使用。 #### 引用[.reference_title] - *1* [关于MyBatisPlus中分页查询为什么要使用拦截器的解释](https://blog.csdn.net/m0_73700925/article/details/131022879)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [MyBatis-plus拦截器](https://blog.csdn.net/winerpro/article/details/126053599)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Mybatis Plus拦截器](https://blog.csdn.net/weixin_42502300/article/details/125607529)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值