读书笔记——MyBatis插件原理分析

1、插件接口

在MyBatis中使用插件,需要实现接口 Interceptor ,定义如下:

package org.apache.ibatis.plugin;

import java.util.Properties;

public interface Interceptor {

    Object intercept(Invocation var1) throws Throwable;

    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    default void setProperties(Properties properties) {
    }
}

该接口中定义了3个方法,简单介绍一下各个方法的作用:

  • intercept 方法:它将直接覆盖拦截对象原有的方法,是我们实现插件的核心方法。其中的参数 Invocation 对象,通过它可以反射调用原来对象的方法。
  • plugin 方法:target 是被拦截的对象,该方法主要用来给被拦截的对象生成一个代理对象,并返回该代理对象。在MyBatis中,一般情况都会使用 Plugin 中的 wrap 静态方法来生成代理对象。
  • setProperties 方法:在插件初始化时会被调用,主要用于配置插件所需的参数

通过提供一个模板,告知模板中的各个方法的作用,由开发者自己来完成方法具体的实现逻辑,这种模式被称为模板模式。

2、插件的初始化

在MyBatis初始化时,完成了对插件的初始化,XMLConfigureBulder 中的代码如下:

public class XMLConfigBuilder extends BaseBuilder {

	.......
	
	//解析MyBatis的配置
	 private void parseConfiguration(XNode root) {
        try {
            this.propertiesElement(root.evalNode("properties"));
            Properties settings = this.settingsAsProperties(root.evalNode("settings"));
            this.loadCustomVfs(settings);
            this.loadCustomLogImpl(settings);
            this.typeAliasesElement(root.evalNode("typeAliases"));
            //初始化插件
            this.pluginElement(root.evalNode("plugins"));
            this.objectFactoryElement(root.evalNode("objectFactory"));
            this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
            this.settingsElement(settings);
            this.environmentsElement(root.evalNode("environments"));
            this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            this.typeHandlerElement(root.evalNode("typeHandlers"));
            this.mapperElement(root.evalNode("mappers"));
        } catch (Exception var3) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
        }
    }

	........

	//初始化插件
    private void pluginElement(XNode parent) throws Exception {
        if (parent != null) {
            Iterator var2 = parent.getChildren().iterator();

            while(var2.hasNext()) {
                XNode child = (XNode)var2.next();
                String interceptor = child.getStringAttribute("interceptor");
                Properties properties = child.getChildrenAsProperties();
                //反射生成插件的实例对象
                Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).newInstance();
                //设置插件参数
                interceptorInstance.setProperties(properties);
                //将该插件保存到插件链当中
                this.configuration.addInterceptor(interceptorInstance);
            }
        }
    }
	........
}

在解析配置文件时,在MyBatis的上下文初始化过程中,开始读入插件节点和配置的参数,使用反射生成对应的插件实例,然后调用插件方法中的 setProperties 方法设置我们自己针对该插件配置的参数,并将插件实例保存到配置对象中,便于后续使用。所以插件的实例对象是一开始就被初始化的,而不是懒加载的,有助于性能的提高。

继续查看插件是如何在 Configuration 配置对象中保存的:

public class Configuration {
	.......
	
    protected final InterceptorChain interceptorChain;

	public void addInterceptor(Interceptor interceptor) {
        this.interceptorChain.addInterceptor(interceptor);
    }
	.......
  }

interceptorChainConfiguration 中的一个属性,它有一个 addInterceptor 方法,继续查看该方法:

public class InterceptorChain {
    private final List<Interceptor> interceptors = new ArrayList();

    public InterceptorChain() {
    }
	
	//使用所有的插件对target对象进行层层代理
    public Object pluginAll(Object target) {
        Interceptor interceptor;
        for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {
            interceptor = (Interceptor)var2.next();
        }
        return target;
    }
    
	//将插件保存到list当中
    public void addInterceptor(Interceptor interceptor) {
        this.interceptors.add(interceptor);
    }

    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(this.interceptors);
    }
}

插件的初始化流程: 首先读取配置文件中的插件节点,使用反射生成插件实例,并设置该插件的参数信息,然后就是将完成初始化的插件保存在InterceptorChain 的 List 当中,等待后续取出使用。

3、插件的代理和反射设计

插件用的是责任链模式,MyBatis 中的责任链是由 interceptorChain 去定义的,在 MyBatis 创建执行器时用到如下代码:

 Executor executor = (Executor)this.interceptorChain.pluginAll(executor);

pluginAll() 方法的实现在上面的 InterceptorChain 源代码中已经贴出,这里再看一下:

    public Object pluginAll(Object target) {
        Interceptor interceptor;
        for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {
            interceptor = (Interceptor)var2.next();
        }

        return target;
    }

通过循环调用 interceptor.plugin(target) 方法,对原始对象 executor 进行层层代理,有多少个 interceptor 插件,就生成多少个代理对象。每一个插件都可以拦截到真实的对象。

自己手动编写代理类的工作量很大,为此 MyBatis 提供了一个常用的工具类用来生成代理对象,它便是 Plugin 类。该类实现了 InvocationHandler 接口,采用的是JDK动态代理,核心源代码如下:


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);
        return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
    }

	@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
            return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
        } catch (Exception var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    }
    
	......
}

wrap 方法使用JDK动态代理技术生成了原始对象的动态代理对象。在代理对象调用方法时,会进入到 invoke 方法中。在 invoke 方法中,如果存在签名的拦截方法,插件的 interceptor 方法就会在这里调用,然后返回结果。如果不存在签名方法,那么将直接反射调度原始对象的方法。

MyBatis 把被代理对象、反射方法及方法参数都传递给了 Invocation 类的构造方法,然后将该对象作为参数调用了插件的 intercepter() 方法。其中 Invocation 的源代码如下:

public class Invocation {
    private final Object target;
    private final Method method;
    private final Object[] args;

    public Invocation(Object target, Method method, Object[] args) {
        this.target = target;
        this.method = method;
        this.args = args;
    }

    public Object getTarget() {
        return this.target;
    }

    public Method getMethod() {
        return this.method;
    }

    public Object[] getArgs() {
        return this.args;
    }

	//反射调度被代理对象的真实方法
    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return this.method.invoke(this.target, this.args);
    }
}

从源码可以看见 Invocation 对象存在一个 processd() 方法,该方法通过反射的方式调度被代理对象的真实方法。

假设有n个插件,第一个传递的参数是四大对象本身(Excutor、StatementHandler、ParameterHandler、ResultSetHandler),然后调用 Plugin 的 wrap 方法产生第一个代理对象简称为 Proxy1,这时Proxy1反射的就是四大对象本身的真实方法。如果有第二个插件,我们会将 Proxy1 传递给 Plugin 的 wrap 方法,生成第二个代理对象 Proxy2,这时反射的是 Proxy1 的invoke方法,以此类推直至最后一个代理对象。

如果每一个代理对象都调用 proceed() 方法,那么最后四大对象本身的方法也会被调用,顺序是从最后一个代理对象的 invoke 方法运行到第一个代理对象的 invoke 方法,直至四大对象的真实方法、

所以当使用多个插件时,由于使用了责任链模式,它的执行顺序是:从最后一个插件开始,先执行其 proceed() 方法之前的代码,然后进入前一个插件 proceed() 方法之前的代码。以此类推直至到非代理对象(四大对象中的一个)真实方法的调用,然后依次从第一个插件 proceed() 方法后的代码、第二个插件 proceed() 方法后的代码…知道最后一个插件 proceed() 方法后的代码。

在大部分情况下,我们使用 MyBatis 提供的 Plugin 类生成代理对象就足够了。也可以不用这个类,自己写动态代理的规则,但是必须非常小心,因为这样会覆盖底层的方法。

4、开发插件时常用的工具类——MetaObject

在MyBatis中,四大对象提供的 public 设置参数的方法很少,难以通过其自身得到相关的属性信息,为此MyBatis 提供了一个工具类 MetaObject,通过这个工具类可以来读取或者修改这些对象的熟悉。

MetaObject 有3个主要的方法:

  • MetaObject forObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory)方法用于包装对象。这个方法已经不再使用了,而是用MyBatis提供的SystemMetaObject.forObject(Object obj)、
  • Object getValue(String name) 方法用户获取对象属性值
  • void setValue(String name, Object value) 方法用户修改对象属性值

MyBatis 的四大对象大量使用了这个类进行包装,所以我们可以通过它来给四大对象的某些属性赋值来满足我们的需求。

例如,拦截 StatementHandler 对象可以通过 MetaObject 提供的 getValue 方法来获取当前执行的 SQL 及其参数,然后通过其 setValue 方法来修改它们,只是在此之前要通过
SystemMetaObject.forObject(statementHandler)将其绑定为 MetaObject 对象,代码如下:

		//通过Invocation对象获取被代理的对象
		StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
		
		//MetaObject与被代理对象绑定
		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");

		//如果是select语句,修改sql,限制返回100条
		if (sql != null && sql.toLowerCase().trim().indexOf("select") == 0){
			sql = "select * from ( " + sql+" ) limit_table limit 100";
			//将修改后的SQ进行L赋值
			metaStatementHandler.setValue("delegate.boundSql.sql",sql);
		}

为什么分离代理对象链的时候是使用 metaStatementHandler.hasGetter(“h”) 方法来判断呢?因为我们获取的 metaStatementHandler 它可能还是一个代理对象,也就是 java.lang.reflect.Proxy ,而 Proxy 这个类中有一个 InvocationHandler 类型的属性,变量名为 “h” ,所以如果 metaStatementHandler 中存在变量名为 “h” 的属性的 Getter 方法,就说明当前还是一个代理对象,需要继续分离查找真实对象。Proxy 的源代码如下:

public class Proxy implements java.io.Serializable {
	......
	
	 /**
     * the invocation handler for this proxy instance.
     * @serial
     */
    protected InvocationHandler h;

	  @CallerSensitive
    public static InvocationHandler getInvocationHandler(Object proxy) throws IllegalArgumentException
    {
        /*
         * Verify that the object is actually a proxy instance.
         */
        if (!isProxyClass(proxy.getClass())) {
            throw new IllegalArgumentException("not a proxy instance");
        }

        final Proxy p = (Proxy) proxy;
        final InvocationHandler ih = p.h;
        if (System.getSecurityManager() != null) {
            Class<?> ihClass = ih.getClass();
            Class<?> caller = Reflection.getCallerClass();
            if (ReflectUtil.needsPackageAccessCheck(caller.getClassLoader(),
                                                    ihClass.getClassLoader()))
            {
                ReflectUtil.checkPackageAccess(ihClass);
            }
        }

        return ih;
    }
	
	......
}

拦截的 StatementHandler 实际是 RoutingStatementHandler 对象,它的 delegate 属性才是真实服务的 StatementHandler ,真实的 StatementHandler 有一个属性 boundSql,它的下面又有一个属性 sql 。所以才有了路径 delegate.boundSql.sql。通过这个路径去获取运行时的 SQL,并做改写,这样就可以限制所有的查询 SQL 都只能最多返回100条记录。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值