返回介绍

7.5 插件开发过程和实例

发布于 2025-04-26 13:08:34 字数 10194 浏览 0 评论 0 收藏

有了对插件的理解,我们再学习插件的运用就简单多了。例如,开发一个互联网项目需要去限制每一条 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 条数据,这样就可以限制一条语句的返回记录数,插件运行成功。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。