乐于分享
好东西不私藏

还在硬改 MyBatis 源码?学会插件机制,零侵入扩展才是正确姿势!

还在硬改 MyBatis 源码?学会插件机制,零侵入扩展才是正确姿势!

>在之前的文章

互联网应用主流框架整合【MyBatis底层运转逻辑】

davieyang,公众号:轻雨科技馆互联网应用主流框架整合【MyBatis底层运转逻辑】

中,深入挖掘了SqlSession的四大对象Executor、StatementHandler、PrameterHandler和ResultSetHandler的运行过程,在Configuration对象的构建方法里,MyBatis用责任链封装它们,关于责任链可以翻回去看

[互联网轻量级框架整合之设计模式]

davieyang,公众号:轻雨科技馆互联网应用主流框架整合【设计模式】

,在四大对象调度时插入代码执行特殊要求便是MyBatis的插件技术

==使用插件意味着修改了MyBatis的底层封装,提供了灵活性的同时也带来了巨大的风险,只有掌握了底层运转逻辑和实现原理才能更好的开发高效的插件==

插件接口设计

在MyBatis中使用插件就必须先实现Interceptor接口,该接口的定义如下源码所示

/** * 拦截器接口,用于对特定目标对象进行拦截操作。 * 允许用户在目标对象的方法执行前后添加自定义逻辑。 */package org.apache.ibatis.plugin;import java.util.Properties;public interface Interceptor {    /**     * 对目标对象的 method 方法进行拦截,开发插件的核心方法,它将覆盖被拦截对象的原有方法     *      * @param var1 包含方法调用信息的对象,如被调用的方法、参数等,通过它可以反射调度原来对象的方法     * @return 返回方法执行的结果,其类型取决于目标方法的返回类型。     * @throws Throwable 如果拦截过程中发生异常,则抛出。     */    Object intercept(Invocation var1) throws Throwable;    /**     * 给目标对象target生成一个代理对象,并将代理对象返回。     * MyBatis在org.apache.ibatis.plugin.Plugin中提供了wrap静态方法,用于生成代理对象     * @param target 需要被拦截的目标对象。     * @return 返回被拦截对象的代理实例。     */    default Object plugin(Object target) {        return Plugin.wrap(target, this);    }    /**     * 允许在MyBatis配置文件中,通过plugin元素设置拦截器的属性。     * 这些属性可以在拦截器的逻辑中使用,以提供更灵活的拦截行为,该方法在插件初始化时就会被调用,把插件对象存到配置中,以便后续使用     * 接口定义的方法默认为空实现     * @param properties 拦截器的属性集合,键值对形式。     */    default void setProperties(Properties properties) {    }}

插件初始化过程

插件的初始化是在MyBatis初始化的时候完成的,通过XMLConfigBuilder中的代码便可知晓

 /**     * 从给定的XNode父元素中解析插件元素,并将其添加到配置中。     * @param parent XNode类型,表示父节点,不能为null。该节点代表一个配置项,包含拦截器的配置信息。     * @throws Exception 如果解析过程中发生错误,将抛出异常。     */    private void pluginElement(XNode parent) throws Exception {        if (parent != null) {            // 遍历父节点的所有子节点            Iterator var2 = parent.getChildren().iterator();            while(var2.hasNext()) {                XNode child = (XNode)var2.next();                // 获取当前子节点中"interceptor"属性的值,该值表示拦截器的类名                String interceptor = child.getStringAttribute("interceptor");                // 将当前子节点的所有子节点作为属性解析,并存储到Properties对象中                Properties properties = child.getChildrenAsProperties();                // 根据拦截器类名创建拦截器实例                Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).getDeclaredConstructor().newInstance();                // 设置拦截器的属性                interceptorInstance.setProperties(properties);                // 将拦截器实例添加到配置中                this.configuration.addInterceptor(interceptorInstance);            }        }    }

解析配置文件,从配置文件的上下文中找到插件节点和参数,使用反射技术生成对应的插件实例,然后调用插件中的“`setProperties“`方法设置参数,将插件实例保存到“`Configuration“`对象中,当用到它的时候可以直接拿出来用

/** 	 * 保护级别的final InterceptorChain实例,用于拦截器链的管理。 	 * InterceptorChain接口允许注册和执行一系列拦截器,这些拦截器可以按需修改或增强方法的执行行为。 	 * 这个成员变量在类中被定义为最终的,意味着它一旦被初始化,就不能再被修改。 	 */protected final InterceptorChain interceptorChain;    publicConfiguration() {		......        /**		 * 初始化拦截器链		 * 这个方法用于创建一个新的拦截器链实例,并将其赋值给当前对象的拦截器链属性。		 * 拦截器链是用于在请求处理过程中,按照特定顺序对请求进行拦截、处理的机制。		 * 它可以用于实现诸如认证、日志记录、请求转换等功能。		 * @return void		 */this.interceptorChain = new InterceptorChain();        ......    }    /**     * 向拦截器链中添加一个拦截器。     * 这个方法允许在运行时动态地增加新的拦截器到拦截器链中,被添加的拦截器将会参与到后续的请求处理中。     *      * @param interceptor 待添加的拦截器实例。拦截器是实现了Interceptor接口的对象,用于在请求处理前、后或异常时执行自定义逻辑。     */    publicvoidaddInterceptor(Interceptor interceptor) {        this.interceptorChain.addInterceptor(interceptor);    }

interceptorChain在Configuration里是一个属性,它可以通过addInterceptor方法放多个插件(Interceptor),而从源码中很显然Interceptor是个List

/** * 代表拦截器链的类,用于管理和调度一系列的拦截器。 */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() {    }    /**     * 通过所有的拦截器对目标对象进行拦截。     *     * @param target 需要被拦截的目标对象     * @return 经过所有拦截器处理后的目标对象     */    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;    }    /**     * 向拦截器链中添加一个拦截器。     *     * @param interceptor 需要被添加的拦截器     */    public void addInterceptor(Interceptor interceptor) {        this.interceptors.add(interceptor);    }    /**     * 获取当前拦截器链中所有的拦截器列表。     *     * @return 不可修改的拦截器列表     */    public List<InterceptorgetInterceptors() {        return Collections.unmodifiableList(this.interceptors);    }}

插件的代理和反射设计

插件用的是责任链模式,MyBatis的责任链是由interceptorChain定义的,前边文章中关于MyBatis的执行器Executor有如下代码“`executor= (Executor)interceptorChain.pluginAll(executor);“`,pluginAll方法的源码也是在InterceptorChain 类中定义的

/** * 代表拦截器链的类,用于管理和调度一系列的拦截器。 */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() {    }    /**     * 通过所有的拦截器对目标对象进行拦截。     *     * @param target 需要被拦截的目标对象     * @return 经过所有拦截器处理后的目标对象     */    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;    }    /**     * 向拦截器链中添加一个拦截器。     *     * @param interceptor 需要被添加的拦截器     */    public void addInterceptor(Interceptor interceptor) {        this.interceptors.add(interceptor);    }    /**     * 获取当前拦截器链中所有的拦截器列表。     *     * @return 不可修改的拦截器列表     */    public List<InterceptorgetInterceptors() {        return Collections.unmodifiableList(this.interceptors);    }}

>代码中interceptors存放的是插件列表,pluginAll方法的参数target是目标对象,它是之前说的SqlSession的四大对象Executor、StatementHandler、PrameterHandler和ResultSetHandler中的一个,如果存在插件,则将目标对象传递给第一个插件的plugin方法,然后返回一个代理,再将第一个代理对象传递给第二个插件的plugin方法,返回第一个代理对象的代理依此类推,有多少个插件就生成多少个代理对象,每一个插件都可以拦截到目标对象,如此便可以在目标对象运行前后,通过插件植入自己的逻辑。实际上MyBatis的四大对象也是这样的处理逻辑

开发者自己编写代理类工作量非常大,因此MyBatis提供了一个常用的工具类(plugin类),用来生成代理对象,它实现了InvocationHandler接口,采用JDK的动态代理,源码如下

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;/** * Plugin 类实现了 InvocationHandler 接口,用于动态代理目标对象,从而能够在方法调用前后添加额外的逻辑。 * 它是 MyBatis 中插件功能的核心实现。 */public class Plugin implements InvocationHandler {    private final Object target; // 目标对象,即被插件拦截的对象    private final Interceptor interceptor; // 拦截器对象,实现了 Interceptor 接口,定义了拦截逻辑    private final Map<Class<?>, Set<Method>> signatureMap; // 签名映射,记录了需要被拦截的方法    /**     * Plugin 构造函数,私有构造以防止外部直接创建实例。     *     * @param target 目标对象     * @param interceptor 拦截器对象     * @param signatureMap 签名映射,记录了拦截器中定义的需要拦截的方法     */    private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {        this.target = target;        this.interceptor = interceptor;        this.signatureMap = signatureMap;    }    /**     * 对指定的目标对象进行包装,如果目标对象的类实现了至少一个接口,则返回一个代理对象,该代理对象会拦截指定拦截器声明的方法调用。     *      * @param target 需要被包装的目标对象。     * @param interceptor 用于拦截目标对象方法调用的拦截器。     * @return 如果目标对象实现了至少一个接口,则返回一个代理对象,否则返回原目标对象。     */    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;    }/**	 * 调用代理对象上的方法。	 * 如果方法是被拦截的方法,则通过拦截器进行拦截处理;否则直接调用目标对象上的方法。@param proxy 代理对象,即调用方法的对象。@param method 被调用的方法。@param args 方法调用时的参数数组。@return 调用方法后的返回结果,如果方法被拦截,则返回拦截后的结果;否则返回直接调用目标方法的结果。@throws Throwable 如果方法调用过程中发生异常,则抛出。	 */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) {	        // 捕获并抛出方法调用过程中的异常,进行异常的统一处理	        Exception e = var5;	        throw ExceptionUtil.unwrapThrowable(e);	    }	}/**	 * 根据给定的拦截器对象,获取其注解声明的拦截方法的签名映射。	 * 这个方法主要用来解析拦截器类上的@Intercepts注解,收集所有被拦截的方法签名。@param interceptor 拦截器对象,必须是一个被@Intercepts注解标记的类的实例。@return 一个映射,键是拦截的方法的类型(Class<?>),值是该类型下被拦截的方法集合(Set<Method>)。@throws PluginException 如果没有找到@Intercepts注解,或者注解中指定的方法不存在时抛出。	 */private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {	    // 尝试获取拦截器类上的@Intercepts注解	    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<Method>)signatureMap.computeIfAbsent(sig.type(), (k) -> new HashSet<>());	            try {	                // 尝试根据签名信息获取方法对象,并添加到方法集合中	                Method method = sig.type().getMethod(sig.method(), sig.args());	                methods.add(method);	            } catch (NoSuchMethodException var10) {	                // 如果指定的方法不存在,抛出异常	                NoSuchMethodException e = var10;	                throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);	            }	        }	        return signatureMap; // 返回构建完成的签名映射	    }	}/**	 * 获取给定类型及其超类型的所有接口。	 * 这个方法会递归遍历给定类型的继承链,包括其超类,收集所有在signatureMap中存在的接口。@param type 需要查询接口的起始类型。@param signatureMap 包含接口方法签名映射的Map,用于过滤接口,只有在该Map中存在的接口才会被收集。@return 一个包含所有符合条件的接口的数组。	 */private static Class<?>[] getAllInterfaces(Class<?> typeMap<Class<?>, Set<Method>> signatureMap) {	    // 初始化一个空的HashSet用于存放符合条件的接口	    HashSet<Class<?>> interfaces;	    // 遍历类型及其超类型	    for (; type != nulltype = type.getSuperclass()) {	        // 获取当前类型的直接接口	        Class<?>[] var3 = type.getInterfaces();	        int var4 = var3.length;	        // 遍历直接接口,检查每个接口是否在signatureMap中 	       for (int var5 = 0; var5 < var4; ++var5) { 	           Class<?> c = var3[var5];	            // 如果接口在signatureMap中,则添加到结果集合中 	           if (signatureMap.containsKey(c)) { 	               interfaces.add(c);	            }	        }	    }	    // 将HashSet转换为Class<?>数组并返回	    return (Class[]) interfaces.toArray(new Class[interfaces.size()]);	}}

它使用了JDK动态代理技术实现了“`InvocationHandler“`接口,其中静态方法wrap生成这个对象的动态代理对象;再看Invoke方法,如果使用Plugin类为插件生成代理对象,代理对象在调用方法时就会进入invoke方法,在invoke方法中,如果存在签名的拦截方法,插件的intercept方法就会在这里调用,然后返回结果,如果不存在签名方法,则直接反射调度要执行的方法,再仔细看这行代码“`return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);“`把目标对象target、反射方法method及其参数args都传递给了Invocation类的构造方法,用以生成一个Invocation对象,Invocation类中有一个proceed方法,源码如下

/** * 表示一个方法调用的封装类。 * 用于通过反射机制调用指定对象上的方法。 */import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;public class Invocation {    private final Object target; // 目标对象,调用方法的对象    private final Method method; // 目标方法    private final Object[] args; // 方法调用参数数组    /**     * 构造一个方法调用实例。     *      * @param target 调用方法的对象     * @param method 要调用的方法     * @param args 方法调用参数数组     */    public Invocation(Object target, Method method, Object[] args) {        this.target = target;        this.method = method;        this.args = args;    }    /**     * 获取目标对象。     *      * @return 目标对象     */    public Object getTarget() {        return this.target;    }    /**     * 获取目标方法。     *      * @return 目标方法     */    public Method getMethod() {        return this.method;    }    /**     * 获取方法调用参数数组。     *      * @return 方法调用参数数组     */    public Object[] getArgs() {        return this.args;    }    /**     * 执行方法调用。     *      * @return 方法调用结果     * @throws InvocationTargetException 如果调用目标抛出异常,则抛出此异常     * @throws IllegalAccessException 如果没有权限访问方法,则抛出此异常     */    public Object proceed() throws InvocationTargetException, IllegalAccessException {        return this.method.invoke(this.target, this.args);    }}

从源码中可以看到,proceed方法是用过反射的方式调度目标对象的真实方法的,假设有N个插件,第一个插件传递的参数是四大对象本身,然后调用一次wrap方法产生第一个代理对象,这里的反射就是四大对象的真实方法;如果有第二个插件,那么它将第一个代理对象传递给wrap方法,生成第二个代理对象,这里的反射就是指第一个代理对象的invoke方法,以此类推直到最后一个代理对象

>如果每一个代理对象都会调用proceed方法,那么最后四大对象的方法也会被调用,只是它会从最后一个代理对象的invoke方法运行到第一个代理对象的invoke方法,直至四大对象的真实方法

==如果存在多个插件,那么执行顺序又如何呢?因为是在四大对象的前后添加逻辑,因此这个顺序要分四大对象真实方法之前和之后;在调用被拦截对象真实方法之前,由于是先从最后一个插件开始执行,所以是先执行最后一个代理插件proceed方法之前的代码,然后执行前一个插件proceed方法之前的代码,以此类推,直至四大对象之一的真实方法被调用;当执行被拦截对象方法后,会以此执行第一个插件proceed方法后的代码,第二个插件proceed方法后的代码……直至最后一个插件proceed方法后的代码==

插件工具类MetaObject&SystemMetaObject

MetaObject类和SystemMetaObject类有如下方法

/** * 根据给定的对象创建一个 MetaObject 实例。 * <p>如果提供的对象为 null,则返回一个代表 null 值的系统定义的 MetaObject。</p> *  * @param object 需要创建 MetaObject 的对象。可以是任何类型的对象,包括 null。 * @param objectFactory 用于创建对象的 ObjectFactory 实例。 * @param objectWrapperFactory 用于创建对象包装器的 ObjectWrapperFactory 实例。 * @param reflectorFactory 用于创建反射器的 ReflectorFactory 实例。 * @return 返回一个 MetaObject 实例,如果对象为 null,则返回一个特殊的 null MetaObject。 */public static MetaObject forObject(ObjectobjectObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {    // 判断对象是否为 null,为 null 返回系统定义的空 MetaObject,否则创建新的 MetaObject 实例    return object == null ? SystemMetaObject.NULL_META_OBJECT : new MetaObject(object, objectFactory, objectWrapperFactory, reflectorFactory);}
/** * 根据给定的对象创建一个 MetaObject 实例。MetaObject 是一个动态元数据对象,用于提供一种便捷的方式来访问和设置对象的属性值, * 以及执行一些动态的逻辑操作。 *  * @param object 需要创建 MetaObject 的对象。可以是任意类型的对象。 * @return 返回一个 MetaObject 实例,该实例与传入的对象相关联。 */public static MetaObject forObject(Objectobject) {    // 使用默认的 ObjectFactory、ObjectWrapperFactory 和 ReflectorFactory 创建 MetaObject    return MetaObject.forObject(objectDEFAULT_OBJECT_FACTORYDEFAULT_OBJECT_WRAPPER_FACTORYnew DefaultReflectorFactory());}

都是用于包装对象,以便读写对象的属性,其中第一个方法(MetaObject类中)已经不再使用了而更多的是用SystemMetaObject类提供的这个

MetaObject还提供了如下方法

/** * 根据给定的属性名获取对应的值,支持OGNL *  * @param name 属性名,可以是嵌套属性,例如"user.name"。 * @return 返回属性对应的值。如果属性不存在或者属性值为null,则返回null。 */public Object getValue(String name) {    // 使用PropertyTokenizer解析属性名,支持嵌套属性的解析    PropertyTokenizer prop = new PropertyTokenizer(name);    if (prop.hasNext()) {        // 获取当前属性名对应的元对象        MetaObject metaValue = this.metaObjectForProperty(prop.getIndexedName());        // 如果元对象不为空,则递归获取嵌套属性的值;否则返回null        return metaValue == SystemMetaObject.NULL_META_OBJECT ? null : metaValue.getValue(prop.getChildren());    } else {        // 如果属性名不包含嵌套属性,则直接通过objectWrapper获取值        return this.objectWrapper.get(prop);    }}
 /**     * 设置给定名称的属性值,支持OGNL     * 如果属性名称包含索引,则会处理嵌套属性,否则直接设置属性值。     *      * @param name 属性的完整名称,可以包含索引,例如"user.name[0]"。     * @param value 要设置的属性值。     */    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) {                // 如果元对象不存在,判断值是否为null,为null则不进行任何操作                if (value == null) {                    return;                }                // 创建嵌套属性的实例                metaValue = this.objectWrapper.instantiatePropertyValue(name, prop, this.objectFactory);            }            // 设置嵌套属性的值            metaValue.setValue(prop.getChildren(), value);        } else {            // 如果属性名不包含索引,直接设置属性值            this.objectWrapper.set(prop, value);        }    }

MyBatis四大对象大量使用了这个类进行包装,因此可以通过它来读取四大对象的属性,也可以给一些属性重新赋值,例如拦截StatementHandler对象可以通过MetaObject提供的getValue()方法获取当前执行的SQL及参数,然后通过setValue()方法修改参数值,只是在做这个动作之前,要通过SystemMetaObject.forObject(StatementHandler)将StatementHandler绑定为一个MetaObject对象,如下代码所示

/** * 拦截器方法,用于拦截并修改SQL语句。 * 这个方法会修改StatementHandler中的SQL语句,主要是对查询SQL进行限制,以防止查询结果过多。 *  * @param invocation 提供了对被调用方法的调用对象,参数等信息的访问。 * @return 返回拦截操作后的方法调用结果。 * @throws Throwable 如果执行过程中出现异常,则抛出。 */@Overridepublic Object intercept(Invocation invocation) throws Throwable {    // 将invocation的目标对象转换为StatementHandler    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();    // 使用SystemMetaObject对StatementHandler进行元对象绑定,以便于通过反射方式访问和修改属性    MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);    Object object = null;    // 分离代理对象链,获取最原始的目标对象    while (metaStatementHandler.hasGetter("h")) {        object = metaStatementHandler.getValue("h");        metaStatementHandler = SystemMetaObject.forObject(object);    }    statementHandler = (StatementHandlerobject;    // 获取原始SQL语句,并检查是否为SELECT语句    String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");    // 如果是SELECT语句,则对其进行修改,限制返回结果的数量    if(sql!=null && sql.toLowerCase().trim().indexOf("select")==0){        sql = "select * from (" + sql + ") $_$limit_$table_ limit 1000";        metaStatementHandler.setValue("delegate.boundSql.sql", sql);    }    // 此处省略了方法的后续执行逻辑...}

>如此便限制了所有查询的SQL都只能返回最多1000条数据

插件的开发过程

-确定需要拦截的签名

MyBatis中允许拦截四大对象中的任意一个对象,而通过Plugin源码也看到了,需要先注册签名才能使用插件,在开发插件工作之前首先要确认的是需要拦截哪个对象,然后确定需要配置什么样的签名,进而完成拦截方法的逻辑

-确定要拦截的对象

在MyBatis的插件机制里,只可以拦截四大对象:

-Executor:其方法包含执行SQL的全过程,包括组装参数、组装结果集、执行SQL过程,因此方法粒度很大,通常定位不到细节很少对其进行拦截,而且根据是否启用缓存参数,决定是否使用CachingExecutor进行封装

-StatementHandler:其将SQL的执行过程分解为多个方法,我们可以对其任何一个方法进行拦截改造,按需植入逻辑

-ParameterHandler:其主要是执行SQL的参数组装,拦截它并重写,可以修改组装参数规则

-ResultSetHandler:其主要是执行结果的组装,拦截它可以修改组装结果的规则

==假设我们要在SQL预编译之前修改SQL,这样才能得到我们想要的特殊效果,那需要拦截的是Statementhandler==

-拦截方法和参数

确定了拦截对象后,接下来要确定拦截的方法及方法的参数,这些都需要在熟知MyBatis四大对象运转逻辑基础上才能确定的

>查询的过程是Executor调度StatementHandler完成的,调度StatementHandler的prepare方法对SQL进行预编译,于是拦截的方法就是prepare,在此之前修改SQL,再进行预编译执行

StatementHandler接口源码如下:

// 此接口定义了如何处理SQL语句,包括准备、参数化、执行更新、查询等操作。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 {    /**     * 准备SQL语句。     *      * @param var1 Connection对象,用于创建Statement。     * @param var2 事务隔离级别,可能为null。     * @return 配置好的Statement对象。     * @throws SQLException 如果准备过程中出现SQL异常。     */    Statement prepare(Connection var1, Integer var2) throws SQLException;    /**     * 参数化SQL语句。     *      * @param var1 需要参数化的Statement对象。     * @throws SQLException 如果参数化过程中出现SQL异常。     */    void parameterize(Statement var1) throws SQLException;    /**     * 将一批SQL语句添加到Statement中。     *      * @param var1 用于执行批处理的Statement对象。     * @throws SQLException 如果添加过程中出现SQL异常。     */    void batch(Statement var1) throws SQLException;    /**     * 执行更新操作。     *      * @param var1 用于执行更新的Statement对象。     * @return 更新行数。     * @throws SQLException 如果执行过程中出现SQL异常。     */    int update(Statement var1) throws SQLException;    /**     * 执行查询操作,并返回结果列表。     *      * @param var1 用于执行查询的Statement对象。     * @param var2 处理查询结果的ResultHandler。     * @param <E> 查询结果的类型。     * @return 查询结果的列表。     * @throws SQLException 如果执行过程中出现SQL异常。     */    <E> List<E> query(Statement var1, ResultHandler var2) throws SQLException;    /**     * 执行查询操作,并返回Cursor对象。     *      * @param var1 用于执行查询的Statement对象。     * @param <E> 查询结果的类型。     * @return 包含查询结果的Cursor对象。     * @throws SQLException 如果执行过程中出现SQL异常。     */    <E> Cursor<E> queryCursor(Statement var1) throws SQLException;    /**     * 获取SQL语句的BoundSql对象。     *      * @return 包含SQL语句、参数等信息的BoundSql对象。     */    BoundSql getBoundSql();    /**     * 获取SQL语句的ParameterHandler对象。     *      * @return 用于处理SQL语句参数的ParameterHandler对象。     */    ParameterHandler getParameterHandler();}

以上任何方法都可以拦截,拦截prepare方法拦截器代码如下:

/** * 该注解定义了一个拦截器,作用于StatementHandler类型的对象的prepare方法。 * 拦截器会在目标方法执行前、执行后或出现异常时被调用,可以用来进行日志记录、性能监控等操作。 * * @Intercepts 注解指定了要拦截的类型、方法和方法参数。 * @Signature 中的type指定了要拦截的对象类型是StatementHandler类。 * method指定了要拦截的方法是prepare方法。 * args指定了prepare方法的参数类型,分别是Connection类和Integer类。 */@Intercepts({    @Signature(type = StatementHandler.class, // 拦截的类型为StatementHandler    method = "prepare",  // 拦截的方法为prepare    args = { Connection.class, Integer.class } // 拦截方法的参数类型    ) })public classMyPluginimplementsInterceptor{	// 此处省略了...

-实现拦截方法

package com.ssm.plugin;import java.sql.Connection;import java.util.Properties;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.Plugin;import org.apache.ibatis.plugin.Signature;import org.apache.ibatis.reflection.MetaObject;import org.apache.ibatis.reflection.SystemMetaObject;import org.apache.log4j.Logger;/** * 该注解定义了一个拦截器,作用于StatementHandler类型的对象的prepare方法。 * 拦截器会在目标方法执行前、执行后或出现异常时被调用,可以用来进行日志记录、性能监控等操作。 * * @Intercepts 注解指定了要拦截的类型、方法和方法参数。 * @Signature 中的type指定了要拦截的对象类型是StatementHandler类。 * method指定了要拦截的方法是prepare方法。 * args指定了prepare方法的参数类型,分别是Connection类和Integer类。 */@Intercepts({@Signature(type = StatementHandler.class,				method = "prepare",				args = { Connection.classInteger.class })})public class MyPlugin implements Interceptor {private Logger log = Logger.getLogger(MyPlugin.class);private Properties props = null;/**	 * 拦截器方法,用于拦截并记录SQL语句的执行。	 *@param invocation 提供了对被拦截对象方法的调用能力,可以获取方法参数、目标对象等信息。@return 返回被拦截方法的执行结果。@throws Throwable 如果被拦截方法抛出异常,则此处需要进行异常处理。	 */@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 将invocation的目标对象转换为StatementHandler类型StatementHandler statementHandler = (StatementHandler) invocation.getTarget();// 使用SystemMetaObject对statementHandler进行元对象绑定,以便于通过反射方式访问其属性MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);Object object = null;// 分离代理对象链(目标类可能被多个拦截器拦截从而形成多次代理),通过循环可以分离出最原始的目标对象while (metaStatementHandler.hasGetter("h")) {object = metaStatementHandler.getValue("h");			metaStatementHandler = SystemMetaObject.forObject(object);		}		statementHandler = (StatementHandlerobject;// 获取并记录SQL语句和参数String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");Long parameterObject = (Long) metaStatementHandler.getValue("delegate.boundSql.parameterObject");		log.info("执行的SQL:【" + sql + "】");		log.info("参数:【" + parameterObject + "】");		log.info("before ......");// 调用下一个拦截器或目标对象的方法,执行具体的逻辑Object obj = invocation.proceed();		log.info("after ......");return obj;	}/**	 * 通过插件机制对目标对象进行代理。	 *@param target 需要被代理的目标对象。@return 返回代理后的对象,该对象可以是目标对象的任何一种代理形式。	 */@Overridepublic Object plugin(Object target) {// 生成代理对象,将目标对象和当前插件实例作为参数传递给Plugin.wrap方法return Plugin.wrap(target, this);	}/**	 * 设置属性集合。	 * 该方法将传入的属性集合赋值给当前对象的属性字段,并记录配置参数的日志信息。	 *@param props 属性集合,包含各种配置参数。	 */@Overridepublic void setProperties(Properties props) {this.props = props; // 将传入的属性集合赋值给当前对象的属性字段// 记录配置参数的日志信息		log.info("dbType = " + this.props.get("dbType"));	}}

>这个插件从代理对象中分离出拦截对象,因为被拦截的对象是通过层层代理封装的,所以使用了循环进行层层分离

>分离出来后,再通过工具类MetaObject获取要执行的SQL和参数,并在反射方法执行前后分别打印before和after,这就表示在方法前后执行特殊代码

-配置和运行

        <!-- 配置插件,指定插件的拦截器类和其属性 -->        <plugininterceptor="com.ssm.plugin.MyPlugin">            <!-- 设置插件的属性,此处为数据库类型 -->            <propertyname="dbType"value="mysql"/>        </plugin>

插件实例-分页插件

在MyBatis中存在一个RowBounds用于分页,但它是基于第一次查询结果的分页,也就是先让SQL查询所有记录,然后再分页,显然性能不高;当然也可以通过拆分SQL来实现,例如一条用于当前页查询,一条用于查询总数,但这样即便实现了分页也还是性能不高且工作量变大

通过分页插件也是实现分页的一种形式,并且分页插件的实现也不止一种形式,如下是一种,先定义一个分页参数的POJO

/** * 分页参数类,用于封装分页查询时所需的各项参数。 */public class PageParams {// 当前页码private Integer page;// 每页限制条数private Integer pageSize;// 是否启动插件,如果不启动,则不作分页处理private Boolean useFlag;// 是否检测页码的有效性,如果为true,而页码大于最大页数,则抛出异常private Boolean checkFlag;// 是否清除SQL语句中最后的order by后面的语句private Boolean cleanOrderBy;// 总条数,插件会计算并回填这个值private Integer total;// 总页数,插件会计算并回填这个值private Integer totalPage;/**	 * 获取当前页码@return 当前页码	 */public Integer getPage() {return page;	}/**	 * 设置当前页码@param page 当前页码	 */public void setPage(Integer page) {this.page = page;	}/**	 * 获取每页限制的条数@return 每页限制的条数	 */public Integer getPageSize() {return pageSize;	}/**	 * 设置每页限制的条数@param pageSize 每页限制的条数	 */public void setPageSize(Integer pageSize) {this.pageSize = pageSize;	}/**	 * 获取是否启用分页插件的标志@return 是否启用分页插件	 */public Boolean getUseFlag() {return useFlag;	}/**	 * 设置是否启用分页插件的标志@param useFlag 是否启用分页插件	 */public void setUseFlag(Boolean useFlag) {this.useFlag = useFlag;	}/**	 * 获取是否检测页码有效性的标志@return 是否检测页码有效性	 */public Boolean getCheckFlag() {return checkFlag;	}/**	 * 设置是否检测页码有效性的标志@param checkFlag 是否检测页码有效性	 */public void setCheckFlag(Boolean checkFlag) {this.checkFlag = checkFlag;	}/**	 * 获取是否清除ORDER BY语句的标志@return 是否清除ORDER BY语句	 */public Boolean getCleanOrderBy() {return cleanOrderBy;	}/**	 * 设置是否清除ORDER BY语句的标志@param cleanOrderBy 是否清除ORDER BY语句	 */public void setCleanOrderBy(Boolean cleanOrderBy) {this.cleanOrderBy = cleanOrderBy;	}/**	 * 获取总条数@return 总条数	 */public Integer getTotal() {return total;	}/**	 * 设置总条数@param total 总条数	 */public void setTotal(Integer total) {this.total = total;	}/**	 * 获取总页数@return 总页数	 */public Integer getTotalPage() {return totalPage;	}/**	 * 设置总页数@param totalPage 总页数	 */public void setTotalPage(Integer totalPage) {this.totalPage = totalPage;	}}

total和totalPage这两个属性是插件需要回填的内容,这样当使用这个分页POJO传递分页信息时,也可以通过这个分页POJO得到记录总数和分页总数,接下来就是要在编译SQL之前修改SQL,并且增加分页参数并计算出查询总条数,根据MyBatis的运行原理,需要拦截StatementHandler的prepare方法,拦截方法签名代码如下

@Intercepts({// 签名@Signature(		type = StatementHandler.class, // 拦截对象method = "prepare",  // 拦截方法args = { Connection.class, Integer.class } // 方法参数})

在插件中有个三个方法需要自己完成,其中的plugin方法可以不在实现它,因为MyBatis的Interceptor接口给了默认实现,它使用wrap方法生成代理对象,当PageParams的useFlag属性为false时,也就是禁用此分页插件时,没有必要生成代理对象,毕竟使用代理会造成性能下降;setProperties方法一方面给PageParams属性定义了默认值,这些默认值可以通过配置文件改变,另一方面,当它接受来自MyBatis配置文件的plugins标签的配置参数时,也可以满足不同的需求

/** * 设置配置属性。 * 该方法用于读取传入的Properties对象中的配置信息,并将其设置为类的默认参数值。 * @param props 包含配置信息的Properties对象。配置项包括默认页码、每页大小、是否启用分页参数、 * 是否检查页码正确性以及是否清除order by语句等。 */@Overridepublic void setProperties(Properties props) {// 从配置属性中读取默认参数值String strDefaultPage = props.getProperty("default.page""1"); // 默认页码String strDefaultPageSize = props.getProperty("default.pageSize""50"); // 每页大小String strDefaultUseFlag = props.getProperty("default.useFlag""false"); // 是否启用分页参数String strDefaultCheckFlag = props.getProperty("default.checkFlag""false"); // 是否检查页码正确性String StringDefaultCleanOrderBy = props.getProperty("default.cleanOrderBy""false"); // 是否清除order by语句// 将读取的字符串配置值转换为相应的类型,并设置为类的默认参数值this.defaultPage = Integer.parseInt(strDefaultPage);this.defaultPageSize = Integer.parseInt(strDefaultPageSize);this.defaultUseFlag = Boolean.parseBoolean(strDefaultUseFlag);this.defaultCheckFlag = Boolean.parseBoolean(strDefaultCheckFlag);this.defaultCleanOrderBy = Boolean.parseBoolean(StringDefaultCleanOrderBy);}

然后便是插件的核心方法intercept了

/** * 拦截器方法,用于拦截SQL执行并在执行前进行分页处理。 * 如果是查询SQL语句,并且存在有效的分页参数,则对SQL进行分页处理, * 否则直接放行。 * * @param invocation 提供了对目标方法的调用能力,包含方法参数等信息。 * @return 返回处理后的结果,可能是分页后的查询结果,也可能是原查询结果。 * @throws Throwable 如果目标方法执行过程中发生异常,则抛出。 */@Overridepublic Object intercept(Invocation invocation) throws Throwable {    // 将目标对象进行反代理,分离真实的拦截对象(获取真实的StatementHandler对象)    StatementHandler stmtHandler = (StatementHandler) getUnProxyObject(invocation.getTarget());    // 使用SystemMetaObject为stmtHandler绑定元对象,方便动态访问属性    MetaObject metaStatementHandler = SystemMetaObject.forObject(stmtHandler);    // 通过元对象获取即将执行的SQL语句    String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");    // 检查SQL语句是否为查询语句,如果不是,则直接执行原方法    if (!checkSelect(sql)) {        return invocation.proceed();    }    // 获取BoundSql对象,其中包含了SQL执行所需的全部信息    BoundSql boundSql         = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");    // 尝试从参数对象中解析出分页参数    Object parameterObject = boundSql.getParameterObject();    PageParams pageParams = getPageParamsForParamObj(parameterObject);    // 如果无法解析出分页参数,则直接执行原方法    if (pageParams == null) {        return invocation.proceed();    }    // 获取是否启用分页的标志    Boolean useFlag = pageParams.getUseFlag() == null ?            this.defaultUseFlag : pageParams.getUseFlag();    // 如果未启用分页,则直接执行原方法    if (!useFlag) {        return invocation.proceed();    }    // 获取并处理分页参数:页码、每页大小、是否检查页码有效性、是否清除ORDER BY子句    Integer pageNum = pageParams.getPage() == null ?            defaultPage : pageParams.getPage();    Integer pageSize = pageParams.getPageSize() == null ?            defaultPageSize : pageParams.getPageSize();    Boolean checkFlag = pageParams.getCheckFlag() == null ?             defaultCheckFlag : pageParams.getCheckFlag();    Boolean cleanOrderBy = pageParams.getCleanOrderBy() == null ?             defaultCleanOrderBy : pageParams.getCleanOrderBy();    // 计算总记录数    int total = getTotal(invocation, metaStatementHandler, boundSql, cleanOrderBy);    // 将计算出的总记录数回填到分页参数中    pageParams.setTotal(total);    // 计算总页数并回填    int totalPage = total % pageSize == 0 ?     		total / pageSize : total / pageSize + 1;    pageParams.setTotalPage(totalPage);    // 检查当前页码的有效性    checkPage(checkFlag, pageNum, totalPage);    // 修改SQL语句,使之支持分页    return preparedSQL(invocation, metaStatementHandler, boundSql, pageNum, pageSize);}

其中用于从责任链中分离对象获得真实的StatementHandler对象,使用的方法“`getUnProxyObject“`代码如下:

/** * 从目标对象中获取未被代理的原始对象。 * 该方法主要用于分离代理对象链,找出最原始的目标类。 * @param target 目标对象,即可能被代理的对象。 * @return 返回未被代理的原始对象。如果目标对象本身就是未被代理的对象,则返回目标对象本身。 */private Object getUnProxyObject(Object target) {    // 使用SystemMetaObject为目标对象创建元对象,以便于通过反射方式访问对象属性    MetaObject metaStatementHandler = SystemMetaObject.forObject(target);    /* 分离代理对象链     * 由于目标类可能被多个拦截器拦截,从而形成多次代理,     * 通过循环遍历代理对象的h属性,可以分离出最原始的目标类     */    Object object = null;    // 循环遍历,直到找到最原始的未被代理的目标对象    while (metaStatementHandler.hasGetter("h")) {          object = metaStatementHandler.getValue("h");          metaStatementHandler = SystemMetaObject.forObject(object);      }      // 如果未找到代理对象,则说明目标对象本身未被代理,直接返回目标对象    if (object == null) {        return target;    }    // 返回最原始的目标对象    return object;}

通过这个方法,可以把JDK动态代理责任链上原始的StatementHandler对象分离出来,然后在intercept方法中将StatementHandler对象通过工具类MetaObject绑定,如此便可以为后续通过它分离出当前执行的SQL和参数做准备;默认情况下插件是拦截所有类型的SQL的,而分页通常只需要拦截Select语句,因此要做个判断,代码如下:

/** * 检查提供的SQL语句是否为SELECT语句。 *  * @param sql 待检查的SQL语句字符串。 * @return 如果提供的SQL语句是以"select"开头(不区分大小写),则返回true;否则返回false。 */private boolean checkSelect(String sql) {    // 移除sql字符串两端的空白字符,以便更准确地进行判断    String trimSql = sql.trim();    // 将trimSql转换为小写,并查找"select"字符串在其中的索引    int idx = trimSql.toLowerCase().indexOf("select");    // 如果"select"字符串位于trimSql的起始位置,即索引为0,返回true;否则返回false    return idx == 0;}

一旦判断不是Select语句,则intercept方法就不再拦截,而是直接推动责任链前进,如果是Select则拦截它进入分页逻辑,代码如下

/** * 根据传入的参数对象获取PageParams实例。 * 该方法支持多种类型的参数: * 1. 如果参数是Map类型,则遍历Map,如果Map的任何值是PageParams类型,则直接返回该PageParams实例。 * 2. 如果参数直接是PageParams类型或其子类,则直接返回该参数对象。 * 3. 如果参数是普通的Java对象(POJO),则尝试从该对象的属性中找到类型为PageParams的属性,并返回该属性的值。 * 如果无法从上述任何一种方式中获取到PageParams实例,则返回null。 *  * @param parameterObject 可以是Map、PageParams或其子类、或包含PageParams属性的POJO对象。 * @return 返回解析出的PageParams实例,如果无法解析则返回null。 * @throws Exception 如果在处理过程中发生错误,则抛出异常。 */public PageParams getPageParamsForParamObj(Object parameterObject)         throws Exception {    PageParams pageParams = null;    if (parameterObject == null) {        return null;    }    // 处理Map类型的参数    if (parameterObject instanceof Map) {        @SuppressWarnings("unchecked")        Map<StringObject> paramMap = (Map<StringObject>) parameterObject;        Set<String> keySet = paramMap.keySet();        Iterator<String> iterator = keySet.iterator();        // 遍历Map,查找值为PageParams类型的条目        while (iterator.hasNext()) {            String key = iterator.next();            Object value = paramMap.get(key);            if (value instanceof PageParams) {                return (PageParams) value;            }        }    } else if (parameterObject instanceof PageParams) { // 直接处理PageParams类型或其子类        return (PageParams) parameterObject;    } else { // 从POJO属性中尝试获取PageParams        Field[] fields = parameterObject.getClass().getDeclaredFields();        // 遍历POJO的属性,查找类型为PageParams的属性        for (Field field : fields) {            if (field.getType() == PageParams.class) {                PropertyDescriptor pd                     = new PropertyDescriptor(field.getName(), parameterObject.getClass());                Method method = pd.getReadMethod();                return (PageParams) method.invoke(parameterObject);            }        }    }    return pageParams;}

通过这个方法分离出分页参数,一旦分离失败则返回null,返回null,则intercept方法直接推进责任链前进结束方法,如果不返回null,则会分析这个分页参数,一旦这个参数配置了不启用插件,这intercept方法也是直接推进责任链前进结束方法,而分页参数可以填值也可能不填值,如果不填值则使用默认配置

计算SQL返回条数总数是插件难点之一,代码如下:

/** * 计算总数的方法。 * 该方法会根据传入的原始SQL语句,构造一个用于统计总数的SQL语句,然后执行该语句并返回总数。 *  * @param ivt Invocation对象,代表当前的MyBatis执行邀请,用于获取执行SQL所需的Connection对象。 * @param metaStatementHandler MetaObject对象,用于反射式访问StatementHandler的内部属性,以获取MappedStatement等信息。 * @param boundSql BoundSql对象,代表原始SQL语句的绑定参数,用于构造统计总数的BoundSql。 * @param cleanOrderBy 布尔值,指示是否需要移除原始SQL语句中的ORDER BY子句。 * @return int 返回查询到的总数。 * @throws Throwable 如果执行过程中出现异常,则抛出。 */private int getTotal(Invocation ivt, MetaObject metaStatementHandler,         BoundSql boundSql, Boolean cleanOrderBy) throws Throwable {    // 获取当前的MappedStatement    String mappedStatementPath = "delegate.mappedStatement";    MappedStatement mappedStatement  = (MappedStatement)         metaStatementHandler.getValue(mappedStatementPath);    // 配置对象    Configuration cfg = mappedStatement.getConfiguration();    // 获取需要执行的SQL语句,并在需要时清理ORDER BY子句    String sql         = (String) metaStatementHandler.getValue("delegate.boundSql.sql");    if (cleanOrderBy) {        sql = this.cleanOrderByForSql(sql);    }    // 构造统计总数的SQL语句    String countSql = "select count(*) as total from (" + sql + ") $_paging";    // 获取Connection对象,用于执行统计总数的SQL语句    Connection connection = (Connection) ivt.getArgs()[0];    PreparedStatement ps = null;    int total = 0;    try {        // 预编译统计总数的SQL语句        ps = connection.prepareStatement(countSql);        // 构建统计总数的BoundSql        BoundSql countBoundSql = new BoundSql(cfg, countSql,                 boundSql.getParameterMappings(), boundSql.getParameterObject());        // 构建ParameterHandler,用于设置统计总数SQL的参数        ParameterHandler handler = new DefaultParameterHandler(mappedStatement,                 boundSql.getParameterObject(), countBoundSql);        // 设置参数        handler.setParameters(ps);        // 执行查询并计算总数        ResultSet rs = ps.executeQuery();        while (rs.next()) {            total = rs.getInt("total");        }    } finally {        // 关闭PreparedStatement        if (ps != null) {            ps.close();        }    }    return total;}
/** * 清理SQL语句中的ORDER BY部分。 * 该方法将检查给定的SQL查询字符串是否包含ORDER BY子句。如果包含,它将删除ORDER BY子句及其之后的所有内容,返回结果为一个新的SQL查询字符串。 *  * @param sql 需要进行清理的原始SQL查询字符串。 * @return 清理后的SQL查询字符串。如果原始字符串中不包含ORDER BY,则返回原始字符串。 */private String cleanOrderByForSql(String sql) {    StringBuilder sb = new StringBuilder(sql);    String newSql = sql.toLowerCase();    // 检查SQL语句中是否包含"order",不包含则直接返回原SQL    if (newSql.indexOf("order") == -1) {        return sql;    }    // 找到"order"关键字出现的最后一个位置    int idx = newSql.lastIndexOf("order");    // 返回删除ORDER BY子句及其之后内容的新的SQL字符串    return sb.substring(0, idx).toString();}

>首先从BoundSql中分离出SQL,通过分页参数判断是否需要去掉order by语句,如果去掉则删除,因为它会影响SQL的执行性能,然后通过StatementHandler的prepare方法的参数(第一个是Connection对象,另一个是超时配置)获取数据库连接资源,使用JDBC方法获取总条数,根据MyBatis的底层运转逻辑知道,SQL参数是通过ParameterHandler设置的,在这里我们使用总数的SQL构建了BoundSql和ParameterHandler对象,通过总数的ParameterHandler对象设置总数SQL的参数,这样就能执行总数SQL求出这条SQL所能查询出的总数后,将其返回

求出总数后,就可以根据每页条数限制求总页数,并与总数一起回填到分页参数中,接下来判断当前页码是否合规,代码如下

/** * 检查传入的页码是否合法。 * @param checkFlag 检查标志,如果为true,则会进行页码合法性检查。 * @param pageNum 要检查的页码。 * @param pageTotal 总页数。 * @throws Throwable 如果页码不合法,则抛出异常。 */private void checkPage(Boolean checkFlag, Integer pageNum, Integer pageTotal)throws Throwable {if (checkFlag) {// 检查页码是否合法if (pageNum > pageTotal) {String msg = "查询失败,查询页码【" + pageNum + "】" + "大于总页数【" + pageTotal + "】!";throw new Exception(msg);		}	}}

首先判断分页参数checkFlag的属性值,如果为true则进行当前页是否大于总页数的判断,如果大于总页数则抛出异常,否则继续查询当前页

查询当前页需要修改原有SQL,并且加入SQL的分页参数,这样就很容易查询到分页数据,代码如下:

/** * 准备SQL语句以支持分页查询。 * 该方法通过修改原始SQL语句,将其转换成支持分页的SQL语句, * 然后继续执行预处理过程,并设置分页参数。 * * @param invocation 提供对当前方法调用的详细信息,包括参数等。 * @param metaStatementHandler 用于操作和获取StatementHandler的元对象,以便修改SQL语句。 * @param boundSql 包含实际执行的SQL语句和其他相关绑定信息的对象。 * @param pageNum 请求的页码。 * @param pageSize 每页显示的记录数。 * @return 经过分页处理后的查询对象,用于后续的数据库操作。 * @throws Exception 如果执行过程中出现错误,则抛出异常。 */private Object preparedSQL(Invocation invocation,         MetaObject metaStatementHandler, BoundSql boundSql,         int pageNum, int pageSize) throws Exception {    // 获取并构建新的分页SQL语句    String sql = boundSql.getSql();    String newSql = "select * from (" + sql + ") $_paging_table limit ?, ?";    // 修改StatementHandler中的SQL语句为新的分页SQL    metaStatementHandler.setValue("delegate.boundSql.sql", newSql);    // 执行预处理,准备执行查询    Object statementObj = invocation.proceed();    // 设置分页参数到PreparedStatement中    this.preparePageDataParams(            (PreparedStatement) statementObj, pageNum, pageSize);    return statementObj;}
  /**     * 准备分页数据的参数。     * 这个方法主要用于在查询时设置分页参数,以便正确地限制查询结果的数量并指定查询的起始位置。     *      * @param ps PreparedStatement对象,用于设置SQL查询的参数。     * @param pageNum 要查询的页码,第一页为1。     * @param pageSize 每页显示的记录数。     * @throws Exception 如果准备语句或设置参数时发生错误,则抛出异常。     */private void preparePageDataParams(PreparedStatement ps, 	        int pageNum, int pageSize) throws Exception {	    // 根据PreparedStatement中参数的数量来确定分页参数的位置	    int idx = ps.getParameterMetaData().getParameterCount();	    // 设置分页参数:开始行和限制条数	    ps.setInt(idx - 1, (pageNum - 1) * pageSize); // 设置开始行,基于0的索引	    ps.setInt(idx, pageSize); // 设置每页的记录数	}

先取出原有SQL,再改写为分页SQL,分页SQL存在两个新的参数,第一个是偏移量,第二个是限制条数,这两个参数在分页SQL最后的两个位置上,推动了责任链的进行,实际上就是调用了StatementHandler的prepare方法,这个方法的作用就是预编译SQL的参数,在这里我们添加了两个分页参数,但prepare方法并没有把这两个分页参数预编译在内,就需要使用“`preparePageDataParams“`方法来完成这个任务。

执行完preparedSQL方法后,就完成了查询总条数和总页数以及分页SQL的预编译,最后返回编译好的PreparedStatement对象,到此就完成了分页的逻辑,之后还需要配置分页插件

 <!-- 定义插件配置,启用PagePlugin分页插件 -->    <plugins>        <plugininterceptor="com.ssm.plugin.PagePlugin">            <!-- 设置默认页码为1 -->            <propertyname="default.page"value="1" />            <!-- 设置默认每页条数为20 -->            <propertyname="default.pageSize"value="20" />            <!-- 启动分页插件功能,默认为true -->            <propertyname="default.useFlag"value="true" />            <!-- 是否检查页码有效性,如果设置为true,则非有效页码会抛出异常,默认为false -->            <propertyname="default.checkFlag"value="false" />            <!-- 是否针对含有order by的SQL语句去掉最后一个order by,以提高性能,默认为false -->            <propertyname="default.cleanOrderBy"value="false" />        </plugin>    </plugins>
<select id="findRolesByPage" parameterType="string" resultType="role">select id, role_name as roleName, note from t_rolewhere role_name like concat('%'#{roleName}, '%')	</select>

然后执行就可以得到分页结果了

package com.ssm.main;import java.util.List;import org.apache.ibatis.session.SqlSession;import org.apache.log4j.Logger;import com.ssm.dao.RoleDao;import com.ssm.param.PageParams;import com.ssm.pojo.Role;import com.ssm.utils.SqlSessionFactoryUtils;/** * 主程序类,用于演示通过MyBatis进行分页查询角色信息。 */public class Main {    /**     * 主方法。     * @param args 命令行参数(未使用)     */public static void main(String[] args) {// 初始化日志对象,用于记录操作日志Logger log = Logger.getLogger(Main.class);        try (SqlSession sqlSession = SqlSessionFactoryUtils.openSqlSession()) {            // 获取SqlSession,通过SqlSession获取RoleDao接口的实现            RoleDao roleDao = sqlSession.getMapper(RoleDao.class);            // 设置分页参数            PageParams pageParams = new PageParams();            pageParams.setPage(2);            pageParams.setPageSize(10);            // 调用分页查询方法            List<Role> roleList = roleDao.findRolesByPage(pageParams, "role_name_");            // 记录查询结果数量日志            log.info(roleList.size());        } // 自动关闭SqlSession    }}

>插件生成的是层层代理对象的责任链模式,通过反射方法运行,因此性能不高,减少插件即是减少代理

>在SQL语句中,问号(?)作为占位符出现,主要用于预编译语句(Prepared Statements)中。预编译语句是一种安全且效率较高的SQL执行方式,它可以防止SQL注入攻击,并且对于多次执行同一个语句结构但参数不同的情况,预编译语句可以提高执行效率;在实际执行这个带占位符的SQL语句之前,需要为这些问号提供具体的值。这通常通过编程语言(如Java)中的PreparedStatement对象完成

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 还在硬改 MyBatis 源码?学会插件机制,零侵入扩展才是正确姿势!

评论 抢沙发

9 + 5 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮