很多小伙伴用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件核心事(对应我们的例子)
- 扫描指定包(com.xxx.mapper),找到所有「接口」(比如我们的UserMapper),排除非接口、已注册的类;
- 最关键的一步:修改扫描到的接口的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中。
具体来说:
- MyBatis的Configuration(全局配置中心),内部有一个MapperRegistry属性(mapperRegistry);
- 当ClassPathMapperScanner扫描到UserMapper接口后,除了注册成Spring Bean,还会把UserMapper接口,注册到Configuration的mapperRegistry中;
- 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)中。
具体来说:
- Configuration内部有一个Map<String, MappedStatement> mappedStatements;
- key:就是我们拼的SOCO(statementId);
- 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等对象。
核心执行流程(对应例子)
- 通过SqlSession获取数据库连接(Connection);
- 根据MappedStatement中的SQL文本,创建PreparedStatement;
- 处理参数:把方法传入的id(1L),替换SQL中的#{id};
- 执行SQL:调用PreparedStatement.execute(),查询数据库;
- 结果映射:把数据库返回的结果集,映射成User对象(根据XML中的resultType配置);
- 关闭连接、释放资源(MyBatis自动管理);
- 把User对象返回给Service,最终返回给前端。
关键记忆点
拿到MappedStatement → MyBatis封装JDBC执行 → 结果映射 → 返回Java对象。
总结:完整流程
用咱们的例子,把9步浓缩成一条直线,记熟就能应对所有相关问题:
- @MapperScan → 导入MapperScannerRegistrar(指挥官)
- 指挥官new ClassPathMapperScanner(士兵)→ 士兵扫描com.xxx.mapper包
- 扫到UserMapper接口 → 把它的Bean类型改成MapperFactoryBean → 注册到Spring
- 注入UserMapper → Spring调用MapperFactoryBean.getObject()
- getObject() → 调用sqlSession.getMapper(UserMapper.class)
- sqlSession交给MapperRegistry(MyBatis的Mapper注册器)→ 从knownMappers拿到代理工厂
- 代理工厂用JDK动态代理 → 生成UserMapper代理对象(注入Service)
- 调用userMapper.selectById(1L) → 进入MapperProxy.invoke() → 拼SOCO(com.xxx.mapper.UserMapper.selectById)
- 用SOCO从Configuration的mappedStatements中找SQL → 执行JDBC → 返回User对象
补充:两个关键疑问
-
Q:Mapper接口和SQL,何时放进Configuration和MapperRegistry?
A:Spring启动时,MyBatis初始化Configuration,同时:① 扫描到的Mapper接口,注册到Configuration的MapperRegistry(knownMappers);② 解析XML/注解SQL,封装成MappedStatement,注册到Configuration的mappedStatements。
-
Q:我们注入的UserMapper是什么?
A:不是实现类,是JDK动态代理生成的代理对象,代理逻辑在MapperProxy中,负责拼SOCO、找SQL、执行SQL。