理解 mybatis 源码:vibe-coding一个mini-mybatis

从 mini-mybatis 到 mini-mybatis-spring:理解 MyBatis 和 Spring 集成原理

来源: github.com/Nicander93/...

学习 MyBatis 时,如果一上来就看 SqlSessionMapperProxyMappedStatement,很容易觉得这些概念是凭空出现的。

更自然的理解路径应该是:

text 复制代码
JDBC  
  -> DAO 层  
  -> 手写 DAO 实现类的重复劳动  
  -> MyBatis  -> mybatis-spring  

因为 MyBatis 本质上不是"发明了一种新的数据库访问方式",而是站在 JDBC 和 DAO 模式之上,把大量重复、机械、容易出错的代码框架化了。

平时使用 MyBatis 时,我们最熟悉的代码大概是这样的:

java 复制代码
UserMapper mapper = sqlSession.getMapper(UserMapper.class);  
User user = mapper.findById(1L);  

在 Spring 项目里,这段代码通常进一步变成:

java 复制代码
@Service  
class UserService {  
    private final UserMapper userMapper;  
    UserService(UserMapper userMapper) {        this.userMapper = userMapper;    }}  

看起来像是在调用一个普通 Java 接口,但背后真正发生的是:

text 复制代码
Mapper 接口方法  
  -> 定位 SQL  -> 解析参数  
  -> 执行 JDBC  -> 映射 ResultSet  -> 返回 Java 对象  

这篇笔记基于一个学习项目来拆解这条链路。项目分为两个模块:

text 复制代码
mini-mybatis-parent  
├── mini-mybatis          原生 MyBatis 核心机制  
└── mini-mybatis-spring   mini 版 mybatis-spring 适配层  

它不是完整 MyBatis,也不是完整 mybatis-spring。它刻意保留主干逻辑,删掉动态 SQL、缓存、插件、复杂事务同步、异常翻译等高级能力,让我们可以更清楚地看见核心原理。

从 JDBC 开始:最原始的问题

Java 访问数据库的基础是 JDBC。最直接的查询代码大概长这样:

java 复制代码
String sql = "select id, email, name, age from users where id = ?";  
  
try (Connection connection = dataSource.getConnection();  
     PreparedStatement ps = connection.prepareStatement(sql)) {    ps.setLong(1, id);  
    try (ResultSet rs = ps.executeQuery()) {        if (!rs.next()) {            return null;        }        User user = new User();        user.setId(rs.getLong("id"));        user.setEmail(rs.getString("email"));        user.setName(rs.getString("name"));        user.setAge(rs.getInt("age"));        return user;    }}  

这段代码非常清楚,也非常底层。它暴露了几个问题:

  1. 每个查询都要写获取连接、创建 PreparedStatement、关闭资源。
  2. 每个 SQL 都要手动按顺序设置参数。
  3. 每个查询结果都要手动从 ResultSet 映射到对象。
  4. SQL 字符串、参数绑定、对象映射混在一起,业务代码会越来越重。

如果项目里只有几条 SQL,这不是问题。但真实业务里有几十、几百个表和查询时,这些样板代码会迅速膨胀。

所以第一个抽象自然出现了:DAO 层。

DAO 层:把数据访问从业务里拆出去

DAO,也就是 Data Access Object,核心思想很朴素:

text 复制代码
业务层不直接写 JDBC。  
业务层只依赖一个数据访问接口。  
具体数据库访问细节放到 DAO 实现类里。  

例如:

java 复制代码
public interface UserDao {  
    User findById(long id);}  

业务层只关心:

java 复制代码
User user = userDao.findById(1L);  

至于底层怎么查数据库,交给实现类:

java 复制代码
public class JdbcUserDao implements UserDao {  
    private final DataSource dataSource;  
    public User findById(long id) {        String sql = "select id, email, name, age from users where id = ?";        // JDBC: getConnection / prepareStatement / setLong / executeQuery / ResultSet mapping    }}  

DAO 模式带来了一个重要好处:业务层和数据库访问细节解耦。

但它没有消除 JDBC 重复劳动。每个 DAO 实现类里还是会反复出现:

text 复制代码
获取连接  
创建 PreparedStatement绑定参数  
执行 SQL遍历 ResultSet映射 JavaBean关闭资源  

于是问题变成:

text 复制代码
DAO 接口是稳定的,SQL 是开发者想自己控制的;  
但 DAO 实现类里大量 JDBC 模板代码是重复的。  
能不能只写 DAO 接口和 SQL,不写 DAO 实现类?  

这就是理解 MyBatis 的入口。

MyBatis 的位置:自动生成 DAO 实现

从 DAO 的角度看,MyBatis 做的事情可以这样理解:

text 复制代码
你写:  
  UserMapper 接口  
  SQL 注解或 XML  
MyBatis 负责:  
  生成接口代理  执行 JDBC  绑定参数  
  映射结果  

所以 MyBatis 的 Mapper 接口,其实可以看成 DAO 接口的一种演进:

java 复制代码
public interface UserMapper {  
    User findById(long id);}  

区别是:传统 DAO 需要你手写 JdbcUserDao 实现类;MyBatis 用动态代理在运行期帮你"补上"这个实现类。

于是原来手写 DAO 实现类里的逻辑:

text 复制代码
findById()  
  -> SQL  -> PreparedStatement  -> 参数绑定  
  -> ResultSet  -> User  

被 MyBatis 拆成了几个框架组件:

DAO 实现类里的工作 MyBatis 中的组件
找到要执行的 SQL MappedStatement / Configuration
实现 DAO 接口方法 MapperProxy
绑定 PreparedStatement 参数 SqlSourceParser / ParamNameResolver
执行 JDBC Executor
映射查询结果 ResultSet 映射 / PropertyAccessor

这样再看 MyBatis,就不是凭空多出来一堆类,而是把手写 JDBC DAO 实现类拆成了框架内部的几个职责。

先看两张架构图

第一张是原生 mini-mybatis 的宏观分层。它回答的是:一次 Mapper 接口调用,如何一路走到 JDBC?

flowchart TB; subgraph ApplicationLayer; Service[业务代码]; Mapper[Mapper接口]; end; subgraph SessionProxyLayer; Factory[MiniSqlSessionFactory]; Session[SqlSession]; Proxy[MapperProxy]; end; subgraph MappingLayer; Config[Configuration]; Statement[MappedStatement]; SqlSource[注解和XML SQL]; end; subgraph ExecutionLayer; Parser[SqlSourceParser]; Executor[SimpleExecutor]; ResultMap[结果映射]; end; subgraph DatabaseLayer; JDBC[DataSource和JDBC]; DB[(Database)]; end; Service --> Mapper; Mapper --> Proxy; Factory --> Session; Session --> Proxy; SqlSource --> Config; Config --> Statement; Proxy --> Statement; Proxy --> Session; Session --> Executor; Statement --> Parser; Parser --> Executor; Executor --> JDBC; JDBC --> DB; DB --> JDBC; JDBC --> ResultMap; ResultMap --> Service;

第二张是 mini-mybatis-spring 的宏观分层。它回答的是:Spring 容器如何把 Mapper 接口变成可注入的 Bean?

flowchart TB; subgraph SpringApplicationLayer; ConfigClass[Configuration]; MapperScan[MapperScan]; UserService[Service]; MapperBean[UserMapper Bean]; end; subgraph MyBatisSpringAdapterLayer; SFB[SqlSessionFactoryBean]; Template[SqlSessionTemplate]; Registrar[MapperScannerRegistrar]; Scanner[SimpleMapperScanner]; MFB[MapperFactoryBean]; end; subgraph NativeMiniMyBatisLayer; NativeFactory[MiniSqlSessionFactory]; NativeSession[SqlSession]; NativeProxy[MapperProxy]; NativeExecutor[Executor和JDBC]; end; ConfigClass --> SFB; ConfigClass --> Template; MapperScan --> Registrar; Registrar --> Scanner; Scanner --> MFB; SFB --> NativeFactory; Template --> NativeFactory; MFB --> Template; MFB --> MapperBean; UserService --> MapperBean; MapperBean --> Template; Template --> NativeSession; NativeSession --> NativeProxy; NativeProxy --> NativeExecutor;

所以可以先记住这两个核心结论:

text 复制代码
mini-mybatis:负责把 Mapper 方法调用变成 SQL 执行。  
mini-mybatis-spring:负责把 Mapper 代理注册成 Spring Bean。  

一、MyBatis 的核心问题

基于 JDBC 和 DAO 的脉络,MyBatis 要解决的核心问题可以概括成一句话:

text 复制代码
开发者保留 DAO 接口和 SQL 控制权,框架接管 JDBC 模板代码和 DAO 实现类。  

所以它至少需要解决四件事:

  1. 怎么保存"DAO/Mapper 接口方法"和"SQL"的对应关系?
  2. 怎么让一个没有实现类的 Mapper 接口可以被调用?
  3. 怎么把 #{id} 这样的参数占位符绑定到 PreparedStatement
  4. 怎么把 ResultSet 映射成 Java 对象?

如果换成传统 JDBC DAO 的语言,也就是:

text 复制代码
Mapper 接口 = DAO 接口  
MapperProxy = DAO 实现类的运行期替代品  
MappedStatement = DAO 方法背后的 SQL 元数据  
Executor = 被框架抽出来的 JDBC 模板流程  

mini-mybatis 模块里,这四件事分别对应:

问题 mini-mybatis 中的类
保存接口方法和 SQL 的对应关系 MappedStatementConfiguration
Mapper 接口如何被调用 DefaultSqlSessionMapperProxy
SQL 参数如何绑定 SqlSourceParserBoundSqlParamNameResolver
查询结果如何映射 SimpleExecutorPropertyAccessor

二、MappedStatement:一条 SQL 的元信息

MyBatis 不是执行时才去 XML 或注解里临时找 SQL。更合理的方式是:启动或构建阶段先把 SQL 解析出来,保存成统一的元信息。

在这个项目里,这个元信息叫:

java 复制代码
public final class MappedStatement {  
    private final String id;    private final SqlCommandType commandType;    private final String sql;    private final Class<?> resultType;}  

它描述的是"一条可执行 SQL":

text 复制代码
id          = com.example.UserMapper.findById  
commandType = SELECT  
sql         = select id, name from users where id = #{id}  
resultType  = User.class  

所有 MappedStatement 会注册到 Configuration

text 复制代码
Configuration  
  -> Map<String, MappedStatement>  

后面执行 Mapper 方法时,只要拼出 statementId,就能从 Configuration 里找到对应 SQL。

三、statementId:接口方法和 SQL 的桥

Mapper 接口通常长这样:

java 复制代码
public interface UserMapper {  
    @Select("select id, email, name, age from users where id = #{id}")    User findById(@Param("id") long id);}  

XML Mapper 则长这样:

xml 复制代码
<mapper namespace="com.example.UserMapper">  
    <select id="findById" resultType="com.example.User">        select id, email, name, age        from users        where id = #{id}    </select></mapper>  

无论注解还是 XML,最终都会归一成同一个规则:

text 复制代码
statementId = 接口全名 + "." + 方法名  

例如:

text 复制代码
com.example.UserMapper.findById  

这就是 Mapper 方法和 SQL 之间的桥。

四、SqlSession:用户入口

SqlSession 是原生 MyBatis 暴露给用户的主要入口。

mini-mybatis 中,它的能力很小:

java 复制代码
public interface SqlSession extends AutoCloseable {  
    <T> T getMapper(Class<T> mapperType);  
    <T> T selectOne(String statementId, Object parameter);  
    <T> List<T> selectList(String statementId, Object parameter);  
    int insert(String statementId, Object parameter);  
    int update(String statementId, Object parameter);  
    int delete(String statementId, Object parameter);}  

它有两种用法。

第一种是直接按 statementId 执行:

java 复制代码
session.selectOne("com.example.UserMapper.findById", 1L);  

第二种是获取 Mapper:

java 复制代码
UserMapper mapper = session.getMapper(UserMapper.class);  
mapper.findById(1L);  

日常开发里我们更常用第二种,因为它更类型安全,也更像普通业务代码。

五、MapperProxy:接口为什么能执行

Mapper 是接口,没有实现类:

java 复制代码
public interface UserMapper {  
    User findById(long id);}  

那为什么能调用?

答案是 JDK 动态代理。

DefaultSqlSession#getMapper() 里,会创建一个代理对象:

java 复制代码
Object proxy = Proxy.newProxyInstance(  
        mapperType.getClassLoader(),        new Class<?>[]{mapperType},        new MapperProxy<>(this, mapperType));  

业务代码调用:

java 复制代码
mapper.findById(1L);  

其实会进入:

java 复制代码
MapperProxy#invoke(...)  

它的核心逻辑是:

text 复制代码
1. 根据接口类型和方法名拼出 statementId2. 把 Java 方法参数整理成 parameterObject3. 从 Configuration 获取 MappedStatement4. 判断 SQL 类型和返回值类型  
2. 委托 SqlSession 执行 selectOne/selectList/update  

所以 Mapper 接口不是"真的有实现类",而是在运行时由代理对象拦截方法调用,再转成框架内部的 SQL 执行。

六、参数是怎么绑定的

MyBatis 里我们常写:

sql 复制代码
select * from users where id = #{id}  

JDBC 不能直接执行 #{id},它需要的是:

sql 复制代码
select * from users where id = ?  

然后再调用:

java 复制代码
preparedStatement.setObject(1, id);  

mini-mybatis 里,这一步由 SqlSourceParser 完成。

它把原始 SQL:

sql 复制代码
select id, name from users where id = #{id}  

解析成 BoundSql

text 复制代码
sql = select id, name from users where id = ?  
parameterPaths = ["id"]  

如果 SQL 是:

sql 复制代码
insert into users(id, name) values (#{user.id}, #{user.name})  

那么参数路径就是:

text 复制代码
["user.id", "user.name"]  

真正绑定参数时,SimpleExecutor 会按顺序读取这些路径:

text 复制代码
id  
user.id  
user.name  
param1.name  

路径读取由 PropertyAccessor 完成。它支持从 Map 里取值,也支持从 JavaBean 的 getter 或字段里取值。

七、方法参数如何变成 parameterObject

Mapper 方法参数长这样:

java 复制代码
User findByEmailAndAge(@Param("email") String email, @Param("age") int age);  

SQL 里写的是:

sql 复制代码
where email = #{email} and age = #{age}  

这中间需要一个参数名解析过程。

在项目里,这个过程由 ParamNameResolver 完成。

它会把多个参数转成 Map:

text 复制代码
email  -> 参数值  
age    -> 参数值  
param1 -> 第一个参数值  
param2 -> 第二个参数值  

如果方法只有一个参数,并且没有 @Param,就直接把这个参数作为根对象。

这就是为什么下面两种写法都能工作:

java 复制代码
User findById(@Param("id") long id);  
sql 复制代码
where id = #{id}  

以及:

java 复制代码
int insert(@Param("user") User user);  
sql 复制代码
values (#{user.id}, #{user.name})  

八、Executor:真正执行 JDBC 的地方

SimpleExecutor 是真正接触 JDBC 的地方。

查询流程是:

text 复制代码
MappedStatement  
  -> SqlSourceParser 解析成 BoundSql  -> DataSource 获取 Connection  -> 创建 PreparedStatement  -> 按 parameterPaths 绑定参数  
  -> executeQuery  -> ResultSet 映射成 Java 对象  

更新流程类似,只是最后调用 executeUpdate,返回影响行数。

在完整 MyBatis 中,Executor 还会处理一级缓存、二级缓存、批处理、延迟加载、插件拦截等复杂逻辑。这个项目里只保留最直观的 JDBC 主线。

九、结果集如何映射成对象

查询得到的是 ResultSet,业务代码想要的是 User

项目里的映射规则很简单:

text 复制代码
简单类型:返回第一列  
JavaBean:按列名找 setter 或字段  

例如 SQL 返回:

sql 复制代码
select id, email, name, age from users  

就会依次调用:

text 复制代码
setId(...)  
setEmail(...)  
setName(...)  
setAge(...)  

同时支持简单的下划线转驼峰:

text 复制代码
user_name -> userName  

这部分对应完整 MyBatis 中更强大的 ResultMapTypeHandlerMetaObject 等机制。

十、原生 mini-mybatis 的整体链路

把上面串起来,一次 Mapper 调用大致是:

text 复制代码
mapper.findById(1L)  
  -> MapperProxy.invoke()  -> statementId = UserMapper.findById  -> Configuration.getMappedStatement(statementId)  -> ParamNameResolver 解析参数  
  -> SqlSession.selectOne()  -> SimpleExecutor.query()  -> SqlSourceParser 解析 #{}  -> PreparedStatement 绑定参数  
  -> JDBC 执行  
  -> ResultSet 映射成 User  -> 返回给业务代码  

这就是 MyBatis "接口调用即 SQL 执行"的核心。

十一、Spring 集成解决了什么

原生 MyBatis 通常这样用:

java 复制代码
try (SqlSession session = factory.openSession()) {  
    UserMapper mapper = session.getMapper(UserMapper.class);    mapper.findById(1L);}  

但在 Spring 项目里,我们更希望:

java 复制代码
@Service  
class UserService {  
    private final UserMapper userMapper;  
    UserService(UserMapper userMapper) {        this.userMapper = userMapper;    }}  

也就是说,Spring 集成要解决的问题是:

text 复制代码
如何把 MyBatis 的 Mapper 代理变成 Spring Bean?  

这就是 mybatis-spring 的核心价值。

mini-mybatis-spring 模块里,保留了几个关键类:

作用
SqlSessionFactoryBean 把 Spring 配置转换成 MiniSqlSessionFactory
SqlSessionTemplate Spring 单例安全的 SqlSession 门面
MapperFactoryBean 把 Mapper 接口暴露成 Spring Bean
@MapperScan 扫描 Mapper 接口并注册 MapperFactoryBean

十二、SqlSessionFactoryBean:接入 Spring 生命周期

MiniSqlSessionFactory 原本要这样创建:

java 复制代码
MiniSqlSessionFactory factory = new MiniSqlSessionFactoryBuilder()  
        .dataSource(dataSource)        .addMapper(UserMapper.class)        .addXmlMapper("mapper/UserXmlMapper.xml")        .build();  

但 Spring 更习惯用 Bean 配置来声明依赖:

java 复制代码
@Bean  
SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {  
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();    factoryBean.setDataSource(dataSource);    factoryBean.setMapperInterfaces(new Class<?>[]{UserMapper.class});    factoryBean.setMapperLocations(new String[]{"mapper/UserMapper.xml"});    return factoryBean;}  

SqlSessionFactoryBean 实现了 Spring 的 FactoryBean<MiniSqlSessionFactory>

这意味着:

text 复制代码
Spring 管理的是 SqlSessionFactoryBean真正暴露出去的是 getObject() 返回的 MiniSqlSessionFactory  

所以它是一层适配器:

text 复制代码
Spring Bean 生命周期  
  -> afterPropertiesSet()  -> MiniSqlSessionFactoryBuilder  -> MiniSqlSessionFactory  

为什么要包这一层?因为 MyBatis 的工厂构建流程不属于 Spring 原生 Bean 创建方式,需要一个桥把两边接起来。

十三、SqlSessionTemplate:为什么需要 Template

如果直接把原生 SqlSession 放进 Spring 单例 Bean,会有生命周期问题。

在完整 MyBatis 中,SqlSession 通常代表一次数据库会话,它不适合被多个线程长期共享。

所以 mybatis-spring 提供了 SqlSessionTemplate

在这个项目里,SqlSessionTemplate 的策略很简单:

text 复制代码
每次调用  
  -> sqlSessionFactory.openSession()  -> 委托原生 SqlSession 执行  
  -> close()  

它自己可以是 Spring 单例,但真正的原生 SqlSession 是短生命周期的。

这让下面这种注入方式变得安全:

java 复制代码
@Bean  
SqlSessionTemplate sqlSessionTemplate(MiniSqlSessionFactory sqlSessionFactory) {  
    return new SqlSessionTemplate(sqlSessionFactory);}  

真实 mybatis-spring 的 SqlSessionTemplate 还会接入 Spring 事务管理。比如同一个事务中复用同一个 SqlSession,事务结束再统一提交和关闭。这个项目里先省略了事务同步,只保留门面和委托的主线。

十四、MapperFactoryBean:接口变 Bean 的关键

Spring 可以创建普通 class:

java 复制代码
new UserService(...)  

但不能直接创建接口:

java 复制代码
new UserMapper() // 不可能  

所以需要 MapperFactoryBean

它实现:

java 复制代码
FactoryBean<T>  

作用是:

text 复制代码
Spring 实例化 MapperFactoryBeanMapperFactoryBean 内部调用 sqlSessionTemplate.getMapper(UserMapper.class)getObject() 返回 Mapper 代理  
Spring 把这个代理作为 UserMapper Bean 暴露出去  

所以当你写:

java 复制代码
UserMapper mapper = context.getBean(UserMapper.class);  

拿到的其实不是 MapperFactoryBean,而是它 getObject() 返回的 Mapper 代理。

十五、MapperScan:自动注册 MapperFactoryBean

手写每个 Mapper 的 MapperFactoryBean 很重复:

java 复制代码
@Bean  
MapperFactoryBean<UserMapper> userMapper(SqlSessionTemplate sqlSessionTemplate) {  
    MapperFactoryBean<UserMapper> factoryBean = new MapperFactoryBean<>(UserMapper.class);    factoryBean.setSqlSessionTemplate(sqlSessionTemplate);    return factoryBean;}  

所以需要扫描:

java 复制代码
@Configuration  
@MapperScan("com.example.mapper")  
class AppConfig {  
}  

mini-mybatis-spring 中,扫描链路是:

text 复制代码
@MapperScan  
  -> @Import(MapperScannerRegistrar.class)  -> Spring 回调 ImportBeanDefinitionRegistrar  -> MapperScannerRegistrar 读取 basePackages  -> SimpleMapperScanner 扫描接口  
  -> 为每个接口注册 MapperFactoryBean 的 BeanDefinition  

这里的关键是 Spring 的扩展点:

java 复制代码
ImportBeanDefinitionRegistrar  

它允许我们在 Spring 容器启动早期动态注册 BeanDefinition。

扫描到 UserMapper 后,注册进去的不是:

text 复制代码
UserMapper.class  

因为接口不能实例化。

真正注册的是:

text 复制代码
beanName = userMapper  
beanClass = MapperFactoryBean  
constructorArg = UserMapper.class  
property sqlSessionTemplate = ref("sqlSessionTemplate")  

之后 Spring 创建 userMapper Bean 时,走的是 MapperFactoryBean#getObject(),最终拿到 Mapper 代理。

十六、Spring 集成后的整体链路

Spring 容器启动阶段:

text 复制代码
读取 @Configuration  -> 创建 DataSource  -> 创建 SqlSessionFactoryBean  -> 暴露 MiniSqlSessionFactory  -> 创建 SqlSessionTemplate  -> @MapperScan 扫描 Mapper 接口  
  -> 注册 MapperFactoryBean  -> 暴露 Mapper 代理 Bean  

业务调用阶段:

text 复制代码
UserService.userMapper.findById(1L)  
  -> Spring 注入的 Mapper 代理  
  -> SqlSessionTemplate  -> openSession()  -> 原生 SqlSession.getMapper()  -> MapperProxy.invoke()  -> Executor / JDBC  -> 返回 User  

所以 mybatis-spring 本质上没有改变 MyBatis 的 SQL 执行原理。

它主要做的是:

text 复制代码
把 MyBatis 对象接入 Spring 容器  
把 Mapper 代理注册成 Spring Bean把 SqlSession 生命周期托管起来  

十七、这个 mini 项目刻意省略了什么

为了保留主线,这个项目省略了很多完整框架能力。

mini-mybatis 省略了:

  • 动态 SQL,如 <if><where><foreach>
  • 一级缓存、二级缓存
  • 插件机制
  • TypeHandler 注册表
  • 复杂 ResultMap
  • 事务提交回滚

mini-mybatis-spring 省略了:

  • Spring 事务同步
  • SqlSessionUtils
  • 异常翻译
  • SqlSessionFactory / SqlSessionTemplate 选择
  • Mapper 扫描的复杂过滤条件
  • Spring Boot 自动配置

这些省略不是因为它们不重要,而是因为学习时先抓主干更重要。

十八、总结

MyBatis 的核心是:

text 复制代码
Mapper 接口  
  + SQL 元数据  
  + 动态代理  
  + JDBC 执行  
  + 结果映射  

mybatis-spring 的核心是:

text 复制代码
SqlSessionFactoryBean  
  + SqlSessionTemplate  + MapperFactoryBean  + MapperScan  

前者解决"接口方法如何变成 SQL 执行",后者解决"Mapper 代理如何变成 Spring Bean"。

如果把两层合起来看,一次最常见的调用可以浓缩成:

text 复制代码
Service 调用 Mapper  -> Spring 注入的是 Mapper 代理  
  -> Mapper 代理委托 SqlSessionTemplate  -> SqlSessionTemplate 打开原生 SqlSession  -> 原生 MapperProxy 定位 MappedStatement  -> Executor 执行 JDBC  -> ResultSet 映射成对象  
相关推荐
小呆呆6662 小时前
Codex 穷鬼大救星
前端·人工智能·后端
FelixBitSoul3 小时前
缓存淘汰策略全解:从原理到手写实现(Java / Go / Python)
后端·面试
AI人工智能+电脑小能手3 小时前
【大白话说Java面试题】【Java基础篇】第29题:静态代理和动态代理的区别是什么
java·开发语言·后端·面试·代理模式
dovens4 小时前
SpringBoot集成MQTT客户端
java·spring boot·后端
❀͜͡傀儡师4 小时前
Spring Boot 集成 RocksDB 实战:打造高性能 KV 存储加速层
java·spring boot·后端·rocksdb
TeamDev4 小时前
如何在 DotNetBrowser 中使用本地 AI 模型
前端·后端·.net
Rust语言中文社区5 小时前
【Rust日报】2026-05-02 Temper - 用 Rust 编写的 Minecraft 服务器项目发布 0.1.0 版
运维·服务器·开发语言·后端·rust
陈随易5 小时前
2年没用Nodejs了,Bun很香
前端·后端·程序员