MyBatis知识Part4(插件)

本文深入解析MyBatis插件机制,介绍如何利用责任链模式和动态代理技术实现拦截器,通过自定义插件修改运行时SQL,限制查询结果行数。

MyBatis四大对象Executor、ParameterHandler、StatementHandler、ResultHandler,在Configuration对象的创建方法里MyBatis用责任链封装它们。

在MyBatis中使用插件,必须实现接口Interceptor
package org.apache.ibatis.plugin;
import java.util.Properties;
public interface Interceptor {
  Object intercept(Invocation invocation) throws Throwable;
  Object plugin(Object target);
  void setProperties(Properties properties);
}
各个方法的作用:
	intercept:将直接覆盖拦截对象原有的方法,因此它是插件的核心方法。intercept里面有个参数Invocation对象,通过它可以反射调用原来对象的方法。
	plugin方法:target是被拦截对象,它的作用是给被拦截对象生成一个代理对象,并返回它。在MyBatis中,它提供了org.apache.ibatis.plugin.Plugin中的wrap静态(static)方法生成代理对象,一般情况下都会使用它来生成代理对象,当然也可以自定义。
	setProperties方法:允许在plugin元素中配置所需参数,方法在插件初始化时就被调用了一次,然后把插件对象存入到配置中。
这是插件的骨架,这样的模式被称为模版(template)模式,就是提供一个骨架,并且告知骨架中的方法是干什么的。
插件的初始化是在Mybatis初始化时完成的通过XMLConfigBuilder的代码:
package org.apache.ibatis.builder.xml;
public class XMLConfigBuilder extends BaseBuilder {
  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);
      }
    }
  }
}
在解析配置文件时,在MyBatis的上下文初始化过程,就开始读入插件结点和配置的参数,同时使用反射技术生成对应的 插件实例,然后调用插件方法中的setProperties方法,设置我们配置的参数,将插件实例保存到配置对象中,以便读取和使用它。所以插件的实例对象是一开始就被初始化的,而不是用到才初始化,用它的时候,直接拿,有助于性能提高。

插件在Configuration对象中的保存
  public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
  }
  interceptChain在Configuration里面是一个属性,它里面有个addInterceptor方法
  package org.apache.ibatis.plugin;
  public class InterceptorChain {
  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
 完成初始化的插件保存在这个List对象中等待取出使用。

插件的代理和反射设计

插件使用的是责任链模式
interceptChain的pluginAll()
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
plugin方法是生成代理对象的方法,它是从Configuration对象中取出插件的。从第一个对象(四大对象中的一个)开始,将对象传递给了plugin方法,然后返回一个代理。如果存在第二个插件,那么就将第一个代理对象,传递给plugin方法 ,再返回第一个代理对象的代理。依次类推,有多少个拦截器就生成多少个代理对象。每一个插件都可以拦截到真实的对象。就好比每一个插件都可以一层层处理被拦截的对象。

MyBatis提供了一个常用的工具类,来生成代理对象,这就是Plugin类。Plugin类实现了InvocationHandler接口,采用的是JDK动态代理。
package org.apache.ibatis.plugin;
public class Plugin implements InvocationHandler {
  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;
  }

  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);
    }
  }
使用JDK动态代理技术实现了InvocationHandler接口,其中wrap方法生成这个对象的动态代理对象。

如果使用这个类为插件生成代理对象,那么代理对象在调用方法时会进入到invoke方法中。在invoke方法中,如果存在签名的拦截方法,插件的intercept方法就会在这里调用,然后返回结果。如果不存在签名方法,那么将直接反射调度要执行的方法。

MyBatis把被代理对象、反射方法及其参数,都传递给了Invocation类的构造方法,用以生成一个Invocation对象,Invocation类中有用一个proceed()方法
package org.apache.ibatis.plugin;
public class Invocation {

  private Object target;
  private Method method;
  private Object[] args;

  public Invocation(Object target, Method method, Object[] args) {
    this.target = target;
    this.method = method;
    this.args = args;
  }
  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }

}
proceed()方法就是通过反射的方式调度被代理对象的真实方法的,假设有n个插件,第一个传递的参数是四大对象本身,然后调用一次wrap方法产生第一个代理对象,而这里的反射就是反射四大对象本身的真实方法。如果有第二个插件,会将第一个代理对象传递给wrap方法,生成第二个代理对象,这里的反射就是指第一个代理对象的invoke方法,依此类推、如果每一个代理对象都调用proceed方法,那么最后四大对象本身的方法也会被调用,只是它会从最后一个代理对象的invoke方法运行到第一个代理对象的invoke方法,直至四大对象的真实方法。

责任链模式,所以会从最后一个插件开始,执行其proceed()方法之前的代码 ,依次类推到费代理对象真实方法的调用,然后第一个插件proceed()方法后的代码知道最后一个插件proceed()方法后的代码,

MetaObject

四大对象提供的public设置参数的方法很少,难以通过其自身得到相关的属性,但是有了MetaObject这个工具类就可以通过其他的技术手段来读取或者修改这些重要对象的属性。

MetaObject3个方法
	MetaObject forObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory) 方法用于包装对象。这个方法已经不再使用了,而是MyBatis提供的SystemMetaObject.forObject(Object obj);
	Object getValue(String name)方法用于获取对象属性,支持OGNL
	void setValue(String name, Object value) 用于修改对象属性值,支持OGNL
MyBatis对象,包括四大对象大量使用了这个类进行包装,因此可以通过它来给四大对象的某些属性赋值。

拦截StatementHandler对象可以通过MetaObject提供的getValue来获取当前执行的SQL及其参数,然后通过setValue来修改,只是在此之前要通过SystemMetaObject.forObject(StatementHandler)将其绑定为一个MetaObject对象

package org.apache.ibatis.reflection.MetaObject
在插件下修改运行参数
Intercepts 可以配置多个@Signature,里面参数的定义:
	type: 表示拦截的类,这里为Executor的实现类
	method:表示拦截的方法,
	args:表示方法参数
@Intercepts({@Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class})})
public class ExamplePlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); //被代理对象
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        // 进行绑定
        // 分离代理对象链(由于目标类可能被多个插件拦截,从而形成多次代理,通过循环可以分离出最原始的目标类)
        while (metaStatementHandler.hasGetter("h")) {
            Object object = metaStatementHandler.getValue("h");
            metaStatementHandler = SystemMetaObject.forObject(object);
        }

        // 获取当前调用的SQl
        String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");

        // 判断SQl是否是select语句,如果不是select语句,则不需要处理
        // 如果是,则修改它,最多返回1000行,这里用的是MySQL数据库,其他数据库要写成别的
        if (sql != null && sql.toLowerCase().trim().indexOf("select") == 0) {
            // 通过SQL重写来实现,这里起一个奇怪的别名,避免与表名重复
           sql = "select * from (" + sql +") $_$limit_$table_ limit 1000";
           metaStatementHandler.setValue("delegate.boundSql.sql",sql);
        }
        Method method = invocation.getMethod(); //代理方法
        Object[] args = invocation.getArgs(); //方法参数
        // do something ...... 方法拦截前执行代码块
        Object result = invocation.proceed();
        // do something .......方法拦截后执行代码块
        return result;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

自定义插件实现类

StatementHandler实际是RoutingStatementHandler对象,它的delegate属性才是真实服务的StatementHandler,真实的StatementHandler有一个属性boundSql,它下面又有一个属性sql,所以才有了路径delegate.boundSQl.sql.通过这个路径去获取或者修改对应运行时的sql,通过这样改写,就可以限制所有查询的SQl都只能至多返回1000行记录。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值