MyBatis完整流程详解

很多小伙伴用MyBatis很久,却始终没理清「从写Mapper接口,到最终执行SQL」的完整链路,尤其是Spring整合MyBatis后,各种注册器、工厂Bean、代理对象让人眼花缭乱。

今天就以「扫描接口 → 注册 → MapperFactoryBean → getObject → getMapper → 创建代理 → 拼SOCO → 找SQL → 执行」为核心,结合具体例子和精简源码,一步步拆解开,保证看完能懂、记住能说,甚至能应对面试提问!

先提前对齐关键名词(避免理解偏差):

  • 扫描接口:Spring+MyBatis扫描我们写的Mapper接口(比如UserMapper);
  • 注册:把扫描到的Mapper接口,注册成Spring能识别的Bean;
  • SOCO(通用唯一识别码):对应MyBatis官方的「statementId」,即「接口全类名.方法名」,是SQL的唯一标识;
  • configuration:MyBatis的全局配置中心,存储所有SQL信息、Mapper接口信息;
  • MapperRegistry:MyBatis的Mapper注册器,管理所有Mapper接口和对应的代理工厂。

例子准备:我们以最常见的「UserMapper」为例,接口如下(简单好记):

java 复制代码
// Mapper接口(被扫描的接口)
public interface UserMapper {
    // 方法名:selectById,对应XML里的SQL
    User selectById(Long id);
}
xml 复制代码
// 对应的MyBatis XML(SQL存储)
<select id="selectById" parameterType="java.lang.Long" resultType="com.xxx.entity.User">
    select id, name, age from user where id = #{id}
</select>

下面全程围绕这个例子,把完整流程拆成9步。

第一步:启动触发扫描------@MapperScan注解的作用

我们在SpringBoot启动类上写的@MapperScan,是整个流程的「开关」,也是一切的开始。

java 复制代码
// 启动类注解,指定要扫描的Mapper接口包
@SpringBootApplication
@MapperScan("com.xxx.mapper") // 扫描com.xxx.mapper包下所有Mapper接口
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

源码关键操作

@MapperScan注解的核心作用,是「导入」MyBatis提供的一个类:MapperScannerRegistrar(Mapper扫描注册器)。

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
	// ...
	String[] basePackages() default {};
	// ...
}

这个类实现了Spring的ImportBeanDefinitionRegistrar接口,Spring启动时,会自动调用它的registerBeanDefinitions()方法------这一步就是「准备扫描」,相当于告诉Spring:"该去扫Mapper接口了"。

java 复制代码
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
	// ...
	void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
      BeanDefinitionRegistry registry, String beanName) {
      // ...
	}
	// ...
}

关键记忆点

@MapperScan → 导入MapperScannerRegistrar → 启动扫描流程,此时还没真正扫接口,只是"做好了扫描的准备"。

第二步:真正扫描接口------ClassPathMapperScanner干活

上一步的MapperScannerRegistrar(注册器),不直接扫描接口,它的核心工作是「创建真正的扫描器,并配置好」,这个真正干活的扫描器就是:ClassPathMapperScanner。

源码关键操作(精简,保留核心)

java 复制代码
// MapperScannerRegistrar的核心方法
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    // 1. 解析@MapperScan注解里的配置(比如扫描的包路径com.xxx.mapper)
    AnnotationAttributes mapperScanAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    String[] basePackages = mapperScanAttrs.getStringArray("basePackages"); // 拿到扫描包

    // 2. 关键:创建真正的扫描器------ClassPathMapperScanner(在Registrar内部new出来)
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

    // 3. 给扫描器配置:扫到接口后,要把接口变成MapperFactoryBean(重点!)
    scanner.setMapperFactoryBeanClass(MapperFactoryBean.class);

    // 4. 触发扫描:让扫描器去指定包下扫接口
    scanner.doScan(StringUtils.toStringArray(basePackages));
}

扫描器干的2件核心事(对应我们的例子)

  1. 扫描指定包(com.xxx.mapper),找到所有「接口」(比如我们的UserMapper),排除非接口、已注册的类;
  2. 最关键的一步:修改扫描到的接口的Bean类型------把UserMapper接口的BeanClass,强行改成「MapperFactoryBean.class」。

简单说:Spring原本以为要创建UserMapper接口的Bean,但扫描器告诉它:"不用,你创建MapperFactoryBean的Bean就行,它能帮你拿到UserMapper的实例"。

关键记忆点

MapperScannerRegistrar(指挥官)→ new ClassPathMapperScanner(士兵)→ 士兵扫描接口 → 把接口Bean改成MapperFactoryBean。

第三步:注册Bean------把Mapper接口(伪装成MapperFactoryBean)注册到Spring

扫描完成后,ClassPathMapperScanner会把每个扫描到的Mapper接口,都注册成一个「BeanDefinition」(Spring的Bean定义),然后交给Spring的BeanFactory管理。

对应我们的例子

扫描到UserMapper接口后,注册一个BeanDefinition,这个Bean的信息是:

  • Bean名称:userMapper(默认接口名首字母小写);
  • Bean类型:MapperFactoryBean(不是UserMapper接口);
  • 关联的Mapper接口:UserMapper.class(告诉MapperFactoryBean,它要帮我们创建哪个接口的实例)。

关键记忆点

这一步的「注册」,不是注册UserMapper接口本身,而是注册「MapperFactoryBean」,为后续创建代理对象做准备。

第四步:初始化Bean------调用MapperFactoryBean.getObject()

当我们在Service里注入UserMapper时(如下),Spring会去创建这个Bean,而因为这个Bean的类型是MapperFactoryBean,Spring会调用它的核心方法:getObject()(工厂Bean的核心方法,用来创建真正的实例)。

java 复制代码
@Service
public class UserService {
    // 注入的不是UserMapper接口的实现类,是代理对象
    @Autowired
    private UserMapper userMapper;

    // 调用方法时,触发后续流程
    public User getUser(Long id) {
        return userMapper.selectById(id);
    }
}

源码关键操作(MapperFactoryBean.getObject())

java 复制代码
// MapperFactoryBean的核心方法,返回真正的Mapper实例(代理对象)
@Override
public T getObject() throws Exception {
    // 关键:通过SqlSession获取Mapper接口的实例
    return getSqlSession().getMapper(this.mapperInterface);
}

// 获取SqlSession(默认是DefaultSqlSession,MyBatis的核心会话对象)
public SqlSession getSqlSession() {
    return this.sqlSessionTemplate.getSqlSession();
}

对应我们的例子

this.mapperInterface就是UserMapper.class,所以getObject()本质就是调用:sqlSession.getMapper(UserMapper.class),目的是拿到UserMapper的实例。

关键记忆点

注入Mapper → Spring调用MapperFactoryBean.getObject() → 最终调用sqlSession.getMapper(接口.class)。

第五步:获取Mapper实例------sqlSession.getMapper()触发代理创建

我们拿到的SqlSession,默认是MyBatis的DefaultSqlSession,它的getMapper()方法,并不会直接创建实例,而是交给MyBatis的「MapperRegistry」(也就是咱们说的mapperRegistar)来处理。

先讲一个关键前提:MapperRegistry何时存储Mapper接口?

这里要回答你最关心的问题:Mapper接口何时放进MapperRegistry和Configuration?

答案:Spring启动时,MyBatis初始化Configuration(全局配置),同时会把扫描到的所有Mapper接口,注册到MapperRegistry中。

具体来说:

  1. MyBatis的Configuration(全局配置中心),内部有一个MapperRegistry属性(mapperRegistry);
  2. 当ClassPathMapperScanner扫描到UserMapper接口后,除了注册成Spring Bean,还会把UserMapper接口,注册到Configuration的mapperRegistry中;
  3. MapperRegistry里有一个Map(knownMappers),key是Mapper接口(UserMapper.class),value是「代理工厂(MapperProxyFactory)」,用于后续创建代理对象。

源码关键操作(DefaultSqlSession.getMapper() + MapperRegistry.getMapper())

java 复制代码
// 1. DefaultSqlSession的getMapper方法
@Override
public <T> T getMapper(Class<T> type) {
    // 交给Configuration的mapperRegistry处理
    return configuration.getMapper(type, this);
}

// 2. Configuration的getMapper方法
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 交给MapperRegistry处理
    return mapperRegistry.getMapper(type, sqlSession);
}

// 3. MapperRegistry的getMapper方法(核心)
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 从knownMappers中拿到代理工厂(之前注册好的)
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    // 用代理工厂创建代理对象
    return mapperProxyFactory.newInstance(sqlSession);
}

对应我们的例子

type就是UserMapper.class,从knownMappers中拿到UserMapper对应的MapperProxyFactory,然后调用newInstance()方法,创建UserMapper的代理对象。

关键记忆点

扫描接口时 → Mapper接口被注册到Configuration的MapperRegistry(knownMappers)→ getMapper()时,从MapperRegistry拿到代理工厂 → 准备创建代理。

第六步:创建代理对象------JDK动态代理(核心步骤)

MyBatis创建的Mapper代理对象,用的是「JDK动态代理」(因为Mapper是接口,JDK动态代理只支持接口),核心类是「MapperProxy」(实现了InvocationHandler接口,代理的核心逻辑在这里)。

源码关键操作(MapperProxyFactory.newInstance())

java 复制代码
// 代理工厂创建代理对象
protected T newInstance(SqlSession sqlSession) {
    // 1. 创建InvocationHandler:MapperProxy,封装了代理逻辑
    MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    // 2. JDK动态代理,生成代理对象(代理的是Mapper接口)
    return (T) Proxy.newProxyInstance(
        mapperInterface.getClassLoader(), // 类加载器
        new Class[]{mapperInterface},     // 要代理的接口(UserMapper)
        mapperProxy                       // 代理逻辑(InvocationHandler)
    );
}

对应我们的例子

这里生成的代理对象,就是我们@Autowired注入的UserMapper实例------也就是说,我们注入的不是UserMapper的实现类,而是JDK动态代理生成的代理对象。

关键记忆点

MapperProxyFactory → new MapperProxy(代理逻辑)→ JDK动态代理 → 生成Mapper代理对象(注入Service)。

第七步:调用方法------拼出SOCO(statementId)

当我们在Service里调用userMapper.selectById(1L)时,因为注入的是代理对象,所以会进入MapperProxy的invoke()方法(JDK动态代理的核心方法),在这里拼出我们的SOCO。

源码关键操作(MapperProxy.invoke() + 拼SOCO)

java 复制代码
// 代理对象的invoke方法,所有Mapper方法调用都会走这里
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 1. 缓存MapperMethod(封装方法信息和SOCO)
    MapperMethod mapperMethod = cachedMapperMethod(method);
    // 2. 执行方法,核心是先拿到SOCO,再找SQL
    return mapperMethod.execute(sqlSession, args);
}

// 缓存MapperMethod,创建时拼出SOCO
private MapperMethod cachedMapperMethod(Method method) {
    return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}

// MapperMethod的构造方法,拼SOCO(statementId)
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    // 关键:SOCO = 接口全类名 + "." + 方法名
    this.command = new SqlCommand(config, mapperInterface, method);
}

// SqlCommand类,封装SOCO
public SqlCommand(Configuration config, Class<?> mapperInterface, Method method) {
    // 拼SOCO:com.xxx.mapper.UserMapper.selectById(这就是我们说的SOCO)
    String statementId = mapperInterface.getName() + "." + method.getName();
    this.name = statementId; // 把SOCO赋值给name,后续用它找SQL
}

对应我们的例子

mapperInterface是UserMapper.class(全类名:com.xxx.mapper.UserMapper),method是selectById方法,所以拼出的SOCO是:

com.xxx.mapper.UserMapper.selectById

这个SOCO,就是MyBatis中「一条SQL的唯一标识」,和我们XML里的select标签id="selectById"一一对应。

关键记忆点

调用Mapper方法 → 进入MapperProxy.invoke() → 创建MapperMethod → 拼SOCO(接口全类名.方法名)。

第八步:找到SQL------用SOCO从Configuration中获取MappedStatement

拼出SOCO后,下一步就是用这个SOCO,去MyBatis的Configuration(全局配置)中,找到对应的SQL信息。

先讲关键前提:SQL何时放进Configuration?

答案:MyBatis初始化时,会解析所有Mapper XML文件(或注解式SQL),把每一条SQL都封装成MappedStatement对象,存储到Configuration的mappedStatements(一个Map)中。

具体来说:

  1. Configuration内部有一个Map<String, MappedStatement> mappedStatements;
  2. key:就是我们拼的SOCO(statementId);
  3. value:MappedStatement对象,封装了SQL文本、参数类型、返回类型、结果映射等所有SQL相关信息。

源码关键操作(用SOCO找SQL)

java 复制代码
// MapperMethod.execute()方法中,会先拿到SOCO,再找SQL
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    // 根据SQL类型(SELECT/INSERT等)执行,核心是先拿到MappedStatement
    switch (command.getType()) {
        case SELECT:
            // 1. 用SOCO(command.getName())从Configuration中找MappedStatement
            MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName());
            // 2. 执行SQL,处理参数和结果
            result = sqlSession.selectOne(command.getName(), param);
            break;
        // 其他类型(INSERT/UPDATE/DELETE)逻辑类似
        default:
            throw new BindingException("Unknown execution method for: " + command.getName());
    }
    return result;
}

// Configuration.getMappedStatement(),根据SOCO找SQL
public MappedStatement getMappedStatement(String id) {
    return getMappedStatement(id, true);
}

// 从mappedStatements中根据SOCO(id)获取MappedStatement
private MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
    MappedStatement ms = mappedStatements.get(id);
    // 如果没找到,就抛出异常(比如SOCO写错、XML没配置SQL)
    if (ms == null) {
        throw new BindingException("Invalid bound statement (not found): " + id);
    }
    return ms;
}

对应我们的例子

command.getName()就是我们拼的SOCO(com.xxx.mapper.UserMapper.selectById),用这个SOCO去Configuration的mappedStatements中,就能找到对应的MappedStatement对象,这个对象里就包含了XML中写的:

select id, name, age from user where id = #{id}

关键记忆点

MyBatis初始化 → 解析XML/注解SQL → 封装成MappedStatement → 存入Configuration的mappedStatements → 用SOCO作为key,从Map中取出对应SQL。

第九步:执行SQL------MyBatis底层执行JDBC

拿到MappedStatement(SQL信息)后,MyBatis就会进入底层的JDBC执行流程,这一步MyBatis帮我们封装了所有繁琐操作,我们不用关心JDBC的Connection、PreparedStatement等对象。

核心执行流程(对应例子)

  1. 通过SqlSession获取数据库连接(Connection);
  2. 根据MappedStatement中的SQL文本,创建PreparedStatement;
  3. 处理参数:把方法传入的id(1L),替换SQL中的#{id};
  4. 执行SQL:调用PreparedStatement.execute(),查询数据库;
  5. 结果映射:把数据库返回的结果集,映射成User对象(根据XML中的resultType配置);
  6. 关闭连接、释放资源(MyBatis自动管理);
  7. 把User对象返回给Service,最终返回给前端。

关键记忆点

拿到MappedStatement → MyBatis封装JDBC执行 → 结果映射 → 返回Java对象。

总结:完整流程

用咱们的例子,把9步浓缩成一条直线,记熟就能应对所有相关问题:

  1. @MapperScan → 导入MapperScannerRegistrar(指挥官)
  2. 指挥官new ClassPathMapperScanner(士兵)→ 士兵扫描com.xxx.mapper包
  3. 扫到UserMapper接口 → 把它的Bean类型改成MapperFactoryBean → 注册到Spring
  4. 注入UserMapper → Spring调用MapperFactoryBean.getObject()
  5. getObject() → 调用sqlSession.getMapper(UserMapper.class)
  6. sqlSession交给MapperRegistry(MyBatis的Mapper注册器)→ 从knownMappers拿到代理工厂
  7. 代理工厂用JDK动态代理 → 生成UserMapper代理对象(注入Service)
  8. 调用userMapper.selectById(1L) → 进入MapperProxy.invoke() → 拼SOCO(com.xxx.mapper.UserMapper.selectById)
  9. 用SOCO从Configuration的mappedStatements中找SQL → 执行JDBC → 返回User对象

补充:两个关键疑问

  1. Q:Mapper接口和SQL,何时放进Configuration和MapperRegistry?

    A:Spring启动时,MyBatis初始化Configuration,同时:① 扫描到的Mapper接口,注册到Configuration的MapperRegistry(knownMappers);② 解析XML/注解SQL,封装成MappedStatement,注册到Configuration的mappedStatements。

  2. Q:我们注入的UserMapper是什么?

    A:不是实现类,是JDK动态代理生成的代理对象,代理逻辑在MapperProxy中,负责拼SOCO、找SQL、执行SQL。


相关推荐
zzwq.2 小时前
PyMySQL 详解:从入门到实战,Python 操作 MySQL 一站式指南
开发语言·python
码码哈哈0.02 小时前
Spring AI 1.0.0 + ChromaDB 最新版踩坑:Collection does not exist 404 报错全记录
java·人工智能·spring
Z1Jxxx2 小时前
C++ P1151 子数整数
开发语言·c++·算法
User_芊芊君子2 小时前
Python+Agent入门实战:0基础搭建可复用AI智能体
开发语言·人工智能·python
开开心心就好2 小时前
操作简单的ISO文件编辑转换工具
java·前端·科技·edge·pdf·安全威胁分析·ddos
卷卷说风控2 小时前
工作流的 Skill 怎么写?
java·javascript·人工智能·chrome·安全
SunnyDays10112 小时前
Java实战指南:如何高效将PDF转换为高质量TIFF图片
java·pdf转tiff
Seven972 小时前
【从0到1构建一个ClaudeAgent】规划与协调-TodoWrite
java
Yeh2020582 小时前
maven
java·maven