java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > MyBatis Mapper 接口

MyBatis 原理解析之Mapper 接口没有实现类SQL 执行过程

作者:花生了什么事a

MyBatis是一个优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射,MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的过程,MyBatis 使用 XML 或注解的方式将接口与 SQL 语句关联起来,从而实现数据的持久化操作

使用 MyBatis 时,我们通常只需要定义一个 Mapper 接口:

public interface UserMapper {
    User selectById(Long id);
}

然后直接调用:

User user = userMapper.selectById(1L);

整个过程中,没有写实现类,没有手动创建 Connection、PreparedStatement,甚至没有在 Java 代码里写 SQL。那么这个接口方法到底是谁实现的?SQL 又是在什么时候执行的?

MyBatis 内部通过一套分层架构完成 SQL 解析和执行,Mapper 接口通过动态代理生成运行时实现类。

MyBatis 整体架构

MyBatis 可以分为三层:

接口层

接口层是开发者直接接触的部分,主要是 SqlSession 和 Mapper 接口。

两种调用方式最终都会进入 MyBatis 内部执行流程:

// 方式一:通过 SqlSession 直接调用
sqlSession.selectOne("com.demo.UserMapper.selectById", 1L);
// 方式二:通过 Mapper 接口调用
userMapper.selectById(1L);

核心处理层

核心处理层负责 SQL 的整个生命周期,主要组件:

Configuration 负责保存 MyBatis 的配置:Mapper 信息、SQL 映射关系、数据源配置。

Executor 负责真正执行查询,处理一级缓存、二级缓存,调用 StatementHandler。

StatementHandler 负责和 JDBC 交互:创建 PreparedStatement、设置参数、执行 SQL。

ResultSetHandler 负责处理数据库返回的数据,把 ResultSet 转成 Java 对象。

基础支撑层

这一层提供底层能力:数据源管理、事务管理、缓存、类型转换。比如数据库连接不会每次查询都重新创建,而是通过连接池进行复用。

一次查询的完整链路

假设有一个 Mapper:

User user = userMapper.selectById(1);

实际调用链:

userMapper.selectById(1)
        │
        ▼
MapperProxy.invoke()
        │
        ▼
MapperMethod.execute()
        │
        ▼
SqlSession.selectOne()
        │
        ▼
Executor.query()
        │
        ▼
StatementHandler.query()
        │
        ▼
ResultSetHandler
        │
        ▼
返回 User 对象

前半部分解决 Mapper 接口方法如何找到对应 SQL,后半部分解决 SQL 如何通过 JDBC 执行并转换成 Java 对象。后半部分涉及的 Executor、StatementHandler、ResultSetHandler 是更底层的组件,后续文章再展开。

Mapper 接口为什么可以直接调用

Java 接口本身不能创建对象:

UserMapper mapper = new UserMapper();  // 编译报错

但 MyBatis 返回给你的:

UserMapper mapper = sqlSession.getMapper(UserMapper.class);

实际上不是 UserMapper 的实现类,而是一个 JDK 动态代理对象。可以理解为:

UserMapper 接口
      │
      ▼
JDK Proxy 生成代理对象
      │
      ▼
MapperProxy 处理方法调用
      │
      ▼
执行对应 SQL

Mapper 代理是怎么创建的

核心代码:

public class MapperProxyFactory<T> {
    private final Class<T> mapperInterface;  // 比如 UserMapper.class
    public T newInstance(SqlSession sqlSession) {
        MapperProxy<T> mapperProxy = new MapperProxy<>(
            sqlSession,
            mapperInterface,
            methodCache  // 缓存每个方法对应的 MapperMethod
        );
        return (T) Proxy.newProxyInstance(
            mapperInterface.getClassLoader(),
            new Class[]{mapperInterface},
            mapperProxy  // 所有方法调用都会被它拦截
        );
    }
}

Proxy.newProxyInstance 是 JDK 自带的动态代理 API,创建的对象实现了 UserMapper 接口,但方法调用不会进入接口,而是进入 MapperProxy.invoke()

MapperProxy 做了什么

核心代码:

public class MapperProxy<T> implements InvocationHandler {
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        // Object 类的方法直接放行
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }
        // 找到当前方法对应的 MapperMethod
        MapperMethod mapperMethod = methodCache.computeIfAbsent(
            method,
            m -> new MapperMethod(mapperInterface, m, sqlSession.getConfiguration())
        );
        // 执行 SQL
        return mapperMethod.execute(sqlSession, args);
    }
}

主要做两件事。

第一,找到当前方法对应的 MapperMethod。 比如调用 userMapper.selectById(1),MyBatis 会找到 UserMapper.selectById 对应的 SQL 配置。

第二,调用 SqlSession 执行。 MapperMethod.execute() 根据 SQL 类型(SELECT/INSERT/UPDATE/DELETE)调用对应的 SqlSession 方法:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
        case INSERT:
            result = sqlSession.insert(command.getName(), args);
            break;
        case UPDATE:
            result = sqlSession.update(command.getName(), args);
            break;
        case DELETE:
            result = sqlSession.delete(command.getName(), args);
            break;
        case SELECT:
            if (method.returnsMany()) {
                result = sqlSession.selectList(command.getName(), args);
            } else {
                result = sqlSession.selectOne(command.getName(), args);
            }
            break;
        default:
            throw new BindingException("未知的 SQL 类型");
    }
    return result;
}

command.getName() 是 SQL 的唯一标识,格式是 namespace + id,比如 com.example.mapper.UserMapper.selectById。MyBatis 靠这个标识找到对应的 SQL 语句。

最终进入 SqlSession → Executor → StatementHandler → JDBC 执行。

和 JDBC 的对比

JDBC 查询同一条数据:

Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
User user = null;
try {
    conn = DriverManager.getConnection(url, username, password);
    String sql = "SELECT * FROM user WHERE id = ?";
    ps = conn.prepareStatement(sql);
    ps.setLong(1, id);
    rs = ps.executeQuery();
    if (rs.next()) {
        user = new User();
        user.setId(rs.getLong("id"));
        user.setName(rs.getString("name"));
    }
} catch (SQLException e) {
    e.printStackTrace();
} finally {
    try { if (rs != null) rs.close(); } catch (SQLException e) {}
    try { if (ps != null) ps.close(); } catch (SQLException e) {}
    try { if (conn != null) conn.close(); } catch (SQLException e) {}
}

MyBatis 做同样的事:

User user = userMapper.selectById(1L);

MyBatis 实际封装的工作:

JDBC 工作MyBatis 负责
获取连接数据源管理
创建 StatementStatementHandler
设置参数参数映射
执行 SQLExecutor
ResultSet 转换ResultSetHandler
资源释放框架管理

小结

MyBatis 的 Mapper 接口之所以没有实现类,是因为运行时通过 Proxy.newProxyInstance 生成了动态代理对象。

一次 Mapper 调用经过的路径:

接口方法 → MapperProxy → MapperMethod → SqlSession → Executor → JDBC

动态代理解决了接口调用问题,执行器体系解决了 SQL 执行问题。开发者看到的是 userMapper.selectById(1),而 MyBatis 背后完成的是解析方法、定位 SQL、创建 Statement、执行查询、映射对象、返回结果。这也是 MyBatis 能够让开发者只关注 SQL,而不用关心 JDBC 细节的原因。

到此这篇关于MyBatis 原理解析之Mapper 接口没有实现类SQL 执行过程的文章就介绍到这了,更多相关MyBatis Mapper 接口内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文