手写 MyBatis(3):SqlSession 工厂 + Executor 执行器,吃透 ORM 执行链路
作者:武子康的个人博客

TL;DR
- 场景 :面向已读完《手写 MyBatis(2)框架骨架与 XML 解析层》并完成
Configuration+MappedStatement+ 双 XML 配置加载、准备进入"用户调用 → SQL 执行 → 结果封装"主流程的 Java 后端工程师;本篇正式落地简化版 MyBatis 的执行层,把"Mapper 接口不用写实现类""selectList/selectOne命令式 API""#{}占位符解析 +PreparedStatement绑定 + 反射封装结果集"三件事一次性串通。 - 结论 :简化版 MyBatis 的"执行链路"可以拆成三块------会话层 (
SqlSessionFactoryBuilder读sqlMapConfig.xml构建DefaultSqlSessionFactory,后者openSession()产出DefaultSqlSession)、代理层 (DefaultSqlSession.getMapper(Class)用 JDKProxy.newProxyInstance按namespace + "." + methodName拼statementId,并按方法返回类型自动分流到selectList/selectOne)、执行层 (Executor接口委派给SimpleExecutor,完成"取连接 →GenericTokenParser解析#{}→ 反射绑定参数 →PreparedStatement.executeQuery→PropertyDescriptor反射写 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;
}
}

类定义与注解
DefaultSqlSession 是 SqlSession 接口的默认实现,也是当前框架中连接用户调用和底层 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。 - 根据
statementId从Configuration中获取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 并返回结果。