第10篇:Mybatis的插件设计分析
参考文档: 官方文档
一、 插件设计介绍
Mybatis 中的插件都是通过代理方式来实现的,通过拦截执行器中指定的方法来达到改变核心执行代码的方式。举一个列子,查询方法核心都是通过 Executor来进行sql执行的。那么我们就可以通过拦截下面的方法来改变核心代码。基本原理就是这样,下面我们在来看 Mybatis 是如何处理插件。
1public interface Executor {
2
3 ResultHandler NO_RESULT_HANDLER = null;
4
5 int update(MappedStatement ms, Object parameter) throws SQLException;
6
7 <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
8
9 <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
10
11 <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
12 ...
13}
| 名称 | 类型 | 描述 |
|---|---|---|
Interceptor | 接口 | 插件都需要实现的接口,封装代理执行方法及参数信息 |
InterceptorChain | 类 | 拦截链 |
InvocationHandler | 接口 | JDK代理的接口,凡是JDK中的代理都要实现该接口 |
@Intercepts | 注解 | 用于声明要代理和 @Signature 配合使用 |
@Signature | 注解 | 用于声明要代理拦截的方法 |
Plugin | 类 | 代理的具体生成类 |
1.1 Interceptor
插件都需要实现的接口,封装代理执行方法及参数信息
1public interface Interceptor {
2 // 执行方法体的封装,所有的拦截方法逻辑都在这里面写。
3 Object intercept(Invocation invocation) throws Throwable;
4 // 如果要代理,就用Plugin.wrap(...),如果不代理就原样返回
5 Object plugin(Object target);
6 // 可以添加配置,主要是xml配置时候可以从xml中读取配置信息到拦截器里面自己解析
7 void setProperties(Properties properties);
8}1.2 InterceptorChain
拦截链,为什么需要拦截链,假如我们要对A进行代理, 具体的代理类有B和C。 我们要同时将B和C的逻辑都放到代理类里面,那我们会首先将A和B生成代理类,然后在前面生成代理的基础上将C和前面生成的代理类在生成一个代理对象。这个类就是要做这件事 pluginAll
1public class InterceptorChain {
2
3 private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
4
5 // 这里target就是A,而List中的Interceptor就相当于B和C,通过循环方式生成统一代理类
6 public Object pluginAll(Object target) {
7 for (Interceptor interceptor : interceptors) {
8 //1. 是否需要代理,需要代理生成代理类放回,不需要原样返回。通过for循环的方式将所有对应的插件整合成一个代理对象
9 target = interceptor.plugin(target);
10 }
11 return target;
12 }
13 ...
14}1.3 InvocationHandler
JDK代理的接口,凡是JDK中的代理都要实现该接口。这个比较基础,如果这个不清楚,那么代理就看不懂了。所以就不说了。
1public interface InvocationHandler {
2 public Object invoke(Object proxy, Method method, Object[] args)
3 throws Throwable;
4}1.4 @Intercepts 和 @Signature
这两个注解是配合使用的,用于指定要代理的类和方法。前面①说了,插件的核心逻辑是拦截执行器的方法,那么这里我们看下如何声明要拦截的类和方法。我们看一下分页插件如何声明拦截。
| 属性 | 解释 |
|---|---|
| type | 就是要拦截的类(Executor/ParameterHandler/ResultSetHandler/StatementHandler) |
| method | 要拦截的方法 |
| args | 要拦截的方法的参数(因为有相同的方法,所以要指定拦截的方法和方法参数) |
1@Intercepts(@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
2 RowBounds.class, ResultHandler.class }))
3public class MybatisPagerPlugin implements Interceptor {
4}args 要拦截的方法的入参(因为有相同的方法,所以要指定拦截的方法和方法参数),比如 Executor 中就有2个 query 方法。所以要通过args来确定要拦截哪一个。

1.5 Plugin
代理的具体生成类,解析 @Intercepts 和 @Signature 注解生成代理。
我们看几个重要的方法。
| 方法名 | 处理逻辑 |
|---|---|
| getSignatureMap | 解析@Intercepts和@Signature,找到要拦截的方法 |
| getAllInterfaces | 找到代理类的接口,jdk代理必须要有接口 |
| invoke | 是否需要拦截判断 |
1public class Plugin implements InvocationHandler {
2
3 //解析@Intercepts和@Signature找到要拦截的方法
4 private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
5 Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
6 // issue #251
7 if (interceptsAnnotation == null) {
8 throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
9 }
10 Signature[] sigs = interceptsAnnotation.value();
11 Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
12 for (Signature sig : sigs) {
13 Set<Method> methods = signatureMap.get(sig.type());
14 if (methods == null) {
15 methods = new HashSet<Method>();
16 signatureMap.put(sig.type(), methods);
17 }
18 try {
19 //通过方法名和方法参数查找方法
20 Method method = sig.type().getMethod(sig.method(), sig.args());
21 methods.add(method);
22 } catch (NoSuchMethodException e) {
23 throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
24 }
25 }
26 return signatureMap;
27 }
28
29 //因为是jdk代理所以必须要有接口,如果没有接口,就不会生成代理
30 private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
31 Set<Class<?>> interfaces = new HashSet<Class<?>>();
32 while (type != null) {
33 for (Class<?> c : type.getInterfaces()) {
34 if (signatureMap.containsKey(c)) {
35 interfaces.add(c);
36 }
37 }
38 type = type.getSuperclass();
39 }
40 return interfaces.toArray(new Class<?>[interfaces.size()]);
41 }
42
43 @Override
44 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
45 try {
46 //执行时候看当前执行的方法是否需要被拦截,如果需要就调用拦截器中的方法
47 Set<Method> methods = signatureMap.get(method.getDeclaringClass());
48 if (methods != null && methods.contains(method)) {
49 return interceptor.intercept(new Invocation(target, method, args));
50 }
51 return method.invoke(target, args);
52 } catch (Exception e) {
53 throw ExceptionUtil.unwrapThrowable(e);
54 }
55 }
56}二、问题总结
2.1 插件能拦截那些类?
前面已经说过了,这里在总结下。这部分的源码在 Configuration。可以看到很简单只有一行。InterceptorChain#pluginAll
1 public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
2 ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
3 parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
4 return parameterHandler;
5 }
6
7 public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
8 ResultHandler resultHandler, BoundSql boundSql) {
9 ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
10 resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
11 return resultSetHandler;
12 }
13
14 public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
15 StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
16 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
17 return statementHandler;
18 }
19
20 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
21 executorType = executorType == null ? defaultExecutorType : executorType;
22 executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
23 Executor executor;
24 if (ExecutorType.BATCH == executorType) {
25 executor = new BatchExecutor(this, transaction);
26 } else if (ExecutorType.REUSE == executorType) {
27 executor = new ReuseExecutor(this, transaction);
28 } else {
29 executor = new SimpleExecutor(this, transaction);
30 }
31 if (cacheEnabled) {
32 executor = new CachingExecutor(executor);
33 }
34 executor = (Executor) interceptorChain.pluginAll(executor);
35 return executor;
36 }2.1.1 ParameterHandler
ParameterHandler的核心方法是setParameters()方法,该方法主要负责调用PreparedStatement的set*()方法为SQL语句绑定实参: 这里能做到的扩展不多。
1public interface ParameterHandler {
2 // 对方法的入参进行处理,注意只有在 statementType="CALLABLE" 生效
3 Object getParameterObject();
4 // 预处理参数处理
5 void setParameters(PreparedStatement ps) throws SQLException;
6}我们来实现一下,我们插入user信息,通过插件的方式修改入参。
1 /**
2 * 注意getParameterObject只会在 statementType="CALLABLE"生效
3 * insert into T_USER (token_id, uid, name)
4 * values (#{tokenId,jdbcType=CHAR}, #{uid,jdbcType=INTEGER}, #{name,jdbcType=CHAR})
5 */
6 @Intercepts(@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}))
7 public static class ParameterInterceptor implements Interceptor {
8 @Override
9 public Object intercept(Invocation invocation) throws Throwable {
10 Object proceed = invocation.proceed();
11 PreparedStatement preparedStatement = (PreparedStatement) invocation.getArgs()[0];
12 // 插入时候修改第三个参数,也就是name = 孙悟空
13 int parameterCount = preparedStatement.getParameterMetaData().getParameterCount();
14 if (parameterCount != 0) {
15 preparedStatement.setString(3, "孙悟空");
16 }
17 return proceed;
18 }
19 }
20
21 @Test
22 public void parameterHandler() {
23 // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录)
24 InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml");
25 // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行
26 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
27 // 获取Mybatis配置信息
28 Configuration configuration = sqlSessionFactory.getConfiguration();
29 configuration.addInterceptor(new ParameterInterceptor());
30 // 参数: autoCommit,从名字上看就是是否自动提交事务
31 SqlSession sqlSession = sqlSessionFactory.openSession(false);
32 // 获取Mapper
33 TUserMapper mapper = configuration.getMapperRegistry().getMapper(TUserMapper.class, sqlSession);
34 TUser tUser = new TUser();
35 tUser.setName("唐三藏");
36 tUser.setTokenId("testTokenId1");
37 mapper.insert(tUser);
38 // 这里虽然设置的名字是唐三藏,但是插件中修改为了孙悟空
39 System.out.println(mapper.selectAll());
40 // 数据插入后,执行查询,然后回滚数据
41 sqlSession.rollback();
42 }2.1.2 ResultSetHandler
从名字就可以看出来是对结果集进行处理。这里我们通过插件的方式, 在查询语句中增加一条数据库原本不存在的数据。
1 /**
2 * 通过对list集合的数据进行修改,增加一条数据库不存在的数据
3 */
4 @Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}))
5 public static class ResultSetHandlerInterceptor implements Interceptor {
6 @Override
7 public Object intercept(Invocation invocation) throws Throwable {
8 Object proceed = invocation.proceed();
9 if (proceed instanceof List) {
10 ArrayList<TUser> newResult = (ArrayList<TUser>) proceed;
11 TUser tUser = new TUser();
12 tUser.setName("如来佛祖");
13 newResult.add(tUser);
14 proceed = newResult;
15 }
16 return proceed;
17 }
18 }
19
20 @Test
21 public void resultSetHandlerTest() {
22 // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录)
23 InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml");
24 // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行
25 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
26 // 获取Mybatis配置信息
27 Configuration configuration = sqlSessionFactory.getConfiguration();
28 configuration.addInterceptor(new ResultSetHandlerInterceptor());
29 // 参数: autoCommit,从名字上看就是是否自动提交事务
30 SqlSession sqlSession = sqlSessionFactory.openSession(false);
31 // 获取Mapper
32 TUserMapper mapper = configuration.getMapperRegistry().getMapper(TUserMapper.class, sqlSession);
33 System.out.println(mapper.selectAll());
34 // 数据插入后,执行查询,然后回滚数据
35 sqlSession.rollback();
36 }2.1.3 StatementHandler
1 /**
2 * 我们本来是一条查询语句,我们打印下sql信息
3 */
4 @Intercepts(@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}))
5 public static class StatementHandlerInterceptor implements Interceptor {
6
7 @Override
8 public Object intercept(Invocation invocation) throws Throwable {
9 Object proceed = invocation.proceed();
10 Object[] args = invocation.getArgs();
11 if (args[0] instanceof ClientPreparedStatement) {
12 ClientPreparedStatement statement = (ClientPreparedStatement) args[0];
13 if (statement.getQuery() instanceof ClientPreparedQuery) {
14 System.out.println(((ClientPreparedQuery) statement.getQuery()).getOriginalSql());
15 }
16 }
17 return proceed;
18 }
19 }
20
21 @Test
22 public void resultSetHandlerTest() {
23 // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录)
24 InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml");
25 // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行
26 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
27 // 获取Mybatis配置信息
28 Configuration configuration = sqlSessionFactory.getConfiguration();
29 configuration.addInterceptor(new StatementHandlerInterceptor());
30 // 参数: autoCommit,从名字上看就是是否自动提交事务
31 SqlSession sqlSession = sqlSessionFactory.openSession(false);
32 // 获取Mapper
33 TUserMapper mapper = configuration.getMapperRegistry().getMapper(TUserMapper.class, sqlSession);
34 System.out.println(mapper.selectAll());
35 // 数据插入后,执行查询,然后回滚数据
36 sqlSession.rollback();
37 }2.1.4 Executor
Executor 是个好东西,从他能获取基本你能想到的所有信息。你可以在这里做sql动态变更、也可以做sql语句分析,同时也可以获取某个Mapper的签名信息。总之功能非常强大。一般的插件都是 在这里做文章。如下面例子就是动态的修改了sql。
1 /**
2 * 动态修改sql信息。
3 * 这里因为我们知道要使用查询语句,所以不做sql分析。如果要学习sql分析请看其他文章
4 */
5 @Intercepts(@Signature(type = Executor.class, method = "query",
6 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
7 public static class ExecutorInterceptor implements Interceptor {
8 @Override
9 public Object intercept(Invocation invocation) throws Throwable {
10 Object[] args = invocation.getArgs();
11 if (args[0] instanceof MappedStatement) {
12 MappedStatement arg = (MappedStatement) args[0];
13 Configuration configuration = arg.getConfiguration();
14 StaticSqlSource staticSqlSource = new StaticSqlSource(configuration, "select name from T_USER");
15 Field sqlSourceField = arg.getClass().getDeclaredField("sqlSource");
16 sqlSourceField.setAccessible(true);
17 sqlSourceField.set(arg, staticSqlSource);
18 }
19 return invocation.proceed();
20 }
21 }
22
23 @Test
24 public void executor() {
25 // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录)
26 InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml");
27 // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行
28 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
29 // 获取Mybatis配置信息
30 Configuration configuration = sqlSessionFactory.getConfiguration();
31 configuration.addInterceptor(new ExecutorInterceptor());
32 // 参数: autoCommit,从名字上看就是是否自动提交事务
33 SqlSession sqlSession = sqlSessionFactory.openSession(false);
34 // 获取Mapper
35 TUserMapper mapper = configuration.getMapperRegistry().getMapper(TUserMapper.class, sqlSession);
36 System.out.println(mapper.selectAll());
37 // 数据插入后,执行查询,然后回滚数据
38 sqlSession.rollback();
39 }2.2 如何定义一个拦截器?
| 属性 | 解释 |
|---|---|
| type | 就是要拦截的类(Executor/ParameterHandler/ResultSetHandler/StatementHandler) |
| method | 要拦截的方法 |
| args | 要拦截的方法的参数(因为有相同的方法,所以要指定拦截的方法和方法参数) |
1 @Intercepts(@Signature(type = Executor.class, method = "query",
2 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
3 public static class ExecutorInterceptor implements Interceptor {
4 @Override
5 public Object intercept(Invocation invocation) throws Throwable {
6 Object[] args = invocation.getArgs();
7 if (args[0] instanceof MappedStatement) {
8 MappedStatement arg = (MappedStatement) args[0];
9 Configuration configuration = arg.getConfiguration();
10 StaticSqlSource staticSqlSource = new StaticSqlSource(configuration, "select name from T_USER");
11 Field sqlSourceField = arg.getClass().getDeclaredField("sqlSource");
12 sqlSourceField.setAccessible(true);
13 sqlSourceField.set(arg, staticSqlSource);
14 }
15 return invocation.proceed();
16 }
17 }2.3 插件的设计缺陷
InterceptorChain 的设计非常简单,里面就是一个list集合。但是在进行代理的时候,并没有顺序。假设我们要对sql进行代理。
- 第一个插件,我们在sql后加上
where id > 1 - 第二个插件,我们在sql后机上
limit 10
按照我们设想的最终sql会变成 select * from users where id > 1 limit 10
但是我们知道mybatis是没有顺序的, 那么很可能会出现最终的sql变成 select * from user limit 10 where id > 1,此时就会报错。
所以我们要注意这里。
1 public void addInterceptor(Interceptor interceptor) {
2 interceptorChain.addInterceptor(interceptor);
3 }
4 public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
5 StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
6 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
7 return statementHandler;
8 }三、可以借鉴的知识点
3.1 插件的设计模式
拦截链 + 插件设计
1public class Test {
2 public static void main(String[] args) {
3 InterceptorChain chain = new InterceptorChain();
4 PrintInterceptor printInterceptor = new PrintInterceptor();
5 Properties properties = new Properties();
6 properties.setProperty("name","https://blog.springlearn.cn");
7 printInterceptor.setProperties(properties);
8 chain.addInterceptor(printInterceptor);
9 Animal person = (Animal) chain.pluginAll(new Person());
10 String nihao = person.say("nihao");
11 System.out.println(nihao);
12 }
13
14 public interface Animal{
15 String say(String message);
16 String say(String name, String message);
17 }
18
19 public static class Person implements Animal {
20 public String say(String message) {
21 return message;
22 }
23
24 public String say(String name, String message) {
25 return name + " say: " + message;
26 }
27 }
28
29 @Intercepts(@Signature(type = Animal.class, method = "say", args = {String.class}))
30 public static class PrintInterceptor implements Interceptor {
31 private String name;
32
33 @Override
34 public Object intercept(Invocation invocation) throws Throwable {
35 System.out.println(name + ": before print ...");
36 Object proceed = invocation.proceed();
37 System.out.println(name + ": after print ...");
38 return proceed;
39 }
40 }
41}
42