# 面经手册 · 第35篇《Mapper 接口没有实现类,怎么执行的?动态代理源码解析》

作者:小傅哥
博客:https://bugstack.cn (opens new window)

沉淀、分享、成长,让自己和他人都能有所收获!😄

# 一、前言

如果你用过 MyBatis,一定写过这样的代码:

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.findById(1L);
1
2

看起来平平无奇,但仔细想想——UserMapper 是一个接口,没有实现类,findById 方法也没有方法体,那这行代码到底是怎么执行的?谁来实现了这个接口?方法调用怎么就变成了 SQL 执行?

这就是 MyBatis 最巧妙的设计:用 JDK 动态代理为 Mapper 接口生成了实现类,让你以面向接口的方式操作数据库,而不需要写任何实现代码

但面试官不会只问"动态代理"四个字就放过你。MapperProxy 是什么时候创建的?invoke 方法里做了什么?MapperMethod 又是怎么路由到 SqlSession 的?方法能不能重载?namespace 到底绑定了什么?——这些才是区分"背过八股"和"真看过源码"的分水岭。

本文从源码出发,把 Mapper 接口的执行链路彻底讲透。

# 二、面试题

谢飞机,小记!,上次背了 #{}${} 的区别,这次信心满满又来了。

面试官:谢飞机,MyBatis 的 Mapper 接口没有实现类,它是怎么工作的?

谢飞机:动态代理!MyBatis 用 JDK 动态代理给 Mapper 接口生成了代理对象。

面试官:嗯,那代理对象的 invoke 方法里做了什么?

谢飞机:呃... 好像是调了 SqlSession 的方法?

面试官:中间还有 MapperMethod,你知道它的作用吗?

谢飞机:MapperMethod... 就是封装了一下?

面试官:封装了什么?SqlSession 那么多方法,它怎么知道该调 select 还是 insert?

谢飞机:这个... 根据方法名?

面试官:Mapper 接口的方法能重载吗?为什么?

谢飞机:应该... 不能?因为... 同名方法会冲突?

面试官:冲突在哪?你说清楚。

谢飞机:我... 再见!ヾ( ̄▽ ̄)

# 三、Mapper 接口工作原理全链路

# 1. 从 getMapper 说起

一切从 sqlSession.getMapper(UserMapper.class) 开始:

SqlSession.getMapper(UserMapper.class)
  → Configuration.getMapper(UserMapper.class, sqlSession)
    → MapperRegistry.getMapper(UserMapper.class, sqlSession)
      → MapperProxyFactory.newInstance(sqlSession)
        → Proxy.newProxyInstance(...)  ← JDK 动态代理创建代理对象
1
2
3
4
5

最终返回的不是 UserMapper 的实现类实例,而是一个 JDK 动态代理对象,它的 InvocationHandler 是 MapperProxy

# 2. 调用链路总览

userMapper.findById(1L)
  → MapperProxy.invoke()              ← 代理拦截
    → MapperMethod.execute()           ← 方法路由
      → SqlSession.selectOne()         ← 具体 SQL 执行
        → Executor.query()             ← 执行器
          → StatementHandler.query()   ← 语句处理器
            → PreparedStatement.execute()  ← JDBC 执行
1
2
3
4
5
6
7

一句话概括:接口方法调用 → 代理拦截 → 方法路由 → SqlSession 执行 SQL。

# 四、源码追踪:从注册到执行

# 1. MapperRegistry — Mapper 的注册中心

MyBatis 启动时,会解析所有的 Mapper 接口和 XML 映射文件,把每个接口注册到 MapperRegistry 中:

// org.apache.ibatis.binding.MapperRegistry
public class MapperRegistry {
    // 核心:接口 → MapperProxyFactory 的映射
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            if (hasMapper(type)) {
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            try {
                // 为每个接口创建一个 MapperProxyFactory
                knownMappers.put(type, new MapperProxyFactory<>(type));
            } catch (Exception e) {
                throw new BindingException("Error adding mapper.", e);
            }
        }
    }

    @SuppressWarnings("unchecked")
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
            // 通过工厂创建代理实例
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance.", e);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

关键发现MapperRegistry 内部维护了一个 knownMappers 集合,Key 是 Mapper 接口的 Class 对象,Value 是 MapperProxyFactory。每个 Mapper 接口对应一个工厂,由工厂负责创建代理对象。

# 2. MapperProxyFactory — 代理对象的工厂

// org.apache.ibatis.binding.MapperProxyFactory
public class MapperProxyFactory<T> {
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        // JDK 动态代理:创建实现 mapperInterface 接口的代理对象
        return (T) Proxy.newProxyInstance(
            mapperInterface.getClassLoader(),
            new Class[]{mapperInterface},
            mapperProxy
        );
    }

    public T newInstance(SqlSession sqlSession) {
        // 创建 MapperProxy(InvocationHandler)
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        // 创建代理对象
        return newInstance(mapperProxy);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

关键发现

  • methodCache 是一个 ConcurrentHashMap,缓存了 Method → MapperMethod 的映射。同一个方法不会重复创建 MapperMethod 对象。
  • Proxy.newProxyInstance() 是 JDK 动态代理的核心 API,它要求目标必须是接口,这正是 Mapper 只能是接口的原因。

# 3. MapperProxy — 代理拦截的核心

// org.apache.ibatis.binding.MapperProxy
public class MapperProxy<T> implements InvocationHandler, Serializable {
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;

    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // 如果调用的是 Object 的方法(toString、hashCode、equals 等),直接执行
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            }
            // 从缓存获取或创建 MapperMethod,然后执行
            return cachedMapperMethod(method).execute(sqlSession, args);
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
    }

    private MapperMethod cachedMapperMethod(Method method) {
        return methodCache.computeIfAbsent(method,
            k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

关键发现

  • invoke 是代理拦截的入口。每次调用 Mapper 接口的方法,都会走进这个方法。
  • 如果调用的是 Object 类的方法(如 toString()),不走代理逻辑,直接反射调用。
  • 核心逻辑就一行:cachedMapperMethod(method).execute(sqlSession, args)——找到对应的 MapperMethod,把 SqlSession 和参数传给它执行。
  • MapperMethod 有缓存,不会每次调用都新建。

# 4. MapperMethod — 方法路由器

// org.apache.ibatis.binding.MapperMethod
public class MapperMethod {
    private final SqlCommand command;
    private final MethodSignature method;

    public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
        this.command = new SqlCommand(config, mapperInterface, method);
        this.method = new MethodSignature(config, mapperInterface, method);
    }

    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        switch (command.getType()) {
            case INSERT:
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.insert(command.getName(), param);
                break;
            case UPDATE:
                param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.update(command.getName(), param);
                break;
            case DELETE:
                param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.delete(command.getName(), param);
                break;
            case SELECT:
                if (method.returnsVoid() && method.hasResultHandler()) {
                    executeWithResultHandler(sqlSession, args);
                    result = null;
                } else if (method.returnsMany()) {
                    result = executeForMany(sqlSession, args);
                } else if (method.returnsMap()) {
                    result = executeForMap(sqlSession, args);
                } else if (method.returnsCursor()) {
                    result = executeForCursor(sqlSession, args);
                } else {
                    // 默认:返回单个对象
                    param = method.convertArgsToSqlCommandParam(args);
                    result = sqlSession.selectOne(command.getName(), param);
                }
                break;
            case FLUSH:
                result = sqlSession.flushStatements();
                break;
            default:
                throw new BindingException("Unknown execution method for: " + command.getName());
        }
        // 返回值校验
        if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
            throw new BindingException("Mapper method '" + command.getName()
                + "' attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
        }
        return result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

关键发现

  • MapperMethod 内部有两个重要组件:SqlCommand(SQL 命令信息)和 MethodSignature(方法签名信息)。
  • execute 方法根据 command.getType() 决定调用 SqlSession 的哪个方法(select/insert/update/delete)。
  • 对于 SELECT,还要根据返回值类型(单个对象、列表、Map、游标、ResultHandler)做进一步路由。
  • command.getName() 返回的是 namespace.id 格式的全限定名,这就是定位 MappedStatement 的唯一标识。

# 5. SqlCommand — SQL 命令的元信息

// org.apache.ibatis.binding.MapperMethod.SqlCommand
public static class SqlCommand {
    private final String name;       // MappedStatement 的 ID = namespace + "." + 方法名
    private final SqlCommandType type; // SQL 类型:SELECT / INSERT / UPDATE / DELETE

    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
        final String methodName = method.getName();
        final String declaringClass = method.getDeclaringClass().getName();
        String statementName = declaringClass + "." + methodName;  // ← 关键:全限定名
        if (configuration.hasStatement(statementName)) {
            this.name = statementName;
        } else {
            // 兜底:用接口全限定名 + 方法名
            String debugStatementName = mapperInterface.getName() + "." + methodName;
            this.name = debugStatementName;
        }
        this.type = configuration.getMappedStatement(this.name).getSqlCommandType();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

关键发现statementName 的构造方式是 接口全限定名 + "." + 方法名,这正好对应 XML 中 <select id="findById">namespace="com.example.UserMapper" 的组合。这就是 Mapper 接口方法和 XML 映射 SQL 的绑定纽带。

# 五、方法重载问题

# 1. 为什么 Mapper 接口方法不能重载?

Java 允许接口方法重载:

public interface UserMapper {
    List<User> findByIds(List<Long> ids);
    List<User> findByIds(Long[] ids);  // 编译通过,但运行时会出问题
}
1
2
3
4

但 MyBatis 中不允许方法重载。原因要从 MappedStatement 的 ID 构造规则说起。

# 2. 根因分析

MappedStatement 的唯一标识 = namespace + "." + 方法名

接口:com.example.UserMapper
方法1:List<User> findByIds(List<Long> ids)   → MappedStatement ID: com.example.UserMapper.findByIds
方法2:List<User> findByIds(Long[] ids)       → MappedStatement ID: com.example.UserMapper.findByIds
                                                        ↑ 两个方法生成了相同的 ID!
1
2
3
4

MyBatis 在解析 XML 映射文件时,每个 <select>/<insert>/<update>/<delete> 标签都会生成一个 MappedStatement 对象,以 namespace.id 作为唯一 Key 存入 ConfigurationmappedStatements 集合(StrictMap):

// org.apache.ibatis.session.Configuration
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<>("Mapped Statements collection");

public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);  // 如果 ID 重复,StrictMap 会抛异常
}
1
2
3
4
5
6

StrictMap 不允许重复 Key:

// org.apache.ibatis.session.Configuration.StrictMap
@Override
public V put(String key, V value) {
    if (containsKey(key)) {
        throw new IllegalArgumentException(name + " already contains value for " + key);
    }
    return super.put(key, value);
}
1
2
3
4
5
6
7
8

两种冲突场景

  • XML 方式:两个重载方法对应同一个 <select id="findByIds">,ID 重复 → 启动时抛异常。
  • 注解方式:两个重载方法都加了 @Select,生成的 MappedStatement ID 相同 → 启动时抛异常。

# 3. 结论

Mapper 接口方法不能重载,因为 MappedStatement 的 ID 只由 namespace + 方法名 组成,不包含参数签名。重载方法同名会导致 ID 冲突。

# 六、接口绑定的两种方式

# 1. XML 绑定

通过 XML 映射文件的 namespace 指定 Mapper 接口的全限定名:

<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
    <select id="findById" resultType="User">
        SELECT * FROM user WHERE id = #{id}
    </select>

    <insert id="insert">
        INSERT INTO user(name, age) VALUES(#{name}, #{age})
    </insert>
</mapper>
1
2
3
4
5
6
7
8
9
10
// UserMapper.java
public interface UserMapper {
    User findById(Long id);
    int insert(User user);
}
1
2
3
4
5

绑定规则namespace = 接口全限定名,id = 方法名。

# 2. 注解绑定

通过在接口方法上添加注解直接编写 SQL:

// UserMapper.java
public interface UserMapper {

    @Select("SELECT * FROM user WHERE id = #{id}")
    User findById(Long id);

    @Insert("INSERT INTO user(name, age) VALUES(#{name}, #{age})")
    int insert(User user);

    @Update("UPDATE user SET name = #{name} WHERE id = #{id}")
    int update(User user);

    @Delete("DELETE FROM user WHERE id = #{id}")
    int delete(Long id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

四个核心注解

注解 对应 SQL 类型 示例
@Select SELECT @Select("SELECT * FROM user WHERE id =#{id}")
@Insert INSERT @Insert("INSERT INTO user(name) VALUES(#{name})")
@Update UPDATE @Update("UPDATE user SET name =#{name}")
@Delete DELETE @Delete("DELETE FROM user WHERE id =#{id}")

注解方式还支持 @Options(配置主键回写等)和 @ResultMap(引用结果映射)。

# 3. 注解 vs XML:怎么选?

对比项 注解方式 XML 方式
SQL 可见性 SQL 和接口在一起,一目了然 SQL 集中在 XML,需要切换查看
动态 SQL 支持(<script> 标签),但很丑 天然支持(if/choose/foreach/where 等)
复杂查询 多表关联、嵌套查询写起来痛苦 动态 SQL 标签灵活组合,可读性好
SQL 长度 短 SQL 简洁,长 SQL 臃肿 长短都无影响
维护性 改 SQL 需改 Java 文件,重新编译 改 SQL 只改 XML,热更新方便
调试 不方便断点 可以查看解析后的完整 SQL

实际项目推荐

  • 简单的单表 CRUD → 可以用注解,减少 XML 文件数量。
  • 复杂的多表关联、动态条件查询 → 必须用 XML。
  • 企业项目中绝大多数用 XML,因为业务 SQL 通常较复杂,动态 SQL 是刚需。

注解中的动态 SQL(不推荐,但确实支持):

@Select("<script>" +
        "SELECT * FROM user" +
        "<where>" +
        "  <if test='name != null'>AND name = #{name}</if>" +
        "  <if test='age != null'>AND age = #{age}</if>" +
        "</where>" +
        "</script>")
List<User> findUsers(@Param("name") String name, @Param("age") Integer age);
1
2
3
4
5
6
7
8

这种写法可读性极差,不如直接用 XML。

# 七、namespace 的作用

# 1. namespace 的三重身份

namespace 不仅仅是一个"命名空间",它在 MyBatis 中扮演三个重要角色:

角色一:SQL 隔离

不同 namespace 下可以有同名的 SQL ID,互不冲突:

<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
    <select id="findById">SELECT * FROM user WHERE id = #{id}</select>
</mapper>

<!-- OrderMapper.xml -->
<mapper namespace="com.example.mapper.OrderMapper">
    <select id="findById">SELECT * FROM order WHERE id = #{id}</select>
</mapper>
1
2
3
4
5
6
7
8
9

两个 findById 不冲突,因为完整 ID 分别是 com.example.mapper.UserMapper.findByIdcom.example.mapper.OrderMapper.findById

角色二:绑定 Mapper 接口

当 namespace 等于某个 Mapper 接口的全限定名时,XML 中的 SQL 就和该接口的方法绑定:

namespace = "com.example.mapper.UserMapper"
    + id = "findById"
    → MappedStatement ID = "com.example.mapper.UserMapper.findById"
    → 对应 UserMapper.findById() 方法
1
2
3
4

如果 namespace 写错了(和接口全限定名不一致),启动不会报错,但运行时 getMapper 调用方法会抛 BindingException,找不到对应的 MappedStatement

角色三:唯一标识 MappedStatement

Configuration 中的 mappedStatements 集合以 namespace.id 作为 Key,namespace 保证了全局唯一性。

# 2. namespace 的常见错误

<!-- ❌ 错误:namespace 和接口全限定名不匹配 -->
<mapper namespace="com.example.dao.UserDao">
    <select id="findById">...</select>
</mapper>

<!-- 接口是 com.example.mapper.UserMapper -->
<!-- 调用 userMapper.findById() → 找不到 MappedStatement → BindingException -->
1
2
3
4
5
6
7
<!-- ❌ 错误:namespace 重复 -->
<!-- 两个 XML 文件 namespace 相同,ID 重复会报错 -->
1
2
<!-- ✅ 正确:namespace = 接口全限定名 -->
<mapper namespace="com.example.mapper.UserMapper">
    <select id="findById">...</select>
</mapper>
1
2
3
4

# 3. namespace 与跨 namespace 引用

MyBatis 支持跨 namespace 引用结果映射和 SQL 片段:

<!-- CommonMapper.xml -->
<mapper namespace="com.example.mapper.CommonMapper">
    <sql id="userColumns">id, name, age, email</sql>
</mapper>

<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
    <select id="findAll" resultType="User">
        SELECT <include refid="com.example.mapper.CommonMapper.userColumns"/> FROM user
    </select>
</mapper>
1
2
3
4
5
6
7
8
9
10
11

跨 namespace 引用使用完整的 namespace.sqlId 格式。

# 八、常见面试追问

# Q1:Mapper 接口能通过 new 创建实例吗?

不能。Mapper 接口是纯接口,没有实现类,new UserMapper() 会编译报错。只能通过 sqlSession.getMapper()@Autowired(Spring 集成后)获取代理对象。

如果强行 new 了一个实现类,那也不是 MyBatis 管理的,无法执行 SQL。

# Q2:@Mapper 和 @MapperScan 有什么区别?

  • @Mapper:标注在单个 Mapper 接口上,告诉 MyBatis-Spring 这个接口需要注册为 Bean。每个接口都要加,接口多了很繁琐。
@Mapper
public interface UserMapper { ... }
1
2
  • @MapperScan:标注在 Spring Boot 启动类或配置类上,指定包路径,自动扫描该包下所有 Mapper 接口并注册。推荐方式,一个注解搞定所有 Mapper。
@MapperScan("com.example.mapper")
@SpringBootApplication
public class Application { ... }
1
2
3

本质@MapperScan 是批量版的 @Mapper,底层都调用了 MapperFactoryBean 来创建代理对象并注册到 Spring 容器。

# Q3:一个 Mapper 接口能对应多个 XML 文件吗?

不能。MyBatis 不允许同一个 namespace 出现在多个 XML 文件中。如果两个 XML 的 namespace 相同,启动时会因为 MappedStatement ID 冲突报错。

如果一个接口的 SQL 太多,建议按业务拆分成多个接口(如 UserReadMapperUserWriteMapper),而不是一个接口对应多个 XML。

# Q4:MapperProxy 为什么用 JDK 动态代理而不是 CGLIB?

因为 Mapper 是接口,JDK 动态代理专门用于接口代理,是 Java 原生支持的方案,不需要额外依赖。CGLIB 用于代理类(代理没有接口的类),而 Mapper 接口本身没有实现类,CGLIB 反而用不上。

MyBatis 的设计理念就是面向接口编程,所以 JDK 动态代理是最自然的选择。

# Q5:getMapper 每次调用都创建新的代理对象吗?

是的。每次调用 sqlSession.getMapper() 都会通过 MapperProxyFactory.newInstance() 创建一个新的代理对象。但 MapperMethod 有缓存(methodCache),不会重复创建。

在 Spring 集成中,MapperFactoryBean 是单例的,Spring 容器中每个 Mapper 只有一个代理实例。

# 九、总结

记住三个核心要点:

1. Mapper 接口的执行链路:
   接口方法调用 → MapperProxy.invoke()(JDK动态代理)
   → MapperMethod.execute()(方法路由,根据SQL类型分发)
   → SqlSession.selectXxx/insert/update/delete(执行SQL)

2. Mapper 接口方法不能重载:
   MappedStatement 的 ID = namespace + 方法名
   重载方法同名 → ID 冲突 → 启动报错

3. namespace 的三重作用:
   SQL 隔离(不同namespace可以有同名ID)
   + 绑定接口(namespace = 接口全限定名)
   + 唯一标识(namespace.id 是 MappedStatement 的全局唯一Key)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

面试回答模板

Mapper 接口没有实现类,MyBatis 通过 JDK 动态代理为它生成代理对象。调用流程是:SqlSession.getMapper()MapperRegistry 中找到对应的 MapperProxyFactory,由工厂创建 MapperProxy 代理实例。当调用接口方法时,MapperProxy.invoke() 拦截调用,根据 Method 对象从缓存获取或创建 MapperMethodMapperMethod 内部的 SqlCommand 持有 SQL 类型和 MappedStatement 的 ID,execute 方法根据 SQL 类型路由到 SqlSession 对应的方法执行。

Mapper 接口方法不能重载,因为 MappedStatement 的 ID 只由 namespace + 方法名组成,不包含参数签名,重载方法会生成相同 ID 导致冲突。

接口绑定有 XML 和注解两种方式,XML 通过 namespace 绑定接口,注解通过 @Select/@Insert 等直接在方法上定义 SQL。实际项目推荐 XML,因为动态 SQL 支持更好,复杂查询更易维护。