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

网友投稿 310 2022-11-15

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

作者: 西魏陶渊明

西魏陶渊明 莫笑少年江湖梦,谁不少年梦江湖

参考文档: ​​官方文档​​

一、 插件设计介绍

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

public interface Executor { ResultHandler NO_RESULT_HANDLER = null; int update(MappedStatement ms, Object parameter) throws SQLException; List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException; Cursor queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException; ...}

1.1 Interceptor

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

public interface Interceptor { // 执行方法体的封装,所有的拦截方法逻辑都在这里面写。 Object intercept(Invocation invocation) throws Throwable; // 如果要代理,就用Plugin.wrap(...),如果不代理就原样返回 Object plugin(Object target); // 可以添加配置,主要是xml配置时候可以从xml中读取配置信息到拦截器里面自己解析 void setProperties(Properties properties);}

1.2 InterceptorChain

拦截链,为什么需要拦截链,假如我们要对A进行代理, 具体的代理类有B和C。 我们要同时将B和C的逻辑都放到代理类里面,那我们会首先将A和B生成代理类,然后在前面生成代理的基础上将C和前面生成的代理类在生成一个代理对象。这个类就是要做这件事 ​​pluginAll​​

public class InterceptorChain { private final List interceptors = new ArrayList(); // 这里target就是A,而List中的Interceptor就相当于B和C,通过循环方式生成统一代理类 public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { //1. 是否需要代理,需要代理生成代理类放回,不需要原样返回。通过for循环的方式将所有对应的插件整合成一个代理对象 target = interceptor.plugin(target); } return target; } ...}

1.3 InvocationHandler

JDK代理的接口,凡是JDK中的代理都要实现该接口。这个比较基础,如果这个不清楚,那么代理就看不懂了。所以就不说了。

public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;}

1.4 ​​@Intercepts​​​ 和 ​​@Signature​​

属性

解释

type

就是要拦截的类(Executor/ParameterHandler/ResultSetHandler/StatementHandler)

method

要拦截的方法

args

要拦截的方法的参数(因为有相同的方法,所以要指定拦截的方法和方法参数)

@Intercepts(@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }))public class MybatisPagerPlugin implements Interceptor {}

​​args​​​ 要拦截的方法的入参(因为有相同的方法,所以要指定拦截的方法和方法参数),比如 ​​Executor​​​ 中就有2个 ​​query​​ 方法。所以要通过args来确定要拦截哪一个。

1.5 Plugin

代理的具体生成类,解析 ​​@Intercepts​​​ 和 ​​@Signature​​ 注解生成代理。

我们看几个重要的方法。

方法名

处理逻辑

getSignatureMap

解析@Intercepts和@Signature,找到要拦截的方法

getAllInterfaces

找到代理类的接口,jdk代理必须要有接口

invoke

是否需要拦截判断

public class Plugin implements InvocationHandler { //解析@Intercepts和@Signature找到要拦截的方法 private static Map, Set> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); // issue #251 if (interceptsAnnotation == null) { throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } Signature[] sigs = interceptsAnnotation.value(); Map, Set> signatureMap = new HashMap, Set>(); for (Signature sig : sigs) { Set methods = signatureMap.get(sig.type()); if (methods == null) { methods = new HashSet(); signatureMap.put(sig.type(), methods); } try { //通过方法名和方法参数查找方法 Method method = sig.type().getMethod(sig.method(), sig.args()); methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; } //因为是jdk代理所以必须要有接口,如果没有接口,就不会生成代理 private static Class[] getAllInterfaces(Class type, Map, Set> signatureMap) { Set> interfaces = new HashSet>(); while (type != null) { for (Class c : type.getInterfaces()) { if (signatureMap.containsKey(c)) { interfaces.add(c); } } type = type.getSuperclass(); } return interfaces.toArray(new Class[interfaces.size()]); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { //执行时候看当前执行的方法是否需要被拦截,如果需要就调用拦截器中的方法 Set methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) { return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } }}

二、问题总结

2.1 插件能拦截那些类?

前面已经说过了,这里在总结下。这部分的源码在 ​​Configuration​​​。可以看到很简单只有一行。​​InterceptorChain#pluginAll​​

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; } public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; } public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } if (cacheEnabled) { executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }

2.1.1 ParameterHandler

ParameterHandler的核心方法是setParameters()方法,该方法主要负责调用PreparedStatement的set*()方法为SQL语句绑定实参: 这里能做到的扩展不多。

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

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

/** * 注意getParameterObject只会在 statementType="CALLABLE"生效 * insert into T_USER (token_id, uid, name) * values (#{tokenId,jdbcType=CHAR}, #{uid,jdbcType=INTEGER}, #{name,jdbcType=CHAR}) */ @Intercepts(@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class})) public static class ParameterInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object proceed = invocation.proceed(); PreparedStatement preparedStatement = (PreparedStatement) invocation.getArgs()[0]; // 插入时候修改第三个参数,也就是name = 孙悟空 int parameterCount = preparedStatement.getParameterMetaData().getParameterCount(); if (parameterCount != 0) { preparedStatement.setString(3, "孙悟空"); } return proceed; } } @Test public void parameterHandler() { // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录) InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml"); // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development"); // 获取Mybatis配置信息 Configuration configuration = sqlSessionFactory.getConfiguration(); configuration.addInterceptor(new ParameterInterceptor()); // 参数: autoCommit,从名字上看就是是否自动提交事务 SqlSession sqlSession = sqlSessionFactory.openSession(false); // 获取Mapper TUserMapper mapper = configuration.getMapperRegistry().getMapper(TUserMapper.class, sqlSession); TUser tUser = new TUser(); tUser.setName("唐三藏"); tUser.setTokenId("testTokenId1"); mapper.insert(tUser); // 这里虽然设置的名字是唐三藏,但是插件中修改为了孙悟空 System.out.println(mapper.selectAll()); // 数据插入后,执行查询,然后回滚数据 sqlSession.rollback(); }

2.1.2 ResultSetHandler

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

/** * 通过对list集合的数据进行修改,增加一条数据库不存在的数据 */ @Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})) public static class ResultSetHandlerInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object proceed = invocation.proceed(); if (proceed instanceof List) { ArrayList newResult = (ArrayList) proceed; TUser tUser = new TUser(); tUser.setName("如来佛祖"); newResult.add(tUser); proceed = newResult; } return proceed; } } @Test public void resultSetHandlerTest() { // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录) InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml"); // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development"); // 获取Mybatis配置信息 Configuration configuration = sqlSessionFactory.getConfiguration(); configuration.addInterceptor(new ResultSetHandlerInterceptor()); // 参数: autoCommit,从名字上看就是是否自动提交事务 SqlSession sqlSession = sqlSessionFactory.openSession(false); // 获取Mapper TUserMapper mapper = configuration.getMapperRegistry().getMapper(TUserMapper.class, sqlSession); System.out.println(mapper.selectAll()); // 数据插入后,执行查询,然后回滚数据 sqlSession.rollback(); }

2.1.3 StatementHandler

/** * 我们本来是一条查询语句,我们打印下sql信息 */ @Intercepts(@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})) public static class StatementHandlerInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object proceed = invocation.proceed(); Object[] args = invocation.getArgs(); if (args[0] instanceof ClientPreparedStatement) { ClientPreparedStatement statement = (ClientPreparedStatement) args[0]; if (statement.getQuery() instanceof ClientPreparedQuery) { System.out.println(((ClientPreparedQuery) statement.getQuery()).getOriginalSql()); } } return proceed; } } @Test public void resultSetHandlerTest() { // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录) InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml"); // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development"); // 获取Mybatis配置信息 Configuration configuration = sqlSessionFactory.getConfiguration(); configuration.addInterceptor(new StatementHandlerInterceptor()); // 参数: autoCommit,从名字上看就是是否自动提交事务 SqlSession sqlSession = sqlSessionFactory.openSession(false); // 获取Mapper TUserMapper mapper = configuration.getMapperRegistry().getMapper(TUserMapper.class, sqlSession); System.out.println(mapper.selectAll()); // 数据插入后,执行查询,然后回滚数据 sqlSession.rollback(); }

2.1.4 Executor

Executor 是个好东西,从他能获取基本你能想到的所有信息。你可以在这里做sql动态变更、也可以做sql语句分析,同时也可以获取某个Mapper的签名信息。总之功能非常强大。一般的插件都是 在这里做文章。如下面例子就是动态的修改了sql。

/** * 动态修改sql信息。 * 这里因为我们知道要使用查询语句,所以不做sql分析。如果要学习sql分析请看其他文章 */ @Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})) public static class ExecutorInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); if (args[0] instanceof MappedStatement) { MappedStatement arg = (MappedStatement) args[0]; Configuration configuration = arg.getConfiguration(); StaticSqlSource staticSqlSource = new StaticSqlSource(configuration, "select name from T_USER"); Field sqlSourceField = arg.getClass().getDeclaredField("sqlSource"); sqlSourceField.setAccessible(true); sqlSourceField.set(arg, staticSqlSource); } return invocation.proceed(); } } @Test public void executor() { // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录) InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml"); // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development"); // 获取Mybatis配置信息 Configuration configuration = sqlSessionFactory.getConfiguration(); configuration.addInterceptor(new ExecutorInterceptor()); // 参数: autoCommit,从名字上看就是是否自动提交事务 SqlSession sqlSession = sqlSessionFactory.openSession(false); // 获取Mapper TUserMapper mapper = configuration.getMapperRegistry().getMapper(TUserMapper.class, sqlSession); System.out.println(mapper.selectAll()); // 数据插入后,执行查询,然后回滚数据 sqlSession.rollback(); }

2.2 如何定义一个拦截器?

属性

解释

type

就是要拦截的类(Executor/ParameterHandler/ResultSetHandler/StatementHandler)

method

要拦截的方法

args

要拦截的方法的参数(因为有相同的方法,所以要指定拦截的方法和方法参数)

@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})) public static class ExecutorInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); if (args[0] instanceof MappedStatement) { MappedStatement arg = (MappedStatement) args[0]; Configuration configuration = arg.getConfiguration(); StaticSqlSource staticSqlSource = new StaticSqlSource(configuration, "select name from T_USER"); Field sqlSourceField = arg.getClass().getDeclaredField("sqlSource"); sqlSourceField.setAccessible(true); sqlSourceField.set(arg, staticSqlSource); } return invocation.proceed(); } }

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​​,此时就会报错。

所以我们要注意这里。

public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); } public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }

三、可以借鉴的知识点

3.1 插件的设计模式

拦截链 + 插件设计

public class Test { public static void main(String[] args) { InterceptorChain chain = new InterceptorChain(); PrintInterceptor printInterceptor = new PrintInterceptor(); Properties properties = new Properties(); properties.setProperty("name"," printInterceptor.setProperties(properties); chain.addInterceptor(printInterceptor); Animal person = (Animal) chain.pluginAll(new Person()); String nihao = person.say("nihao"); System.out.println(nihao); } public interface Animal{ String say(String message); String say(String name, String message); } public static class Person implements Animal { public String say(String message) { return message; } public String say(String name, String message) { return name + " say: " + message; } } @Intercepts(@Signature(type = Animal.class, method = "say", args = {String.class})) public static class PrintInterceptor implements Interceptor { private String name; @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println(name + ": before print ..."); Object proceed = invocation.proceed(); System.out.println(name + ": after print ..."); return proceed; } }}

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:适配器模式、装饰器模式、代理模式的区别
下一篇:MyBatis的9种动态标签详解
相关文章

 发表评论

暂时没有评论,来抢沙发吧~