Bootstrap

Mybatis使用拦截器添加参数

一、什么是Mybatis拦截器

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步。

1 实现Interceptor接口

Interceptor接口是mbatis提供的拦截器接口。mybatis会执行该接口声明的方法去拦截对应的操作。

Interceptor接口有三个方法,一个必须实现的方法,两个使用Interceptor的默认实现就够用的方法。

1)Object intercept(Invocation var1)

必须实现的方法,使用该方法在拦截中途添加自己想要的自定义操作。

2)plugin(Object target)

可选的方法,将插件添加到mybatis操作中。

3)setProperties(Properties properties)

可选的方法,读取配置中的属性。

2 添加@Intercepts注解

@Intercepts注解的作用是,标记需要拦截的方法列表。mybatis通过该注解去判断当前方法是否需要被拦截。
@Intercepts其实就是一个数组,用来添加复数个@Signature
每个@Signature都指定了一个需要拦截的方法。

@Signature注解有3个参数。

1)type

指定拦截的类的类型,type有4个类型可选
Executor.classParameterHandler.classResultSetHandler.classStatementHandler.class

2)method

指定拦截的方法名

3)args

指定方法的参数类型,去对应的方法里看有哪些参数类型就可以了。如果填错会报错。
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

3 intercept方法详解

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方法的参数数量和类型发生变化。
因此需要分多种情况考虑。

1)无参数

Mapper执行的是一个无参方法时,args[1]null
需要创建一个MapperMethod.ParamMap对象,然后将自定义的参数添加进去,再赋值给arg[1]就可以了。

MapperMethod.ParamMap是一个继承于Map的类。把他当作一个Map<String,Object>就可以了。

2)一个参数

Mapper执行的是一个单一参数方法时,args[1]Object类型。
需要创建一个MapperMethod.ParamMap对象,将已经有的参数放进去,然后将自定义参数放进去。
如果args[1]是一个基本数据类型,就存在一个问题,Mapkey-value结构,args[1]只有值,没有参数名。我们知道value是不够的,sql解析时根据参数名去找对应的value。
所以我们需要去反射实际执行的mapper,拿到对应的参数名。
如果args[1]是一个引用类型,我们就需要解析他所有的字段,将字段名和其值组成key-value键值对,存储到Map中。
因为单一参数是引用时,mybatis会直接忽略参数名,直接匹配参数中的字段名。

3)多个参数

Mapper执行的是一个多参数方法时,args[1]已经是MapperMethod.ParamMap类型。
所以我们无需修改args[1]的类型,直接将自定义的参数添加进去就可以了。

注意点

  1. @Param注解修饰的参数虽然参数名称发生改变,但无需特别处理,因为@Param注解并非修改源参数的名称,而是添加了一个新的参数。

三、应用场景

可以将创建者,更新者,这类通用参数通过拦截器添加到参数中,无需手动添加该参数,也不用写AOP方法。
可以做分库分表,根据上下文将实际操作的数据库名传入其中,无需手动判断操作的是哪个数据库。

;