MyBatis提供了插件机制,我们可以在sql语句执行过程中拦截,进行自定义扩展。
常用的拦截对象
时序图,在整个时序图中,涉及到mybatis插件部分已标红,基本上就是体现在上文中提到的四个类上,对这些类上的方法进行拦截
org.apache.ibatis.plugin.Interceptor
可以实现对目标类的拦截
public interface Interceptor {
Object intercept(Invocation var1) throws Throwable;
Object plugin(Object var1);
void setProperties(Properties var1);
}
该方法用来接收参数,我们可以在全局配置文件中配置,如下:
<plugins>
<plugin interceptor="tk.mybatis.simple.plugin.PageInterceptor">
<property name="dialect" value="tk.mybatis.simple.plugin.MySqlDialect"/>
plugin>
plugins>
这个方法的参数target就是拦截器要拦截的对象
,该方法会在创建
被拦截的接口实现类
时被调用。该方法的实现很简单,只需要调用MyBatis 提供的Plugin类的wrap 静态方法就可以通过Java 的动态代理拦截目标对象。这个接口方法通常的实现代码如下。
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
Plugin .wrap 方法
会自动判断拦截器的签名
和被拦截对象的接口是否匹配
,只有匹配的情况下才会使用动态代理拦截目标对象,因此在上面的实现方法中不必做额外的逻辑判断。
最重要的方法,自定义逻辑一般都写在这里
我们可以通过Invocation对象获取到被拦截的对象、当前被调用的方法、方法的传参或者在处理后调用原方法(就是和动态代理、Aspect类似)
当一个我们设置了多个拦截器时,MyBatis会遍历所有的拦截器,按顺序!!!
执行拦截器的plugin方法。签名匹配会被代理。
如果一个对象被多个拦截器比如A、B、C代理,则执行顺序是:C>B>A>target.proceed()>A>B>C
(一般学过过滤器都可以理解这种顺序)
我们可以使用签名来决定拦截器拦截哪些对象(作用类似Aspect中的@Pointcut指定连接点
)
如下所示:
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class
})
})
public class ResultSetInterceptor implements Interceptor{
1 @Intercepts 注解中
的属性是一个@Signature (签名)数组
,可以在同一个拦截器中同时拦截不同的接口和方法。
2. @Signature 注解包含以下三个属性
+ type :设置拦截的接口,可选值是前面提到的4 个接口
+ method:设置拦截接口中的方法名,可选值是前面4 个接口对应的方法,需要和接口匹配
+ args :设置拦截方法的参数类型数组,通过方法名和参数类型可以确定唯一一个方法(处理重载
)
下面简单记录下前面提到的4个接口的常用方法
<E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;
该方法会在所有SELECT 查询方法执行时被调用。通过这个接口参数可以获取很多有用的
信息,因此这是最常被拦截的一个方法。
<E> Cursor<E> queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException;
该方法只有在查询的返回值类型为Cursor 时被调用。暂时还没用过Cursor
List<BatchResult> flushStatements() throws SQLException;
该方法只在通过SqlSession 方法调用flus hStatements 方法或执行的接口方法中带有@Flush 注解时才被调用
Object getParameterObject();
该方法只在执行存储过程
处理出参的时候被调用
void setParameters(PreparedStatement ps)
throws SQLException;
该方法在所有数据库方法设置SQL 参数时被调用
<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
该方法是3.4.0 版本中新增加的,只会在返回值类型为Cursor < T >的查询方法中被调用
handleOutputParameters
该方法只在使用存储过程处理出参时被调用
Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException;
该方法会在数据库执行前被调用,优先于当前接口中的其他方法而被执行
void parameterize(Statement statement)
throws SQLException;
该方法在prepare 方法之后执行,用于处理参数信息
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
执行SELECT 方法时调用
代码直接取之参考资料
package tk.mybatis.simple.plugin;
import java.sql.Statement;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
/**
* MyBatis Map 类型下划线 Key 转小写驼峰形式
*
* @author liuzenghui
*/
@Intercepts(
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
)
@SuppressWarnings({ "unchecked", "rawtypes" })
public class CameHumpInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//先执行得到结果,再对结果进行处理
List<Object> list = (List<Object>) invocation.proceed();
for(Object object : list){
//如果结果是 Map 类型,就对 Map 的 Key 进行转换
if(object instanceof Map){
processMap((Map)object);
} else {
break;
}
}
return list;
}
/**
* 处理 Map 类型
*
* @param map
*/
private void processMap(Map<String, Object> map) {
Set<String> keySet = new HashSet<String>(map.keySet());
for(String key : keySet){
//大写开头的会将整个字符串转换为小写,如果包含下划线也会处理为驼峰
if((key.charAt(0) >= 'A' && key.charAt(0) <= 'Z') || key.indexOf("_") >= 0){
Object value = map.get(key);
map.remove(key);
map.put(underlineToCamelhump(key), value);
}
}
}
/**
* 将下划线风格替换为驼峰风格
* 处理思路就是如果一个字符的前一个位置是下划线,则将下一个字符放入sb中
*
* @param inputString
* @return
*/
public static String underlineToCamelhump(String inputString) {
StringBuilder sb = new StringBuilder();
boolean nextUpperCase = false;
for (int i = 0; i < inputString.length(); i++) {
char c = inputString.charAt(i);
if(c == '_'){
if (sb.length() > 0) {
nextUpperCase = true;
}
} else {
if (nextUpperCase) {
sb.append(Character.toUpperCase(c));
nextUpperCase = false;
} else {
sb.append(Character.toLowerCase(c));
}
}
}
return sb.toString();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
invocation . proceed ()执行的结果被强制转换为了List 类型。这是因为拦截器接口ResultSetHandler 的handleResultSets 方法的返回值为List 类型
,所以才能在这里直接强制转换。
如果不知道这一点,就很难处理这个返回值。许多接口方法的返回值类型都是List ,但是还有很多其他的类型,所以在写拦截器时,要根据被拦截的方法来确定返回值的类型!!!。
在MyB ati s 拦截器中,最常用的一种就是实现分页插件。如果不使用分页插件来实现分页功能,就需要自己在映射文件的SQL 中增加分页条件,井且为了获得数据的总数还需要额外增加一个count 查询的SQL ,写起来很麻烦。如果要兼容多种数据库,可能要根据databaseId来写不同的分页SQL
,不仅写起来麻烦,也会让SQL 变得脆肿不堪.
下面的插件是由参考书籍的作者实现的,该插件的核心部分由两个类组成
,PageInterceptor 拦截器类和数据库方言接口Dialect
package tk.mybatis.simple.plugin;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* Mybatis - 通用分页拦截器
*
* @author liuzh
* @version 1.0.0
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Intercepts(
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class}
)
)
public class PageInterceptor implements Interceptor {
private static final List<ResultMapping> EMPTY_RESULTMAPPING
= new ArrayList<ResultMapping>(0);
private Dialect dialect;
private Field additionalParametersField;
@Override
public Object intercept(Invocation invocation) throws Throwable {
//获取拦截方法的参数
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameterObject = args[1];
RowBounds rowBounds = (RowBounds) args[2];
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms.getId(), parameterObject, rowBounds)) {
ResultHandler resultHandler = (ResultHandler) args[3];
//当前的目标对象
Executor executor = (Executor) invocation.getTarget();
BoundSql boundSql = ms.getBoundSql(parameterObject);
//反射获取动态参数
Map<String, Object> additionalParameters =
(Map<String, Object>) additionalParametersField.get(boundSql);
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms.getId(), parameterObject, rowBounds)){
//根据当前的 ms 创建一个返回值为 Long 类型的 ms
MappedStatement countMs = newMappedStatement(ms, Long.class);
//创建 count 查询的缓存 key
CacheKey countKey = executor.createCacheKey(
countMs,
parameterObject,
RowBounds.DEFAULT,
boundSql);
//调用方言获取 count sql
String countSql = dialect.getCountSql(
boundSql,
parameterObject,
rowBounds,
countKey);
BoundSql countBoundSql = new BoundSql(
ms.getConfiguration(),
countSql,
boundSql.getParameterMappings(),
parameterObject);
//当使用动态 SQL 时,可能会产生临时的参数,这些参数需要手动设置到新的 BoundSql 中
for (String key : additionalParameters.keySet()) {
countBoundSql.setAdditionalParameter(
key, additionalParameters.get(key));
}
//执行 count 查询
Object countResultList = executor.query(
countMs,
parameterObject,
RowBounds.DEFAULT,
resultHandler,
countKey,
countBoundSql);
Long count = (Long) ((List) countResultList).get(0);
//处理查询总数
dialect.afterCount(count, parameterObject, rowBounds);
if(count == 0L){
//当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(
new ArrayList(),
parameterObject,
rowBounds);
}
}
//判断是否需要进行分页查询
if (dialect.beforePage(ms.getId(), parameterObject, rowBounds)){
//生成分页的缓存 key
CacheKey pageKey = executor.createCacheKey(
ms,
parameterObject,
rowBounds,
boundSql);
//调用方言获取分页 sql
String pageSql = dialect.getPageSql(
boundSql,
parameterObject,
rowBounds,
pageKey);
BoundSql pageBoundSql = new BoundSql(
ms.getConfiguration(),
pageSql,
boundSql.getParameterMappings(),
parameterObject);
//设置动态参数
for (String key : additionalParameters.keySet()) {
pageBoundSql.setAdditionalParameter(
key, additionalParameters.get(key));
}
//执行分页查询
List resultList = executor.query(
ms,
parameterObject,
RowBounds.DEFAULT,
resultHandler,
pageKey,
pageBoundSql);
return dialect.afterPage(resultList, parameterObject, rowBounds);
}
}
//返回默认查询
return invocation.proceed();
}
/**
* 根据现有的 ms 创建一个新的,使用新的返回值类型
*
* @param ms
* @param resultType
* @return
*/
public MappedStatement newMappedStatement(
MappedStatement ms, Class<?> resultType) {
MappedStatement.Builder builder = new MappedStatement.Builder(
ms.getConfiguration(),
ms.getId() + "_Count",
ms.getSqlSource(),
ms.getSqlCommandType()
);
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
if (ms.getKeyProperties() != null
&& ms.getKeyProperties().length != 0) {
StringBuilder keyProperties = new StringBuilder();
for (String keyProperty : ms.getKeyProperties()) {
keyProperties.append(keyProperty).append(",");
}
keyProperties.delete(
keyProperties.length() - 1, keyProperties.length());
builder.keyProperty(keyProperties.toString());
}
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
//count查询返回值int
List<ResultMap> resultMaps = new ArrayList<ResultMap>();
ResultMap resultMap = new ResultMap.Builder(
ms.getConfiguration(),
ms.getId(),
resultType,
EMPTY_RESULTMAPPING).build();
resultMaps.add(resultMap);
builder.resultMaps(resultMaps);
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
return builder.build();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
String dialectClass = properties.getProperty("dialect");
try {
dialect = (Dialect) Class.forName(dialectClass).newInstance();
} catch (Exception e) {
throw new RuntimeException(
"使用 PageInterceptor 分页插件时,必须设置 dialect 属性");
}
dialect.setProperties(properties);
try {
//反射获取 BoundSql 中的 additionalParameters 属性
additionalParametersField = BoundSql.class.getDeclaredField(
"additionalParameters");
additionalParametersField.setAccessible(true);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
}
方法分析
当前方法的BoundSql
,这个对象中包含了要执行的SQL 和对应的参数。通过这个对象的SQL 和参数生成一个count 查询的BoundSql
,由于这种情况下的MappedStatement 对象中的resultMap 或resultType 类型为当前查询结果的类型,并不适合返回count 查询值,因此通过newMappedStatement 方法根据当前的MappedStatement 生成了一个返回值类型为Long 的对象
,然后通过Executor 执行查询,得到了数据总数同count 查询类似
,得到分页数据的结果后,通过dialect 对结果进行处理并返回。这只是一个接口,不同数据库需要有不同的实现。
package tk.mybatis.simple.plugin;
import java.util.List;
import java.util.Properties;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.session.RowBounds;
/**
* 数据库方言,针对不同数据库进行实现
*
* @author liuzh
*/
@SuppressWarnings("rawtypes")
public interface Dialect {
/**
* 跳过 count 和 分页查询
*
* @param msId 执行的 MyBatis 方法全名
* @param parameterObject 方法参数
* @param rowBounds 分页参数
* @return true 跳过,返回默认查询结果,false 执行分页查询
*/
boolean skip(String msId, Object parameterObject, RowBounds rowBounds);
/**
* 执行分页前,返回 true 会进行 count 查询,false 会继续下面的 beforePage 判断
*
* @param msId 执行的 MyBatis 方法全名
* @param parameterObject 方法参数
* @param rowBounds 分页参数
* @return
*/
boolean beforeCount(String msId, Object parameterObject, RowBounds rowBounds);
/**
* 生成 count 查询 sql
*
* @param boundSql 绑定 SQL 对象
* @param parameterObject 方法参数
* @param rowBounds 分页参数
* @param countKey count 缓存 key
* @return
*/
String getCountSql(BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey);
/**
* 执行完 count 查询后
*
* @param count 查询结果总数
* @param parameterObject 接口参数
* @param rowBounds 分页参数
*/
void afterCount(long count, Object parameterObject, RowBounds rowBounds);
/**
* 执行分页前,返回 true 会进行分页查询,false 会返回默认查询结果
*
* @param msId 执行的 MyBatis 方法全名
* @param parameterObject 方法参数
* @param rowBounds 分页参数
* @return
*/
boolean beforePage(String msId, Object parameterObject, RowBounds rowBounds);
/**
* 生成分页查询 sql
*
* @param boundSql 绑定 SQL 对象
* @param parameterObject 方法参数
* @param rowBounds 分页参数
* @param pageKey 分页缓存 key
* @return
*/
String getPageSql(BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey);
/**
* 分页查询后,处理分页结果,拦截器中直接 return 该方法的返回值
*
* @param pageList 分页查询结果
* @param parameterObject 方法参数
* @param rowBounds 分页参数
* @return
*/
Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds);
/**
* 设置参数
*
* @param properties 插件属性
*/
void setProperties(Properties properties);
}
PageRowBounds 扩展了RowBounds
/**
* 可以记录 total 的分页参数
*
* @author liuzh
*/
public class PageRowBounds extends RowBounds{
private long total;
public PageRowBounds() {
super();
}
public PageRowBounds(int offset, int limit) {
super(offset, limit);
}
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
}
}
/**
* MySql 实现
*
* @author liuzh
*/
@SuppressWarnings("rawtypes")
public class MySqlDialect implements Dialect {
@Override
public boolean skip(String msId, Object parameterObject, RowBounds rowBounds) {
//这里使用 RowBounds 分页,默认没有 RowBounds 参数时,会使用 RowBounds.DEFAULT 作为默认值
if(rowBounds != RowBounds.DEFAULT){
return false;
}
return true;
}
@Override
public boolean beforeCount(String msId, Object parameterObject, RowBounds rowBounds) {
//只有使用 PageRowBounds 才能记录总数,否则查询了总数也没用
if(rowBounds instanceof PageRowBounds){
return true;
}
return false;
}
@Override
public String getCountSql(BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) {
//简单嵌套实现 MySql count 查询
return "select count(*) from (" + boundSql.getSql() + ") temp";
}
@Override
public void afterCount(long count, Object parameterObject, RowBounds rowBounds) {
//记录总数,按照 beforeCount 逻辑,只有 PageRowBounds 时才会查询 count,所以这里直接强制转换
((PageRowBounds)rowBounds).setTotal(count);
}
@Override
public boolean beforePage(String msId, Object parameterObject, RowBounds rowBounds) {
if(rowBounds != RowBounds.DEFAULT){
return true;
}
return false;
}
@Override
public String getPageSql(BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
//pageKey 会影响缓存,通过固定的 RowBounds 可以保证二级缓存有效
pageKey.update("RowBounds");
return boundSql.getSql() + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();
}
@Override
public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
return pageList;
}
@Override
public void setProperties(Properties properties) {
}
}
@Test
public void testSelectAllByRowBounds(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//查询前两个,使用 RowBounds 类型不会查询总数
RowBounds rowBounds = new RowBounds(0, 2);
List<SysUser> list = userMapper.selectAll(rowBounds);
for(SysUser user : list){
System.out.println("用户名:" + user.getUserName());
}
//使用 PageRowBounds 会查询总数
PageRowBounds pageRowBounds = new PageRowBounds(2, 2);
list = userMapper.selectAll(pageRowBounds);
//获取总数
System.out.println("查询总数:" + pageRowBounds.getTotal());
for(SysUser user : list){
System.out.println("用户名2:" + user.getUserName());
}
//再次查询
pageRowBounds = new PageRowBounds(4, 2);
list = userMapper.selectAll(pageRowBounds);
//获取总数
System.out.println("查询总数:" + pageRowBounds.getTotal());
for(SysUser user : list){
System.out.println("用户名3:" + user.getUserName());
}
} finally {
sqlSession.close();
}
sqlSession = getSqlSession();
try {
System.out.println("-----------测试二级缓存------------");
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//查询前两个,使用 RowBounds 类型不会查询总数
RowBounds rowBounds = new RowBounds(0, 2);
List<SysUser> list = userMapper.selectAll(rowBounds);
for(SysUser user : list){
System.out.println("用户名:" + user.getUserName());
}
//使用 PageRowBounds 会查询总数
PageRowBounds pageRowBounds = new PageRowBounds(2, 2);
list = userMapper.selectAll(pageRowBounds);
//获取总数
System.out.println("查询总数:" + pageRowBounds.getTotal());
for(SysUser user : list){
System.out.println("用户名2:" + user.getUserName());
}
//再次查询
pageRowBounds = new PageRowBounds(4, 2);
list = userMapper.selectAll(pageRowBounds);
//获取总数
System.out.println("查询总数:" + pageRowBounds.getTotal());
for(SysUser user : list){
System.out.println("用户名3:" + user.getUserName());
}
} finally {
sqlSession.close();
}
}
MyBatis的默认分页是逻辑分页
在 sql 查询出所有结果的基础上截取数据的,所以在数据量大的sql中并不适用,它更适合在返回数据结果较少的查询中使用。测试,关闭插件,查看上面的测试代码
将查询改为
RowBounds rowBounds = new RowBounds(1, 1);
日志还是查询出了两条记录,缺点很明显
DEBUG [main] - Cache Hit Ratio [zyc.mybatis.simple.mapper.UserMapper]: 0.0
DEBUG [main] - ==> Preparing: select id, user_name userName, user_password userPassword, user_email userEmail, user_info userInfo, head_img headImg, create_time createTime from sys_user
DEBUG [main] - ==> Parameters:
TRACE [main] - <== Columns: id, userName, userPassword, userEmail, userInfo, headImg, createTime
TRACE [main] - <== Row: 1, admin, 123456, [email protected], <<BLOB>>, <<BLOB>>, 2016-06-07 01:11:12
TRACE [main] - <== Row: 1001, test, 123456, [email protected], <<BLOB>>, <<BLOB>>, 2016-06-07 00:00:00