即使你没开发过 MyBatis 插件,估计也能猜出来,MyBatis 插件是通过拦截器来起作用的,MyBatis 框架在设计的时候,就已经为插件的开发预留了相关接口,如下:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
这个接口中就三个方法,第一个方法必须实现,后面两个方法都是可选的。三个方法作用分别如下:
intercept:这个就是具体的拦截方法,我们自定义 MyBatis 插件时,一般都需要重写该方法,我们插件所完成的工作也都是在该方法中完成的。
plugin:这个方法的参数 target 就是拦截器要拦截的对象,一般来说我们不需要重写该方法。
Plugin.wrap 方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,如果匹配,才会通过动态代理拦截目标对象。
setProperties:这个方法用来传递插件的参数,可以通过参数来改变插件的行为。我们定义好插件之后,需要对插件进行配置,在配置的时候,可以给插件设置相关属性,设置的属性可以通过该方法获取到。插件属性设置像下面这样:
<plugins>
<plugin interceptor="org.javaboy.mybatis03.plugin.CamelInterceptor">
<property name="xxx" value="xxx"/>
</plugin>
</plugins>
.MyBatis 拦截器签名
拦截器定义好了后,拦截谁?
这个就需要拦截器签名来完成了!
拦截器签名是一个名为 @Intercepts 的注解,该注解中可以通过 @Signature 配置多个签名。@Signature 注解中则包含三个属性:
type: 拦截器需要拦截的接口,有 4 个可选项,分别是:Executor、ParameterHandler、ResultSetHandler 以及 StatementHandler。
method: 拦截器所拦截接口中的方法名,也就是前面四个接口中的方法名,接口和方法要对应上。
args: 拦截器所拦截方法的参数类型,通过方法名和参数类型可以锁定唯一一个方法。
一个简单的签名可能像下面这样:
@Intercepts(@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}
))
public class CamelInterceptor implements Interceptor {
//...
}
可以被被拦截的对象
根据前面的介绍,被拦截的对象主要有如下四个:
Executor
:
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
int update(MappedStatement ms, Object parameter) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
List<BatchResult> flushStatements() throws SQLException;
void commit(boolean required) throws SQLException;
void rollback(boolean required) throws SQLException;
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
boolean isCached(MappedStatement ms, CacheKey key);
void clearLocalCache();
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
Transaction getTransaction();
void close(boolean forceRollback);
boolean isClosed();
void setExecutorWrapper(Executor executor);
}
各方法含义分别如下:
update:该方法会在所有的 INSERT、 UPDATE、 DELETE 执行时被调用,如果想要拦截这些操作,可以通过该方法实现。
query:该方法会在 SELECT 查询方法执行时被调用,方法参数携带了很多有用的信息,如果需要获取,可以通过该方法实现。
queryCursor:当 SELECT 的返回类型是 Cursor 时,该方法会被调用。
flushStatements:当 SqlSession 方法调用 flushStatements 方法或执行的接口方法中带有 @Flush 注解时该方法会被触发。
commit:当 SqlSession 方法调用 commit 方法时该方法会被触发。
rollback:当 SqlSession 方法调用 rollback 方法时该方法会被触发。
getTransaction:当 SqlSession 方法获取数据库连接时该方法会被触发。
close:该方法在懒加载获取新的 Executor 后会被触发。
isClosed:该方法在懒加载执行查询前会被触发。
ParameterHandler
:
public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement ps) throws SQLException;
}
各方法含义分别如下:
getParameterObject:在执行存储过程处理出参的时候该方法会被触发。
setParameters:设置 SQL 参数时该方法会被触发。
ResultSetHandler
:
public interface ResultSetHandler {
<E> List<E> handleResultSets(Statement stmt) throws SQLException;
<E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
void handleOutputParameters(CallableStatement cs) throws SQLException;
}
各方法含义分别如下:
handleResultSets:该方法会在所有的查询方法中被触发(除去返回值类型为 Cursor 的查询方法),一般来说,如果我们想对查询结果进行二次处理,可以通过拦截该方法实现。
handleCursorResultSets:当查询方法的返回值类型为 Cursor 时,该方法会被触发。
handleOutputParameters:使用存储过程处理出参的时候该方法会被调用。
StatementHandler
:
public interface StatementHandler {
Statement prepare(Connection connection, Integer transactionTimeout)
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;
<E> Cursor<E> queryCursor(Statement statement)
throws SQLException;
BoundSql getBoundSql();
ParameterHandler getParameterHandler();
}
各方法含义分别如下:
prepare:该方法在数据库执行前被触发。
parameterize:该方法在 prepare 方法之后执行,用来处理参数信息。
batch:如果 MyBatis 的全剧配置中配置了 defaultExecutorType=”BATCH”,执行数据操作时该方法会被调用。
update:更新操作时该方法会被触发。
query:该方法在 SELECT 方法执行时会被触发。
queryCursor:该方法在 SELECT 方法执行时,并且返回值为 Cursor 时会被触发。
在开发一个具体的插件时,我们应当根据自己的需求来决定到底拦截哪个方法。
案例1:
当我们查询数据时。返回的这种结果可能不是我们想要的,这时,我们可以对其进行拦截,然后自定义结果。
// 拦截谁?
@Intercepts(@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}
))
public class CamelInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
List<Object> list = (List<Object>) invocation.proceed(); // proceed 查询到的结果
list.forEach(item -> { // 遍历list
if (item instanceof Map) { // 如果是个map
Map<String, Object> map = (Map<String, Object>) item;
HashSet<String> keys = new HashSet<>(map.keySet());
for (String key : keys) {
Object value = map.get(key);
map.remove(key);
map.put(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL,key),value ); // 使用谷歌的工具类把下划线命名转为驼峰命名
}
}
});
return list;
}
}
在Mybatis配置文件中使用我们刚刚配置的插件:
最后的结果:
案例2: 自定义分页插件
@Intercepts(@Signature(
type = Executor.class, /*拦截executor 执行器*/
method = "query", /*拦截query方法 当执行查询操作时,就会被拦截下来*/
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} /*query 有一个重载方法 需要通过参数锁定*/
))
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
/*Invocation(Object target, Method method, Object[] args)
* args[0] ---> MappedStatement
* args[1] ---> 拦截到方法的参数
* args[2] ---> Rowbounds 默认会有一个Rowbounds ,传的话就用传的那个Rowbound 不传的话就用默认的
* args[3] ---> ResultHandler
* */
Object[] args = invocation.getArgs(); // 获取参数
MappedStatement ms = (MappedStatement) args[0]; // 获取到方法对应的mapper文件里面写的那些 执行语句
Object parameterObject = args[1]; // 获取到拦截的方法的参数
System.out.println("parameterObject = " + parameterObject);
RowBounds rowBounds = (RowBounds) args[2]; // mybatis所有的查询都有一个默认的RowBounds
// 如果不等于 就是 需要分页
if (rowBounds != RowBounds.DEFAULT) {
Executor executor = (Executor) invocation.getTarget(); // 拿到执行器
BoundSql boundSql = ms.getBoundSql(parameterObject); // 拿到拦截下来的那条sql 以及那条sql的参数
System.out.println("boundSql.toString() = " + boundSql.toString());
/*
BoundSql 对象里有一个参数 : additionalParameters 这个参数里面放的是 sql的一些额外参数
BoundSql 没有直接提供这个属性 ,所以需要通过反射获取
* */
Field additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
additionalParametersField.setAccessible(true);
Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
Set<String> strings = additionalParameters.keySet();
for (String string : strings) {
System.out.println(string + "---" + additionalParameters.get(string));
}
// 创建出缓存的key 当前配置的key
CacheKey cacheKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 防止二级缓存失效
// cacheKey.update();
// 重新组装sql 在原来的sql上加上分页
String pageSql = boundSql.getSql() + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);
// 需要把原来的额外参数 放到新的pageBoundSql 里面
Set<String> keySet = additionalParameters.keySet();
keySet.forEach(key ->{
pageBoundSql.setAdditionalParameter(key,additionalParameters.get(key));
});
// 执行查询
List list = executor.query(ms, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], cacheKey, pageBoundSql);
return list;
}
return invocation.proceed();
}
}
<!--配置拦截器-->
<plugins>
<!----><plugin interceptor="com.java.lang.plugin.CamelInterceptor"></plugin>
<plugin interceptor="com.java.lang.plugin.PageInterceptor"></plugin>
</plugins>