插件的实现就是使用动态代理。将mybytis四大对象(Executor、StatementHandler 、ParameterHandler、ResultHandler)在构造以后利用动态代理悄悄的将其代理,插入一些自己的逻辑,这里的动态代理是对实现类的动态代理,而不是像Dao层接口这种的直接对一个接口类的动态代理,理解起来也相对容易些。
插件实现过程中的一些关键类
(1)Interceptor 自定义插件接口类,所有Mybatis插件必须实现这个类,统一管理,注册在mybatis_config里,会在初始化时候就加载到Configuration类里。(2)InterceptorChain 插件的缓存类,所有的插件都初始化到这个类中的一个缓存list中,四大对象构造时候将其包装
(3)Plugin Mybatis提供的一个实现InvocationHandler类,方便应用,统一到这里的invoke方法处理
比较简单, 基本上就这么三个关键类。
下面用一个经常使用的Mybats的分页插件为例子讲解插件从初始化到代理四大对象的整个过程:
@Intercepts(value=
{@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class}),
})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("PageInterceptor -- intercept");
if (invocation.getTarget() instanceof StatementHandler) {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, new DefaultObjectFactory(),
new DefaultObjectWrapperFactory());
MappedStatement mappedStatement=(MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
String selectId=mappedStatement.getId();
System.out.println(selectId);
if(selectId.matches(".*Page$"))
{
BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
// 分页参数作为参数对象parameterObject的一个属性
String sql = boundSql.getSql();
Demo co=(Demo)(boundSql.getParameterObject());
// 重写sql
String countSql=concatCountSql(sql);
String pageSql=concatPageSql(sql,co);
System.out.println("重写的 count sql :"+countSql);
System.out.println("重写的 select sql :"+pageSql);
Connection connection = (Connection) invocation.getArgs()[0];
java.sql.PreparedStatement countStmt = null;
ResultSet rs = null;
int totalCount = 0;
try {
countStmt = connection.prepareStatement(countSql);
rs = countStmt.executeQuery();
if (rs.next()) {
totalCount = rs.getInt(1);
}
} catch (SQLException e) {
System.out.println("Ignore this exception"+e);
} finally {
try {
rs.close();
countStmt.close();
} catch (SQLException e) {
System.out.println("Ignore this exception"+ e);
}
}
metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);
//绑定count
co.setTotalRecord(totalCount);
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
// TODO Auto-generated method stub
}
public String concatCountSql(String sql){
StringBuffer sb=new StringBuffer("select count(*) from ");
sql=sql.toLowerCase();
if(sql.lastIndexOf("order")>sql.lastIndexOf(")")){
sb.append(sql.substring(sql.indexOf("from")+4, sql.lastIndexOf("order")));
}else{
sb.append(sql.substring(sql.indexOf("from")+4));
}
return sb.toString();
}
public String concatPageSql(String sql,Page co){
StringBuffer sb=new StringBuffer();
sb.append(sql);
sb.append(" limit ").append((co.getCurrentPage()-1)*co.getPageSize()).append(" , ").append(co.getPageSize());
return sb.toString();
}
public void setPageCount(){
}
}
例子也比较容易理解,实现Mybatis的插件接口,重写他的三个方法,plugin方法实现代理,intercept插入自己的操作,setProperties注入一些自己需要的属性。注解的意思是type表示准备要代理的类,method 代理类中准备要代理方法,args 代理方法的参数。
主要流程:
1.注册interceptor,会在初始化的时候实例化
2.sqlSession中4大对象在实例化的时候会调用pluginAll方法,该方法中会用interceptot创建代理
3.具体的,首先调用interceptor的plugin方法,该方法一般会使用Plugin类的wrap方法,Plugin类实现了invocationHandler,wrap中完成了代理创建
4.Plugin的invoke方法会调用intercptor的intercept方法,植入自己的逻辑,完成插件
5.执行完成以后,调用proceed方法,回归到原来的方法调用中
下面开始源代码的分析:
<plugins>
<plugin interceptor="com.yanzh.PageInterceptor">
</plugin>
</plugins>
第一步将插件注册到Configuration。前面的初始化不说了,直接看进入到对插件节点的解析
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
看一下这个方法,拿到plugins节点,一次性可以注册多个plugin子节点。取到插件以后,将其利用反射实例化,插件的第三个方法会在这里被调用,然后直接注册到了configuration里了。
//Configuration里的注册方法,实际上是将其注入到一个interceptorChain里
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
//interceptorChain里的方法,放到了interceptors缓存里
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
interceptors是一个list,private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); 初始化就结束了,最终就是把插件注入到一个拦截器链的缓存List里。
第二步看缓存中的拦截器是如何被执行的
具体dao方法的执行流程就不讲解了,直接进入插件的相关过程,在sqlSessionFactory获取sqlSession的过程中调用openSession方法,这个流程中会初始化Executor对象。final Executor executor = configuration.newExecutor(tx, execType);就是这句(在DefaultSqlSessionFactory类中)
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
函数的前面是Mybatis几种不同的执行器,初始化的时候可以自己设置,如果不设置,默认即是SimpleExecutor,这些都与插件没有关系,看return前的最后一句,构建出Executor对象以后,不是立即返回,而是经历了pluginAll,玄机就在这里,看函数名也是添加所有插件。
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
看到没有,这里就是把初始化时候缓存的拦截器都拿出来,一层层的包裹这个target对象(Executor),有几个拦截器就代理几次。最终会返回一个被包裹的对象,已经不是本身的Executor的对象了。被代理就是发生在plugin方法里。
直接利用分页拦截器实例讲解,大多数都是这种模式。
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
分页拦截器拦截的是StatementHandler对象,原理是一样的,假如此处的判断条件是target instanceof Executro.直接利用mybatis提供的动态代理工具类来包装target.
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;
}
首先将@signature注解封装在一个map中,然后取到被代理类的.class类型,接着获取被代理类实现的接口,下面就是动态代理实现的真面目了,取接口,取类类型都是为代理准备参数。此处返回的就是一个被代理过的Executor对象。封装注解的函数就不说明了, 其实就是一个解析注解的过程,不熟悉的可以看下自定义注解,就是反射的知识,很容易看懂。那么自己准备要插入的逻辑(intercept函数)是怎么注入到代理中的呢。关键就在Plugin类的invoke方法中。
再啰嗦几句,分析一下动态代理的几个要素,首先被代理类(target,也就是Executor),代理类实现的接口(getAllInterface方法返回了,jdk动态代理必须有接口类),实现InvocationHandler的类(Plugin类)。以后调用被代理类的方法的时候就会自动进入Plugin的invoke方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
先取到注解中要拦截的方法,判断这次调用的被代理的方法是否在被拦截方法之列,不在的话直接反射调用被代理方法,在就调用拦截器的intercept方法,并且将被代理对象,方法,参数都封装在一个Invocation类中。
看一下分页拦截器的方法,简单说一下。
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("PageInterceptor -- intercept");
if (invocation.getTarget() instanceof StatementHandler) {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, new DefaultObjectFactory(),
new DefaultObjectWrapperFactory());
MappedStatement mappedStatement=(MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
String selectId=mappedStatement.getId();
System.out.println(selectId);
if(selectId.matches(".*Page$"))
{
BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
// 分页参数作为参数对象parameterObject的一个属性
String sql = boundSql.getSql();
Demo co=(Demo)(boundSql.getParameterObject());
// 重写sql
String countSql=concatCountSql(sql);
String pageSql=concatPageSql(sql,co);
System.out.println("重写的 count sql :"+countSql);
System.out.println("重写的 select sql :"+pageSql);
Connection connection = (Connection) invocation.getArgs()[0];
java.sql.PreparedStatement countStmt = null;
ResultSet rs = null;
int totalCount = 0;
try {
countStmt = connection.prepareStatement(countSql);
rs = countStmt.executeQuery();
if (rs.next()) {
totalCount = rs.getInt(1);
}
} catch (SQLException e) {
System.out.println("Ignore this exception"+e);
} finally {
try {
rs.close();
countStmt.close();
} catch (SQLException e) {
System.out.println("Ignore this exception"+ e);
}
}
metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);
//绑定count
co.setTotalRecord(totalCount);
}
}
return invocation.proceed();
}
文中经过各种手段从被拦截对象中取到了即将要执行的sql语句和占位符参数,然后调用concatPageSql将分页参数append上去,偷天换日,最后一句又将换过的sql归还到原执行过程,invocation.proceed(),注意这句必须调用,不然归回不到原过程了,里面就是一个元方法的反射调用。
到这里一整套Mybatis的插件就走完了。sqlSession下的四大对象都是这么一个流程。
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
一模一样的流程。插件整体比较简单,整个过程就是一个动态代理的实现过程,只要对动态代理了解,基本上没什么问题,不过还需要一些dao接口执行流程的熟悉知识,不然可能会感到一头雾水。