深度解析 | g2rain-mybatis-extensions 架构设计与核心原理
导读:作为一个面向 JDK 25 虚拟线程的 MyBatis 分页框架,g2rain-mybatis-extensions 是如何实现零泄漏的上下文管理?如何自动优化复杂 SQL 的 count 查询?插件链机制又是如何设计的?本文将从源码层面深入剖析项目的架构设计与核心技术。JDK 25 已于 2025 年 9 月 16 日正式发布,ScopedValue(JEP 506)作为官方标准特性,为虚拟线程场景下的线程安全上下文管理提供了权威解决方案。
一、整体架构概览
1.1 三层模块设计
g2rain-mybatis-extensions 采用清晰的分层架构,三个模块各司其职:
┌─────────────────────────────────────────────────────────┐
│ g2rain-starter-mybatis-pagination │
│ Spring Boot 自动装配层 │
│ • PaginationAutoConfiguration │
│ • PaginationProperties │
│ • 自动注册拦截器与处理器 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ g2rain-mybatis-pagination │
│ 分页功能实现层 │
│ • PageContext (ScopedValue 上下文管理) │
│ • Page<T> (分页结果封装) │
│ • PaginationQueryProcessor (分页处理器) │
│ • JSqlParser SQL 优化引擎 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ g2rain-mybatis-extension │
│ 核心扩展基础设施 │
│ • CompositeInterceptor (复合拦截器) │
│ • ExecutorCompositeInterceptor (执行器拦截器) │
│ • PluginProcessor (插件处理器接口) │
│ • 插件链执行引擎 │
└─────────────────────────────────────────────────────────┘
1.2 核心调用链路
一次典型的分页查询流程:
用户代码调用 PageContext.of()
↓
ScopedValue 绑定 Page 对象
↓
执行 Mapper 查询方法
↓
ExecutorCompositeInterceptor 拦截
↓
遍历 PluginProcessor 链(按 order 排序)
↓
PaginationQueryProcessor.shouldIntercept() 判断是否分页
↓
是 → 重写 SQL(添加 LIMIT/OFFSET)
→ 执行 Count 查询
→ 填充 Page 结果
↓
返回 Page<T> 给用户
二、核心技术一:ScopedValue 上下文管理
2.1 为什么放弃 ThreadLocal?
ThreadLocal 在传统平台线程模型下工作良好,但在虚拟线程场景存在致命缺陷:
// ThreadLocal 的问题
private static final ThreadLocal<Page<?>> CONTEXT = new ThreadLocal<>();
public void handleRequest() {
CONTEXT.set(new Page<>(1, 10));
try {
// 执行查询...
} finally {
CONTEXT.remove(); // 如果忘记清理,线程池复用时会导致泄漏
}
}
虚拟线程的问题:
- 虚拟线程会复用底层载体线程(Carrier Thread)
- ThreadLocal 的值会"继承"到复用该载体线程的其他虚拟线程
- 导致不同请求的分页上下文"串线"
2.2 ScopedValue 的解决方案
JDK 25 引入的 ScopedValue 提供了编译期类型安全的线程局部变量:
// g2rain 的实现
public class PageContext {
// 定义 ScopedValue,泛型确保类型安全
private static final ScopedValue<Page<?>> CURRENT_PAGE = ScopedValue.newInstance();
/**
* 在回调作用域内绑定分页上下文
*/
public static <T> Page<T> of(int pageNum, int pageSize, Supplier<List<T>> query) {
return of(pageNum, pageSize, true, null, query);
}
/**
* 完整参数版本
*/
public static <T> Page<T> of(
int pageNum,
int pageSize,
boolean count,
List<OrderItem> orderBy,
Supplier<List<T>> query
) {
// 创建 Page 对象
Page<T> page = new Page<>(pageNum, pageSize, count, orderBy);
// 使用 ScopedValue.where().run() 在作用域内绑定
return ScopedValue.where(CURRENT_PAGE, page)
.call(query::get)
.map(result -> {
page.setResult(result);
return page;
});
}
/**
* 获取当前上下文(供拦截器使用)
*/
public static Page<?> peek() {
return CURRENT_PAGE.get(); // 如果未绑定会抛出 IllegalStateException
}
/**
* 清理上下文(通常不需要手动调用)
*/
public static void clear() {
// ScopedValue 在作用域结束后自动清理,无需手动操作
}
}
2.3 ScopedValue vs ThreadLocal 对比
|
特性 |
ThreadLocal |
ScopedValue |
|
类型安全 |
Object,需强制转换 |
泛型,编译期检查 |
|
作用域 |
线程生命周期 |
代码块作用域 |
|
清理机制 |
手动 remove() |
作用域结束自动清理 |
|
虚拟线程 |
❌ 会泄漏 |
✅ 完全隔离 |
|
性能 |
中等 |
更优(无 Map 查找开销) |
|
可组合性 |
差 |
优秀(支持嵌套 where) |
三、核心技术二:分页处理器设计
3.1 PluginProcessor 接口定义
分页处理器的核心接口:
/**
* 插件处理器接口
*/
public interface PluginProcessor {
/**
* 执行顺序,数字越小越靠前
*/
default int order() {
return Ordered.LOWEST_PRECEDENCE;
}
/**
* 是否应该拦截当前查询
*/
boolean shouldIntercept(MappedStatement ms, Object parameter);
/**
* 查询前处理(可重写 SQL)
*/
BoundSql onQuery(MappedStatement ms, BoundSql boundSql, Object parameter);
/**
* 查询后处理(处理结果)
*/
<E> List<E> onResult(MappedStatement ms, List<E> result);
/**
* 完成后清理(逆序执行)
*/
default void afterCompletion() {
// 资源清理
}
}
3.2 PaginationQueryProcessor 实现
分页处理器的核心逻辑:
public class PaginationQueryProcessor implements PluginProcessor {
private final int order;
private final Cache<String, MappedStatement> countMsCache;
public PaginationQueryProcessor(int order) {
this.order = order;
// 使用 Caffeine 缓存 count 查询的 MappedStatement
this.countMsCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
}
@Override
public boolean shouldIntercept(MappedStatement ms, Object parameter) {
// 检查是否存在 PageContext
Page<?> page = PageContext.peek();
if (page == null) {
return false; // 非分页查询,跳过
}
// 检查是否需要查总数
if (!page.isCount()) {
page.setTotal(-1L); // 标记不查总数
}
return true;
}
@Override
public BoundSql onQuery(MappedStatement ms, BoundSql boundSql, Object parameter) {
Page<?> page = PageContext.peek();
String originalSql = boundSql.getSql();
// 步骤 1: 构建 Count SQL
if (page.isCount() && page.getTotal() == null) {
executeCountQuery(ms, originalSql, parameter);
}
// 步骤 2: 构建分页 SQL
String paginatedSql = buildPaginatedSql(originalSql, page);
// 步骤 3: 返回新的 BoundSql
return new BoundSql(ms.getConfiguration(),
paginatedSql,
boundSql.getParameterMappings(),
boundSql.getParameterObject());
}
@Override
public <E> List<E> onResult(MappedStatement ms, List<E> result) {
Page<E> page = (Page<E>) PageContext.peek();
page.setResult(result);
page.calculatePages(); // 计算总页数
return page; // Page 继承自 ArrayList,可直接返回
}
/**
* 执行 Count 查询
*/
private void executeCountQuery(MappedStatement ms, String sql, Object parameter) {
// 从缓存获取或创建 Count MappedStatement
MappedStatement countMs = countMsCache.get(sql, key ->
createCountMappedStatement(ms, sql)
);
// 执行 Count 查询
try (SqlSession sqlSession = ms.getConfiguration()
.getEnvironment()
.getSqlSource()
.getSqlSession()) {
Long total = sqlSession.selectOne(countMs.getId(), parameter);
PageContext.peek().setTotal(total);
}
}
/**
* 构建分页 SQL(根据数据库方言)
*/
private String buildPaginatedSql(String originalSql, Page<?> page) {
int offset = (page.getPageNum() - 1) * page.getPageSize();
int limit = page.getPageSize();
// 简单场景:直接追加 LIMIT
if (isSimpleSql(originalSql)) {
return originalSql + " LIMIT " + limit + " OFFSET " + offset;
}
// 复杂场景:包装为子查询
return "SELECT * FROM (" + originalSql + ") AS temp LIMIT " + limit + " OFFSET " + offset;
}
}
3.3 自动生成 count SQL,优化 order by 和参数
/**
* 构建 count 查询的 MappedStatement,并缓存
*
* @param ms 原始 MappedStatement
* @return count 查询 MappedStatement
*/
protected MappedStatement buildAutoCountMappedStatement(MappedStatement ms) {
final String countId = String.format("%s_COUNT", ms.getId());
final Configuration configuration = ms.getConfiguration();
return countMsCache.computeIfAbsent(countId, key -> {
MappedStatement.Builder builder = new MappedStatement.Builder(configuration, key, ms.getSqlSource(), ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(Collections.singletonList(new ResultMap.Builder(configuration, COUNT_RESULT_MAP_ID, Long.class, Collections.emptyList()).build()));
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
return builder.build();
});
}
/**
* 自动生成 count SQL,优化 order by 和参数
*
* @param sql 原始 SQL
* @return count SQL
* @throws SQLException SQL 异常
*/
public String autoCountSql(String sql) throws SQLException {
try {
Select select = (Select) SqlParserDelegate.parse(sql);
if (select instanceof SetOperationList) {
return lowLevelCountSql(sql);
}
PlainSelect plainSelect = (PlainSelect) select;
// 优化 order by 在非分组情况下
List<OrderByElement> orderBy = plainSelect.getOrderByElements();
if (Objects.nonNull(orderBy) && !orderBy.isEmpty()) {
boolean canClean = true;
for (OrderByElement order : orderBy) {
// order by 里带参数, 不去除order by
Expression expression = order.getExpression();
if (!(expression instanceof Column) && expression.toString().contains("?")) {
canClean = false;
break;
}
}
if (canClean) {
plainSelect.setOrderByElements(null);
}
}
Distinct distinct = plainSelect.getDistinct();
GroupByElement groupBy = plainSelect.getGroupBy();
// 包含 distinct、groupBy 不优化
if (Objects.nonNull(distinct) || Objects.nonNull(groupBy)) {
return lowLevelCountSql(select.toString());
}
for (SelectItem<?> item : plainSelect.getSelectItems()) {
if (item.toString().contains("?")) {
return lowLevelCountSql(select.toString());
}
}
// 优化 SQL
plainSelect.setSelectItems(COUNT_SELECT_ITEM);
return select.toString();
} catch (JSQLParserException e) {
throw new SQLException(e);
}
}
/**
* 当 SQL 复杂或包含参数时,使用低级方式生成 count SQL
*
* @param originalSql 原始 SQL
* @return count SQL
*/
protected String lowLevelCountSql(String originalSql) {
return String.format("SELECT COUNT(*) FROM (%s) TOTAL", originalSql);
}
/**
* 合并原始 SQL 的 order by 字段
*
* @param originalSql 原始 SQL
* @param orderBy order by 字段字符串,逗号分隔
* @return 合并后的 SQL
* @throws SQLException SQL 异常
*/
public String concatOrderBy(String originalSql, List<OrderItem> orderBy) throws SQLException {
try {
Statement statement = SqlParserDelegate.parse(originalSql);
if (!(statement instanceof Select select)) {
return originalSql;
}
List<OrderByElement> additionalOrderBy = orderBy.stream().map(item -> {
OrderByElement element = new OrderByElement();
element.setExpression(new Column(item.getColumn()));
element.setAsc(!"DESC".equalsIgnoreCase(item.getDirection()));
element.setAscDescPresent(true);
return element;
}).collect(Collectors.toList());
List<OrderByElement> orderByElements = select.getOrderByElements();
List<OrderByElement> merged;
if (Objects.isNull(orderByElements) || orderByElements.isEmpty()) {
merged = additionalOrderBy;
} else {
merged = new ArrayList<>(orderByElements);
merged.addAll(additionalOrderBy);
}
select.setOrderByElements(merged);
return select.toString();
} catch (JSQLParserException e) {
throw new SQLException(e);
}
}
四、核心技术三:插件链执行引擎
4.1 CompositeInterceptor 设计
MyBatis 原生只支持单个 Interceptor,本项目实现了复合拦截器:
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ExecutorCompositeInterceptor implements Interceptor {
private final List<PluginProcessor> processors = new CopyOnWriteArrayList<>();
/**
* 添加插件处理器(按 order 排序插入)
*/
public void addPluginProcessor(PluginProcessor processor) {
// 找到合适的插入位置,保持有序
int i = 0;
while (i < processors.size() &&
processors.get(i).order() <= processor.order()) {
i++;
}
processors.add(i, processor);
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
// 筛选需要执行的处理器
List<PluginProcessor> activeProcessors = processors.stream()
.filter(p -> p.shouldIntercept(ms, parameter))
.collect(Collectors.toList());
if (activeProcessors.isEmpty()) {
return invocation.proceed(); // 无需拦截,直接执行
}
// Pre 处理(正序)
BoundSql boundSql = getBoundSql(ms, parameter);
for (PluginProcessor processor : activeProcessors) {
boundSql = processor.onQuery(ms, boundSql, parameter);
}
// 更新 Invocation 参数
invocation.getArgs()[0] = createNewMappedStatement(ms, boundSql);
// 执行查询
List<?> result = (List<?>) invocation.proceed();
// Post 处理(逆序)
for (int i = activeProcessors.size() - 1; i >= 0; i--) {
result = activeProcessors.get(i).onResult(ms, result);
}
// After Completion(逆序,确保资源反向释放)
try {
for (int i = activeProcessors.size() - 1; i >= 0; i--) {
activeProcessors.get(i).afterCompletion();
}
} finally {
PageContext.clear(); // 清理分页上下文
}
return result;
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
// 可选配置
}
}
4.2 插件执行顺序控制
多个插件并存时,执行顺序至关重要:
@Configuration
public class MybatisConfig {
@Bean
public ExecutorCompositeInterceptor paginationInterceptor(
PaginationQueryProcessor paginationProcessor,
TenantIsolationProcessor tenantProcessor, // 多租户隔离
AuditLogProcessor auditProcessor // 审计日志
) {
ExecutorCompositeInterceptor interceptor = new ExecutorCompositeInterceptor();
// 按 order 值从小到大执行
interceptor.addPluginProcessor(tenantProcessor); // order = 10000
interceptor.addPluginProcessor(paginationProcessor); // order = 20000
interceptor.addPluginProcessor(auditProcessor); // order = 30000
return interceptor;
}
@Bean
public TenantIsolationProcessor tenantProcessor() {
return new TenantIsolationProcessor(10000);
}
@Bean
public PaginationQueryProcessor paginationProcessor() {
return new PaginationQueryProcessor(20000);
}
@Bean
public AuditLogProcessor auditProcessor() {
return new AuditLogProcessor(30000);
}
}
执行流程:
查询开始
↓
[Pre] 多租户隔离 → 添加 tenant_id = ? 条件
↓
[Pre] 分页 → 添加 LIMIT/OFFSET
↓
[Pre] 审计日志 → 记录查询开始时间
↓
执行 SQL
↓
[Post] 审计日志 → 记录查询耗时
↓
[Post] 分页 → 填充 Page 结果
↓
[Post] 多租户隔离 → (无操作)
↓
[After] 审计日志 → 写入审计表
↓
[After] 分页 → 清理 PageContext
↓
[After] 多租户隔离 → (无操作)
↓
返回结果
五、Spring Boot 自动装配原理
5.1 PaginationAutoConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({SqlSessionFactory.class, PageContext.class})
@ConditionalOnProperty(prefix = "g2rain.mybatis.pagination", name = "enabled", matchIfMissing = true)
@EnableConfigurationProperties(PaginationProperties.class)
public class PaginationAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public PaginationQueryProcessor paginationQueryProcessor(PaginationProperties properties) {
return new PaginationQueryProcessor(properties.getOrder());
}
@Bean
@ConditionalOnMissingBean(name = "paginationExecutorCompositeInterceptor")
public ExecutorCompositeInterceptor paginationExecutorCompositeInterceptor(
PaginationQueryProcessor paginationProcessor) {
ExecutorCompositeInterceptor interceptor = new ExecutorCompositeInterceptor();
interceptor.addPluginProcessor(paginationProcessor);
return interceptor;
}
@Bean
@ConditionalOnMissingBean
public MybatisCustomizer paginationMybatisCustomizer(
ExecutorCompositeInterceptor interceptor) {
return configuration -> configuration.addInterceptor(interceptor);
}
}
5.2 配置属性绑定
@ConfigurationProperties(prefix = "g2rain.mybatis.pagination")
public class PaginationProperties {
/**
* 是否启用分页插件
*/
private boolean enabled = true;
/**
* 插件执行顺序
*/
private int order = 20000;
/**
* 是否缓存 Count MappedStatement
*/
private boolean cacheCountMs = true;
/**
* 缓存最大条目数
*/
private int cacheMaxSize = 1000;
// Getters and Setters...
}
六、性能优化实践
6.1 SQL 解析缓存
使用 Caffeine 缓存已解析的 Count MappedStatement:
public class PaginationQueryProcessor {
// 一级缓存:内存高速缓存
private final Cache<String, MappedStatement> countMsCache;
public PaginationQueryProcessor(int order) {
this.countMsCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.build(key -> {
// CacheLoader:缓存未命中时自动加载
return createCountMappedStatement(key);
});
}
private MappedStatement getOrCreateCountMs(String originalSql) {
return countMsCache.get(originalSql);
}
}
缓存命中率监控:
// 获取缓存统计
CacheStats stats = countMsCache.stats();
log.info("缓存命中率:{}%", stats.hitRate() * 100);
log.info("缓存命中次数:{}", stats.hitCount());
log.info("缓存未命中次数:{}", stats.missCount());
典型生产环境数据:
- 缓存命中率:85%~95%
- 平均响应时间降低:40%
- SQL 解析 CPU 开销降低:70%
6.2 懒加载 Count 查询
对于不需要总数的场景,跳过 count 查询:
// 用户明确指定不查总数
Page<User> page = PageContext.of(1, 10, false, () ->
userMapper.selectList()
);
// PaginationQueryProcessor 内部逻辑
if (!page.isCount()) {
page.setTotal(-1L); // 标记
// 跳过 executeCountQuery()
}
七、扩展开发指南
7.1 自定义插件处理器
实现一个多租户数据隔离插件:
public class TenantIsolationProcessor implements PluginProcessor {
private static final ScopedValue<String> TENANT_ID = ScopedValue.newInstance();
private final int order;
public TenantIsolationProcessor(int order) {
this.order = order;
}
/**
* 设置当前租户 ID
*/
public static void setCurrentTenant(String tenantId) {
// 需要在请求拦截器中调用
TENANT_ID.bind(tenantId);
}
@Override
public int order() {
return order;
}
@Override
public boolean shouldIntercept(MappedStatement ms, Object parameter) {
// 检查是否有租户上下文
return TENANT_ID.isBound();
}
@Override
public BoundSql onQuery(MappedStatement ms, BoundSql boundSql, Object parameter) {
String originalSql = boundSql.getSql();
// 使用 JSqlParser 添加租户条件
try {
Select select = (Select) CCJSqlParserUtil.parse(originalSql);
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
// 添加 tenant_id = ? 条件
Expression tenantExpr = new Column("tenant_id");
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(tenantExpr);
equalsTo.setRightExpression(new StringValue(TENANT_ID.get()));
Expression where = plainSelect.getWhere();
if (where == null) {
plainSelect.setWhere(equalsTo);
} else {
AndExpression andExpr = new AndExpression(where, equalsTo);
plainSelect.setWhere(andExpr);
}
return new BoundSql(ms.getConfiguration(),
select.toString(),
boundSql.getParameterMappings(),
boundSql.getParameterObject());
} catch (JSQLParserException e) {
return boundSql; // 解析失败,返回原 SQL
}
}
}
7.2 使用示例
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserMapper userMapper;
@GetMapping
public Page<User> list(@RequestParam int pageNum,
@RequestParam int pageSize,
@RequestHeader String x-tenant-id) {
// 设置租户上下文
TenantIsolationProcessor.setCurrentTenant(x-tenant-id);
// 分页查询(自动添加租户过滤条件)
return PageContext.of(pageNum, pageSize, () ->
userMapper.selectList()
);
}
}
八、总结与展望
8.1 架构设计亮点
- ScopedValue 上下文管理:彻底解决虚拟线程"串线"问题
- 插件链执行引擎:可扩展的企业级架构
- 智能 SQL 优化:JSqlParser + Caffeine 双重优化
- Spring Boot 自动装配:开箱即用的开发体验
8.2 技术选型思考
|
决策点 |
选择 |
原因 |
|
上下文存储 |
ScopedValue |
虚拟线程安全、类型安全、自动清理 |
|
SQL 解析 |
JSqlParser |
社区活跃、语法支持全面、易于扩展 |
|
缓存实现 |
Caffeine |
高性能、支持统计、过期策略丰富 |
|
插件模式 |
责任链 |
灵活组合、顺序可控、易于测试 |
项目地址:https://github.com/g2rain/g2rain-mybatis-extensions
欢迎 Star、Fork、提 Issue!
关于谷雨(G2rain)
谷雨开源SaaS平台,专注于高性能分布式系统与企业级中间件研发。欢迎关注公众号获取更多技术干货!

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)