众所周知,SpringBoot
中有拦截器,其实Mybatis
里面也有拦截器,之前因为业务的需求,数据库中有一张表的一个字段是GEOMETRY
类型,这是用来存储地理坐标的。
因为使用了Mybatis-Plus
,所以很久没写SQL
了,人也变懒了?。
可是Mybatis-Plus
无法处理这个类型,只能写SQL
来进行存储和查询。
- 新增要使用
SQL
函数:ST_GEOMFROMTEXT()
- 查询要使用
SQL
函数:ST_ASTEXT()
作为一个懒人,是无法忍受的?,今天就来写一个拦截器(插件)来处理这种类型的数据吧。
Mybatis拦截器
动手之前,先介绍一下Mybatis
拦截器。
首先要实现org.apache.ibatis.plugin.Interceptor
这个接口。复写其中的intercept
方法。
其实还有一个重要的东西是注解:@Intercepts
这个注解里面还要配置一个注解,也可以是多个@Signature
如下:
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}
)})
type
有以下类型,括号里的是method
- 执行
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) - 请求参数处理
ParameterHandler (getParameterObject, setParameters) - 返回结果集处理
ResultSetHandler (handleResultSets, handleOutputParameters) - SQL语句构建
StatementHandler (prepare, parameterize, batch, update, query)
完整例子
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.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}),
})
当然一个拦截器一般只针对一种情况处理,所以只需要一个@Signature
就可以了。
准备工作
为了能区分Geometry
类型的字段,所以我自定义了一个注解。
@Documented
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Geometry {
}
新建了一张表

对应的实体类
public class User extends Model<User> {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private Integer name;
@Geometry
private String location;
@Override
protected Serializable pkVal() {
return this.id;
}
}
拦截器
首先我们要知道在什么时候拦截?
- 执行的时候肯定不行,
Mybatis
的执行SQL
都是经过预处理的,在这里拦截是无法调用SQL
函数的. - 请求参数处理的时候,比如插入的时候,我在参数外面包一层
ST_GEOMFROMTEXT ()
,还是因为预处理的原因,这个在SQL
执行的时候是不会把它当做函数的,还是不行。同样查询的时候,拦截返回结果集处理也不行。
貌似只有一种方式,那就是SQL
语句构建的时候。
MybatisGeometryHandler
import com.ler.manager.annotation.Geometry;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.SqlCommandType;
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.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.RowBounds;
import org.springframework.core.annotation.AnnotationUtils;
/**
* @author lww
* @date 2020-08-01 18:35
*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class MybatisGeometryHandler implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
//SELECT操作
if (SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
List resultMaps = mappedStatement.getResultMaps();
ResultMap map = resultMaps.get(0);
Class> type = map.getType();
List locations = selectGetLocations(type);
StringBuilder sb = new StringBuilder();
sb.append("SELECT ");
String originalSql = boundSql.getSql();
String reSql = originalSql.replace("SELECT", "");int from = reSql.indexOf("FROM");
String subSql = reSql.substring(0, from);
String[] split = subSql.split(",");for (int i = 0; i if (locations.contains(i + 1)) {
sb.append("ST_ASTEXT(").append(split[i]).append(") AS ").append(split[i]).append(",");
} else {
sb.append(split[i]).append(",");
}
}
String substring = sb.substring(0, sb.length() - 1);
String lastSql = reSql.substring(from);
metaObject.setValue("delegate.boundSql.sql", substring + " " + lastSql);
metaObject.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
metaObject.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);return invocation.proceed();
} else {
Object paramObj = boundSql.getParameterObject();
Field[] fields = paramObj.getClass().getDeclaredFields();
List locations = insertGetLocations(fields, paramObj);
String originalSql = boundSql.getSql();int j = 0;
StringBuilder sb = new StringBuilder();for (int i = 0; i String subStr = originalSql.substring(i, i + 1);if ("?".equalsIgnoreCase(subStr)) {
j++;if (locations.contains(j)) {
sb.append("ST_GEOMFROMTEXT(" + subStr + ")");
} else {
sb.append(subStr);
}
} else {
sb.append(subStr);
}
}
metaObject.setValue("delegate.boundSql.sql", sb.toString());return invocation.proceed();
}
}private List selectGetLocations(Class> type) {
List locations = new ArrayList<>();
Field[] fields = type.getDeclaredFields();int i = 0;for (Field field : fields) {if (!"serialVersionUID".equalsIgnoreCase(field.getName())) {
i++;if (field.getAnnotation(Geometry.class) != null) {
locations.add(i);
}
}
}return locations;
}private List insertGetLocations(Field[] declaredFields, Object parameterObject) {
List locations = new ArrayList<>();int i = 0;for (Field declaredField : declaredFields) {
declaredField.setAccessible(true);try {
Object o = declaredField.get(parameterObject);if (o != null && !"serialVersionUID".equalsIgnoreCase(declaredField.getName())) {
i++;
Geometry annotation = AnnotationUtils.findAnnotation(declaredField, Geometry.class);if (annotation != null) {
locations.add(i);
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}return locations;
}private Object realTarget(Object target) {if (Proxy.isProxyClass(target.getClass())) {
MetaObject metaObject = SystemMetaObject.forObject(target);return realTarget(metaObject.getValue("h.target"));
}return target;
}@Overridepublic Object plugin(Object o) {return Plugin.wrap(o, this);
}@Overridepublic void setProperties(Properties properties) {
}
}
realTarget
方法是com.baomidou.mybatisplus.core.toolkit.PluginUtils#realTarget
这个拷贝过来的,是为了减少第三方Jar
包依赖。主要用于获得真正的处理对象。
metaObject.setValue("delegate.boundSql.sql", SQL语句);
是用来修改SQL
的。
selectGetLocations
这个方法的作用:在查询的时候,判断对象里哪些字段上有@Geometry
注解,并记录位置。
insertGetLocations
这个方法的作用:在插入的时候,判断对象里哪些字段上有@Geometry
注解,并记录位置。
这个位置就是我们修改SQL
语句的重要依据。
新增操作,替换对应位置的?
号。
查询操作,替换对应位置的字段。
配置
因为使用的是Mybatis-Plus
,所以配置拦截器很简单,在Mybatis
配置类中,创建SqlSessionFactory
这个Bean
的sqlSessionFactory()
方法里,加一句configuration.addInterceptor(new MybatisGeometryHandler());

新增操作
@ApiOperation("添加")
@PostMapping(value = "/add", name = "添加")
public HttpResult add() {
User user = new User();
user.setName(123456);
user.setLocation("POINT(121.58623 31.150897)");
user.insert();
return HttpResult.success();
}


查询操作
@ApiOperation("查询一个")
@GetMapping(value = "/one", name = "查询一个")
public HttpResult one() {
User user = userService.getById(1L);
return HttpResult.success(user);
}
@ApiOperation("查询多个")
@GetMapping(value = "/list", name = "查询多个")
public HttpResult list() {
List list = userService.list(new QueryWrapper<>());return HttpResult.success(list);
}

查一个查多个
如果不使用这个拦截器,新增会报错,查询会乱码。


最后
欢迎大家关注我的公众号,共同学习,一起进步。加油?
