浅谈Mybatis的插件机制
关于MyBatis
在日常的开发中,操作数据库是非常频繁的操作,而一个优秀的ORM框架的作用是能帮助开发人员简化数据库操作,提高开发效率的。
MyBatis 就是一款优秀且高效的orm框架,他的前身是iBATIS。iBATIS最初是由Clinton Begin在2002年创建的,在2005年,iBATIS成为Apache基金会的顶级项目,并改名为MyBatis。MyBatis在iBATIS的基础上进行了许多改进和优化,例如支持更多的数据源、动态SQL、缓存等功能。MyBatis的目标是提供一种简单、灵活和高效的持久化解决方案,以帮助开发人员更加轻松地进行数据库编程。
关于MyBatis的常规使用和操作此处就不展开了,有兴趣的读者可以自行了解。下文将重点介绍Mybatis的插件机制
MyBatis插件的作用
MyBatis的插件给我们提供了一种扩展机制,使我们能在MyBatis运行过程中拦截某些操作,然后进行自定义的处理。例如拦截SQL语句的执行、参数的设置、结果集的处理等等,MyBatis的插件可使用于以下场景:
1 扩展功能
MyBatis插件可以对MyBatis进行自定义的扩展,例如添加一些自定义的功能或者对MyBatis的功能进行优化。
2 监控性能
MyBatis插件可以拦截SQL语句的执行,统计SQL语句的执行时间等信息,从而可以对系统的性能进行监控和优化。
3 安全检查
MyBatis插件可以拦截SQL语句的执行,对SQL语句进行安全性检查,从而可以防止SQL注入等安全问题。
4 数据库路由
MyBatis插件可以根据不同的条件,将SQL语句路由到不同的数据库中执行,从而可以实现多数据源的支持。
MyBatis插件的使用
我们在代码里要使用MyBatis的插件,只需要实现接口:org.apache.ibatis.plugin.Interceptor 即可,此接口有三个方法,相关说明如下:
public interface Interceptor {
/**
*
* 实现插件功能的方法,
* 在真正的方法sql逻辑执行前、执行后或执行异常时执行一些自定义的逻辑。
* 入参invocation封装了当前方法的信息,包括方法名、参数、目标对象等。
*
*/
Object intercept(Invocation invocation) throws Throwable;
/**
*
* 该方法用于在插件初始化时设置插件的属性。
* MyBatis会在加载插件时调用该方法,并将插件的属性值通过Properties对象传递给插件。
*
*/
default void setProperties(Properties properties) {
// NOP
}
/**
*
* 返回代理原有对象的代理类,每个被拦截的对象都必须有代理类
* 默认使用Plugin.wrap生成代理类,有特殊需要,也可以自行生成
*
*/
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
在实现了自己的Interceptor类后,我们还需要声明这个Interceptor需要拦截哪些方法,示例如下
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
public class MyInterceptor implements Interceptor{
....
}
上述的MyInterceptor插件的声明,就是代表拦截了所有的查询语句,亦即所有查询语句都得进入MyInterceptor做处理。 @Signature注解的type代表的是类,method是此类的方法名,args即是方法参数,三个组合起来就能唯一标识一个具体的接口方法了。
@Intercepts 只能拦截mybatis自身的接口方法,一般用于拦截Executor接口,但是如有需要,也可用于拦截以下的接口:
- Executor接口
- StatementHandler接口
- ParameterHandler接口
- ResultSetHandler接口
最后,我们需要把自己的插件注册到mybatis里,并指定执行的顺序
mybatis的插件是严格按照声明的顺序执行的,先声明的插件先执行。
如果mybatis是用xml的形式配置的话,可以通过plugins标签声明自己的插件及其顺序
<plugins>
<plugin interceptor="example.FirstPlugin"/>
<plugin interceptor="example.SecondPlugin"/>
</plugins>
上述声明了两个插件,且执行顺序是firstPlugin先执行,secondPlugin后执行。
如果mybatis是和spring整合的话,则还可以通过以下方法声明插件
@Configuration
public class LoadPluginsConf{
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@PostConstruct
public void addPlugins() {
Interceptor interceptor1 = new FirstPlugin();
Interceptor interceptor2 = new SecondPlugin();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(interceptor1);
sqlSessionFactory.getConfiguration().addInterceptor(interceptor2);
}
}
}
上述方法也实现了插件的注册,同时,执行顺序也是firstPlugin先执行,secondPlugin后执行。
MyBatis插件的实现原理
在我们自定义的插件成功工作后,我们还需要进一步去了解插件是怎样工作的,Mybatis在这个过程中帮我们做了什么事情。
拦截器链:InterceptorChain
在我们成功注册一个插件时,如上文的声明示例,其本质是调用了org.apache.ibatis.session.Configuration#addInterceptor方法,而Configuration#addInterceptor做的事情,就是把此插件加入到InterceptorChain里,此InterceptorChain是org.apache.ibatis.session.Configuration的内部对象。
拦截器链InterceptorChain的实现如下:
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
如上代码,addInterceptor 和 getInterceptors 很好理解,一个是把插件加入到拦截器链,另一个是获取当前拦截器链中的所有插件。
而pluginAll方法则是生成最终代理类的关键,如上文,每个插件都有自己的plugin方法用于生成目标对象的代理类,而拦截器链的pluginAll 则是为目标对象生成当前所有生效插件的统一代理类,他的实现方法是在for循环里,根据声明顺序,逐个调用插件的plugin生成的,亦即插件A为目标对象生成了代理对象A1,然后把A1作为新的目标对象,生成插件B的代理对象B1,以此类推,直到所有插件都生成完成为止。
有了上面的介绍,后面的处理就很清晰了,mybatis几个重要的内部对象:Executor、ResultSetHandler、StatementHandler、ParameterHandler 都是通过org.apache.ibatis.session.Configuration的以下方法生成的:
- newExecutor
- newResultSetHandler
- newStatementHandler
- newParameterHandler
而这些方法的实现都会调用InterceptorChain.pluginAll生成代理对象,从而实现插件功能,这也就呼应了上文的为啥@Intercepts只能拦截这几个类的实现方法的原因了。
代理类的生成原理
上文说到,插件的代理对象默认是通过Plugin.wrap方法生成的,现在我们来探讨下这个方法的原理: 我们先看下Plugin.wrap的源码:
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
上面的代码执行逻辑可以表示为:
1 获取此插件对象的signatureMap,亦即我们声明插件时的@Intercepts及@Signature注解
2 获取当前需要代理的类是否符合插件的注解
3 如果符合的话,interfaces.length > 0 ,使用jdk 动态代理生成代理类
4 如果不符合的话,interfaces.length = 0,不生成代理类,直接返回原对象
说明:Executor、ResultSetHandler、StatementHandler、ParameterHandler 这几个都是接口类,所以能使用动态代理生成代理对象,JDK的动态代理此处就不展开了,有兴趣的读者可自行了解
简单的示例
例1:统计查询SQL耗时
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
public CostTimePlugin implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable{
Object target = invocation.getTarget();
if (target instanceof RoutingStatementHandler) {
RoutingStatementHandler statementHandler = (RoutingStatementHandler) target;
StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(statementHandler, "delegate");
BoundSql boundSql = delegate.getBoundSql();
String sql = getSql(boundSql);
long beginTime = System.currentTimestamp();
Object targetResult = invocation.proceed();
log.info("sql={}的执行时间:{}ms", sql, System.currentTimestamp() - beginTime );
return targetResult;
}else{
return invocation.proceed();
}
}
private String getSql(BoundSql boundSql) {
String sql = boundSql.getSql();
//此处可以对sql进行一些美化工作
return sql
}
}
例2: 简单的分页查询
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement mappedStatement = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
if (mappedStatement.getSqlCommandType() != SqlCommandType.SELECT) {
return invocation.proceed();
}
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
String sql = boundSql.getSql().trim();
//拼接查询数量的sql,实际可能需要考虑不同数据库的写法
String countSql = "select count(*) from (" + sql + ") t";
Connection connection = mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection();
PreparedStatement countStmt = connection.prepareStatement(countSql);
BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
setParameters(countStmt, mappedStatement, countBS, parameter);
ResultSet rs = countStmt.executeQuery();
int totalCount = 0;
if (rs.next()) {
totalCount = rs.getInt(1);
}
rs.close();
countStmt.close();
connection.close();
this.totalCount = totalCount;
//page 和 pageSize在实际使用时动态获取,此处只是演示代码,所以固定为1和10
int page = 1;
int pageSize = 10;
//拼接分页查询sql,实际可能需要考虑不同数据库的写法
String pageSql = sql + " limit " + (page-1)*pageSize + " " + pageSize;
args[2] = new RowBounds(RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT);
args[0] = copyFromMappedStatement(mappedStatement, new BoundSql(mappedStatement.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter));
return invocation.proceed();
}
private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql, Object parameter) throws SQLException {
ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameter, boundSql);
parameterHandler.setParameters(ps);
}
private MappedStatement copyFromMappedStatement(MappedStatement ms, BoundSql boundSql) {
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId() + "_cnt", 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());
builder.resultMaps(ms.getResultMaps());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
return builder.build();
}
}