设计考虑因素
- 历史兼容性:
-
- MyBatis早期版本主要依赖XML配置,后来才引入接口绑定方式
- 同时支持两种方式可以保证向后兼容
- 明确性:
-
- 显式指定两种路径可以使映射关系更加明确
- 减少因命名不一致导致的潜在问题
- 性能考虑:
-
- 同时扫描可以一次性建立完整的映射关系
- 如果只扫描一种路径再查找另一种,可能需要额外的IO操作
- 灵活性:
-
- 允许XML和接口不同步存在(如部分映射用接口,部分用XML)
- 某些复杂场景可能需要XML的完整表达能力
技术实现角度
虽然技术上可以实现:
- 仅扫描接口 → 通过接口名查找XML
- 仅扫描XML → 通过namespace生成代理
但这样会增加运行时的不确定性,比如:
- 如果只扫描接口但找不到对应XML怎么办?
- 如果XML的namespace与接口名不一致怎么办?
只扫描接口和Mapper文件其中一个在技术上可行,
但MyBatis的设计选择在明确性、可靠性和灵活性之间取得了平衡,这也是大多数企业级框架的常见设计哲学。
XML 文件与注解方式的 SQL 定义
在 MyBatis 中,SQL 语句可以通过两种方式定义:
- XML 文件:通过 XML 文件定义 SQL 语句。
- 注解:通过注解直接在 Mapper 接口上定义 SQL 语句。
虽然 XML 文件中的 namespace
属性指定了对应的 Mapper 接口,并且理论上可以通过反射获取接口上的注解,但在实际的 Spring 和 MyBatis 整合中,扫描 Mapper 接口仍然是必要的。以下是详细的解释:
为什么扫描 Mapper 接口仍然是必要的?
1. 动态代理对象生成
- Spring 的职责:Spring 需要为每个 Mapper 接口生成一个动态代理对象,并将其注册为 Spring 容器中的 Bean。
- MapperScannerConfigurer 或 @MapperScan:这些工具的作用是扫描指定包路径下的所有 Mapper 接口,并为它们生成代理对象。
- 即使 XML 文件中有
namespace
属性指向了某个接口,Spring 也需要知道这些接口的存在,以便生成代理对象并将其纳入 Spring 的生命周期管理。
2. 支持注解方式的 SQL 定义
- XML 文件无法触发注解的解析 :尽管 XML 文件中的
namespace
属性可以指向某个接口,但 Spring 并不会主动去解析该接口上的注解。换句话说,仅依赖 XML 文件无法自动识别和处理接口上的注解定义的 SQL。 - MapperScannerConfigurer 或 @MapperScan:通过扫描接口路径,Spring 可以识别出哪些接口上有注解定义的 SQL,并将其纳入 MyBatis 的配置中。
3. 明确的职责分离
- 清晰的职责划分:通过显式扫描 Mapper 接口路径,可以让 Spring 和 MyBatis 的职责更加明确:
-
- Spring:负责扫描接口、生成代理对象、注册 Bean 并提供依赖注入支持。
- MyBatis:负责解析 SQL 配置(无论是 XML 文件还是注解)并执行 SQL。
- 提高代码可维护性:显式扫描接口路径可以让开发者更容易理解和维护代码,知道哪些包路径下的接口是 Mapper 接口。
4. 类加载机制
- Java 类加载机制 :即使 XML 文件中有
namespace
属性指向某个接口,Java 虚拟机并不会自动加载这些类,除非它们被显式引用。 - 通过扫描接口路径:Spring 可以确保这些接口被加载并注册为 Bean,从而避免潜在的类加载问题。
XML 文件有接口的类路径,反射这个路径不是有所有的注解吗?
1. 实现复杂度
- 自动解析接口注解的实现复杂度较高:虽然可以通过反射获取接口上的注解,但这需要额外的逻辑来解析这些注解并将它们纳入 MyBatis 的配置中。
- 现有框架的设计:MyBatis 和 Spring Boot 的设计已经明确了通过扫描接口路径的方式来识别 Mapper 接口,这使得实现更为简单和直观。
2. 性能考虑
- 性能影响:通过反射获取接口上的注解并在运行时解析这些注解可能会带来一定的性能开销,尤其是在大型项目中有大量 Mapper 接口的情况下。
1 扫描到 Mapper 接口时,Spring 会立即为接口生成代理对象,而不需要等待对应的 Mapper XML 文件加载完成。这种设计是由 Spring 和 MyBatis 的整合机制决定的。
-
代理对象的生成与 SQL 配置无关:Spring 只需要知道某个类是一个 Mapper 接口即可为其生成代理对象,而不需要提前加载 SQL 配置。
-
SQL 配置的加载延迟:MyBatis 会在代理对象实际被调用时,才去查找对应的 SQL 配置(无论是 XML 文件还是注解)。如果找不到对应的 SQL 配置,则会抛出异常。
-
Mapper 接口与代理对象
-
- 当 Spring 扫描到 Mapper 接口时,它会为该接口生成一个动态代理对象。
- 这个代理对象实际上是一个"傀儡"或"代理",它并不直接包含任何 SQL 语句或执行逻辑。它的主要作用是拦截方法调用,并将这些调用转发给 MyBatis 的
SqlSession
对象进行处理。
- Configuration 和 MappedStatement
-
- 在 MyBatis 中,所有的 Mapper XML 文件和注解定义的 SQL 都会被解析并存储在一个名为
Configuration
的核心类中。 Configuration
类中有一个关键的数据结构叫做MappedStatement
,它用来表示每个 SQL 语句的详细信息(包括 SQL 本身、参数映射、结果映射等)。Configuration
类维护了一个Map<String, MappedStatement>
结构(可以理解为你提到的statemappermaping
),其中键是命名空间加上 SQL ID(通常是<namespace>.<sql-id>
或者接口全限定名加上方法名),值是对应的MappedStatement
对象。
- 在 MyBatis 中,所有的 Mapper XML 文件和注解定义的 SQL 都会被解析并存储在一个名为
- SQL 执行流程
-
- 当应用程序通过代理对象调用某个 Mapper 方法时,代理对象会根据方法签名找到对应的
MappedStatement
对象。 - 代理对象持有
SqlSession
实例,它会调用SqlSession
的相关方法(如selectOne
,update
等)来执行 SQL。 SqlSession
会根据传入的方法名从Configuration
中获取对应的MappedStatement
,然后通过执行器(Executor)执行 SQL 并返回结果。
- 当应用程序通过代理对象调用某个 Mapper 方法时,代理对象会根据方法签名找到对应的
- 为什么可以先生成代理对象?
MyBatis 的工作机制
- MyBatis 的核心是基于接口的方法调用和 SQL 映射之间的绑定。
- 当你定义了一个 Mapper 接口(例如
UserMapper
),MyBatis 并不要求在生成代理对象时就加载其对应的 SQL 配置。 - 代理对象的工作是拦截接口方法调用,并将方法签名转换为 SQL 执行请求。具体的 SQL 执行逻辑由 MyBatis 的底层实现完成。
- 具体流程
以下是 Spring 和 MyBatis 整合时的具体工作流程:
第一步:扫描 Mapper 接口
- Spring 启动时,
MapperScannerConfigurer
或@MapperScan
会扫描指定包路径下的所有 Mapper 接口。 - 对于每个扫描到的 Mapper 接口,Spring 会为其生成一个动态代理对象,并将其注册为 Spring 容器中的 Bean。
第二步:初始化 SqlSessionFactoryBean
SqlSessionFactoryBean
是 MyBatis 和 Spring 整合的核心组件。- 在初始化
SqlSessionFactoryBean
时,会加载配置文件(如mybatis-config.xml
)以及指定的 Mapper XML 文件(通过mapperLocations
属性)。 - MyBatis 会解析这些 XML 文件,并将其中的 SQL 配置存储到内存中。
第三步:代理对象的实际调用
- 当应用程序调用某个 Mapper 接口的方法时,代理对象会拦截这个方法调用。
- 代理对象会根据方法签名找到对应的 SQL 配置(无论是注解还是 XML 文件),并将方法参数传递给 MyBatis 的底层执行逻辑。
- 如果找不到对应的 SQL 配置,则会抛出异常(例如
BindingException
)。
- 为什么可以延迟加载 SQL 配置?
- 懒加载的设计:MyBatis 的设计理念之一是懒加载。只有在真正调用某个方法时,才会去查找对应的 SQL 配置。
- 提高启动效率:如果在启动时就加载所有 SQL 配置,可能会导致启动时间变长(特别是当项目中有大量 Mapper 接口和 XML 文件时)。
- 减少内存占用:延迟加载可以避免加载不必要的 SQL 配置,从而减少内存占用。
- 代理对象的生成与 SQL 配置无关:Spring 只需要知道某个类是一个 Mapper 接口即可为其生成代理对象,而不需要提前加载 SQL 配置。
- SQL 配置的加载延迟:MyBatis 会在代理对象实际被调用时,才去查找对应的 SQL 配置。
- 提高性能:通过延迟加载 SQL 配置,可以提高应用的启动效率并减少内存占用。
总结
- 代理对象的作用:代理对象的主要职责是拦截方法调用,并将这些调用转发给 MyBatis 的底层执行逻辑。它本身不保存任何 SQL 语句或执行逻辑。
- Configuration 和 MappedStatement :所有 SQL 语句及其相关信息都被存储在
Configuration
中的MappedStatement
对象里。代理对象通过SqlSession
访问这些信息来执行 SQL。 - 统一的工作机制 :无论是基于 XML 还是注解定义的 SQL,所有的 Mapper 接口代理对象都遵循相同的工作机制。它们都依赖于
SqlSession
和Configuration
来完成 SQL 的执行。
// 分工合作 各司其职
- Spring 的职责:负责扫描接口、生成代理对象、注册 Bean 并提供依赖注入支持。
- MyBatis 的职责:负责解析 SQL 配置(无论是 XML 文件还是注解)并执行 SQL。