7.5 插件开发过程和实例
有了对插件的理解,我们再学习插件的运用就简单多了。例如,开发一个互联网项目需要去限制每一条 SQL 返回数据的行数。限制的行数需要是个可配置的参数,业务可以根据自己的需要去配置。这样很有必要,因为大型互联网系统一旦同时传输大量数据很容易宕机。这里我们可以通过修改 SQL 来完成它。
7.5.1 确定需要拦截的签名
正如 MyBatis 插件可以拦截四大对象中的任意一个一样。从 Plugin 源码中我们可以看到它需要注册签名才能够运行插件。签名需要确定一些要素。
1. 确定需要拦截的对象
首先要根据功能来确定你需要拦截什么对象。
Executor 是执行 SQL 的全过程,包括组装参数,组装结果集返回和执行 SQL 过程,都可以拦截,较为广泛,我们一般用的不算太多。
StatementHandler 是执行 SQL 的过程,我们可以重写执行 SQL 的过程。这是我们最常用的拦截对象。
ParameterHandler,很明显它主要是拦截执行 SQL 的参数组装,你可以重写组装参数规则。
ResultSetHandler 用于拦截执行结果的组装,你可以重写组装结果的规则。
我们清楚需要拦截的是 StatementHandler 对象,应该在预编译 SQL 之前,修改 SQL 使得结果返回数量被限制。
2. 拦截方法和参数
当你确定了需要拦截什么对象,接下来就要确定需要拦截什么方法及方法的参数,这些都是在你理解了 MyBatis 四大对象运作的基础上才能确定的。
查询的过程是通过 Executor 调度 StatementHandler 来完成的。调度 StatementHandler 的 prepare 方法预编译 SQL,于是我们需要拦截的方法便是 prepare 方法,在此之前完成 SQL 的重新编写。让我们先看看 StatementHandler 接口的定义,如代码清单 7-9 所示。
代码清单 7-9:StatementHandler 接口的定义
public interface StatementHandler { Statement prepare(Connection connection) throws SQLException; void parameterize(Statement statement) throws SQLException; void batch(Statement statement) throws SQLException; int update(Statement statement) throws SQLException; <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException; BoundSql getBoundSql(); ParameterHandler getParameterHandler(); }
以上的任何方法都可以拦截。从接口定义而言,prepare 方法有一个参数 Connection 对象,因此我们按代码清单 7-10 的方法来设计拦截器。
代码清单 7-10:定义插件的签名
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})}) public class MyPlugin implements Interceptor { ...... }
其中,@Intercepts 说明它是一个拦截器。@Signature 是注册拦截器签名的地方,只有签名满足条件才能拦截,type 可以是四大对象中的一个,这里是 StatementHandler。method 代表要拦截四大对象的某一种接口方法,而 args 则表示该方法的参数,你需要根据拦截对象的方法参数进行设置。
7.5.2 实现拦截方法
这里说原理不如学习代码来得清晰明了,有了上面的原理分析,我们来看一个最简单的插件实现方法,如代码清单 7-11 所示,注意看代码注解你就很明白了。
代码清单 7-11:实现插件拦截方法
@Intercepts({@Signature( type = Executor.class, //确定要拦截的对象 method ="update", //确定要拦截的方法 args = {MappedStatement.class, Object.class}//拦截方法的参数 )}) public class MyPlugin implements Interceptor { Properties props = null; /** * 代替拦截对象方法的内容 * @param invocation 责任链对象 */ @Override public Object intercept(Invocation invocation) throws Throwable { System.err.println("before....."); //如果当前代理的是一个非代理对象,那么它就回调用真实拦截对象的方法,如果不是它会调度下个插件代理对象的 invoke 方法 Object obj = invocation.proceed(); System.err.println("after....."); return obj; } /** * 生成对象的代理,这里常用 MyBatis 提供的 Plugin 类的 wrap 方法 * @param target 被代理的对象 */ @Override public Object plugin(Object target) { //使用 MyBatis 提供的 Plugin 类生成代理对象 System.err.println("调用生成代理对象...."); return Plugin.wrap(target, this); } /** * 获取插件配置的属性,我们在 MyBatis 的配置文件里面去配置 * @param props 是 MyBatis 配置的参数 */ public void setProperties(Properties props) { System.err.println(props.get("dbType")); this.props = props; } }
这就是一个最简单的插件,实现了一些简单的打印顺序功能,告诉大家一些常用的方法和含义。
7.5.3 配置和运行
我们需要在 MyBatis 配置文件里面配置才能够使用插件,如代码清单 7-12 所示。请注意 plugins 元素的配置顺序,你配错了顺序系统就会报错,让我们学习它。
代码清单 7-12:配置插件
<plugins> <plugin interceptor="xxx.MyPlugin"> <property name="dbType" value="mysql" /> </plugin> </plugins>
显然,我们需要清楚配置的哪个类是插件。它会去解析注解,知道拦截哪个对象、方法和方法的参数,在初始化的时候就会调用 setProperties 方法,初始化参数。
让我们运行一个插入数据的操作,看看日志打印了什么。
mysql log4j:WARN No appenders could be found for logger (org.apache.ibatis. logging.LogFactory). log4j:WARN Please initialize the log4j system properly. log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info. 调用生成代理对象.... before..... 调用生成代理对象.... 调用生成代理对象.... 调用生成代理对象.... After.....
这里我们可以清晰地看到 MyBatis 调度插件的顺序。
7.5.4 插件实例
有了上面的知识来实现一个真实的插件就容易多了。在一个大型的互联网系统,我们使用的是 MySQL 数据库,对数据库查询返回数据量需要限制,以避免数据量过大造成网站瓶颈。假设这个数据量可以配置,当前要配置 50 条数据。让我们讨论一下它的实现。
首先我们先确定需要拦截四大对象中的哪一个,根据功能我们需要修改 SQL 的执行。SqlSession 运行原理告诉我们需要拦截的是 StatementHandler 对象,因为是由它的 prepare 方法来预编译 SQL 语句的,我们可以在预编译前修改语句来满足我们的需求。所以我们选择拦截 StatementHandler 的 prepare() 方法,在它预编译前,需要重写 SQL,以达到要求的结果。它有一个参数(Connection connection),所以我们就很轻易地得到了签名注解,其实现方法如代码清单 7-13 所示。
代码清单 7-13:QueryLimitPlugin.java 限制返回行数拦截器
@Intercepts({ @Signature(type = StatementHandler.class, // 确定要拦截的对象 method = "prepare", // 确定要拦截的方法 args = { Connection.class})// 拦截方法的参数 }) public class QueryLimitPlugin implements Interceptor { // 默认限制查询返回行数 private int limit; private String dbType; //限制表中间别名,避免表重名所以起得怪些 private static final String LMT_TABLE_NAME = "limit_Table_Name_xxx"; @Override public Object intercept(Invocation invocation) throws Throwable { //取出被拦截对象 StatementHandler stmtHandler = (StatementHandler) invocation. getTarget(); MetaObject metaStmtHandler = SystemMetaObject.forObject(stmtHandler); // 分离代理对象,从而形成多次代理,通过两次循环最原始的被代理类,MyBatis 使用的是 JDK 代理 while (metaStmtHandler.hasGetter("h")) { Object object = metaStmtHandler.getValue("h"); metaStmtHandler= SystemMetaObject.forObject(object); } // 分离最后一个代理对象的目标类 while (metaStmtHandler.hasGetter("target")) { Object object = metaStmtHandler.getValue("target"); metaStmtHandler = SystemMetaObject.forObject(object); } // 取出即将要执行的 SQL String sql = (String) metaStmtHandler.getValue("delegate.boundSql.sql"); String limitSql; //判断参数是不是 MySQL 数据库且 SQL 有没有被插件重写过 if ("mysql".equals(this.dbType) && sql.indexOf(LMT_TABLE_NAME) == -1) { //去掉前后空格 sql = sql.trim(); //将参数写入 SQL limitSql = "select * from (" + sql +") " + LMT_TABLE_NAME + " limit " + limit; //重写要执行的 SQL metaStmtHandler.setValue("delegate.boundSql.sql", limitSql); } //调用原来对象的方法,进入责任链的下一层级 return invocation.proceed(); } @Override public Object plugin(Object target) { //使用默认的 MyBatis 提供的类生成代理对象 return Plugin.wrap(target, this); } @Override public void setProperties(Properties props) { String strLimit = (String)props.getProperty("limit", "50"); this.limit = Integer.parseInt(strLimit); //这里我们读取设置的数据库类型 this.dbType = (String) props.getProperty("dbtype", "mysql"); } }
在 setProperties 方法中可以读入配置给插件的参数,一个是数据库的名称,另外一个是限制的记录数。从初始化代码可知,它在 MyBaits 初始化的时候就已经被设置进去了,在需要的时候我们可以直接使用它。
在 plugin 方法里,我们使用了 MyBatis 提供的类来生成代理对象。那么插件就会进入 plugin 的 invoke 方法,它最后会使用到拦截器的 intercept 方法。
这个插件的 intercept 方法就会覆盖掉 StatementHandler 的 prepare 方法,我们先从代理对象分离出真实对象,然后根据需要修改 SQL,来达到限制返回行数的需求。最后使用 invocation.proceed() 来调度真实 StatementHandler 的 prepare 方法完成 SQL 预编译,最后需要在 MyBatis 配置文件里面配置才能运行这个插件,如代码清单 7-14 所示。
代码清单 7-14:配置插件
<plugins> <plugin interceptor="com.learn.chapter7.plugin.QueryLimitPlugin"> <property name="dbtype" value="mysql"/> <property name="limit" value="50"/> </plugin> </plugins>
配置 log4j 日志(具体请看第 2 章),运行一个查询语句,可以得到下面的日志信息。
DEBUG 2015-11-18 00:53:16,622 org.apache.ibatis.logging.LogFactory: Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter. DEBUG 2015-11-18 00:53:16,653 org.apache.ibatis.datasource.pooled.PooledDataSource: PooledDataSource forcefully closed/removed all connections. DEBUG 2015-11-18 00:53:16,653 org.apache.ibatis.datasource.pooled.PooledDataSource: PooledDataSource forcefully closed/removed all connections. DEBUG 2015-11-18 00:53:16,654 org.apache.ibatis.datasource.pooled.PooledDataSource: PooledDataSource forcefully closed/removed all connections. DEBUG 2015-11-18 00:53:16,654 org.apache.ibatis.datasource.pooled.PooledDataSource: PooledDataSource forcefully closed/removed all connections. DEBUG 2015-11-18 00:53:16,741 org.apache.ibatis.transaction.jdbc. JdbcTransaction: Opening JDBC Connection DEBUG 2015-11-18 00:53:16,967 org.apache.ibatis.datasource.pooled.PooledDataSource: Created connection 909295153. DEBUG 2015-11-18 00:53:16,968 org.apache.ibatis.transaction.jdbc.JdbcTransa ction:Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connec tion@3632be31] DEBUG 2015-11-18 00:53:16,971 org.apache.ibatis.logging.jdbc.Base JdbcLogger: ==> Preparing: select * from (SELECT id, role_name as roleName, create_date as createDate, end_date as stopDate, end_flag as stopFlag, note FROM t_role where role_name like concat('%', ?, '%')) limit_Table_Name_xxx limit 50 DEBUG 2015-11-18 00:53:17,001 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: test(String) DEBUG 2015-11-18 00:53:17,030 org.apache.ibatis.logging.jdbc.BaseJdbc Logger: <== Total: 2 DEBUG 2015-11-18 00:53:17,032 org.apache.ibatis.transaction.jdbc.Jdbc Transaction: Resetting autocommit to true on JDBC Connection [com.mysql.jdbc. JDBC4Connection@3632be31] DEBUG 2015-11-18 00:53:17,033 org.apache.ibatis.transaction.jdbc.JdbcTransaction: Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@3632be31] DEBUG 2015-11-18 00:53:17,033 org.apache.ibatis.datasource.pooled.PooledDataSource: Returned connection 909295153 to pool.
在通过反射调度 prepare() 方法之前,SQL 被我们的插件重写了,所以无论什么查询都只可能返回至多 50 条数据,这样就可以限制一条语句的返回记录数,插件运行成功。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论