mybatis/mybatis-plus模糊查詢語句特殊字符轉義攔截器的實現(xiàn)
mybatis/mybatis-plus模糊查詢語句特殊字符轉義攔截器
在開發(fā)中,我們通常會遇到這樣的情況。用戶在錄入信息是錄入了‘%',而在查詢時無法精確匹配‘%'。究其原因,‘%'是MySQL的關鍵字,如果我們想要精確匹配‘%',那么需要對其進行轉義。
1.使用mybatis提供的攔截器攔截所有的查詢請求。
具體實現(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模糊查詢語句特殊字符轉義攔截器
*
* @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 = "\\?";
/**
* 所有的轉義器
*/
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ù)對象
* @param boundSql 綁定SQL對象
*/
private void transferLikeSql(String sql, Object parameterObject, BoundSql boundSql) {
if (!isEscape(sql)) {
return;
}
sql = sql.replaceAll(" {2}", " ");
// 獲取關鍵字的個數(shù)(去重)
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語句中是否含有l(wèi)ike關鍵字
*
* @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 lieber
*/
@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中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 = "\\.";
/**
* 已經替換過的標記
*/
final static String REPLACED_LIKE_KEYWORD_MARK = "replaced.keyword";
/**
* 轉義特殊字符
*
* @param sql SQL語句
* @param fields 字段列表
* @param parameter 參數(shù)對象
*/
public void convert(String sql, Set<String> fields, T parameter) {
for (String field : fields) {
if (this.hasMybatisPlusLikeSql(sql)) {
if (this.hasWrapper(field)) {
// 第一種情況:在業(yè)務層進行條件構造產生的模糊查詢關鍵字,使用QueryWrapper,LambdaQueryWrapper
this.transferWrapper(field, parameter);
} else {
// 第二種情況:未使用條件構造器,但是在service層進行了查詢關鍵字與模糊查詢符`%`手動拼接
this.transferSelf(field, parameter);
}
} else {
// 第三種情況:在Mapper類的注解SQL中進行了模糊查詢的拼接
this.transferSplice(field, parameter);
}
}
}
/**
* 轉義條件構造的特殊字符
* 在業(yè)務層進行條件構造產生的模糊查詢關鍵字,使用QueryWrapper,LambdaQueryWrapper
*
* @param field 字段名稱
* @param parameter 參數(shù)對象
*/
public abstract void transferWrapper(String field, T parameter);
/**
* 轉義自定義條件拼接的特殊字符
* 未使用條件構造器,但是在service層進行了查詢關鍵字與模糊查詢符`%`手動拼接
*
* @param field 字段名稱
* @param parameter 參數(shù)對象
*/
public abstract void transferSelf(String field, T parameter);
/**
* 轉義自定義條件拼接的特殊字符
* 在Mapper類的注解SQL中進行了模糊查詢的拼接
*
* @param field 字段名稱
* @param parameter 參數(shù)對象
*/
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方法出現(xiàn)異常", parameter, field, e);
}
}
/**
* 判斷是否是級聯(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條件構造器
*
* @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類型參數(shù)的情況
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import java.util.Map;
import java.util.Objects;
/**
* 參數(shù)對象為Map的轉換器
*
* @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,截取字符串之后,獲取第三個,即為參數(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()));
}
}
/**
* 處理級聯(lián)屬性
*
* @param field 級聯(lián)字段名
* @param parameter 參數(shù)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類型參數(shù)的情況
import lombok.extern.slf4j.Slf4j;
/**
* 通用參數(shù)的轉換器
*
* @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. 注冊到Spring中
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* mybatis/mybatis-plus模糊查詢語句特殊字符轉義配置
*
* @author lieber
*/
@Configuration
public class MybatisLikeSqlConfig {
@Bean
public MybatisLikeSqlInterceptor mybatisSqlInterceptor() {
return new MybatisLikeSqlInterceptor();
}
}
到此,即可進行關鍵字的精確匹配。
到此這篇關于mybatis/mybatis-plus模糊查詢語句特殊字符轉義攔截器的實現(xiàn)的文章就介紹到這了,更多相關mybatis-plus模糊查詢特殊字符轉義攔截器內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
spring的jdbctemplate的crud的基類dao
本文主要介紹了使用spring的jdbctemplate進行增刪改查的基類Dao的簡單寫法,需要的朋友可以參考下2014-02-02
Java 日期格式y(tǒng)yyy-MM-dd與YYYY-MM-dd區(qū)別
我們在java中常用的規(guī)范格式為:2023-11-11
yyyy-MM-dd HH:mm:ss:SSS 24小時制或yyyy-MM-dd hh:mm:ss:SSS 12小時制,本文就來介紹一下兩者的區(qū)別,感興趣的可以了解一下
淺談SpringBoot集成Quartz動態(tài)定時任務
這篇文章主要介紹了SpringBoot集成Quartz動態(tài)定時任務,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-04-04
SpringData JPA基本/高級/多數(shù)據(jù)源的使用詳解
這篇文章主要介紹了SpringData JPA基本/高級/多數(shù)據(jù)源的使用詳解,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02
解決springboot服務啟動報錯:Unable?to?start?embedded?contain
這篇文章主要介紹了解決springboot服務啟動報錯:Unable?to?start?embedded?contain的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08
使用TraceId在Spring Cloud中實現(xiàn)線上問題快速定位
在微服務架構中,服務間的互相調用使得問題定位變得復雜,在此背景下,TraceId為我們提供了一個在復雜環(huán)境中追蹤請求路徑和定位問題的工具,本文不僅介紹TraceId的基本概念,還將結合真實場景,為您展示如何在Spring Cloud中應用它2023-09-09

