- 1. 问题描述
- 2. 解决方案
- 3. 设计实现
- 3.1 环境准备
- 3.2 代码实现
- 3.3 拦截器实现
- 4. 测试验证
MyBatis作为目前最常用的ORM数据库访问持久层框架,其本身支持动态SQL存储映射等高级特性也非常优秀,通过Mapper文件采用动态代理模式使SQL与业务代码相解耦,日常开发中使用也非常广泛。
正常模糊匹配查询时是没有什么问题的,但是如果需要模糊查询字段含有特殊字符比如% _ / 等时就会出现查询不准确的问题。本文就是通过mybatis拦截器实现特殊字符转义实现mybatis特殊字符查询问题。
2. 解决方案MybatisLikeSqlInterceptor:
通过 @Intercepts 注解指定拦截器插件的属性:分别指定了拦截器类型 Executor, 拦截方法名 query (共有2个query方法)。
拦截方法参数(方法1)MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class
拦截方法参数(方法2)MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,CacheKey.class, BoundSql.class
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
MyBatis 允许使用插件来拦截的方法调用包括:
Executor 、ParameterHandler、ResultSetHandler 、StatementHandler ,方法时序如下:
-- 创建用户表
CREATE TABLE `user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID主键',
`name` varchar(64) DEFAULT NULL COMMENT '姓名',
`sex` varchar(8) DEFAULT NULL COMMENT '性别',
`age` int(4) DEFAULT NULL COMMENT '年龄',
`born` date DEFAULT NULL COMMENT '出生日期',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='用户表';
-- 查询用户表
select * from user;
-- 新增数据
INSERT INTO `category`.`user`(`id`, `name`, `sex`, `age`, `born`) VALUES (1, '%张三%', '男', 18, '2022-04-22');
INSERT INTO `category`.`user`(`id`, `name`, `sex`, `age`, `born`) VALUES (2, '李四', '女', 27, '2022-04-01');
-- 执行sql
select id, name, sex, age, born from user WHERe name like concat('%','%','%');
select id, name, sex, age, born from user WHERe name like concat('%','','%');
select id, name, sex, age, born from user WHERe name like concat('%','/','%');
select id, name, sex, age, born from user WHERe name like concat('%','张','%');
3.2 代码实现
UserController
package com.jerry.market.controller;
import com.jerry.market.entity.User;
import com.jerry.market.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import com.jerry.market.entity.Response;
import java.util.List;
import javax.annotation.Resource;
@RestController
@RequestMapping("/user")
@Api(tags = "UserController", description = "用户控制器")
public class UserController {
@Resource
private UserService userService;
@ApiOperation("通过主键查询单条数据")
@RequestMapping(value = "get", method = RequestMethod.GET)
public Response selectOne(User user) {
User result = userService.selectById(user.getId());
if (result != null) {
return Response.success("查询成功", result);
}
return Response.fail("查询失败");
}
@ApiOperation("新增一条数据")
@RequestMapping(value = "insert", method = RequestMethod.POST)
public Response insert(@RequestBody User user) {
int result = userService.insert(user);
if (result > 0) {
return Response.success("新增成功", user);
}
return Response.fail("新增失败");
}
@ApiOperation("批量新增")
@RequestMapping(value = "batchInsert", method = RequestMethod.POST)
public Response batchInsert(@RequestBody List users) {
int result = userService.batchInsert(users);
if (result > 0) {
return Response.success("新增成功", result);
}
return Response.fail("新增失败");
}
@ApiOperation("修改一条数据")
@RequestMapping(value = "update", method = RequestMethod.PUT)
public Response update(@RequestBody User user) {
User result = userService.update(user);
if (result != null) {
return Response.success("修改成功", result);
}
return Response.fail("修改失败");
}
@ApiOperation("删除一条数据")
@RequestMapping(value = "delete", method = RequestMethod.DELETE)
public Response delete(User user) {
int result = userService.deleteById(user.getId());
if (result > 0) {
return Response.success("删除成功", null);
}
return Response.fail("删除失败");
}
@ApiOperation("查询全部")
@RequestMapping(value = "selectAll", method = RequestMethod.GET)
public Response> selectAll() {
List users = userService.selectAll();
if (users != null) {
return Response.success("查询成功", users);
}
return Response.fail("查询失败");
}
@ApiOperation("通过实体作为筛选条件查询")
@RequestMapping(value = "selectList", method = RequestMethod.GET)
public Response> selectList(User user) {
List users = userService.selectList(user);
if (users != null) {
return Response.success("查询成功", users);
}
return Response.fail("查询失败");
}
@ApiOperation("分页查询")
@RequestMapping(value = "selectPage", method = RequestMethod.GET)
public Response> selectPage(Integer start, Integer limit) {
List users = userService.selectPage(start, limit);
if (users != null) {
return Response.success("查询成功", users);
}
return Response.fail("查询失败");
}
}
UserService
package com.jerry.market.service;
import com.jerry.market.entity.User;
import java.util.List;
import java.util.Map;
public interface UserService {
User selectById(Object id);
List selectPage(int start, int limit);
List selectAll();
List selectList(User user);
int insert(User user);
int batchInsert(List users);
User update(User user);
int deleteById(Object id);
int count();
}
UserServiceImpl
package com.jerry.market.service.impl;
import com.jerry.market.entity.User;
import com.jerry.market.mapper.UserMapper;
import com.jerry.market.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
@Service("userService")
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
public User selectById(Object id) {
return this.userMapper.selectById(id);
}
@Override
public List selectPage(int start, int limit) {
return this.userMapper.selectPage(start, limit);
}
@Override
public List selectAll() {
return this.userMapper.selectAll();
}
@Override
public List selectList(User user) {
return this.userMapper.selectList(user);
}
@Override
public int insert(User user) {
return this.userMapper.insert(user);
}
@Override
public int batchInsert(List users) {
return this.userMapper.batchInsert(users);
}
@Override
public User update(User user) {
this.userMapper.update(user);
return this.selectById(user.getId());
}
@Override
public int deleteById(Object id) {
return this.userMapper.deleteById(id);
}
@Override
public int count() {
return this.userMapper.count();
}
}
UserMapper
package com.jerry.market.mapper;
import com.jerry.market.entity.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
public interface UserMapper {
User selectById(Object id);
List selectPage(@Param("start") int start, @Param("limit") int limit);
List selectAll();
List selectList(User user);
int insert(User user);
int batchInsert(List users);
int update(User user);
int deleteById(Object id);
int count();
}
UserMapper.xml
3.3 拦截器实现id, name, sex, age, born
1 MybatisLikeSqlInterceptor.java mybatis拦截器
package com.jerry.market.config;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
@Slf4j
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),})
public class MybatisLikeSqlInterceptor implements Interceptor {
private final static String SQL_LIKE = "like ";
private final static String SQL_PLACEHOLDER = "?";
private final static String SQL_PLACEHOLDER_REGEX = "\?";
private static Map 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) {
System.out.println("aaaaaa");
}
private void transferLikeSql(String sql, Object parameterObject, BoundSql boundSql) {
if (!isEscape(sql)) {
return;
}
sql = sql.replaceAll(" {2}", "");
//Get the number of keywords (de-duplication)
Set fields = this.getKeyFields(sql, boundSql);
if (fields == null) {
return;
}
//This can be enhanced, not only to support the Map object, the Map object is only used for the incoming condition is Map or the object passed in using @Param is converted to Map by Mybatis
AbstractLikeSqlConverter converter;
//"Clean" keywords with special characters. If there are special characters, add an escape character () before the special characters
if (parameterObject instanceof Map) {
converter = converterMap.get(Map.class);
} else {
converter = converterMap.get(Object.class);
}
converter.convert(sql, fields, parameterObject);
}
private boolean isEscape(String sql) {
return this.hasLike(sql) && this.hasPlaceholder(sql);
}
private boolean hasLike(String str) {
if (StringUtils.isBlank(str)) {
return false;
}
return str.toLowerCase().contains(SQL_LIKE);
}
private boolean hasPlaceholder(String str) {
if (StringUtils.isBlank(str)) {
return false;
}
return str.toLowerCase().contains(SQL_PLACEHOLDER);
}
private Set getKeyFields(String sql, BoundSql boundSql) {
String[] params = sql.split(SQL_PLACEHOLDER_REGEX);
Set 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 AbstractLikeSqlConverter.java 转换器抽象类
package com.jerry.market.config; import java.beans.IntrospectionException; import java.beans.PropertyDescriptor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Set; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @Slf4j public abstract class AbstractLikeSqlConverter{ private final static String LIKE_SQL_KEY = "%"; private final static String[] ESCAPE_CHAR = new String[]{LIKE_SQL_KEY, "_", "\"}; private final static String MYBATIS_PLUS_LIKE_SQL = "like ?"; private final static String MYBATIS_PLUS_WRAPPER_PREFIX = "ew.paramNameValuePairs."; final static String MYBATIS_PLUS_WRAPPER_KEY = "ew"; final static String MYBATIS_PLUS_WRAPPER_SEPARATOR = "."; final static String MYBATIS_PLUS_WRAPPER_SEPARATOR_REGEX = "\."; final static String REPLACED_LIKE_KEYWORD_MARK = "replaced.keyword"; public void convert(String sql, Set fields, T parameter) { for (String field : fields) { if (this.hasMybatisPlusLikeSql(sql)) { if (this.hasWrapper(field)) { //The first case: use QueryWrapper, LambdaQueryWrapper for fuzzy query keywords generated by conditional construction in the business layer this.transferWrapper(field, parameter); } else { //The second case: The condition constructor is not used, but the query keyword and fuzzy query symbol `%` are manually spliced in the service layer this.transferSelf(field, parameter); } } else { //The third case: Fuzzy queries are spliced in the annotation SQL of the Mapper class this.transferSplice(field, parameter); } } } public abstract void transferWrapper(String field, T parameter); public abstract void transferSelf(String field, T parameter); public abstract void transferSplice(String field, T parameter); String escapeChar(String before) { if (StringUtils.isNotBlank(before)) { before = before.replaceAll("\\", "\\\\"); before = before.replaceAll("_", "\\_"); before = before.replaceAll("%", "\\%"); } return before; } boolean hasEscapeChar(Object obj) { if (!(obj instanceof String)) { return false; } return this.hasEscapeChar((String) obj); } 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("Reflected {} {} get/set method is abnormal", parameter, field, e); } } boolean cascade(String field) { if (StringUtils.isBlank(field)) { return false; } return field.contains(MYBATIS_PLUS_WRAPPER_SEPARATOR) && !this.hasWrapper(field); } private boolean hasMybatisPlusLikeSql(String sql) { if (StringUtils.isBlank(sql)) { return false; } return sql.toLowerCase().contains(MYBATIS_PLUS_LIKE_SQL); } private boolean hasWrapper(String field) { if (StringUtils.isBlank(field)) { return false; } return field.contains(MYBATIS_PLUS_WRAPPER_PREFIX); } private boolean hasEscapeChar(String str) { if (StringUtils.isBlank(str)) { return false; } for (String s : ESCAPE_CHAR) { if (str.contains(s)) { return true; } } return false; } }
3 MapLikeSqlConverter.java 转换器类
package com.jerry.market.config; import lombok.extern.slf4j.Slf4j; import java.lang.reflect.Method; import java.util.Map; import java.util.Objects; @Slf4j public class MapLikeSqlConverter extends AbstractLikeSqlConverter
4 ObjectLikeSqlConverter.java 转换器类
package com.jerry.market.config; import lombok.extern.slf4j.Slf4j; @Slf4j public class ObjectLikeSqlConverter extends AbstractLikeSqlConverter
5 MybatisLikeSqlConfig.java mybatis拦截器注入配置类
package com.jerry.market.config;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import java.util.List;
@Configuration
@Lazy(false)
@Order
//@DependsOn("pageHelperProperties")
public class MybatisLikeSqlConfig implements InitializingBean {
@Autowired
private List sqlSessionFactoryList;
public MybatisLikeSqlInterceptor mybatisSqlInterceptor() {
return new MybatisLikeSqlInterceptor();
}
@Override
public void afterPropertiesSet() throws Exception {
Interceptor interceptor = mybatisSqlInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
List list = configuration.getInterceptors();
if (!containsInterceptor(configuration, interceptor)) {
configuration.addInterceptor(interceptor);
}
}
}
private boolean containsInterceptor(org.apache.ibatis.session.Configuration configuration,
Interceptor interceptor) {
try {
return configuration.getInterceptors().contains(interceptor);
} catch (Exception var4) {
return false;
}
}
}
4. 测试验证
mybatis特殊符号处理前,同样的参数查询出多条数据。
正常mybatis特殊符号未做转义,导致全部查询出来
[nio-8080-exec-1] c.j.market.mapper.UserMapper.selectList : ==> Preparing: select id, name, sex, age, born from user WHERe name like concat('%',?,'%')
[nio-8080-exec-1] c.j.market.mapper.UserMapper.selectList : ==> Parameters: %(String)
[nio-8080-exec-1] c.j.market.mapper.UserMapper.selectList : <== Total: 2
mybatis特殊符号处理后
通过mybatis拦截器将特殊符号过滤后,%作为转义字符串正常查询
[nio-8080-exec-1] c.j.market.mapper.UserMapper.selectList : ==> Preparing: select id, name, sex, age, born from user WHERe name like concat('%',?,'%')
[nio-8080-exec-1] c.j.market.mapper.UserMapper.selectList : ==> Parameters: %(String)
[nio-8080-exec-1] c.j.market.mapper.UserMapper.selectList : <== Total: 1



