linux怎么查看本机内存大小
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;
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
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
二、问题总结
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
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小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~