面试官:MyBatis 的插件是如何实现的?它能在哪些地方进行拦截?分页插件的工作原理是什么?
一、开篇
MyBatis 的插件机制允许我们在四大对象的创建和方法调用过程中插入自定义逻辑,从而实现分页、性能监控、SQL 打印、数据权限控制等功能。
四大拦截点:
Executor:执行器,负责 SQL 执行和一级缓存StatementHandler:语句处理器,负责 JDBC Statement 的操作ParameterHandler:参数处理器,负责为 PreparedStatement 设置参数ResultSetHandler:结果集处理器,负责将 ResultSet 映射成 Java 对象
本篇目标:
- 理解插件的加载与注册过程
- 剖析
Plugin类的动态代理实现(JDK 动态代理) - 学习责任链模式在拦截器链中的应用
- 以分页插件为例,分析自定义插件的实现原理
二、插件核心类
Interceptor(拦截器接口):由开发者实现,是插件的行为定义者,包含intercept()、plugin()和setProperties()三个核心方法。InterceptorChain:拦截器链,持有所有插件的列表。Plugin(代理工具类):MyBatis 内置的工具类,是插件的实现辅助者。它实现了InvocationHandler接口,其wrap()静态方法用于为目标对象创建 JDK 动态代理,并负责在invoke()中根据@Signature注解的判断结果,决定是否调用Interceptor.intercept()方法Invocation:封装了目标对象、方法、参数,传递给intercept方法。
三、插件的加载与注册
3.1 配置方式
在 mybatis-config.xml 中:
<plugins><plugininterceptor="com.example.MyPlugin"><propertyname="property1"value="value1"/></plugin><plugininterceptor="com.example.PageInterceptor"/></plugins>
3.2 解析与注册:XMLConfigBuilder.pluginsElement
private void pluginsElement(XNode context) throws Exception {if (context != null) {for (XNode child : context.getChildren()) {String interceptor = child.getStringAttribute("interceptor");Properties properties = child.getChildrenAsProperties();Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();interceptorInstance.setProperties(properties);configuration.addInterceptor(interceptorInstance);}}}
3.3Configuration存储拦截器
public class Configuration {protected final InterceptorChain interceptorChain = new InterceptorChain();public void addInterceptor(Interceptor interceptor) {interceptorChain.addInterceptor(interceptor);}public List<Interceptor> getInterceptors() {return interceptorChain.getInterceptors();}}
InterceptorChain 内部就是一个 List<Interceptor>,按配置顺序存储。
四、拦截点的生成:InterceptorChain.pluginAll
当 MyBatis 创建这四大核心对象时,在对象实例化完成后,都会显式调用interceptorChain.pluginAll(target) 方法,用于将配置的拦截器链应用到该对象上,最终返回的可能是一个层层包裹的代理对象。
4.1Executor创建时
Configuration.newExecutor 方法中:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {// ... 创建 SimpleExecutor/ReuseExecutor/BatchExecutorif (cacheEnabled) {executor = new CachingExecutor(executor);}return (Executor) interceptorChain.pluginAll(executor);}
4.2StatementHandler创建时
Configuration.newStatementHandler 方法中:
public StatementHandler newStatementHandler(Executor executor, MappedStatement ms,Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {StatementHandler statementHandler = new RoutingStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);return (StatementHandler) interceptorChain.pluginAll(statementHandler);}
4.3ParameterHandler创建时
Configuration.newParameterHandler:
public ParameterHandler newParameterHandler(MappedStatement ms, Object parameter, BoundSql boundSql) {ParameterHandler parameterHandler = new DefaultParameterHandler(ms, parameter, boundSql);return (ParameterHandler) interceptorChain.pluginAll(parameterHandler);}
4.4ResultSetHandler创建时
Configuration.newResultSetHandler:
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement ms,RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, ms, parameterHandler, resultHandler, boundSql, rowBounds);return (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);}
InterceptorChain.pluginAll实现:
public Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) {target = interceptor.plugin(target);}return target;}
每个拦截器可以决定是否对目标对象生成代理(通过 plugin 方法)。通常我们使用 Plugin.wrap 工具方法。
五、Plugin类:JDK 动态代理的核心
Plugin 类实现了 InvocationHandler,是 MyBatis 提供的便利代理工厂。
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) {// 从 @Intercepts 和 @Signature 注解中提取要拦截的接口和方法Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);Class<?> type = target.getClass();Class<?>[] interfaces = getAllInterfaces(type, signatureMap);if (interfaces.length > 0) {return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));}return target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {// 判断当前方法是否需要被拦截Set<Method> methods = signatureMap.get(method.getDeclaringClass());if (methods != null && methods.contains(method)) {// 需要拦截,调用拦截器的 intercept 方法return interceptor.intercept(new Invocation(target, method, args));}// 不需要拦截,直接调用目标对象的方法return method.invoke(target, args);} catch (Exception e) {throw ExceptionUtil.unwrapThrowable(e);}}}
5.1 注解:@Intercepts和@Signature
自定义插件需要指定拦截的目标和方法签名:
@Intercepts({@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})public classMyPluginimplementsInterceptor{// ...}
type:四大接口之一(Executor, StatementHandler,ParameterHandler,ResultSetHandler)。method:接口中的方法名。args属性用于精确匹配接口中的重载方法。因为在Executor等接口中,可能存在方法名相同但参数列表不同的情况,通过指定args参数类型数组,MyBatis 才能准确定位到开发者想要拦截的具体方法。
Plugin.getSignatureMap 会解析这些注解,构建一个 Map<Class<?>, Set<Method>>,用于 invoke 时判断是否拦截。
5.2 Invocation 类
public class Invocation {private final Object target;private final Method method;private final Object[] args;public Object proceed() throws InvocationTargetException, IllegalAccessException {return method.invoke(target, args);}// getters...}
在自定义插件的intercept方法中,拿到Invocation对象后,可以:
- 执行前置逻辑
- 调用
invocation.proceed()继续执行原方法 - 执行后置逻辑
- 甚至完全修改返回值
六、自定义插件示例:分页插件核心逻辑
以下代码来自开源的 PageHelper 分页插件,展示了真实插件如何处理分页逻辑,包括参数个数判断、异步 count、缓存 key 处理等细节。
@Intercepts(@Signature(type = Executor.class, method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))public class PageInterceptor implements Interceptor {private Dialect dialect;// ... 其他字段@Overridepublic Object intercept(Invocation invocation) throws Throwable {try {Object[] args = invocation.getArgs();MappedStatement ms = (MappedStatement) args[0];Object parameter = args[1];RowBounds rowBounds = (RowBounds) args[2];ResultHandler resultHandler = (ResultHandler) args[3];Executor executor = (Executor) invocation.getTarget();CacheKey cacheKey;BoundSql boundSql;// 重要:query 方法有两种重载:4 参数和 6 参数(带 CacheKey 和 BoundSql)if (args.length == 4) {// 4 个参数时:动态创建 BoundSql 和 CacheKeyboundSql = ms.getBoundSql(parameter);cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);} else {// 6 个参数时:直接从参数中获取cacheKey = (CacheKey) args[4];boundSql = (BoundSql) args[5];}checkDialectExists();// 如果 dialect 实现了 BoundSqlInterceptor 链,则处理 boundSqlif (dialect instanceof BoundSqlInterceptor.Chain) {boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);}List resultList;// 判断是否需要分页if (!dialect.skip(ms, parameter, rowBounds)) {// 调试堆栈日志(用于检查分页使用是否正确)debugStackTraceLog();Future<Long> countFuture = null;// 是否需要执行 count 查询if (dialect.beforeCount(ms, parameter, rowBounds)) {if (dialect.isAsyncCount()) {// 异步查询总数countFuture = asyncCount(ms, boundSql, parameter, rowBounds);} else {// 同步查询总数Long count = count(executor, ms, parameter, rowBounds, null, boundSql);if (!dialect.afterCount(count, parameter, rowBounds)) {// 总数为 0,直接返回空列表return dialect.afterPage(new ArrayList(), parameter, rowBounds);}}}// 执行分页查询resultList = ExecutorUtil.pageQuery(dialect, executor,ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);if (countFuture != null) {Long count = countFuture.get();dialect.afterCount(count, parameter, rowBounds);}} else {// 不需要分页,直接执行原始查询(但仍使用已有的 boundSql 和 cacheKey)resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);}return dialect.afterPage(resultList, parameter, rowBounds);} finally {if (dialect != null) {dialect.afterAll(); // 清理 ThreadLocal 等资源}}}@Overridepublic Object plugin(Object target) {// 只对 Executor 类型的对象进行代理if (target instanceof Executor) {return Plugin.wrap(target, this);}return target;}@Overridepublic void setProperties(Properties properties) {// 初始化 dialect 等}}
关键点解读:
- 参数个数判断:
Executor.query有两个重载,4 参数版本(无CacheKey和BoundSql)和 6 参数版本。插件必须兼容两种调用方式。 CacheKey和BoundSql的处理:如果是从 4 参数进入,需要自己创建;如果是从 6 参数进入,直接使用已有对象。- 异步 count:某些场景下可异步获取总记录数,提高性能。
dialect策略模式:不同数据库分页方言实现不同,通过dialect接口解耦。- 资源清理:
finally块中调用dialect.afterAll(),确保ThreadLocal被清理,避免内存泄漏或线程安全问题。
七、责任链模式:多个插件的执行顺序
- 拦截器按配置顺序添加到
InterceptorChain.interceptors列表。 pluginAll遍历列表,对目标对象依次调用interceptor.plugin(target)。- 每个插件都可能返回一个代理对象,pluginAll方法会遍历拦截器列表,后配置的拦截器会包装先配置的拦截器生成的代理,形成洋葱模型。当方法被调用时,从最外层代理(后配置的插件)开始向内执行,内层插件执行完后,再逐层返回到最外层。。
- 当方法被调用时,从最外层代理开始,层层向内,如果插件都拦截同一个方法,执行顺序为:配置最后的插件先执行
intercept,然后invocation.proceed()调用内一层代理。
示例:两个插件 A(先配置)、B(后配置),都拦截 Executor.query。
- 包装后:
B 代理 包装 A 代理 包装 真实 Executor。 - 调用顺序:
B.intercept→invocation.proceed()→A.intercept→invocation.proceed()→ 真实 Executor.query。 - 返回时逆序。
八、面试高频题
Q1:MyBatis 插件能拦截哪些对象和方法?
A:四大对象:Executor、StatementHandler、ParameterHandler、ResultSetHandler。具体拦截的方法通过@Signature注解指定,只要这些类中声明的方法都可以拦截。
Q2:MyBatis 插件使用什么技术实现?为什么能拦截接口方法?
A:使用JDK 动态代理。四大对象都是接口(Executor、StatementHandler、ParameterHandler、ResultSetHandler),JDK 动态代理可以对这些接口生成代理对象,在invoke方法中判断是否执行拦截逻辑。
Q3:多个插件执行顺序是怎样的?
A:按mybatis-config.xml中<plugin>配置的顺序,后配置的插件包装先配置的插件。执行时,最外层的插件(后配置)最先执行intercept,调用proceed()后进入内层插件,最后才到达真实对象。
Q4:自定义插件需要注意什么?
A:
- 实现
Interceptor接口,并使用@Intercepts和@Signature注解。 - 在
plugin方法中通常使用Plugin.wrap(target, this)生成代理。 - 不要忘记调用
invocation.proceed()放行原方法,否则可能导致业务中断。 - 注意线程安全问题(如分页插件使用
ThreadLocal传递分页参数,用完后必须清理)。
Q5:Plugin.wrap方法中,如何判断一个接口方法是否需要拦截?
A:通过解析@Intercepts注解生成signatureMap,invoke时根据当前method的 declaring class 和 method 本身是否在signatureMap中决定。
九、下篇预告
第 10 篇我们将深入TypeHandler 类型转换体系,包括:
- JDBC 类型与 Java 类型的映射关系
- 内置类型处理器全集
- 自定义 TypeHandler 的开发与注册
- 参数赋值和结果集封装底层调用时机
如果觉得有帮助,欢迎点赞、在看、转发支持!
系列持续更新,关注不走丢 👇
夜雨聆风