Mybatis中LIKE特殊字符处理的解决方案

在MYSQL中,使用LIKE关键字进行模糊匹配时候,特殊字符是需要转义的,如下: \ 转义成 \\ , _ 转义成 \_ , % 转义成 \%
在mybatis和mybatis-plus中,没有提供相关的方法来自动转义。其实这是符合逻辑的,转义这种事情该交给开发者自己去处理,官方框架不去入侵这种逻辑代码也是为了更好的灵活性。
在业务开发的环境中,往往会有测试提出这样的bug:“输入特殊字符进行筛选,没有过滤掉对应的数据”。像这种问题其实本质上是不应该存在的,在根源上就应该切除掉,就是说禁止输入特殊字符进行筛选。但往往测试和产品他们根本不懂技术,你也别废口舌去和他们争论了。那么我们就来看看如何处理关于LIKE特殊字符转义问题。

方案一:实现mybatis的插件接口

我们知道mybatis提供了插件接口方便开发者进行二次开发。mybatis-plus对mybatis进行了增强,在mybatis-plus中需要实现InnerInterceptor接口进行拦截。如果只是引入的mybatis,则实现Interceptor接口进行拦截。注意一下:
该方案对 like xxx%like %xxx 的查询方式会有一定的局限性。如:
用户输入 = %a 时,业务按照 like xxx% 的语义查询,是查询xxx开头的数据。正常该转义成 like \%a%
用户输入 = a% 时,业务按照 like %xxx 的语义查询,是查询xxx结尾的数据。正常该转义成 like %a\%
但程序在转换时,依然会按照 like %xxx% 的语义去转义。这是不可避免的,因为这确实是歧义的地方。 程序解析 like %a% 时,无法完全地确定哪一部分是用户输入的,所以只有按照标准的 %xxx% 来进行解析转义。
不过在一般的业务查询中,左LIKE 和 右LIKE 很少用到,可以不用考虑此情况。
下面是实现了mybatis-plus的拦截接口,代码如下:

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.sql.SQLException;
import java.util.HashSet;
import java.util.Set;

/**
 * @author shaohuailin
 * @date 2023/10/8
 */
@Slf4j
public class LikeEscapeInnerInterceptor implements InnerInterceptor {

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        escapeLike(boundSql);
    }

    /**
     * like条件中若有关键字,则进行转义
     *
     * @param boundSql
     */
    public void escapeLike(BoundSql boundSql) {
        String sql = boundSql.getSql().toLowerCase();
        if (!sql.contains(" like ") || !sql.contains("?")) {
            return;
        }
        MetaObject parameterObjectMetaObject = SystemMetaObject.forObject(boundSql.getParameterObject());
        String[] strList = sql.split("\\?");
        Set<String> keyNames = new HashSet<>();
        for (int i = 0; i < strList.length; i++) {
            if (strList[i].contains(" like ")) {
                String keyName = boundSql.getParameterMappings().get(i).getProperty();
                keyNames.add(keyName);
            }
        }
        for (String keyName : keyNames) {
            if (parameterObjectMetaObject.hasGetter(keyName)) {
                Object value = parameterObjectMetaObject.getValue(keyName);
                if (value instanceof  String) {
                    String v = (String) value;
                    parameterObjectMetaObject.setValue(keyName, convert(v));
                }
            } else if (boundSql.getParameterObject() instanceof String) {
                String v = (String) boundSql.getParameterObject();
                boundSql.setAdditionalParameter(keyName, convert(v));
            }
        }
    }

    private String convert(String before) {
        char[] chars = before.toCharArray();
        int len = chars.length;
        int offset = 0;
        char[] result = new char[len*2];
        for (int i = 0; i < len; i++) {
            char c = chars[i];
            // 不是首尾的字符,或者首尾不是“%”
            if ((i > 0 && i < len -1) || c != '%') {
                // 特殊字符增加转义
                if (c == '\\' || c == '_' || c == '%') {
                    result[offset++] = '\\';
                }
            }
            result[offset++] = c;
        }
        return new String(result, 0, offset);
    }
}

然后配置MybatisPlusInterceptor即可,需要注意的是,PaginationInnerInterceptor 插件需要在最末尾。

/**
 * @author shaohuailin
 * @date 2023/10/12
 */
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new LikeEscapeInnerInterceptor());
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}

方案二:使用切面结合注解的方式

相对于方案一,该方案的灵活性要高些,可以针对部分接口加特殊字符处理。同时此方案,不会存在方案一中的歧义问题。
首先定义特殊字符处理注解如下:

import java.lang.annotation.*;

/**
 * 转义like特殊字符
 *
 * @author shaohuailin
 * @date 2023/10/12
 */
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EscapeLike {
}

配置一个特殊字符处理的切面,如下:

/**
 * 转义like查询特殊字符的切面
 * <p> 在需要转义的属性字段上加{@link EscapeLike} 注解
 *
 * @author shaohuailin
 * @date 2023/10/12
 */
@Slf4j
@Aspect
@Component
public class EscapeLikeAspect {

    @Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
    public void pointcut() {}

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Annotation[][] parameterAnnotations = signature.getMethod().getParameterAnnotations();

        for (int i = 0; i < args.length; i++) {
            Object arg = args[i];
            if (arg instanceof String) {
                // 处理字符串参数
                boolean match = Arrays.stream(parameterAnnotations[i]).anyMatch(n -> n instanceof EscapeLike);
                if (match) {
                    // 开始处理
                    args[i] = convert(((String) arg));
                }

            } else {
                // 处理对象参数
                Field[] fields = ClassUtil.getFields(arg);
                for (Field field : fields) {
                    EscapeLike annotation = field.getAnnotation(EscapeLike.class);
                    if (annotation == null) {
                        continue;
                    }
                    String fieldName = field.getName();
                    Object value = ReflectUtil.getValue(arg, fieldName);
                    if (value instanceof String) {
                        ReflectUtil.setValue(arg, field.getName(), convert(((String) value)));
                    }
                }
            }
        }
        return joinPoint.proceed(args);
    }

    /**
     * 特殊字符转换
     *
     * @param before
     * @return
     */
    private String convert(String before) {
        char[] chars = before.toCharArray();
        int len = chars.length;
        int offset = 0;
        char[] result = new char[len*2];
        for (int i = 0; i < len; i++) {
            char c = chars[i];
            // 特殊字符增加转义
            if (c == '\\' || c == '_' || c == '%') {
                result[offset++] = '\\';
            }
            result[offset++] = c;
        }
        return new String(result, 0, offset);
    }
}

代码中所用到的工具类,替换成自己常用的就可以了。注意一下,该方案的 convert 和方案一的 convert 是有区别的哦!这样,在需要增加转义的字段属性加上注解 @EscapeLike 就可以了。

总结

方案一和方案二都解决了like特殊字符的处理,各有利弊吧,得结合实际情况来决定。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值