mybatis/mybatis-plus模糊查詢語句特殊字符轉(zhuǎn)義攔截器

在開發(fā)中,我們通常會(huì)遇到這樣的情況。用戶在錄入信息是錄入了‘%’,而在查詢時(shí)無法精確匹配‘%’。究其原因,‘%’是MySQL的關(guān)鍵字,如果我們想要精確匹配‘%’,那么需要對(duì)其進(jìn)行轉(zhuǎn)義。

1.使用mybatis提供的攔截器攔截所有的查詢請(qǐng)求。

具體實(shí)現(xiàn)在代碼中均有注釋

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模糊查詢語句特殊字符轉(zhuǎn)義攔截器
 *
 * @author lieber
 */
@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 = "\\?";

    /**
     * 所有的轉(zhuǎn)義器
     */
    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 參數(shù)對(duì)象
     * @param boundSql        綁定SQL對(duì)象
     */
    private void transferLikeSql(String sql, Object parameterObject, BoundSql boundSql) {
        if (!isEscape(sql)) {
            return;
        }
        sql = sql.replaceAll(" {2}", " ");
        // 獲取關(guān)鍵字的個(gè)數(shù)(去重)
        Set<String> fields = this.getKeyFields(sql, boundSql);
        if (fields == null) {
            return;
        }
        // 此處可以增強(qiáng),不止是支持Map對(duì)象,Map對(duì)象僅用于傳入的條件為Map或者使用@Param傳入的對(duì)象被Mybatis轉(zhuǎn)為的Map
        AbstractLikeSqlConverter converter;
        // 對(duì)關(guān)鍵字進(jìn)行特殊字符“清洗”,如果有特殊字符的,在特殊字符前添加轉(zhuǎn)義字符(\)
        if (parameterObject instanceof Map) {
            converter = converterMap.get(Map.class);
        } else {
            converter = converterMap.get(Object.class);
        }
        converter.convert(sql, fields, parameterObject);
    }

    /**
     * 是否需要轉(zhuǎn)義
     *
     * @param sql SQL語句
     * @return true/false
     */
    private boolean isEscape(String sql) {
        return this.hasLike(sql) && this.hasPlaceholder(sql);
    }

    /**
     * 判斷SQL語句中是否含有l(wèi)ike關(guān)鍵字
     *
     * @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對(duì)象
     * @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語句轉(zhuǎn)義模板,分別對(duì)Map和Object對(duì)象進(jìn)行處理

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語句轉(zhuǎn)義模板
 *
 * @author lieber
 */
@Slf4j
public abstract class AbstractLikeSqlConverter<T> {

    /**
     * SQL語句like使用關(guān)鍵字%
     */
    private final static String LIKE_SQL_KEY = "%";

    /**
     * SQL語句需要轉(zhuǎn)義的關(guān)鍵字
     */
    private final static String[] ESCAPE_CHAR = new String[]{LIKE_SQL_KEY, "_", "\\"};

    /**
     * mybatis-plus中l(wèi)ike的SQL語句樣式
     */
    private final static String MYBATIS_PLUS_LIKE_SQL = " like ?";

    /**
     * mybatis-plus中參數(shù)前綴
     */
    private final static String MYBATIS_PLUS_WRAPPER_PREFIX = "ew.paramNameValuePairs.";

    /**
     * mybatis-plus中參數(shù)鍵
     */
    final static String MYBATIS_PLUS_WRAPPER_KEY = "ew";

    /**
     * mybatis-plus中參數(shù)分隔符
     */
    final static String MYBATIS_PLUS_WRAPPER_SEPARATOR = ".";

    /**
     * mybatis-plus中參數(shù)分隔符替換器
     */
    final static String MYBATIS_PLUS_WRAPPER_SEPARATOR_REGEX = "\\.";

    /**
     * 已經(jīng)替換過的標(biāo)記
     */
    final static String REPLACED_LIKE_KEYWORD_MARK = "replaced.keyword";

    /**
     * 轉(zhuǎn)義特殊字符
     *
     * @param sql       SQL語句
     * @param fields    字段列表
     * @param parameter 參數(shù)對(duì)象
     */
    public void convert(String sql, Set<String> fields, T parameter) {
        for (String field : fields) {
            if (this.hasMybatisPlusLikeSql(sql)) {
                if (this.hasWrapper(field)) {
                    // 第一種情況:在業(yè)務(wù)層進(jìn)行條件構(gòu)造產(chǎn)生的模糊查詢關(guān)鍵字,使用QueryWrapper,LambdaQueryWrapper
                    this.transferWrapper(field, parameter);
                } else {
                    // 第二種情況:未使用條件構(gòu)造器,但是在service層進(jìn)行了查詢關(guān)鍵字與模糊查詢符`%`手動(dòng)拼接
                    this.transferSelf(field, parameter);
                }
            } else {
                // 第三種情況:在Mapper類的注解SQL中進(jìn)行了模糊查詢的拼接
                this.transferSplice(field, parameter);
            }
        }
    }

    /**
     * 轉(zhuǎn)義條件構(gòu)造的特殊字符
     * 在業(yè)務(wù)層進(jìn)行條件構(gòu)造產(chǎn)生的模糊查詢關(guān)鍵字,使用QueryWrapper,LambdaQueryWrapper
     *
     * @param field     字段名稱
     * @param parameter 參數(shù)對(duì)象
     */
    public abstract void transferWrapper(String field, T parameter);

    /**
     * 轉(zhuǎn)義自定義條件拼接的特殊字符
     * 未使用條件構(gòu)造器,但是在service層進(jìn)行了查詢關(guān)鍵字與模糊查詢符`%`手動(dòng)拼接
     *
     * @param field     字段名稱
     * @param parameter 參數(shù)對(duì)象
     */
    public abstract void transferSelf(String field, T parameter);

    /**
     * 轉(zhuǎn)義自定義條件拼接的特殊字符
     * 在Mapper類的注解SQL中進(jìn)行了模糊查詢的拼接
     *
     * @param field     字段名稱
     * @param parameter 參數(shù)對(duì)象
     */
    public abstract void transferSplice(String field, T parameter);

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

    /**
     * 是否包含需要轉(zhuǎn)義的字符
     *
     * @param obj 待判斷的對(duì)象
     * @return true/false
     */
    boolean hasEscapeChar(Object obj) {
        if (!(obj instanceof String)) {
            return false;
        }
        return this.hasEscapeChar((String) obj);
    }

    /**
     * 處理對(duì)象like問題
     *
     * @param field     對(duì)象字段
     * @param parameter 對(duì)象
     */
    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方法出現(xiàn)異常", parameter, field, e);
        }
    }

    /**
     * 判斷是否是級(jí)聯(lián)屬性
     *
     * @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條件構(gòu)造器
     *
     * @param field 字段
     * @return true/false
     */
    private boolean hasWrapper(String field) {
        if (StringUtils.isBlank(field)) {
            return false;
        }
        return field.contains(MYBATIS_PLUS_WRAPPER_PREFIX);
    }

    /**
     * 判斷字符串是否含有需要轉(zhuǎn)義的字符
     *
     * @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類型參數(shù)的情況

import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;

import java.util.Map;
import java.util.Objects;

/**
 * 參數(shù)對(duì)象為Map的轉(zhuǎn)換器
 *
 * @author lieber
 * @create_date 2020/1/21 12: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,截取字符串之后,獲取第三個(gè),即為參數(shù)名
        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()));
        }
    }

    /**
     * 處理級(jí)聯(lián)屬性
     *
     * @param field     級(jí)聯(lián)字段名
     * @param parameter 參數(shù)Map對(duì)象
     */
    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類型參數(shù)的情況

import lombok.extern.slf4j.Slf4j;

/**
 * 通用參數(shù)的轉(zhuǎn)換器
 *
 * @author lieber
 */
@Slf4j
public class ObjectLikeSqlConverter extends AbstractLikeSqlConverter<Object> {

    @Override
    public void transferWrapper(String field, Object parameter) {
        // 尚未發(fā)現(xiàn)這種情況
    }

    @Override
    public void transferSelf(String field, Object parameter) {
        // 尚未發(fā)現(xiàn)這種情況
    }

    @Override
    public void transferSplice(String field, Object parameter) {
        this.resolveObj(field, parameter);
    }

}

3. 注冊(cè)到Spring中

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * mybatis/mybatis-plus模糊查詢語句特殊字符轉(zhuǎn)義配置
 *
 * @author lieber
 */
@Configuration
public class MybatisLikeSqlConfig {

    @Bean
    public MybatisLikeSqlInterceptor mybatisSqlInterceptor() {
        return new MybatisLikeSqlInterceptor();
    }

}

到此,即可進(jìn)行關(guān)鍵字的精確匹配。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,196評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,456評(píng)論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,955評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,195評(píng)論 0 287
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,725評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,608評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,802評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,048評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評(píng)論 1 281
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,424評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,762評(píng)論 2 372