Mybatis

第10篇:Mybatis的插件设计分析

2026-01-298 min readJava 专题系列
Mybatis

参考文档: 官方文档

一、 插件设计介绍

Mybatis 中的插件都是通过代理方式来实现的,通过拦截执行器中指定的方法来达到改变核心执行代码的方式。举一个列子,查询方法核心都是通过 Executor来进行sql执行的。那么我们就可以通过拦截下面的方法来改变核心代码。基本原理就是这样,下面我们在来看 Mybatis 是如何处理插件。

java
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

插件都需要实现的接口,封装代理执行方法及参数信息

java
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

java
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中的代理都要实现该接口。这个比较基础,如果这个不清楚,那么代理就看不懂了。所以就不说了。

java
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要拦截的方法的参数(因为有相同的方法,所以要指定拦截的方法和方法参数)
java
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是否需要拦截判断
java
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

java
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语句绑定实参: 这里能做到的扩展不多。

java
1public interface ParameterHandler { 2 // 对方法的入参进行处理,注意只有在 statementType="CALLABLE" 生效 3 Object getParameterObject(); 4 // 预处理参数处理 5 void setParameters(PreparedStatement ps) throws SQLException; 6}

我们来实现一下,我们插入user信息,通过插件的方式修改入参。

java
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

从名字就可以看出来是对结果集进行处理。这里我们通过插件的方式, 在查询语句中增加一条数据库原本不存在的数据。

java
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

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

java
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要拦截的方法的参数(因为有相同的方法,所以要指定拦截的方法和方法参数)
java
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进行代理。

  1. 第一个插件,我们在sql后加上 where id > 1
  2. 第二个插件,我们在sql后机上 limit 10

按照我们设想的最终sql会变成 select * from users where id > 1 limit 10

但是我们知道mybatis是没有顺序的, 那么很可能会出现最终的sql变成 select * from user limit 10 where id > 1,此时就会报错。

所以我们要注意这里。

java
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 插件的设计模式

拦截链 + 插件设计

java
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