课程内容
昨天内容
ORM 演化
为什么要学源码?
提高技术能力 解决工作中的问题

环境搭建
体系结构

接口层面向开发者
mybatis-》核心处理层
基础支持层 核心
MyBatis 执行 SQL 时,Mapper 代理调用 SqlSession,SqlSession 委托 Executor,Executor 经缓存与插件处理后交给 StatementHandler 构建并执行 JDBC 语句,ParameterHandler 完成参数映射,ResultSetHandler 将结果映射为对象并返回。
今天重点 -核心处理层
源码结构

源码学习
测试代码
test2

SqlSessionFactory factory = sqlSessionFactoryBuilder.build(in);
SqlSessionFactory
SqlSessionFactory 作用
SqlSessionFactory 是 MyBatis 的"会话工厂",负责根据全局配置创建 SqlSession,并统一管理 MyBatis 的运行环境。
build方法

全局配置解析 映射文件解析 XMLConfigBuilder

///别名注册
public Configuration() {
// 为类型注册别名
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
}
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
XML 解析时如何通过 alias 实例化组件
public class TypeAliasRegistry类 {
public TypeAliasRegistry() {
registerAlias("string", String.class);
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
private final Map<String, Class<?>> typeAliases = new HashMap<>();
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// issue #748
String key = alias.toLowerCase(Locale.ENGLISH);
if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
}
typeAliases.put(key, value);
}
private final Map<String, Class<?>> typeAliases = new HashMap<>();
默认值的初始化

反射组件不知道哪个适合启用了
private void parseConfiguration(XNode root) {
继续分析
return build(parser.parse());
build(Configuration config)
(parser.parse() 解析了Configuration
root包含了整个配置文件
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// ===== MyBatis 全局配置文件(mybatis-config.xml)解析主流程 =====
// 1. 解析 <properties>:加载外部属性文件/变量(用于占位符 ${} 替换) <properties resource="db.properties"/>
propertiesElement(root.evalNode("properties"));
// 2. 解析 <settings>:读取框架核心开关配置(如缓存、懒加载、驼峰映射等)
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 2.1 根据 settings 加载自定义 VFS 实现(用于资源扫描,如 Jar/OSGi 等)
loadCustomVfs(settings);
// 2.2 根据 settings 设置日志实现(SLF4J、LOG4J、STDOUT 等)
loadCustomLogImpl(settings);
// 3. 解析 <typeAliases>:注册类型别名(简化 XML 中类名配置)
typeAliasesElement(root.evalNode("typeAliases"));
// 4. 解析 <plugins>:加载 MyBatis 插件体系(Interceptor 拦截 Executor/StatementHandler 等)
pluginElement(root.evalNode("plugins"));
// 5. 解析 <objectFactory>:指定对象创建工厂(控制结果对象实例化方式)
objectFactoryElement(root.evalNode("objectFactory"));
// 6. 解析 <objectWrapperFactory>:对象包装工厂(用于增强对象属性访问,如 Map/Collection)
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 7. 解析 <reflectorFactory>:反射缓存工厂(提升反射性能,管理 Reflector)
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 8. 应用 <settings> 到 Configuration:将 settings 值真正赋给全局配置对象(默认值也在此补全)
settingsElement(settings);
// 注意:settings 必须在 objectFactory/objectWrapperFactory 初始化后设置(历史 issue #631)
// 9. 解析 <environments>:构建运行环境(事务管理器 + 数据源 DataSource)
// → 最终决定 Connection 从哪里来、事务如何控制
environmentsElement(root.evalNode("environments"));
// 10. 解析 <databaseIdProvider>:数据库厂商识别(用于多数据库 SQL 区分,如 mysql/oracle)
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 11. 解析 <typeHandlers>:注册类型转换器(Java 类型 ↔ JDBC 类型映射)
typeHandlerElement(root.evalNode("typeHandlers"));
// 12. 解析 <mappers>:加载 Mapper 映射器(XML/注解)
// → 生成 MappedStatement,建立 statementId → SQL 的映射关系
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
信息保存
// 12. 解析 <mappers>:加载 Mapper 映射器(XML/注解)
// → 生成 MappedStatement,建立 statementId → SQL 的映射关系
mapperElement(root.evalNode("mappers"));




XMLStatementBuilder
Mapper XML 中每一个 <select> 节点都会对应一个 XMLStatementBuilder 解析器,它基于 XNode 节点内容,在 MapperBuilderAssistant 辅助下生成 MappedStatement,并注册到全局 Configuration 中,供后续 SqlSession 执行时查找调用。
| 对象/字段 | 类型 | 是否"每个SQL节点一个" | 核心作用 | 类比理解 |
|---|---|---|---|---|
statementParser |
XMLStatementBuilder |
✅ 是 | 专门解析某一个 <select>/<insert> 节点,最终生成 MappedStatement |
单条SQL的"编译器" |
builderAssistant |
MapperBuilderAssistant |
❌ 一个Mapper文件共享 | 提供命名空间、辅助注册 statement/resultMap 等 | Mapper构建助手 |
context |
XNode |
✅ 是 | 当前 XML 节点对象(保存SQL标签内容和属性) | DOM节点包装 |
requiredDatabaseId |
String |
❌ 可选 | 多数据库适配用,判断是否加载该SQL | 数据库过滤器 |
configuration |
Configuration |
❌ 全局唯一 | MyBatis运行期核心容器,存放所有Mapper、SQL、插件等 | 全局注册中心 |
typeAliasRegistry |
TypeAliasRegistry |
❌ 全局唯一 | 类型别名管理(XML中短名称→Class) | 类名字典 |
typeHandlerRegistry |
TypeHandlerRegistry |
❌ 全局唯一 | Java类型↔JDBC类型转换器注册表 | 类型转换中心 |



一个标签的转化 增删改查的标签
statementParser.parseStatementNode();
xmlstatemxml
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
// 最关键的一步,在 Configuration 添加了 MappedStatement >>
configuration.addMappedStatement(statement);
return statement;
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
.conflictMessageProducer((savedValue, targetValue) ->
". please check " + savedValue.getResource() + " and " + targetValue.getResource());
statement 对应的crud标签
public class DefaultSqlSessionFactory implements SqlSessionFactory {
时序图

会话对象
DefaultSqlSession
它是应用层与 MyBatis 执行器(Executor)之间的桥梁,负责SQL调用的统一入口与事务管理。
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
configuration.getDefaultExecutorType()
public ExecutorType getDefaultExecutorType() {
return defaultExecutorType;
}
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
* @author Clinton Begin
*/
public enum ExecutorType {
SIMPLE, REUSE, BATCH
}
执行器作用
| ExecutorType | 执行器机制 | 是否复用 Statement | 是否批量执行 | 典型使用场景 | 优点 | 注意点 |
|---|---|---|---|---|---|---|
| SIMPLE | 每次执行创建新的 Statement | 否 | 否 | 默认 CRUD、普通查询 | 行为最直观、安全 | 性能一般,频繁创建 Statement |
| REUSE | 复用已创建的 Statement | 是 | 否 | 同一 SQL 多次执行 | 减少 Statement 创建开销 | 只复用 Statement,不合并 SQL |
| BATCH | 批量缓存 SQL,统一提交 | 是(批量) | 是 | 批量 insert / update | 大幅提升批量写性能 | 需手动 flushStatements() |
ExecutorType决定 MyBatis SQL 的执行模型 ,defaultExecutorType决定 全局默认行为。
public final class Environment {
private final String id;
private final TransactionFactory transactionFactory;
private final DataSource dataSource;
Environment类在 MyBatis 中是非常核心的配置对象,它主要用来封装一个运行环境下 数据库连接和事务相关的配置。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
///获取数据库信息
final Environment environment = configuration.getEnvironment();
// 获取事务工厂
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 创建事务
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 根据事务工厂和默认的执行器类型,创建执行器 >>
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
final Executor executor = configuration.newExecutor(tx, execType); 执行sql语句的执行器
Rule
// 缓存 Statement
private final Map<String, Statement> statementMap = new HashMap<>();
public ReuseExecutor(Configuration configuration, Transaction transaction) {
super(configuration, transaction);
}
executor = new ReuseExecutor(this, transaction);// 缓存
不同的策略对应不同的选择,例如缓存,就可以直接从缓存中拿

public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
this.executor = executor;
this.dirty = false;
this.autoCommit = autoCommit;
}

select代理创建
sql语句的执行

getMapper
@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. Cause: " + e, e);
}
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
// 1:类加载器:2:被代理类实现的接口、3:实现了 InvocationHandler 的触发管理类
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public class MapperProxy<T> implements InvocationHandler, Serializable {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// toString hashCode equals getClass等方法,无需走到执行SQL的流程
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 提升获取 mapperMethod 的效率,到 MapperMethodInvoker(内部接口) 的 invoke
// 普通方法会走到 PlainMethodInvoker(内部类) 的 invoke
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}


代理执行
代理对象的MapperProxy
public class MapperProxy<T> implements InvocationHandler, Serializable {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// toString hashCode equals getClass等方法,无需走到执行SQL的流程
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 提升获取 mapperMethod 的效率,到 MapperMethodInvoker(内部接口) 的 invoke
// 普通方法会走到 PlainMethodInvoker(内部类) 的 invoke
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}

/// 返回的MapperMethodInvoker 这个实例
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
// SQL执行的真正起点
return mapperMethod.execute(sqlSession, args);
}
}
sql执行

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//拿取指之前初始化的MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
// 如果 cacheEnabled = true(默认),Executor会被 CachingExecutor装饰
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
// cache 对象是在哪里创建的? XMLMapperBuilder类 xmlconfigurationElement()
// 由 <cache> 标签决定
if (cache != null) {
// flushCache="true" 清空一级二级缓存 >>
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
// 获取二级缓存
// 缓存通过 TransactionalCacheManager、TransactionalCache 管理
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 写入二级缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 走到 SimpleExecutor | ReuseExecutor | BatchExecutor
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}




增强
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
// 植入插件逻辑(返回代理对象)
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// StatementType 是怎么来的? 增删改查标签中的 statementType="PREPARED",默认值 PREPARED
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
// 创建 StatementHandler 的时候做了什么? >>
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
stmt = prepareStatement(handler, ms.getStatementLog());
//获取链接
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
//获取Statement
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
statement = instantiateStatement(connection);
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}

| 层次 | 对象 | 关系 |
|---|---|---|
| JDBC | Statement / PreparedStatement | 一条 SQL 对应一个 Statement |
| MyBatis | SqlSession | 可以执行多条 SQL,一个 SqlSession 可以生成多个 Statement |
| MyBatis | Executor | 每条 SQL 会通过 Executor 调用 StatementHandler 生成 Statement |
返回处理
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
// 设置当前错误上下文:正在处理结果集,并绑定当前 MappedStatement 的 id
ErrorContext.instance()
.activity("handling results")
.object(mappedStatement.getId());
// 用于存放所有结果集解析后的结果(可能是多个 ResultSet)
final List<Object> multipleResults = new ArrayList<>();
// 当前处理到第几个 ResultSet(从 0 开始)
int resultSetCount = 0;
// 获取第一个 ResultSet,并包装成 ResultSetWrapper(便于统一处理元数据)
ResultSetWrapper rsw = getFirstResultSet(stmt);
// 从 MappedStatement 中获取所有配置的 ResultMap
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
// ResultMap 的数量
int resultMapCount = resultMaps.size();
// 校验 ResultSet 与 ResultMap 的数量是否匹配(防止配置错误)
validateResultMapsCount(rsw, resultMapCount);
/**
* 第一阶段:
* 处理“主结果集”
* - 一个 ResultSet 对应一个 ResultMap
* - 常见于普通查询或多结果集返回
*/
while (rsw != null && resultMapCount > resultSetCount) {
// 根据当前索引获取对应的 ResultMap
ResultMap resultMap = resultMaps.get(resultSetCount);
// 处理当前 ResultSet:
// - rsw:当前结果集
// - resultMap:映射规则
// - multipleResults:将结果加入到该集合中
// - null:此时不是嵌套结果映射
handleResultSet(rsw, resultMap, multipleResults, null);
// 获取下一个 ResultSet(Statement 可能返回多个)
rsw = getNextResultSet(stmt);
// 清理当前 ResultSet 处理过程中产生的临时状态
cleanUpAfterHandlingResultSet();
// 结果集索引递增
resultSetCount++;
}
/**
* 第二阶段:
* 处理“嵌套结果集”(多 ResultSet 关联)
* - 通常用于存储过程
* - 通过 resultSets 属性指定 ResultSet 名称
* - 与父结果集进行关联映射
*/
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
// 继续遍历剩余的 ResultSet
while (rsw != null && resultSetCount < resultSets.length) {
// 根据 resultSets 中的名称,找到对应的父 ResultMapping
ResultMapping parentMapping =
nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
// 获取嵌套 ResultMap 的 id
String nestedResultMapId =
parentMapping.getNestedResultMapId();
// 从 Configuration 中获取嵌套 ResultMap
ResultMap resultMap =
configuration.getResultMap(nestedResultMapId);
// 处理嵌套 ResultSet:
// - multipleResults 为 null(不作为独立结果返回)
// - parentMapping 用于将结果挂接到父对象
handleResultSet(rsw, resultMap, null, parentMapping);
}
// 获取下一个 ResultSet
rsw = getNextResultSet(stmt);
// 清理临时状态
cleanUpAfterHandlingResultSet();
// 索引递增
resultSetCount++;
}
}
/**
* 如果最终只有一个结果集:
* - 返回该结果本身(而不是 List<List<>>)
* 如果有多个结果集:
* - 返回 List<Object>
*/
return collapseSingleResultList(multipleResults);
}
/**
* 处理单个 ResultSet,并根据是否为嵌套映射决定结果的去向
*
* @param rsw ResultSet 的包装对象,包含结果集及其元数据
* @param resultMap 当前 ResultSet 对应的 ResultMap 映射规则
* @param multipleResults 用于存放“主结果集”的结果列表(嵌套结果时为 null)
* @param parentMapping 父级 ResultMapping(不为 null 表示这是嵌套结果集)
*/
private void handleResultSet(ResultSetWrapper rsw,
ResultMap resultMap,
List<Object> multipleResults,
ResultMapping parentMapping) throws SQLException {
try {
/**
* 场景一:嵌套结果集(多 ResultSet 关联)
* - parentMapping 不为 null
* - 当前 ResultSet 的数据需要“挂接”到父结果对象上
* - 不作为独立查询结果返回
*/
if (parentMapping != null) {
// 处理行数据:
// - resultHandler 为 null(不需要收集成独立结果)
// - RowBounds.DEFAULT:不进行分页
// - parentMapping:用于父子对象关联
handleRowValues(
rsw,
resultMap,
null,
RowBounds.DEFAULT,
parentMapping
);
} else {
/**
* 场景二:普通结果集(非嵌套)
*/
// 如果用户没有自定义 ResultHandler
if (resultHandler == null) {
// 使用默认的结果处理器
DefaultResultHandler defaultResultHandler =
new DefaultResultHandler(objectFactory);
// 处理行数据:
// - 将每一行映射成对象
// - 由 DefaultResultHandler 收集到内部 List 中
handleRowValues(
rsw,
resultMap,
defaultResultHandler,
rowBounds,
null
);
// 将当前 ResultSet 的结果(一个 List)加入 multipleResults
multipleResults.add(
defaultResultHandler.getResultList()
);
} else {
/**
* 场景三:用户自定义 ResultHandler
* - MyBatis 不再收集结果
* - 由用户自行处理每一行数据(如流式处理、大数据量)
*/
handleRowValues(
rsw,
resultMap,
resultHandler,
rowBounds,
null
);
}
}
} finally {
/**
* 无论是否发生异常,都必须关闭 ResultSet
* issue #228:
* - 修复 ResultSet 未关闭导致的数据库资源泄露问题
*/
closeResultSet(rsw.getResultSet());
}
}
