Mybatis插件介绍
为了提高Mybatis的可扩展能力,Mybatis引入的插件机制,允许开发人员通过责任链编程的方式对目标类进行代理。通过阅读源码可知,Mybatis允许开发人员对Mybatis中的Executor
、ParameterHandler
、StatementHandler
、ResultHandler
进行代理。有一点AOP的味道。
Interceptor(拦截器)
Mybatis通过Interceptor来定义一个拦截器,Interceptor在Mybatis中是一个很重要的接口,通过这个接口我们可以对前面提高的四个类进行拦截,从而可以在这些类方法执行前后加入我们写的拦截逻辑。下面我们来看看Interceptor是如何定义的:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
这个类定义的三个方法:
- intercept:这个方法用于实现主要的拦截逻辑。方法的入参是一个
Invocation
类的实例,Invocation
代表目标类的一次调用,通过它我们可以获得被拦截的目标类,被拦截的方法以及方法的入参。intercept方法放回一个Object
实例。 - plugin:这个方法用来判定是否要对目标类进行拦截,也就是说是否返回被拦截类本身还是返回被拦截类的一个代理。这个方法可以与Mybatis中的
Plugin
类配合使用,有关Plugin
类的用法后面会讲到。 - setProperties:这个方法主要用来获取在Mybatis中配置的系统属性。
Plugin
我们在描述Interceptor
中的plugin方法时,我们提到过该类,这个类主要的作用是通过在目标类上使用注解的方式来决定是否要对目标类进行代理。'Talk is cheep, show me the code.',下面我们就来看看Plugin
的实现:
通过Plugin类的签名我们可以看到这个类实现了InvocationHandler
接口,并且定义了target、interceptor、signatureMap三个实例变量。
wrap方法传入一个目标对象和一个拦截器对象,首先通过getAllInterfaces这个方法获取目标类所时间的接口,然后根据接口的数量来决定是否要目标的一个代理。
Plugin中invoke方法首先获取目标类被拦截的方法,如果是被拦截的方法则调用拦截器的intercept方法,所以要使拦截器链继续沿着链路调用下去,则在拦截器中需要调用Invocation.proceed的方法。
getSignatureMap主要用来获取拦截器上的
Intercepts
注解,根据注解的值来获取拦截器想拦截的目标对象。我们可以看一下Intercepts
的定义:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts { Signature[] value();}
Signature
的定义:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Signature {
Class<?> type(); String method(); Class<?>[] args();
}
说了这么多有关Mybatis插件的实现,那我们应该如何使用Mybatis的插件呢?
Interceptor的使用
下面我们实现一个简单拦截器HelloWorldIntercetor
:
/** * 对StatementHandler的prepare方法进行拦截 */
@Intercepts(
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}))
public class HelloWorldInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("I'm HelloWorld Interceptor");
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
System.out.println(properties.get("prop1"));
}
}
然后在Mybatis的的配置文件中配置:
<properties>
<property name="prop1" value="prop1"/>
</properties>
<plugins>
<plugin interceptor="mybatis.interceptor.HelloWorldInterceptor" /
</plugins>
如与Spring集成的话则在配置SqlSessionFactoryBean
时进行配置如下:
<property name="plugins">
<array>
<bean class="mybatis.interceptor.HelloWorldInterceptor" />
</array>
</property>
<property name="configurationProperties">
<props>
<prop key="prop1">prop1</prop>
</props>
</property>
通过前面的讲解我们大致了解了Mybatis插件的配置使用和原理。下面我们就来看看如何写一个Mybatis的分页插件呢?
Mybatis分页插件
为什么要写一个分页插件呢?我们在写分页查询的时候系要写列表查询又要写总量查询,而且这两个查询的SQL很相似,在写这一类查询的时候有点浪费时间。
在我们了解了Mybatis的插件机制后,写一个分页插件就比较容易了,主要的思路是根据查询入参对查询SQL进行改写。下面我就直接上分页插件的源代码了。
/** * 分页拦截器 */
// 对StatementHandler的prepare方法进行拦截。
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
RoutingStatementHandler target = (RoutingStatementHandler)invocation.getTarget();
BoundSql boundSql = target.getBoundSql();
Object paramObject = boundSql.getParameterObject();
if (hasPagerParam(paramObject)) {
Pager pager = getPagerParam(paramObject);
String sql = boundSql.getSql();
pager.setTotal(getTotal(invocation, target));
String pageSql = getPageSql(sql, pager);
FieldUtils.setFieldValue(boundSql, "sql", pageSql);
}
return invocation.proceed();
}
private String getPageSql(String sql, Pager pager) {
return String.format("%s LIMIT %d, %d", sql, pager.getIndex(), pager.getSize() );
}
private boolean hasPagerParam(Object paramObject) {
if (paramObject instanceof Pager) {
return true;
}
if (paramObject instanceof Map) {
Map<Object, Object> map = (Map<Object, Object>)paramObject;
for (Object value : map.values()) {
if (value instanceof Pager) {
return true;
}
}
}
return false;
}
private Pager getPagerParam(Object paramObject) {
if (paramObject instanceof Pager) {
return (Pager)paramObject;
}
if (paramObject instanceof Map) {
Map<Object, Object> map = (Map<Object, Object>)paramObject;
for (Object value : map.values()) {
if (value instanceof Pager) {
return (Pager)value;
}
}
}
return null;
}
private int getTotal(Invocation invocation, RoutingStatementHandler statementHandler) throws SQLException {
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
String countSql = getCountSql(sql);
FieldUtils.setFieldValue(boundSql, "sql", countSql);
Connection connection = (Connection)invocation.getArgs()[0];
Integer timeout = (Integer)invocation.getArgs()[1];
Statement statement = statementHandler.prepare(connection, timeout);
if (statement instanceof PreparedStatement) {
PreparedStatement ps = (PreparedStatement)statement;
ps.execute();
ResultSet rs = ps.getResultSet();
while (rs.next()) {
return rs.getInt(1);
}
} else {
statement.execute(countSql);
ResultSet rs = statement.getResultSet();
while (rs.next()) {
return rs.getInt(1);
}
}
return 0;
}
private String getCountSql(String sql) {
return String.format("SELECT COUNT(1) FROM(%s) _t", sql);
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {}
}
该分页插件的实现方式是根据查询参数中是否有Pager
对象来判定是否应该进行分页查询,只支持mysql dialect,使用起来有一定的局限性,当然要扩展支持其他的数据库也是比较容易的事情,若我们使用的是Mysql数据库,且对性能要求不是很高的情况下,为何不使用一下分页插件呢?