MyBatis 底层原理。京东面试:MyBatis 如何实现 “面向接口” 查询的 ?

本文 的 原文 地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

尼恩说在前面:

最近大厂机会多了, 在45岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、shein 希音、shopee、百度、网易的面试资格,遇到很多很重要的面试题:

MyBatis 如何实现 “面向接口” 查询的 ?

昨天面试京东问 : mybatis 的用接口怎么实现查询的,Java 线程有几种状态[破涕为笑]

前几天 小伙伴面试 京东,遇到了这个问题。但是由于 没有回答好,导致面试挂了。

小伙伴面试完了之后,来求助尼恩。

那么,遇到 这个问题,该如何才能回答得很漂亮,才能 让面试官刮目相看、口水直流。

所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典》V175版本PDF集群,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书

京东面试:MyBatis 如何实现 “面向接口” 查询的 ?

MyBatis 的接口怎么实现 面向查询的?

这是一个关于 MyBatis 的接口绑定(Interface Binding)机制的问题,也是 MyBatis 相比传统 DAO 模式最方便的特性之一。

核心答案

MyBatis 通过JDK 动态代理技术,在运行时, 动态为 Mapper 接口 生成代理实现类(MapperProxy)。

MyBatis JDK 动态代理 的目标: 实现 “仅定义接口、无需手动编写实现类” , 完成数据库操作。

“面向接口” 的核心机制 与 实现步骤

1. 核心机制:JDK 动态代理

MyBatis 是“面向接口” 查询, 接口的实现类是 JDK 动态代理创建 的。 或者说, MyBatis 依赖 JDK 原生动态代理技术实现接口与数据库操作的绑定,而非手动编写接口实现类。

当定义好 Mapper 接口后,MyBatis 会 为该接口创建一个 MapperProxy 代理对象,并将其注册到 Spring 容器(若整合 Spring)中。

MapperProxy 代理对象 在应用初始化阶段(如 Spring 容器启动时)创建。 后续注入接口时,实际获取的是这个代理对象,接口方法的调用会被转发到代理对象的逻辑中。

2. “面向接口” 实现步骤

我们先来看一张核心流程图,帮助你建立整体认知:

这张图就是整个 MyBatis 接口查询的核心流程。下面我们一层一层地展开讲。

第一步:定义 Mapper 接口

创建 Java 接口,接口中的方法签名(方法名、参数类型、返回类型)需与后续 SQL 操作的配置对应,方法名将作为匹配 SQL 的核心标识。


public interface UserMapper {

     // 单条查询:根据ID查用户

     User selectUserById(Long id);

     // 批量查询:查询所有用户

     List<User> selectAllUsers();

     // 插入操作:新增用户

     int insertUser(User user);

}

第二步:配置 SQL(XML 式或注解式)

MyBatis 支持两种 SQL 配置方式,需确保 SQL 与接口方法绑定:

第一种SQL 配置方式: XML 式

创建与接口同名的 XML 文件(如 UserMapper.xml),通过 namespace 指定接口全限定名,id 与接口方法名完全一致。


<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- namespace必须是Mapper接口的全限定名 -->

<mapper namespace="com.jd.dao.UserMapper">

     <!-- id与接口方法名一致,resultType指定返回实体类型 -->

     <select id="selectUserById" resultType="com.jd.model.User">

         SELECT * FROM user WHERE id = #{id}

     </select>

     <select id="selectAllUsers" resultType="com.jd.model.User">

         SELECT * FROM user

     </select>

</mapper>

第一种SQL 配置方式: 注解式

直接在接口方法上通过 @Select/@Insert 等注解编写 SQL,无需 XML 文件,SQL 与方法直接绑定。


public interface UserMapper {

     @Select("SELECT * FROM user WHERE id = #{id}")

     User selectUserById(Long id);

}

第三步:注入接口并使用

在 Service 层通过 @Autowired(或 @Resource)注入 Mapper 接口(实际注入的是 MyBatis 生成的 MapperProxy 代理对象),直接调用接口方法即可触发 SQL 执行。


@Service

public class UserService {

     // 注入的是MyBatis生成的代理对象,而非接口本身

     @Autowired

     private UserMapper userMapper;

     public User getUserById(Long id) {

         // 调用接口方法 → 实际触发代理对象的invoke()逻辑

         return userMapper.selectUserById(id);

     }

}

3. 执行过程(工作原理拆解)

当调用 userMapper.selectUserById(1L) 时,底层执行流程如下:

(1) 代理对象拦截调用:

方法调用被 MapperProxy 代理对象的 invoke() 方法拦截;

(2) 生成 SQL 唯一标识:

invoke() 方法根据 “接口全限定名 + 方法名” 生成唯一标识(如 com.jd.dao.UserMapper.selectUserById);

(3) 匹配 MappedStatement

通过唯一标识从 MyBatis 全局配置(Configuration)中找到对应的 MappedStatement(封装了 SQL 语句、参数映射、结果映射等信息);

(4) 执行 SQL 会话流程:

MyBatis 借助 SqlSession 完成数据库交互:获取数据库连接 → 预编译 SQL → 填充参数 → 执行 SQL → 处理结果集(自动映射为接口声明的返回类型,如 User 对象);

(5) 返回结果:

将映射后的结果返回给调用方(如 Service 层)。

“面向接口” 查询的 案例介绍:

当调用 selectUserById(1L) 时发生了什么?

我们进入最核心的环节。


userMapper.selectUserById(1L);

虽然你是在调接口,但由于它是代理对象,所以会被拦截。具体步骤如下:

Step 1:代理拦截(invoke 方法触发)

MapperProxy.invoke() 被调用,开始处理请求。

Step 2:生成唯一标识

拼出一个全局唯一的 key:


com.example.dao.UserMapper.selectUserById

格式是:接口全限定名 + 方法名

类似于身份证号,确保每个方法都有唯一身份。

Step 3:查找 MappedStatement

拿着这个 key 去 MyBatis 的“中央仓库”(Configuration 对象)里找对应的 SQL 信息包 —— MappedStatement

这个包里包含了:

  • 实际要执行的 SQL 语句
  • 参数怎么映射(#{id} 对应哪个参数)
  • 返回值怎么封装成 User 对象

Step 4:执行 SQL(交给 SqlSession)

MyBatis 拿到 MappedStatement 后,交给 SqlSession 去干活:


sqlSession.selectOne("com.example.dao.UserMapper.selectUserById", 1L);

内部流程:

(1) 获取数据库连接

(2) 预编译 SQL(防止 SQL 注入)

(3) 设置参数(把 1L 填进 #{id})

(4) 执行查询

(5) 把结果集自动映射成 User 对象(靠反射 + 字段名匹配)

Step 5:返回结果

最后把查到的 User 对象原路返回给你。

深度思考(关键问题解析)

深度思考 1:接口和 SQL(XML / 注解)如何关联?

MyBatis 在初始化阶段通过以下两种方式完成绑定:

(1) XML 与接口关联:

通过 @MapperScan 注解扫描指定包下的 Mapper 接口,同时读取配置的 XML 文件路径(如 mybatis.mapper-locations=classpath:mapper/*.xml),将 XML 中 namespace(接口全限定名)与接口匹配,id(方法名)与接口方法匹配;

(2) 注解与接口关联:

扫描接口时直接解析方法上的 @Select/@Insert 等注解,无需 XML 文件,SQL 通过注解直接与方法绑定,无需额外的 namespace 配置。

深度思考 2:Mapper 接口能不能进行方法重载?

不能

原因是 MyBatis 通过 “方法名” 作为核心标识匹配 SQL(XML 中的 id 或注解绑定的方法),若存在重载方法(方法名相同、参数不同),会导致 SQL 标识冲突,MyBatis 无法区分对应的 SQL,启动时会抛出异常(如 BindingException: Invalid bound statement)。

接口的 动态代理对象 MapperProxy 创建 流程

mybatis 为每个 Mapper 接口创建一个 MapperProxy(代理对象) 。

MapperProxy 是 MyBatis 实现 “接口即实现” 的核心。

MapperProxy 创建发生在应用初始化阶段(若整合 Spring Boot,则在 @MapperScan 扫描时;若单独使用 MyBatis,则在 SqlSessionFactory 初始化时)。

我们可以把MapperProxy 的创建 过程分成 四个阶段,像工厂流水线一样层层推进:

阶段 名称 类比
第1步 扫描所有 Mapper 接口 工厂开始清点要生产的“模具清单”
第2步 注册接口到注册中心 把每个模具登记进管理系统,准备造代理工厂
第3步 创建代理对象(MapperProxy) 按照模具造出能干活的“机器人替身”
第4步 放入 Spring 容器供调用 把机器人送到岗位上,随时听候差遣

(1) 扫描 Mapper 接口(@MapperScan)

在 Spring Boot 启动类或配置类上添加 `@MapperScan("com.example.mapper")`,Spring 会扫描指定包下的所有接口,筛选出符合 Mapper 特征的接口(无特殊注解要求,仅需是接口)。

(2) 注册接口到 MapperRegistry

MyBatis 的核心配置类 `Configuration` 内部维护 `MapperRegistry`(Mapper 注册中心),扫描到的每个 Mapper 接口会被注册到这里:

核心步骤分解

MyBatis 内部有个“注册中心”叫 MapperRegistry,它的任务是:

“每一个 Mapper 接口,我都给你配一个专属的‘代理生成工厂’。”

这个工厂的名字叫:MapperProxyFactory


public class MapperRegistry {

   // 缓存:接口Class → MapperProxy工厂

   private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

   public <T> void addMapper(Class<T> type) {

     // 仅处理接口(排除普通类)

     if (type.isInterface()) {

       // 避免重复注册

       if (hasMapper(type)) return;  

       // 为接口创建MapperProxy工厂并缓存

       knownMappers.put(type, new MapperProxyFactory<>(type));

       // 解析SQL:读取XML或注解中的SQL,封装为MappedStatement

       MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);

       parser.parse();  

     }

   }

}

关键:为每个 Mapper 接口创建一个 MapperProxyFactory(代理工厂),后续通过工厂生成代理对象。

1、MapperProxyFactory 生成代理对象

MapperProxyFactory 是生产 MapperProxy 的工厂类,核心逻辑是通过 JDK 动态代理生成接口实现:


public class MapperProxyFactory<T> {

   private final Class<T> mapperInterface; // 目标Mapper接口

   public T newInstance(SqlSession sqlSession) {

     // 1. 创建InvocationHandler实例(核心代理逻辑封装在MapperProxy中)

     final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface);

     // 2. 生成JDK动态代理对象:实现Mapper接口,委托逻辑到MapperProxy

     return (T) Proxy.newProxyInstance(

         mapperInterface.getClassLoader(), // 接口类加载器

         new Class[] { mapperInterface },  // 实现的接口(仅目标Mapper接口)

         mapperProxy                       // 代理逻辑处理器

     );

   }

}

2、MapperRegistry解析 SQL

parser.parse() 这一步很重要:它会去解析你在 XML 或注解里写的 SQL,比如:


<select id="selectById" resultType="User">
    SELECT * FROM user WHERE id = #{id}
</select>

并把这些 SQL 包装成 MappedStatement 对象,存在 MyBatis 全局配置中。

3、注册代理对象到 Spring 容器

当 Spring 需要把 UserMapper 注入到 Service 中时,它不会new一个接口 ,而是让对应的 MapperProxyFactory 动手造一个“假实现”。

这个“假实现”就是 MapperProxy,它是 JDK 动态代理的产物。

再看原始代码:


public class MapperProxyFactory<T> {

   private final Class<T> mapperInterface;

   public T newInstance(SqlSession sqlSession) {
     final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface);
     return (T) Proxy.newProxyInstance(
         mapperInterface.getClassLoader(),
         new Class[] { mapperInterface },
         mapperProxy
     );
   }
}

生成的 MapperProxy 代理对象(实现了 Mapper 接口)会被注册为 Spring Bean。

当 Service 层通过 @Autowired 注入 Mapper 接口时,Spring 会返回这个MapperProxy 代理对象,而非接口本身。

关键技术点:
  • 使用的是 JDK 动态代理,要求目标必须是接口。
  • Proxy.newProxyInstance(...) 会生成一个新对象,实现了 UserMapper 接口。
  • 所有方法调用都会被拦截,交给 mapperProxy.invoke() 处理(这是 InvocationHandler 的规则)。

Demo代码:模拟一下动态代理的工作原理


// 模拟 Mapper 接口
interface UserMapper {
    User findUserById(int id);
}

// 模拟 MapperProxy 的作用:统一处理所有方法调用
class SimpleMapperProxy implements InvocationHandler {
    private SqlSession sqlSession;

    public SimpleMapperProxy(SqlSession sqlSession) {
        this.sqlSession = sqlSession;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("👉 正在执行 SQL: " + method.getName());
        System.out.println("🔧 参数是: " + Arrays.toString(args));
        
        // 实际上会根据 method 名查找 MappedStatement,然后执行 SQL
        // 这里简化返回一个 mock 数据
        return new User((int)args[0], "张三");
    }
}

// 测试使用
public class ProxyDemo {
    public static void main(String[] args) {
        SqlSession sqlSession = null; // 简化省略初始化

        InvocationHandler handler = new SimpleMapperProxy(sqlSession);
        UserMapper mapper = (UserMapper) Proxy.newProxyInstance(
            UserMapper.class.getClassLoader(),
            new Class[]{UserMapper.class},
            handler
        );

        User user = mapper.findUserById(1001); // 输出拦截日志
        System.out.println("查询结果:" + user);
    }
}

输出效果:


正在执行 SQL: findUserById
 参数是: [1001]
查询结果:User{id=1001, name='张三'}

你看,根本没写实现类,但调用成功了!

这就是 MapperProxy 的本质。

动态代理对象 MapperProxy 的核心工作原理

...................由于平台篇幅限制, 剩下的内容(5000字+),请参参见原文地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

posted @ 2025-09-24 20:20  技术自由圈  阅读(12)  评论(0)    收藏  举报