mybatis插件

来源:互联网 发布:js获取当前城市名称 编辑:程序博客网 时间:2024/05/18 01:23

          前面讨论了四大对象的运行过程,在Configuration对象的创建方法里我们看到了mybatis用责任链去封装它们。换句话说,我们有机会在四大对象调度的时候插入我们的的代码去执行一些特殊的要求以满足特殊的场景需求,这便是mybati的插件技术。

        在没能完全理解mybatis的原理和插件的时候,使用插件是十分危险的。使用插件就意味着mybatis底层封装,它给予我们灵活性的同时,也给我们毁灭mybatis框架的可能性,操作不慎可能摧毁mybatis框架,只有掌握了mybatis的四大对象的协作过程和插件的实现原理,才能构建出安全高效的插件。



一,插件接口

       在mybatis中使用插件,我们必须实现接口Interceptor,让我们先看看它的定义和各个方法的含义,代码 如下:

//// Source code recreated from a .class file by IntelliJ IDEA// (powered by Fernflower decompiler)//package org.apache.ibatis.plugin;import java.util.Properties;public interface Interceptor {    Object intercept(Invocation var1) throws Throwable;    Object plugin(Object var1);    void setProperties(Properties var1);}

       在接口中运用了3个方法,下面先掌握这3个方法发含义:

intercept方法:它将直接覆盖你所拦截对象原有的方法,因此它是插件的核心方法。intercept里面有个参数Invocation对象,通过它可以反射调度原来对象的方法,后面叙述它的使用。

     

plugin方法:target是被拦截对象,它的作用是给被拦截对象生成一个代理的对象,并返回它。为了方便mybatis使用org.apache.ibatis.plugin.Plugin中的wrap静态(static)方法提供生成代理对象,我们往往使用plugin方法便可以生成一个代理对象了。当然你也可以自定义。自定义实现的时候需要小心。

   

setProperties方法:允许在plugin元素中配置所需参数,方法在插件初始化的时候就被调用了一次,然后把插件对象存入到配置中,以便后面再取出。

     这里看到了插件的骨架,这样的模式我们称为模板模式,就是提供一个骨架,并且告知骨架中的方法是干什么用的,由开发者来完成它。


二,插件的初始化

      插件的初始化是在mybatis初始化的时候完成的,这点我们可以通过XMLConfigBuilder中的代码便可知道,代码如下:

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 void addInterceptor(Interceptor interceptor) {        this.interceptorChain.addInterceptor(interceptor);    }

       interceptorChain在Configuration里面是一个属性,它里面有个addInterceptor方法,代码如下:

//// Source code recreated from a .class file by IntelliJ IDEA// (powered by Fernflower decompiler)//package org.apache.ibatis.plugin;import java.util.ArrayList;import java.util.Collections;import java.util.Iterator;import java.util.List;public class InterceptorChain {    private final List<Interceptor> interceptors = new ArrayList();    public 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;    }    public void addInterceptor(Interceptor interceptor) {        this.interceptors.add(interceptor);    }    public List<Interceptor> getInterceptors() {        return Collections.unmodifiableList(this.interceptors);    }}

       显然,完成初始化的插件就保存在这个List对象里面等待将其取出使用。

三,插件的代理和反射设计

        插件使用的是责任链模式。那么首先什么是责任链模式,就是一个对象,在mybatis中可能是四大对象中的一个,在多个角色中传递,处在传递链上的任何角色都有处理它的机会。打个比方,请假流程,首先你需要项目经理批准,然后部门经理批准,最后总裁批准才能完成。你的请假请求就是一个对象,它经过项目经理,部门经理,总裁多个角色审批处理,每个角色都可以对你的请假请求做出修改和批示。这就是责任链模式,它的作用是让每一个在责任链上的角色都有机会去拦截这个对象。在将来如果有新的角色也可以轻松拦截请求对象,进行处理。

        mybatis的责任链是由interceptorChain去定义的,mybatis在创建执行器时用到下面的代码:

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

        下面看看pluginAll()方法是如何实现的,代码如下:

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;    }

        我们知道plugin方法是生成代理对象的方法,当取出插件的时候是从Configuration对象中去取出。从第一个对象(四大对象中的一个)开始,将对象传递给了plugin方法,然后返回一个代理;如果存在第二个插件,那么我们就拿到第一个代理对象,传递给plugin方法再返回第一个代理对象的代理。。。。依次类推,有多少个拦截器就生成多少个代理对象。这样每一个插件都可以拦截到真实的对象。这就好比每一个插件都可以一层层处理被拦截的对象。

       如果我们自己编写代理类,工作量会很大,为此mybatis中提供了一个常用的工具类,用来生成代理对象,它便是Plugin类。Plugin类实现了InvocationHandler接口,采用的是JDK的动态代理,下面先看看这个类的两个重要的方法,如下:

//// Source code recreated from a .class file by IntelliJ IDEA// (powered by Fernflower decompiler)//package org.apache.ibatis.plugin;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.HashSet;import java.util.Map;import java.util.Set;import org.apache.ibatis.reflection.ExceptionUtil;public class Plugin implements InvocationHandler {    private final Object target;    private final Interceptor interceptor;    private final Map<Class<?>, Set<Method>> signatureMap;    private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {        this.target = target;        this.interceptor = interceptor;        this.signatureMap = signatureMap;    }    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;    }    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);        }    }    private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {        Intercepts interceptsAnnotation = (Intercepts)interceptor.getClass().getAnnotation(Intercepts.class);        if (interceptsAnnotation == null) {            throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());        } else {            Signature[] sigs = interceptsAnnotation.value();            Map<Class<?>, Set<Method>> signatureMap = new HashMap();            Signature[] var4 = sigs;            int var5 = sigs.length;            for(int var6 = 0; var6 < var5; ++var6) {                Signature sig = var4[var6];                Set<Method> methods = (Set)signatureMap.get(sig.type());                if (methods == null) {                    methods = new HashSet();                    signatureMap.put(sig.type(), methods);                }                try {                    Method method = sig.type().getMethod(sig.method(), sig.args());                    ((Set)methods).add(method);                } catch (NoSuchMethodException var10) {                    throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + var10, var10);                }            }            return signatureMap;        }    }    private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {        HashSet interfaces;        for(interfaces = new HashSet(); type != null; type = type.getSuperclass()) {            Class[] var3 = type.getInterfaces();            int var4 = var3.length;            for(int var5 = 0; var5 < var4; ++var5) {                Class<?> c = var3[var5];                if (signatureMap.containsKey(c)) {                    interfaces.add(c);                }            }        }        return (Class[])interfaces.toArray(new Class[interfaces.size()]);    }}

        wrap和invoke方法.我们看到它是一个动态代理对象,其中wrap方法为我们生成这个对象的动态代理对象。

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

       我们创建一个Invocation对象,器构造方法的参数包括被代理的对象,方法,及其参数。Invocation对象进行初始化,它有一个proceed()方法,代码如下:

public Object proceed() throws InvocationTargetException, IllegalAccessException {        return this.method.invoke(this.target, this.args);    }

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

       在初始化的时候,我们一个个的加载插件实例,并用setProperties()方法进行初始化。我们可以使用mybatis提供的Plugin.wrap方法生成代理对象,再一层层地使用Invocation对象的proceed()方法来推动代理对象运行。所以在多个插件的环境下调度proceed()方法时,mybatis总是从最后一个代理对象运行到第一个代理对象,最后是真实被拦截的对象方法被运行。大部分情况下,使用mybatis的Plugin类生成代理对象足够我们使用了。


四,常用的工具类-MetaObject

         在编写插件之前我们先学习一个mybatis的工具类-MetaObject,它可以有效读取或者修改一些重要对象的属性。在mybatis中,四大对象给我们提供的public设置参数的方法很少,我们难以通过其自身得到相关的属性信息,但是有了MetaObject这个工具类我们就可以通过其他的技术手段来读取或修改这些重要对象的属性。在mybatis插件中它是一个十分常用的工具类。

   它有3个方法常常被我们用到。

public static MetaObject forObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {        return object == null ? SystemMetaObject.NULL_META_OBJECT : new MetaObject(object, objectFactory, objectWrapperFactory, reflectorFactory);    }

      上面的方法用于包装对象。这个方法已经不再使用了,而是用mybatis为我们提供的SystemMetaObject.for(Object obj)。


public Object getValue(String name) {        PropertyTokenizer prop = new PropertyTokenizer(name);        if (prop.hasNext()) {            MetaObject metaValue = this.metaObjectForProperty(prop.getIndexedName());            return metaValue == SystemMetaObject.NULL_META_OBJECT ? null : metaValue.getValue(prop.getChildren());        } else {            return this.objectWrapper.get(prop);        }    }

          上面的方法用于获取对象属性值,支持OGNL。

public void setValue(String name, Object value) {        PropertyTokenizer prop = new PropertyTokenizer(name);        if (prop.hasNext()) {            MetaObject metaValue = this.metaObjectForProperty(prop.getIndexedName());            if (metaValue == SystemMetaObject.NULL_META_OBJECT) {                if (value == null && prop.getChildren() != null) {                    return;                }                metaValue = this.objectWrapper.instantiatePropertyValue(name, prop, this.objectFactory);            }            metaValue.setValue(prop.getChildren(), value);        } else {            this.objectWrapper.set(prop, value);        }    }

          上面的方法用于修改对象属性值,支持OGNL。

          在mybatis对象中大量使用了这个类进行包装。包括四大对象,使得我们可以通过它来给四大对象的某些属性赋值从而满足我们的需要。

         例如,拦截StatementHandler对象,我们需要先获取它要执行的SQL修改它的一些值。这时候我们可以使用MetaObject,它为我们提供了如下所示的方法:

StatementHandler statementHandler = (StatementHandler)invocation.getTarget();        MetaObject metaStatemnetHandler = SystemMetaObject.forObject(statementHandler);        //进行绑定        //分离代理对象链(由于目标类可能被多个拦截器拦截,从而形成多次代理,通过循环可以分离出原始的目标类)        while (metaStatemnetHandler.hasGetter("h")) {            Object object = metaStatemnetHandler.getValue("h");            metaStatemnetHandler = SystemMetaObject.forObject(object);        }        //BoundSql对象是处理SQL语句用的        String sql = metaStatemnetHandler.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";            metaStatemnetHandler.setValue("delegate.boundSql.sql",sql);        }


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

          由此可见,我们必须掌握好映射器解析的内容,才能准确的在插件中使用这个类,来获取或改变mybatis内部对象的一些重要的属性值,这对编写插件是非常重要的。


五,插件开发过程和实例

       有了对插件的理解,我们再学习插件的运用就简单多了。例如,开发一个互联网项目需要去限制每条sql返回的数据的行数。限制的行数需要是一个可配置的参数,业务可以根据自己的需要去配置。这样很有必要,因为很多系统数据很多,一次传输过多,很容易待机。这里我们可以通过修改sql来完成它。

1,确定需要拦截的签名

      正如mybatis插件可以拦截四大对象中任意一个一样。从Plugin源码中我们可以看到它需要注册签名才能够运行插件。签名需要确定一些要素。

1,确定需要拦截的对象

      首先需要根据功能来确定你需要拦截什么对象

Executor:是执行sql的全过程,包括组装参数,组装结果集返回和执行sql过程,都可以拦截,较为广泛,我们一般用的不算太多。

StatementHandler:是执行sql的过程,我们可以重写执行sql的过程。这是我们最常用的拦截对象。

ParameterHandler:很明显它主要是拦截执行sql的参数组装,你可以重写组装参数规则。

ResultSetHandler:用于拦截执行结果的组装,你可以重写组装结果的规则。

     我们清楚需要拦截的是StatementHandler对象,应该在预编译sql之前,修改sql使得结果返回数量被限制。


2,拦截方法和参数

       当你确定了需要拦截什么对象,接下来就要确定需要拦截什么方法及方法的参数,这些都是在理解了mybatis四大对象运作的基础上才能确定的。

       查询的过程是通过Executor调度StatementHandler来完成。调度StatementHandler的prepare方法预编译sql,于是我们需要拦截的方法便是prepare方法,在此之前完成sql的重新编写。下面看看StatementHandler接口的定义:

//// Source code recreated from a .class file by IntelliJ IDEA// (powered by Fernflower decompiler)//package org.apache.ibatis.executor.statement;import java.sql.Connection;import java.sql.SQLException;import java.sql.Statement;import java.util.List;import org.apache.ibatis.cursor.Cursor;import org.apache.ibatis.executor.parameter.ParameterHandler;import org.apache.ibatis.mapping.BoundSql;import org.apache.ibatis.session.ResultHandler;public interface StatementHandler {    Statement prepare(Connection var1, Integer var2) throws SQLException;    void parameterize(Statement var1) throws SQLException;    void batch(Statement var1) throws SQLException;    int update(Statement var1) throws SQLException;    <E> List<E> query(Statement var1, ResultHandler var2) throws SQLException;    <E> Cursor<E> queryCursor(Statement var1) throws SQLException;    BoundSql getBoundSql();    ParameterHandler getParameterHandler();}

        以上的任何方法都可以拦截。从接口定义而言,prepare方法有一个参数Connection对象,因此我们按下面代码的方法来设计拦截器:

package com.jack.plugin;import org.apache.ibatis.executor.statement.StatementHandler;import org.apache.ibatis.plugin.Interceptor;import org.apache.ibatis.plugin.Intercepts;import org.apache.ibatis.plugin.Invocation;import org.apache.ibatis.plugin.Signature;import java.sql.Connection;import java.util.Properties;/** * create by jack 2017/12/25 */@Intercepts(        {                @Signature(type = StatementHandler.class,//确定要拦截的对象                        method = "prepare",//确定要拦截的方法                        args = {Connection.class}//拦截方法的参数                        )        })public class MyPlugin implements Interceptor{    public Object intercept(Invocation invocation) throws Throwable {        return null;    }    public Object plugin(Object o) {        return null;    }    public void setProperties(Properties properties) {    }}

       

           其中,@Intercepts说明它是一个拦截器。@Signature是注册拦截器签名的地方,只有签名满足条件才能拦截,type可以是四大对象中的一个,这里是StatementHandler。method代表要拦截四大对象的某一种接口方法,而args则表示该方法的参数,需要根据拦截对象的方法参数进行设置。



2,实现拦截方法

     下面是一个最简单的插件实现方法,注意看注解,代码如下:

package com.jack.plugin;import org.apache.ibatis.executor.statement.StatementHandler;import org.apache.ibatis.plugin.*;import java.sql.Connection;import java.util.Properties;/** * create by jack 2017/12/25 */@Intercepts(        {                @Signature(type = StatementHandler.class,//确定要拦截的对象                        method = "prepare",//确定要拦截的方法                        args = {Connection.class}//拦截方法的参数                        )        })public class MyPlugin implements Interceptor{    Properties props = null;    /**     * 代替拦截对象方法的内容     * @param invocation 责任链对象     * @return     * @throws Throwable     */    public Object intercept(Invocation invocation) throws Throwable {        System.out.println("intercept()->before........................");        //如果当前代理的是一个非代理对象,那么它就回调真实拦截对象的方法,如果不是它会调度下个插件代理对象的invoke方法        Object obj = invocation.proceed();        System.out.println("intercept()->after.....................");        return obj;    }    /**     * 生成对象的代理,这里常用的mybatis提供的Plugin类的wrap方法     * @param target 被代理的对象     * @return     */    public Object plugin(Object target) {        //使用mybatis提供的Plugin类生成代理对象        System.out.println("plugin()调用生成代理对象");        return Plugin.wrap(target,this);    }    /**     *获取插件配置的属性,我们在mybatis的配置文件里面去配置     * @param properties 是mybatis配置的参数     */    public void setProperties(Properties properties) {        this.props = properties;        System.out.println(props.getProperty("dbType"));    }}


3,配置和运行

     我们需要在mybatis配置文件里面配置才能够使用插件,如下所示。请注意plugins元素的配置顺序,配置错了系统就会报错:

<plugins><plugin interceptor="com.jack.plugin.MyPlugin"><property name="dbType" value="mysql"/></plugin></plugins>

         显然,我们需要清楚配置的哪个类的哪个插件。他会去解析注解,知道拦截哪个对象,方法和方法的参数,在初始化的时候就会调用setProperties方法,初始化参数。


4,插件实例

         有了前面的知识,下面来实现一个真实的插件就容易很多了。在一个大型的互联网系统中,我们使用的是mysql数据库,对数据库查询返回数据量需要限制,以避免数据量过大造成网站瓶颈。假设这个数据量可以配置,当前要配置50条数据。下面讨论实现:

       首先我们先确定需要拦截四大对象中的哪一个,根据功能我们需要修改sql的执行。SqlSession运行原理告诉我们需要拦截的是StatementHandler对象,因为是由它的prepare方法来预编译sql语句的,我们可以在预编译前修改语句来满足我们的需求。所以我们选择拦截StatementHandler的prepare()方法,在它预编译前需重写sql,以达到要求的结果。它有一个参数(Connection connection),所以我们就很轻易地得到了签名注解其实现方法代码如下:























原创粉丝点击