在开发中,我们通常会遇到这样的情况。用户在录入信息是录入了‘%’,而在查询时无法精确匹配‘%’。究其原因,‘%’是MySQL的关键字,如果我们想要精确匹配‘%’,那么需要对其进行转义,本文就详细的介绍一下

目录

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

在开发中,我们通常会遇到这样的情况。用户在录入信息是录入了‘%',而在查询时无法精确匹配‘%'。究其原因,‘%'是MySQL的关键字,如果我们想要精确匹配‘%',那么需要对其进行转义。

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

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 参数对象

     * @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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

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中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类型参数的情况

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

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

import java.util.Map;

import java.util.Objects;

/**

 * 参数对象为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,截取字符串之后,获取第三个,即为参数名

        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类型参数的情况

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

import lombok.extern.slf4j.Slf4j;

/**

 * 通用参数的转换器

 *

 * @author lieber

 */

@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中

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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();

    }

}

到此,即可进行关键字的精确匹配。

分享不易,如果觉得分享对大家有帮助,老规矩,点赞、关注、留言支持哦!