Mybatis拦截器是mybatis提供的一套接口,用于拦截mabatis访问数据库时的行为,并允许我们在拦截中,添加自己需要的自定义操作。
先给一段代码:
@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}),
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}),
})
public class MybatisInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(MybatisInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement mappedStatements = (MappedStatement) args[0];
Map<String, Object> paramMap = new MapperMethod.ParamMap<>();
if (args[1] != null) {
if (args[1] instanceof MapperMethod.ParamMap) {
paramMap.putAll((Map) args[1]);
} else if (BeanUtils.isSimpleValueType(args[1].getClass())) {
String mapperId = mappedStatements.getId();
Class<?> paramType = args[1].getClass();
String clazzName = mapperId.substring(0, mapperId.lastIndexOf("."));
String methodName = mapperId.substring(mapperId.lastIndexOf(".") + 1);
Class<?> target = Class.forName(clazzName);
Method method = target.getMethod(methodName, paramType);
Parameter[] params = method.getParameters();
String paramName = params[0].getName();
paramMap.put(paramName, args[1]);
} else {
List<Field> fields = new ArrayList<>();
getAllFields(fields, args[1].getClass());
for (Field field : fields) {
Object fieldValue = null;
String fieldName = field.getName();
try {
fieldValue = (new PropertyDescriptor(fieldName, args[1].getClass())).getReadMethod().invoke(args[1]);
} catch (Exception var9) {
logger.error(var9.getMessage(), var9);
}
paramMap.putIfAbsent(field.getName(), fieldValue);
}
}
}
paramMap.put("key", "value");
args[1] = paramMap;
return invocation.proceed();
}
private List<Field> getAllFields(List<Field> fields, Class<?> type) {
fields.addAll(Arrays.asList(type.getDeclaredFields()));
if (type.getSuperclass() != null) {
this.getAllFields(fields, type.getSuperclass());
}
return fields;
}
}
实现一个拦截器主要有3步。
Interceptor接口是mbatis提供的拦截器接口。mybatis会执行该接口声明的方法去拦截对应的操作。
Interceptor接口有三个方法,一个必须实现的方法,两个使用Interceptor的默认实现就够用的方法。
必须实现的方法,使用该方法在拦截中途添加自己想要的自定义操作。
可选的方法,将插件添加到mybatis操作中。
可选的方法,读取配置中的属性。
@Intercepts注解的作用是,标记需要拦截的方法列表。mybatis通过该注解去判断当前方法是否需要被拦截。
@Intercepts其实就是一个数组,用来添加复数个@Signature。
每个@Signature都指定了一个需要拦截的方法。
@Signature注解有3个参数。
指定拦截的类的类型,type有4个类型可选
Executor.class、ParameterHandler.class、ResultSetHandler.class、StatementHandler.class
指定拦截的方法名
指定方法的参数类型,去对应的方法里看有哪些参数类型就可以了。如果填错会报错。
mybatis会根据这三个参数找到对应的方法,并进行拦截。
mybatis给出了允许拦截的方法列表
Executor (update, query, flushStatements, commit, rollback,getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets,handleOutputParameters)
StatementHandler (prepare, parameterize,batch, update, query)
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
这个注解声明了,要拦截的类是
Executor.class
要拦截的方法名是
query
该方法的参数有4个,类型是
MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class
intercept方法用来在拦截时添加自定义的操作。
该方法提供给我们一个参数:Invocation invocation
Invocation类有3个字段,
private final Object target;
private final Method method;
private final Object[] args;
target 被拦截的类
method 被拦截的方法
args 方法的实际参数
既然我们要添加参数到拦截的方法里,所以我们需要关心的主要就是这个args。
args是一个参数数组,里面的参数数量对应着@Signature声明的那几个参数,也就是拦截方法的参数。
举个例子:
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
拦截该方法时,args就是一个长度为4的数组,里面4个参数是
MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class
其中,第二个参数,也就是args[1],是实际执行Mapper时传入的参数,后续sql会根据args[1]里的值来填充sql。
所以我们要做的就是把自己的参数添加到args[1]中。
添加参数也是有说法的。args[1]是一个Object类型,实际类型会根据Mapper方法的参数数量和类型发生变化。
因此需要分多种情况考虑。
Mapper执行的是一个无参方法时,args[1]为null。
需要创建一个MapperMethod.ParamMap对象,然后将自定义的参数添加进去,再赋值给arg[1]就可以了。
MapperMethod.ParamMap是一个继承于Map的类。把他当作一个Map
就可以了。
Mapper执行的是一个单一参数方法时,args[1]是Object类型。
需要创建一个MapperMethod.ParamMap对象,将已经有的参数放进去,然后将自定义参数放进去。
如果args[1]是一个基本数据类型,就存在一个问题,Map是key-value结构,args[1]只有值,没有参数名。我们知道value是不够的,sql解析时根据参数名去找对应的value。
所以我们需要去反射实际执行的mapper,拿到对应的参数名。
如果args[1]是一个引用类型,我们就需要解析他所有的字段,将字段名和其值组成key-value键值对,存储到Map中。
因为单一参数是引用时,mybatis会直接忽略参数名,直接匹配参数中的字段名。
Mapper执行的是一个多参数方法时,args[1]已经是MapperMethod.ParamMap类型。
所以我们无需修改args[1]的类型,直接将自定义的参数添加进去就可以了。
@Param注解修饰的参数虽然参数名称发生改变,但无需特别处理,因为@Param注解并非修改源参数的名称,而是添加了一个新的参数。可以将创建者,更新者,这类通用参数通过拦截器添加到参数中,无需手动添加该参数,也不用写AOP方法。
可以做分库分表,根据上下文将实际操作的数据库名传入其中,无需手动判断操作的是哪个数据库。