一、插件定义介绍
MyBatis插件实际上就是拦截器的实现过程,它是通过Jdk动态代理来实现的。
可拦截的对象如下(注意,这四个对象都是接口,插件会拦截实现了该接口的对象):
- Executor的 update, query, flushStatements, commit, rollback, getTransaction, close, isClosed 方法(执行的
SQL
全过程,包括组装参数、组装结果返回和执行SQL
的过程等都可以拦截)。- StatementHandler的 prepare, parameterize, batch, update, query 方法(执行
SQL
的过程,拦截该对象可以重写执行SQL
的过程)。- ParameterHandler的 getParameterObject, setParameters 方法(执行
SQL
的参数组装,拦截该对象可以重写组装参数的规则)。- ResultSetHandler的 handleResultSets, handleOutputParameters 方法(执行结果的组装,拦截该对象可以重写组装结果的规则)。
1.1、插件接口
插件必须实现 Interceptor 接口。Interceptor 接口共有 3 个方法,如下:
//Interceptor 接口(plugin 和 setProperties 方法都是默认实现的方法,
我们可以选择不覆盖实现,而 intercept 方法则必须实现)
public interface Interceptor {
//核心方法,通过 Invocation 我们可以拿到被拦截的对象,从而实现自己的逻辑。
Object intercept(Invocation invocation) throws Throwable;
//给 target 拦截对象生成一个代理对象,已有默认实现
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
//插件的配置方法,在插件初始化的时候调用。
default void setProperties(Properties properties) {
}
}
1.2、拦截器签名
插件可对多种对象进行拦截,因此我们需要通过拦截器签名来告诉 MyBatis 插件应该拦截何种对象的何种方法。举例如下:
@Intercepts({@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)})
public class XXXPlugin implements Interceptor {
}
以上自定义插件实现由两个注解,说明如下:
- Intercepts注解: 拦截声明,只有 Intercepts 注解修饰的插件才具有拦截功能。
- Signature注解: 签名注解,共 3 个参数,type 参数表示拦截的对象,如下:
StatementHandler,另外还有Executor、ParameterHandler和ResultSetHandler; method参数:表示拦截对象的方法名,即对拦截对象的某个方法进行拦截, 如 prepare,代表拦截 StatementHandler 的 prepare 方法; args:参数表示拦截方法的参数,因为方法可能会存在重载, 因此方法名加上参数才能唯一标识一个方法。
推断可知 XXXPlugin 插件会拦截 StatementHandler对象的 prepare(Connection connection, Integer var2) 方法。
二、实例
目的:实现SQL 执行时间计时插件。插件的功能是日志输出每一条 SQL 的执行用时。
实现步骤:
步骤一:
创建插件实现类SqlStaticsPlugin ,SqlStaticsPlugin 会拦截 StatementHandler的prepare方法,如下:
@Intercepts({@Signature( type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class} )}) public class SqlStaticsPlugin implements Interceptor { private Logger logger = LoggerFactory.getLogger(SqlStaticsPlugin.class); @Override public Object intercept(Invocation invocation) throws Throwable { return invocation.proceed(); } } 对以上方法进行完善,达到实现的的目的,思路分析: 1、首先需要得到 invocation 的拦截对象 StatementHandler, 并从 StatementHandler 中拿到 SQL 语句。 2、得到当前的时间戳 startTime。 3、执行 SQL。 4、得到执行后的时间戳 endTime。 5、计算时间差,并打印 SQL 耗时。 对应的 intercept 方法代码如下: public Object intercept(Invocation invocation) throws Throwable { // 得到拦截对象 StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); MetaObject metaObj = SystemMetaObject.forObject(statementHandler); String sql = (String) metaObj.getValue("delegate.boundSql.sql"); // 开始时间 long startTime = System.currentTimeMillis(); // 执行SQL Object res = invocation.proceed(); // 结束时间 long endTime = System.currentTimeMillis(); long sqlCost = endTime - startTime; // 去掉无用的换行符,打印美观 logger.info("sql: {} - cost: {}ms", sql.replace("\n", ""), sqlCost); // 返回执行的结果 return res; } 注:注意,通过反射调用后的结果 res,我们一定要记得返回。MyBatis 提供了 MetaObject 这个类来方便我们进行拦截对象属性的修改,这里我们简单的使用了getValue方法来得到 SQL 语句。
步骤二:
全局配置文件注册这个插件
<plugins> <plugin interceptor="com.imooc.mybatis.plugin.SqlStaticsPlugin" /> </plugins>
到这,这个插件已经可以工作了,但是我们希望它能更加灵活一点,通过配置来拦截某些类型的 SQL,如只计算 select 类型SQL的耗时。
插件会在初始化的时候通过 setProperties 方法来加载配置,利用它我们可以得到哪些方法需要被计时。如下:
public class SqlStaticsPlugin implements Interceptor {
private List<String> methods = Arrays.asList("SELECT", "INSERT", "UPDATE", "DELETE");
@Override
public void setProperties(Properties properties) {
String methodsStr = properties.getProperty("methods");
if (methodsStr == null || methodsStr.isBlank())
return;
String[] parts = methodsStr.split(",");
methods = Arrays.stream(parts).map(String::toUpperCase).collect(Collectors.toList());
}
}
methods 参数默认可通过 select、insert、update 和 delete 类型的SQL语句,如果插件存在配置项 methods,那么则根据插件配置来覆盖默认配置。
在全局配置文件中,我们来添加上 methods 这个配置:
<plugins>
<plugin interceptor="com.imooc.mybatis.plugin.SqlStaticsPlugin">
<property name="methods" value="select,update"/>
</plugin>
</plugins>
类型之间以 , 隔开,MyBatis 会在插件初始化时,自动将 methods 对应的值通过 setProperties 方法来传递给SqlStaticsPlugin插件。插件拿到 Properties 后解析并替换默认的 methods 配置。
再次完善一下 intercept 方法,使其支持配置拦截:
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObj = SystemMetaObject.forObject(statementHandler);
// 得到SQL类型
String sqlCommandType = metaObj.getValue("delegate.mappedStatement.sqlCommandType").toString();
// 如果方法配置中没有SQL类型,则无需计时,直接返回调用
if (!methods.contains(sqlCommandType)) {
return invocation.proceed();
}
String sql = (String) metaObj.getValue("delegate.boundSql.sql");
long startTime = System.currentTimeMillis();
Object res = invocation.proceed();
long endTime = System.currentTimeMillis();
long sqlCost = endTime - startTime;
logger.info("sql: {} - cost: {}ms", sql.replace("\n", ""), sqlCost);
return res;
}
至此,一个简单的 SQL 计时插件就开发完毕了。
阅读文献: