摘要:
嘿,各位对技术有极致追求的“源码控”们,我是默语 👨💻!欢迎来到我们《MyBatis-Plus保姆级教程》系列的最终封神篇——第九章:源码深度解析!前面的章节,我们学会了如何“使用”MP的十八般武艺,但你是否曾好奇:为什么继承
BaseMapper
就拥有了CRUD能力?@TableField
的自动填充是如何精准工作的?mybatis-plus-boot-starter
是如何“一键”完成所有配置的?🤔 本章,我们将化身技术侦探,手持“放大镜”深入MP的源码内核,为你逐一揭开这些“黑魔法”的神秘面纱。我们将一起探索SQL注入器的扩展原理、剖析MetaObject元对象处理器的精妙机制、追踪Starter自动装配的完整流程,并深入到核心执行器Executor的调用链路。这不仅是对一个框架的深入学习,更是一次对优秀设计思想的朝圣之旅。准备好,迎接这场最硬核的挑战吧!
博主 默语带您 Go to New World.
✍ 个人主页—— 默语 的博客👦🏻 优秀内容
《java 面试题大全》
《java 专栏》
《idea技术专区》
《spring boot 技术专区》
《MyBatis从入门到精通》
《23种设计模式》
《经典算法学习》
《spring 学习》
《MYSQL从入门到精通》数据库是开发者必会基础之一~
🍩惟余辈才疏学浅,临摹之作或有不妥之处,还请读者海涵指正。☕🍭
🪁 吾期望此文有资助于尔,即使粗浅难及深广,亦备添少许微薄之助。苟未尽善尽美,敬请批评指正,以资改进。!💻⌨
默语是谁?
大家好,我是 默语,别名默语博主,擅长的技术领域包括Java、运维和人工智能。我的技术背景扎实,涵盖了从后端开发到前端框架的各个方面,特别是在Java 性能优化、多线程编程、算法优化等领域有深厚造诣。
目前,我活跃在CSDN、掘金、阿里云和 51CTO等平台,全网拥有超过20万的粉丝,总阅读量超过2000 万。统一 IP 名称为 默语 或者 默语博主。我是 CSDN 博客专家、阿里云专家博主和掘金博客专家,曾获博客专家、优秀社区主理人等多项荣誉,并在 2023 年度博客之星评选中名列前 50,2024年博客之星TOP5。我还是 Java 高级工程师、自媒体博主,北京城市开发者社区的主理人,拥有丰富的项目开发经验和产品设计能力。希望通过我的分享,帮助大家更好地了解和使用各类技术产品,在不断的学习过程中,可以帮助到更多的人,结交更多的朋友.
我的博客内容涵盖广泛,主要分享技术教程、Bug解决方案、开发工具使用、前沿科技资讯、产品评测与使用体验。我特别关注云服务产品评测、AI 产品对比、开发板性能测试以及技术报告,同时也会提供产品优缺点分析、横向对比,并分享技术沙龙与行业大会的参会体验。我的目标是为读者提供有深度、有实用价值的技术洞察与分析。
默语:您的前沿技术领航员
👋 大家好,我是默语!
📱 全网搜索“默语”,即可纵览我在各大平台的知识足迹。📣 公众号“默语摸鱼”,每周定时推送干货满满的技术长文,从新兴框架的剖析到运维实战的复盘,助您技术进阶之路畅通无阻。
💬 微信端添加好友“Solitudemind”,与我直接交流,不管是项目瓶颈的求助,还是行业趋势的探讨,随时畅所欲言。
快来加入技术社区,一起挖掘技术的无限潜能,携手迈向数字化新征程!
🚀【MyBatis-Plus源码解析】第九章:深入内核,揭秘框架设计原理(SQL注入器/MetaObject/自动装配)
搜索词条: MyBatis-Plus源码, 源码解析, SQL注入器, ISqlInjector, MetaObjectHandler, 自动装配, MybatisPlusAutoConfiguration, MyBatis Executor, 框架原理
✨ 引言
“知其然,知其所以然。” —— 王充《论衡》
经过前八章的学习,我们已经能够娴熟地运用MyBatis-Plus来构建复杂的企业级应用。我们知道“如何做”,但真正的技术大师,不止于此,他们更渴望了解“为何如此”。框架对我们来说,不应是一个神秘的“黑盒”,理解其内部的运行机制和设计哲学,能带给我们诸多益处:
- 深度定制:当标准功能不满足需求时,能精准地找到扩展点进行定制。
- 高效排错:遇到疑难杂症时,能快速定位问题根源,而不是靠猜测。
- 学习优秀设计:借鉴其优秀的设计模式(如装饰器、策略、模板方法等)和编程思想,提升自己的代码设计能力。
- 贡献社区:具备向开源社区贡献代码、修复Bug的能力。
本章,我们将不再关注API的使用,而是将目光投向框架的“心脏”地带。我们将挑选MyBatis-Plus中几个最具代表性的核心机制进行源码层面的深度剖析。这会是一次充满挑战但收获巨大的旅程,系好安全带,让我们一起潜入源码的深海!
第九阶段:源码深度解析
9.1 SQL注入器扩展原理
❓ 现象:为什么我们的UserMapper
接口仅仅是继承了BaseMapper<User>
,没有编写任何XML和SQL,就可以直接调用insert
、selectById
等方法?这些SQL语句是从哪里来的?
💡 答案:ISqlInjector
在应用启动时,已经为我们动态地“注入”了这些通用的SQL。
核心流程剖析
-
入口:
ISqlInjector
接口
这是SQL注入器的顶层抽象,其核心方法是injectSqlRunner(MapperBuilderAssistant builderAssistant, Class<?> mapperClass)
。它的作用就是检查并向MyBatis的配置中注入SQL。 -
默认实现:
DefaultSqlInjector
MP默认使用的实现类是DefaultSqlInjector
。它的injectSqlRunner
方法会调用getMethodList(Class<?> mapperClass)
来获取一个AbstractMethod
的列表。 -
关键角色:
AbstractMethod
抽象类
这才是“魔法”的真正来源。MP中每一个通用的CRUD方法(如insert
,deleteById
等),都对应一个AbstractMethod
的实现类。例如:Insert.java
对应insert
方法DeleteById.java
对应deleteById
方法SelectById.java
对应selectById
方法
-
SQL的构建与注入
让我们以Insert
类为例,看看它的核心方法inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo)
。这个方法在应用启动时被调用,其内部逻辑(简化后)如下:// com.baomidou.mybatisplus.core.injector.methods.Insert.java (源码简化示意) public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { // 1. 获取预设的SQL模板,SqlMethod.INSERT_ONE 是一个枚举,值为 "INSERT INTO %s %s VALUES %s" SqlMethod sqlMethod = SqlMethod.INSERT_ONE; // 2. 动态拼接SQL // tableInfo.getTableName() -> 表名 // prepareFieldSql(tableInfo) -> (id, name, age) // prepareValuesSql(tableInfo) -> (#{et.id}, #{et.name}, #{et.age}) String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), prepareFieldSql(tableInfo), prepareValuesSql(tableInfo)); // 3. 获取主键生成器 KeyGenerator keyGenerator = new NoKeyGenerator(); // ... 判断主键策略,如果是自增,则使用 Jdbc3KeyGenerator // 4. 创建一个SqlSource,用于MyBatis执行 SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass); // 5. 将方法名("insert")和构建好的SQL、KeyGenerator等信息,一起打包成一个MappedStatement对象,并添加到MyBatis的全局Configuration中 return this.addInsertMappedStatement(mapperClass, modelClass, sqlMethod.getMethod(), sqlSource, keyGenerator, "id", "id"); }
总结:BaseMapper
的通用方法并非运行时动态生成SQL,而是在Spring Boot应用启动,MyBatis初始化的过程中,由ISqlInjector
遍历所有AbstractMethod
实现类,预先将动态拼接好的SQL语句,注册成为一个一个的MappedStatement
对象,存入MyBatis的全局配置中。当你的代码调用userMapper.insert()
时,MyBatis能根据方法名找到这个已经注册好的MappedStatement
,并执行它。
9.2 元对象处理器MetaObject机制
❓ 现象:在第五章中,我们通过实现MetaObjectHandler
接口,并配合@TableField(fill=...)
注解,实现了createTime
等字段的自动填充。MP是如何在我们调用insert
时,精准地修改我们传入的User
实体对象内部的属性值的?
💡 答案:这背后依赖的是MyBatis自身提供的强大工具——MetaObject
。
核心流程剖析
-
MetaObject
是什么?
MetaObject
是MyBatis提供的一个极为强大的对象元数据操作工具类。你可以把它看作是一个对象的“万能遥控器”。无论这个对象是一个简单的POJO,还是一个复杂的嵌套对象,或是一个Map
,MetaObject
都可以用统一的API(如getValue(propName)
、setValue(propName, value)
)来获取和设置其属性值,而无需我们手动编写繁琐的反射代码。 -
拦截点
自动填充的逻辑,是在SQL即将被执行前,在参数被处理时触发的。具体的拦截点在MybatisParameterHandler
中。当MP的拦截器发现当前执行的是一个INSERT
或UPDATE
操作时,它会启动填充逻辑。 -
填充过程
a. 获取INSERT
或UPDATE
操作的参数对象,也就是我们传入的User
实体。
b. 关键一步:将这个User
实体包装成一个MetaObject
对象。MetaObject metaObject = SystemMetaObject.forObject(user);
c. 调用我们自定义的MyMetaObjectHandler
的insertFill(metaObject)
或updateFill(metaObject)
方法,并将刚刚创建的metaObject
作为参数传入。
d. 在我们的处理器中,我们调用的this.setFieldValByName("createTime", now, metaObject);
这样的方法,其底层实际上就是执行了metaObject.setValue("createTime", now);
。
e.MetaObject
内部会利用高效的反射机制,找到user
对象中名为createTime
的字段,并调用其setter方法(或直接修改字段值)来完成赋值。
总结:MP的自动填充功能,是巧妙地在SQL执行前的参数处理阶段设置的一个“钩子”。它利用MyBatis原生的MetaObject
工具,将我们的实体对象包装起来,然后交由我们自定义的MetaObjectHandler
进行处理。我们通过MetaObject
这个“遥控器”,安全、高效地修改了实体对象的属性,从而实现了自动填充,整个过程对业务代码完全透明。
9.3 启动器自动装配流程分析
❓ 现象:在Spring Boot项目中,我们仅仅是在pom.xml
中添加了mybatis-plus-boot-starter
依赖,并在application.yml
中写了些配置,所有核心组件(如SqlSessionFactory
、SqlSessionTemplate
)就都自动配置好了,Mapper接口也被自动扫描并注入到Spring容器。这是如何发生的?
💡 答案:这完全归功于Spring Boot的自动装配(Auto-Configuration)机制。
核心流程剖析
-
入口:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
在mybatis-plus-boot-starter.jar
中,存在这个文件(老版本是spring.factories
),它像一个“路标”,告诉Spring Boot在启动时需要加载哪些自动配置类。其中就明确指向了MybatisPlusAutoConfiguration.class
。 -
核心配置类:
MybatisPlusAutoConfiguration
这个类是所有自动装配逻辑的“总司令部”。它上面布满了各种@ConditionalOn...
注解,以决定在何种条件下才激活这些配置。@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
:确保只有在classpath中存在MyBatis核心类时,此配置才生效。@EnableConfigurationProperties(MybatisPlusProperties.class)
:将我们在application.yml
中所有mybatis-plus
前缀的配置项,自动绑定到一个名为MybatisPlusProperties
的Java配置对象上。
-
核心Bean的创建
MybatisPlusAutoConfiguration
内部通过@Bean
方法为我们创建了所有必需的核心组件:-
SqlSessionFactory
:@Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean(); // 1. 设置数据源 factory.setDataSource(dataSource); // 2. 加载 MybatisPlusProperties 中的配置 factory.setConfiguration(properties.getConfiguration()); // 3. 【关键】将所有插件(Interceptor)注入 factory.setPlugins(this.interceptors); // ... 其他配置,如设置mapperLocations等 return factory.getObject(); }
-
SqlSessionTemplate
: 创建一个线程安全的SqlSessionTemplate
,这是我们实际开发中注入和使用的对象。 -
扫描Mapper: 通过
@Import({ MybatisPlusAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class })
,间接地实现了与@MapperScan
注解类似的功能,它会扫描MybatisPlusProperties
中指定的mapper-locations
,将所有Mapper接口注册为Spring Bean。
-
总结:MyBatis-Plus的“零配置”体验,是严格遵循Spring Boot的SPI(Service Provider Interface)规范实现的。通过AutoConfiguration.imports
文件引导,MybatisPlusAutoConfiguration
类承担了所有繁重的配置工作:它读取我们的yml
配置,创建并组装DataSource
, SqlSessionFactory
, SqlSessionTemplate
等核心对象,注入插件,并扫描注册Mapper接口,最终将一个功能完备的持久层环境呈现在我们面前。
9.4 核心执行器Executor链路追踪
❓ 现象:当我们调用userMapper.selectById(1)
时,这个简单的接口调用,在MyBatis和MP内部,究竟经历了一条怎样的“调用链”,最终才触达数据库?
💡 答案:这是一条由代理和装饰器模式精心构建的执行链路。
核心流程剖析
-
起点:MapperProxy
我们注入的UserMapper
实际上不是接口本身,而是一个由MyBatis通过JDK动态代理创建的代理对象,即MapperProxy
。当我们调用userMapper.selectById(1)
时,实际上是调用了MapperProxy
的invoke
方法。 -
核心会话:
SqlSession
MapperProxy
的invoke
方法会从SqlSessionTemplate
中获取一个线程安全的SqlSession
。SqlSession
是MyBatis执行操作的API门面,它将所有工作委托给一个内部的Executor
对象。 -
执行核心:
Executor
Executor
是MyBatis中真正负责执行SQL的组件。它有几种基本实现:SimpleExecutor
(每次都创建新的Statement
)、ReuseExecutor
(重用Statement
)、BatchExecutor
(批量执行)。 -
精髓:插件链与装饰器模式
这才是最关键的部分。我们配置的所有插件(Interceptor),并不会改变Executor
的内部代码。相反,它们通过装饰器模式,像洋葱一样,一层一层地将原始的Executor
包裹起来。- 假设我们配置了分页插件和乐观锁插件。
- MyBatis创建的
Executor
实际结构是:分页插件代理( 乐观锁插件代理( 原始的SimpleExecutor ) )
。
-
调用链路追踪:
a.SqlSession.selectOne(...)
被调用。
b.SqlSession
调用最外层Executor
(即分页插件代理)的query
方法。
c. 分页插件的intercept
方法被触发。它判断这是一个分页查询,于是改写SQL(加上LIMIT
),然后调用invocation.proceed()
,将请求传递给内层的乐观锁插件代理。
d. 乐观锁插件的intercept
方法被触发。它判断这不是一个update操作,于是直接调用invocation.proceed()
,将请求传递给最内层的**SimpleExecutor
**。
e.SimpleExecutor
执行doQuery
方法,通过JDBC与数据库交互,获取ResultSet
。
f.ResultSet
沿着调用链原路返回。每个插件都可以在返回过程中对结果集进行处理(例如我们上一章自定义的解密插件)。
g. 最终,处理后的结果返回给SqlSession
,再返回给我们的业务代码。
总结:MyBatis-Plus(及MyBatis)的执行链路是一个经典的装饰器模式应用。我们配置的每一个插件,都成为了执行链路上的一环,通过层层代理,在不修改核心代码的情况下,为原始的SQL执行流程增加了额外的功能(如分页、防攻击、乐观锁等)。这种设计具有极高的灵活性和可扩展性。
📝 总结与毕业感言
恭喜你,完成了这场最硬核的源码探索之旅!在本章中,我们不再是功能的“消费者”,而是原理的“探究者”。我们一起:
- 揭示了
ISqlInjector
在启动时预生成SQL的秘密。 - 理解了
MetaObjectHandler
是如何借助MyBatis的**MetaObject
工具**实现透明的自动填充。 - 追踪了
MybatisPlusAutoConfiguration
如何编排了一场完美的自动装配大戏。 - 领略了
Executor
执行链中装饰器模式的优雅与强大。
通过对源码的深度剖析,MyBatis-Plus对我们而言,已经不再是一个“黑盒”。我们理解了它的设计哲学,掌握了它的脉搏。这份从“术”到“道”的理解,将成为你技术生涯中宝贵的财富。
至此,《MyBatis-Plus保姆级教程》系列全部内容已尽数奉上。从入门到精通,从应用到架构,再到源码,我们共同走过了一段漫长而充实的学习之路。愿这个系列能真正成为你手中的利器,助你在技术的道路上披荆斩棘,一往无前。感谢你,坚持到最后的勇者!
天下没有不散的筵席,但技术的探索永不落幕。我们,江湖再会! 👋
📚 参考资料
- MyBatis-Plus Source Code on Gitee (官方镜像)
- MyBatis-3 Source Code on GitHub
- 设计模式之装饰器模式 (Decorator Pattern)
如对本文内容有任何疑问、建议或意见,请联系作者,作者将尽力回复并改进📓;( 联系微信:Solitudemind )
点击下方名片,加入 IT 技术核心学习团队。一起探索科技的未来,共同成长。
为了让您拥有更好的交互体验,特将这行文字设置为可点击样式:点击下方名片,加入 IT
技术核心学习团队。一起探索科技的未来,共同成长。