Java-22 深入浅出 MyBatis - 手写ORM框架3 手写SqlSession、Executor 工作原理

手写 MyBatis(3):SqlSession 工厂 + Executor 执行器,吃透 ORM 执行链路

作者:武子康的个人博客


TL;DR

  • 场景 :面向已读完《手写 MyBatis(2)框架骨架与 XML 解析层》并完成 Configuration + MappedStatement + 双 XML 配置加载、准备进入"用户调用 → SQL 执行 → 结果封装"主流程的 Java 后端工程师;本篇正式落地简化版 MyBatis 的执行层,把"Mapper 接口不用写实现类""selectList / selectOne 命令式 API""#{} 占位符解析 + PreparedStatement 绑定 + 反射封装结果集"三件事一次性串通。
  • 结论 :简化版 MyBatis 的"执行链路"可以拆成三块------会话层 (SqlSessionFactoryBuildersqlMapConfig.xml 构建 DefaultSqlSessionFactory,后者 openSession() 产出 DefaultSqlSession)、代理层 (DefaultSqlSession.getMapper(Class) 用 JDK Proxy.newProxyInstancenamespace + "." + methodNamestatementId,并按方法返回类型自动分流到 selectList / selectOne)、执行层 (Executor 接口委派给 SimpleExecutor,完成"取连接 → GenericTokenParser 解析 #{} → 反射绑定参数 → PreparedStatement.executeQueryPropertyDescriptor 反射写 Java Bean"五步);这套结构完整复刻 MyBatis 3 源码中"配置 → 会话 → 代理 → 执行"的调用栈。
  • 产出 :SqlSessionFactoryBuilder / SqlSessionFactory / DefaultSqlSessionFactory / SqlSession 接口 / DefaultSqlSession(含 getMapper 动态代理)/ Executor 接口 / SimpleExecutor(含 BoundSql 解析 + 反射封装结果集)共 6 个 Java 源文件,对接上篇的 Configuration + MappedStatement + Resources,即可跑通"用户调用 Mapper 方法 → 框架找到 SQL → JDBC 执行 → 返回 Java 对象"全链路。

框架实现

上节已经实现了部分内容 下面我们继续

上一节已经完成了框架的部分基础结构。本节继续补齐 MyBatis 中比较核心的两块内容:

  • SqlSession 相关组件:负责对外提供查询入口。
  • Executor 相关组件:负责真正执行 SQL,并把结果封装成 Java 对象。

这部分代码是整个手写 MyBatis 的主流程:用户调用 Mapper 方法,框架根据方法找到对应 SQL,最后通过 JDBC 执行并返回结果。

SqlSession 相关

SqlSessionFactoryBuilder

java 复制代码
public class SqlSessionFactoryBuilder {

    private Configuration configuration;

    public SqlSessionFactoryBuilder() {
        configuration = new Configuration();
    }

    public SqlSessionFactory build(InputStream inputStream) throws DocumentException, PropertyVetoException, ClassNotFoundException {
        XMLConfigerBuilder xmlConfigerBuilder = new XMLConfigerBuilder(configuration);
        Configuration conf = xmlConfigerBuilder.parseConfiguration(inputStream);
        return new DefaultSqlSessionFactory(conf);
    }

}

SqlSessionFactoryBuilder 主要负责读取配置文件,并构建 SqlSessionFactory

它的执行流程比较简单:

  • 创建 Configuration 对象,用来保存框架配置信息。
  • 通过 XMLConfigerBuilder 解析 XML 配置。
  • 根据解析后的配置创建 DefaultSqlSessionFactory

这里的 Configuration 可以理解为整个框架的配置中心,后续的数据源、SQL 映射、参数类型、返回值类型等信息都会保存到其中。

SqlSessionFactory

java 复制代码
public interface SqlSessionFactory {

    SqlSession openSession();

}

SqlSessionFactory 是一个工厂接口,只暴露了一个核心方法:openSession()

它的职责是创建 SqlSession。在 MyBatis 中,用户通常不会直接操作底层执行器,而是通过 SqlSession 来完成查询、关闭连接等操作。

SqlSession

java 复制代码
public interface SqlSession {

    <E> List<E> selectList(String statementId, Object ...params) throws Exception;

    <T> T selectOne(String statementId, Object ...params) throws Exception;

    void close() throws Exception;
}

SqlSession 是对外暴露的核心会话接口,当前实现中主要包含三个方法:

  • selectList:查询多条数据。
  • selectOne:查询单条数据。
  • close:关闭底层资源。

这里的 statementId 一般由 Mapper 接口全限定名和方法名组成,例如:

java 复制代码
com.example.mapper.UserMapper.selectById

框架会根据这个 statementId 找到对应的 MappedStatement,再执行里面配置的 SQL。

DefaultSqlSession

java 复制代码
@AllArgsConstructor
public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

    private Executor simpleExecutor = new SimpleExecutor();

    public DefaultSqlSession(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public <E> List<E> selectList(String statementId, Object... params) throws Exception {
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        // System.out.println("DefaultSqlSession => selectList => + " + "statementId: " + statementId + ", mappedStatement: " + mappedStatement);
        return simpleExecutor.query(configuration, mappedStatement, params);
    }

    @Override
    public <T> T selectOne(String statementId, Object... params) throws Exception {
        List<Object> objects = selectList(statementId, params);
        if (objects.size() == 1) {
            return (T) objects.get(0);
        }
        throw new RuntimeException("DefaultSqlSession selectOne 返回结果不唯一: " + statementId);
    }

    @Override
    public void close() throws Exception {
        simpleExecutor.close();
    }

    @Override
    public <T> T getMapper(Class<?> mapperClass) {
        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String methodName = method.getName();
                if (method.getDeclaringClass() == Object.class) {
                    return method.invoke(this, args);
                }
                String className = method.getDeclaringClass().getName();
                String statementId = className + "." + methodName;
                Type genericReturnType = method.getGenericReturnType();
                if(genericReturnType instanceof ParameterizedType){
                    List<Object> objects = selectList(statementId, args);
                    return objects;
                }
                return selectOne(statementId,args);

            }
        });
        return (T) proxyInstance;
    }
}

类定义与注解

DefaultSqlSessionSqlSession 接口的默认实现,也是当前框架中连接用户调用和底层 SQL 执行的核心类。

@AllArgsConstructor 是 Lombok 提供的注解,用于生成包含所有字段的构造方法,可以减少样板代码。不过当前代码中已经手动定义了 DefaultSqlSession(Configuration configuration) 构造方法,因此这个注解在这里不是必须的。

DefaultSqlSession 内部主要持有两个对象:

  • Configuration:保存框架解析后的配置信息。
  • Executor:负责执行 SQL,这里默认使用 SimpleExecutor

属性

java 复制代码
private Configuration configuration;

configuration 用来保存全局配置信息,例如数据源、MappedStatement 映射关系等。执行查询时,会根据 statementId 从这里找到对应的 SQL 元数据。

java 复制代码
private Executor simpleExecutor = new SimpleExecutor();

simpleExecutor 是当前使用的执行器对象,负责把 MappedStatement 中的 SQL 交给 JDBC 执行,并把查询结果封装成目标对象。

工作原理总结

DefaultSqlSession 的查询流程如下:

  • 用户调用 Mapper 接口方法。
  • 动态代理拦截方法调用。
  • 根据接口全限定名和方法名拼接出 statementId
  • 根据 statementIdConfiguration 中获取 MappedStatement
  • 判断方法返回值类型,决定调用 selectList 还是 selectOne
  • 最终交给 Executor 执行 SQL。

核心组件说明:

  • Configuration:管理框架配置和 SQL 映射信息。
  • MappedStatement:描述一条 SQL 语句,包括 SQL 文本、参数类型、返回值类型等。
  • Executor:负责执行 SQL 并返回结果。
  • Dynamic Proxy:动态生成 Mapper 接口的代理对象,让用户可以像调用普通接口一样执行 SQL。

异常处理方面,selectOne 要求查询结果只能有一条。如果返回结果数量不是 1,就会抛出异常。这样可以避免单条查询出现结果不明确的问题。

构造方法

DefaultSqlSession(Configuration configuration) 用于接收解析后的配置对象,并赋值给当前会话对象。

这样每一个 DefaultSqlSession 都可以通过 Configuration 获取数据源、SQL 映射和参数映射等信息,从而完成后续查询流程。

Executor 相关

Executor

java 复制代码
public interface Executor {

    <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object[] params) throws Exception;

    void close() throws Exception;
}

Executor 是执行器接口,定义了底层 SQL 执行能力。

当前接口包含两个方法:

  • query:执行查询操作。
  • close:关闭数据库连接等资源。

SqlSession 负责对外提供 API,而 Executor 负责真正和数据库交互。这样可以把"用户调用入口"和"SQL 执行逻辑"拆开,结构会更清晰。

DefaultExecutor

java 复制代码
public class SimpleExecutor implements Executor {

    private Connection connection;

    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object[] params) throws Exception {
        connection = configuration.getDataSource().getConnection();
        String sql = mappedStatement.getSql();
        BoundSql boundSql = getBoundSql(sql);
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
        String parameterType = mappedStatement.getParameterType();
        Class<?> parameterTypeClass = getClassType(parameterType);
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
        int n = 0;
        for (ParameterMapping pm : parameterMappingList) {
            String content = pm.getName();
            Field declaredField = parameterTypeClass.getDeclaredField(content);
            declaredField.setAccessible(true);
            Object object = declaredField.get(params[0]);
            preparedStatement.setObject(n + 1, object);
        }

        // 执行sql
        ResultSet resultSet = preparedStatement.executeQuery();
        String resultType = mappedStatement.getResultType();
        Class<?> resultTypeClass = getClassType(resultType);
        List<Object> objects = new ArrayList<>();

        // 封装返回结果集
        while (resultSet.next()) {
            Object o = resultTypeClass.newInstance();
            // 元数据
            ResultSetMetaData metaData = resultSet.getMetaData();
            for (int i = 1; i <= metaData.getColumnCount(); i ++) {
                // 字段名
                String columnName = metaData.getColumnName(i);
                // 字段的值
                Object value = resultSet.getObject(columnName);

                // 反射 根据数据库和实体 完成
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
                Method writeMethod = propertyDescriptor.getWriteMethod();
                writeMethod.invoke(o, resultTypeClass.getDeclaredField(columnName).getType().cast(value));
            }
            objects.add(o);
        }

        return (List<E>) objects;
    }

    private BoundSql getBoundSql(String sql) {
        // 标记处理类 配置标记解析器来完成对占位符的解析处理工作
        ParameterMappingTokenHandler parameterMappingHandler = new ParameterMappingTokenHandler();
        GenericTokenParser genericTokenParser = new GenericTokenParser(
                "#{", "}",
                parameterMappingHandler
        );
        // 解析出来的sql
        String parseSql = genericTokenParser.parse(sql);
        // #{} 里边的参数
        List<ParameterMapping> parameterMapping = parameterMappingHandler.getParameterMappings();
        BoundSql boundSql = new BoundSql(parseSql, parameterMapping);
        System.out.println("SimpleExecutor getBoundSql: " + boundSql.getSqlText());
        return boundSql;
    }

    private Class<?> getClassType(String parameterType) throws ClassNotFoundException {
        if(parameterType != null){
            return Class.forName(parameterType);
        }
        return null;
    }

    @Override
    public void close() throws Exception {
        connection.close();
    }
}

类的作用

这里的标题虽然写的是 DefaultExecutor,但代码中的实际类名是 SimpleExecutor。它是 Executor 接口的一个简单实现,主要负责完成以下工作:

  • 从数据源中获取数据库连接。
  • 解析 SQL 中的 #{} 占位符。
  • 使用 PreparedStatement 绑定参数并执行 SQL。
  • 读取 ResultSet 查询结果。
  • 通过反射把数据库字段值封装到 Java 对象中。

当前实现只处理了基础查询流程,重点是帮助理解 MyBatis 的执行链路。

代码逻辑解析

SimpleExecutor 中只有一个成员变量:

java 复制代码
private Connection connection;

它用于保存当前查询使用的数据库连接。查询完成后,可以通过 close() 方法关闭连接。

核心方法:query

query 方法是执行器的核心方法,整体流程可以分为四步。

第一步,建立数据库连接:

java 复制代码
connection = configuration.getDataSource().getConnection();

这里从 Configuration 中获取数据源,再通过数据源获取 JDBC 连接。

第二步,获取并解析 SQL:

  • 通过 mappedStatement.getSql() 获取原始 SQL。
  • 调用 getBoundSql(sql) 解析 #{} 占位符。
  • #{id} 这类占位符转换成 JDBC 可识别的 ?
  • 同时保存参数名称,方便后面绑定参数。

例如原始 SQL 可能是:

sql 复制代码
select * from user where id = #{id}

解析后会变成:

sql 复制代码
select * from user where id = ?

第三步,创建 PreparedStatement 并绑定参数:

java 复制代码
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());

之后通过 parameterType 找到参数对象类型,再根据 ParameterMapping 中记录的参数名,用反射从入参对象中取值。

核心逻辑是:

  • 获取参数类型。
  • 遍历 SQL 中解析出来的参数名。
  • 根据参数名找到参数对象中的字段。
  • 取出字段值。
  • 调用 preparedStatement.setObject() 完成参数绑定。

这里有一个细节需要注意:代码中定义了变量 int n = 0,但循环中没有对 n 做自增,所以如果 SQL 中有多个参数,当前写法会一直给第一个占位符赋值。后续可以把它调整为随着参数遍历递增。本文先保留原代码结构,重点放在整体流程理解上。

第四步,执行查询并封装结果集:

java 复制代码
ResultSet resultSet = preparedStatement.executeQuery();

查询完成后,通过 ResultSetMetaData 获取结果集的列信息,然后遍历每一列:

  • 获取数据库字段名。
  • 获取字段对应的值。
  • 根据返回值类型创建 Java 对象。
  • 使用 PropertyDescriptor 找到对应的 setter 方法。
  • 调用 setter 方法把数据库字段值写入 Java 对象。

最终,每一行数据都会被封装成一个 Java 对象,并添加到 List 中返回。

适用场景

当前这部分代码适合用来理解 MyBatis 的核心执行链路,尤其是下面几个关键点:

  • SqlSession 如何作为用户调用入口。
  • Mapper 接口为什么可以不写实现类。
  • 动态代理如何根据方法生成 statementId
  • MappedStatement 如何保存 SQL 元信息。
  • Executor 如何基于 JDBC 执行 SQL。
  • 查询结果如何通过反射封装成 Java 对象。

到这里,一个简化版 MyBatis 的查询主流程已经基本串起来了:配置解析生成 Configuration,用户通过 SqlSession 或 Mapper 接口发起调用,框架找到对应的 MappedStatement,最后由 Executor 执行 SQL 并返回结果。


相关推荐
未若君雅裁1 小时前
JVM 垃圾回收算法与分代回收机制
java·jvm·算法
ikoala1 小时前
Codex 不得不装的 12 个插件,都在这了
前端·javascript·后端
摇滚侠1 小时前
SpringMVC 入门到实战 简介和入门案例 01-13
java·后端·spring·intellij-idea
未若君雅裁1 小时前
JVM 垃圾回收器全景与G1深度解析
java·开发语言·jvm
霸道流氓气质1 小时前
Java 大数据量异步处理方案:线程池 vs 消息队列
java·开发语言
devilnumber1 小时前
想真正吃透 + 灵活运用 Java 代理模式
java·开发语言·代理模式
蝎子莱莱爱打怪1 小时前
自用推荐|XTerminal:我心中 SSH 客户端的终极形态
java·后端·程序员
AIGS0011 小时前
向量空间JBoltAI:重塑工业智能的四大支柱
java·人工智能·ai大模型应用
刘科领1 小时前
修改jdk 第一步: 仓库以及构建(jdk17)
java·开发语言