找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3421

积分

0

好友

466

主题
发表于 昨天 05:04 | 查看: 5| 回复: 0

在之前的文章《互联网应用主流框架整合【MyBatis底层运转逻辑】》中,深入挖掘了 SqlSession 的四大核心对象——ExecutorStatementHandlerParameterHandlerResultSetHandler 的运行过程。在 Configuration 对象的构建方法里,MyBatis 通过责任链模式封装它们。关于责任链的设计原理,可回溯参考《互联网轻量级框架整合之设计模式》一文。

在四大对象调度过程中插入自定义逻辑,正是 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) {
    }
}

intercept() 是插件逻辑的核心入口;plugin() 提供默认代理生成能力;setProperties() 支持从 XML 配置中注入参数,是插件可配置性的关键支撑。

插件初始化过程

插件的初始化发生在 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);
        }
    }
}

整个流程清晰明确:解析 <plugin> 节点 → 反射创建 Interceptor 实例 → 调用 setProperties() 注入配置 → 通过 configuration.addInterceptor() 注册至全局拦截器链。

Configuration 类中维护着一个关键成员:

protected final InterceptorChain interceptorChain;

其构造函数中完成初始化:

public Configuration() {
    // ...
    this.interceptorChain = new InterceptorChain();
    // ...
}

addInterceptor() 方法则委托给 interceptorChain

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

进一步查看 InterceptorChain 源码可知,它内部使用 ArrayList<Interceptor> 存储所有注册插件:

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<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(this.interceptors);
    }
}

interceptors 存放的是插件列表,pluginAll() 方法的参数 target 是目标对象——即 SqlSession 四大对象之一(ExecutorStatementHandlerParameterHandlerResultSetHandler)。若存在插件,则将目标对象传递给第一个插件的 plugin() 方法,返回一个代理;再将该代理传递给第二个插件的 plugin() 方法……如此形成多层代理链。每一层代理都可在目标对象方法执行前后植入逻辑。实际上,MyBatis 四大对象自身的构建也遵循这一模式。

插件的代理和反射设计

开发者手动编写 JDK 动态代理类工作量巨大。为此,MyBatis 提供了工具类 Plugin,它实现了 InvocationHandler 接口,封装了标准代理逻辑:

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<?> type, Map<Class<?>, Set<Method>> signatureMap) {
        // 初始化一个空的HashSet用于存放符合条件的接口
        HashSet<Class<?>> interfaces;
        // 遍历类型及其超类型
        for (; type != null; type = 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()]);
    }
}

Plugin 的核心在于 invoke() 方法:当代理对象执行方法时,它先判断该方法是否在 signatureMap 中注册过;若是,则交由 interceptor.intercept() 处理;否则直接反射调用原始目标方法。

intercept() 方法接收的 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);
    }
}

假设有 N 个插件,第一个插件接收的是四大对象本身,调用 wrap() 生成第一层代理;第二个插件接收第一层代理,生成第二层代理……最终形成 N 层代理链。proceed() 在每一层中调用的都是“下一层”的 invoke(),直到最后一层才真正调用四大对象的原始方法。因此,方法执行前的逻辑按插件注册顺序逆序执行(后注册的先执行),方法执行后的逻辑则按正序执行(先注册的先执行)

插件工具类:MetaObject 与 SystemMetaObject

MetaObjectSystemMetaObject 是 MyBatis 提供的元对象工具,用于便捷读写复杂对象的嵌套属性(支持 OGNL 表达式),极大简化了插件中对 StatementHandler 等对象的属性操作。

核心静态工厂方法如下:

/**
 * 根据给定的对象创建一个 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(Object object, ObjectFactory 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(Object object) {
    // 使用默认的 ObjectFactory、ObjectWrapperFactory 和 ReflectorFactory 创建 MetaObject
    return MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
}

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 四大对象大量使用 MetaObject 进行包装。因此,插件可通过它安全地读取或修改 StatementHandler 的 SQL、参数等关键字段。例如,拦截 StatementHandler 并重写 SQL:

/**
 * 拦截器方法,用于拦截并修改SQL语句。
 * 这个方法会修改StatementHandler中的SQL语句,主要是对查询SQL进行限制,以防止查询结果过多。
 *
 * @param invocation 提供了对被调用方法的调用对象,参数等信息的访问。
 * @return 返回拦截操作后的方法调用结果。
 * @throws Throwable 如果执行过程中出现异常,则抛出。
 */
@Override
public 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 = (StatementHandler) object;
    // 获取原始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 条数据。

插件的开发过程

1. 确定需要拦截的签名

MyBatis 插件机制要求开发者显式声明拦截目标,即通过 @Intercepts@Signature 注解定义拦截规则。开发前需明确:

  • 拦截哪个对象? —— ExecutorStatementHandlerParameterHandlerResultSetHandler
  • 拦截哪个方法? —— 如 StatementHandler.prepare()
  • 方法参数类型? —— 如 (Connection.class, Integer.class)

2. 确定要拦截的对象

MyBatis 仅允许拦截以下四大对象:

  • Executor:方法粒度最大,涵盖 SQL 执行全过程(参数组装、结果集组装、缓存判断等)。通常因粒度太粗、逻辑耦合深,较少直接拦截。
  • StatementHandler:将 SQL 执行拆分为 prepare()parameterize()query() 等细粒度方法,是最常用、最推荐的拦截点
  • ParameterHandler:负责参数组装,拦截可定制参数绑定逻辑。
  • ResultSetHandler:负责结果集映射,拦截可定制结果解析规则。

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

3. 拦截方法和参数

确定对象后,需结合 MyBatis 底层逻辑选择具体方法。例如,查询流程是 Executor 调度 StatementHandler.prepare() 完成预编译,因此若需改写 SQL,应拦截 prepare() 方法。

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,
               method = "prepare",
               args = { Connection.class, Integer.class })
})
public class MyPlugin implements Interceptor {
    // 此处省略了...
}

4. 实现拦截方法

一个典型的 StatementHandler.prepare() 拦截插件示例如下:

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.class, Integer.class })
})
public class MyPlugin implements Interceptor {
    private Logger log = Logger.getLogger(MyPlugin.class);
    private Properties props = null;

    /**
     * 拦截器方法,用于拦截并记录SQL语句的执行。
     *
     * @param invocation 提供了对被拦截对象方法的调用能力,可以获取方法参数、目标对象等信息。
     * @return 返回被拦截方法的执行结果。
     * @throws Throwable 如果被拦截方法抛出异常,则此处需要进行异常处理。
     */
    @Override
    public 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 = (StatementHandler) object;
        // 获取并记录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 返回代理后的对象,该对象可以是目标对象的任何一种代理形式。
     */
    @Override
    public Object plugin(Object target) {
        // 生成代理对象,将目标对象和当前插件实例作为参数传递给Plugin.wrap方法
        return Plugin.wrap(target, this);
    }

    /**
     * 设置属性集合。
     * 该方法将传入的属性集合赋值给当前对象的属性字段,并记录配置参数的日志信息。
     *
     * @param props 属性集合,包含各种配置参数。
     */
    @Override
    public void setProperties(Properties props) {
        this.props = props; // 将传入的属性集合赋值给当前对象的属性字段
        // 记录配置参数的日志信息
        log.info("dbType = " + this.props.get("dbType"));
    }
}

这个插件通过循环 metaStatementHandler.hasGetter("h") 分离出被层层代理封装的真实 StatementHandler 对象;再利用 MetaObject.getValue() 提取 SQL 和参数;最后在 invocation.proceed() 前后打印日志,实现“环绕通知”。

5. 配置和运行

在 MyBatis 主配置文件(如 mybatis-config.xml)中注册插件:

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

插件实例:分页插件

MyBatis 内置的 RowBounds 是基于内存的分页,先查全量再截取,性能极差。手写分页 SQL(如 LIMIT ?, ?)虽高效但重复劳动多。而分页插件能实现零侵入、SQL 自动改写、总数自动计算,是生产环境首选方案。

分页参数 POJO

首先定义一个携带分页上下文的 PageParams

/**
 * 分页参数类,用于封装分页查询时所需的各项参数。
 */
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;

    /** 获取当前页码 */
    public Integer getPage() {
        return page;
    }

    /** 设置当前页码 */
    public void setPage(Integer page) {
        this.page = page;
    }

    /** 获取每页限制的条数 */
    public Integer getPageSize() {
        return pageSize;
    }

    /** 设置每页限制的条数 */
    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }

    /** 获取是否启用分页插件的标志 */
    public Boolean getUseFlag() {
        return useFlag;
    }

    /** 设置是否启用分页插件的标志 */
    public void setUseFlag(Boolean useFlag) {
        this.useFlag = useFlag;
    }

    /** 获取是否检测页码有效性的标志 */
    public Boolean getCheckFlag() {
        return checkFlag;
    }

    /** 设置是否检测页码有效性的标志 */
    public void setCheckFlag(Boolean checkFlag) {
        this.checkFlag = checkFlag;
    }

    /** 获取是否清除ORDER BY语句的标志 */
    public Boolean getCleanOrderBy() {
        return cleanOrderBy;
    }

    /** 设置是否清除ORDER BY语句的标志 */
    public void setCleanOrderBy(Boolean cleanOrderBy) {
        this.cleanOrderBy = cleanOrderBy;
    }

    /** 获取总条数 */
    public Integer getTotal() {
        return total;
    }

    /** 设置总条数 */
    public void setTotal(Integer total) {
        this.total = total;
    }

    /** 获取总页数 */
    public Integer getTotalPage() {
        return totalPage;
    }

    /** 设置总页数 */
    public void setTotalPage(Integer totalPage) {
        this.totalPage = totalPage;
    }
}

totaltotalPage 由插件自动计算并回填,调用方无需关心。

拦截签名与配置

同样拦截 StatementHandler.prepare()

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

setProperties() 方法用于加载默认配置:

/**
 * 设置配置属性。
 * 该方法用于读取传入的Properties对象中的配置信息,并将其设置为类的默认参数值。
 * @param props 包含配置信息的Properties对象。配置项包括默认页码、每页大小、是否启用分页参数、
 * 是否检查页码正确性以及是否清除order by语句等。
 */
@Override
public 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 如果目标方法执行过程中发生异常,则抛出。
 */
@Override
public 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);
}

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

checkSelect() 判断是否为 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;
}

getPageParamsForParamObj() 支持多种参数传入方式(Map、POJO、直接 PageParams):

/**
 * 根据传入的参数对象获取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<String, Object> paramMap = (Map<String, Object>) 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;
}

计算总数:getTotal()

这是分页插件的核心难点之一,需构造 count(*) 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;
}

配套的 cleanOrderByForSql() 清理 ORDER BY 子句以提升性能:

/**
 * 清理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();
}

校验页码与生成分页 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);
        }
    }
}
/**
 * 准备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); // 设置每页的记录数
    }

最终配置与调用

mybatis-config.xml 中启用插件:

    <!-- 定义插件配置,启用PagePlugin分页插件 -->
    <plugins>
        <plugin interceptor="com.ssm.plugin.PagePlugin">
            <!-- 设置默认页码为1 -->
            <property name="default.page" value="1"/>
            <!-- 设置默认每页条数为20 -->
            <property name="default.pageSize" value="20"/>
            <!-- 启动分页插件功能,默认为true -->
            <property name="default.useFlag" value="true"/>
            <!-- 是否检查页码有效性,如果设置为true,则非有效页码会抛出异常,默认为false -->
            <property name="default.checkFlag" value="false"/>
            <!-- 是否针对含有order by的SQL语句去掉最后一个order by,以提高性能,默认为false -->
            <property name="default.cleanOrderBy" value="false"/>
        </plugin>
    </plugins>

Mapper XML 中定义普通查询:

<select id="findRolesByPage" parameterType="string" resultType="role">
select id, role_name as roleName, note from t_role
where role_name like concat('%', #{roleName}, '%')
</select>

Java 调用代码:

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 底层设计与高并发架构实践,可前往 后端 & 架构 板块系统学习。同时,Java 板块也持续更新 Spring Boot、JVM、JUC 等核心主题的实战解析。




上一篇:Spring Cloud服务治理与服务发现:Eureka核心配置与实践
下一篇:MyBatis动态SQL详解:告别手写冗余SQL,高效构建条件查询与更新
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 14:18 , Processed in 0.849487 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表