不推荐反射修改BoundSql的sql字段,因其不可变且每次查询新建实例;应改StaticSqlSource中的sql字段,或优先用拦截器、自定义SqlSource、JSQLParser等安全方案。

在 MyBatis 插件开发中,**不推荐用反射直接修改 BoundSql 的 sql 字段**——因为 BoundSql 是不可变对象,每次调用 getBoundSql() 都会新建实例,改它内部的 sql 字段对实际执行毫无影响。真正有效且相对可控的做法,是**定位并替换底层 SqlSource(如 StaticSqlSource)中被缓存的 SQL 字符串**,但必须清醒认识其风险边界。
为什么 BoundSql 不能直接反射修改
BoundSql 设计为不可变(immutable),其构造后字段全为 final,且 MyBatis 每次查询都会调用 SqlSource.getBoundSql() 新建一个 BoundSql 实例。即使你用反射强行修改了某个 BoundSql 对象的 sql 字段:
- 该修改只作用于当前临时对象,下一次查询仍会生成新对象
- 参数映射(
ParameterMapping)未同步更新,可能导致 SQL 与参数不匹配而报错 - 违反 MyBatis 内部契约,JDK 版本升级或框架迭代后极易失效
安全可行的反射切入点:StaticSqlSource 中的 sql 字段
MyBatis 中多数 XML 或注解定义的 SQL 最终由 StaticSqlSource 封装,它内部持有一个 private final String sql。这个字段在 MappedStatement 初始化时被一次性设置,并被复用——这才是可干预的稳定节点。
操作步骤如下:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
立即学习“Java免费学习笔记(深入)”;
- 从
MappedStatement获取原始SqlSource(通常是RawSqlSource或CachingSqlSource) - 递归获取其内部包装的
StaticSqlSource(可能需穿透 1–2 层代理) - 用
getDeclaredField("sql")获取私有字段,调用setAccessible(true) - 用
set()替换字符串值(注意:仅适用于纯静态 SQL;含 ${} 的拼接 SQL 不建议动)
示例关键代码片段:
Field sqlField = staticSqlSource.getClass().getDeclaredField("sql");sqlField.setAccessible(true);
sqlField.set(staticSqlSource, "SELECT * FROM user WHERE tenant_id = ? AND " + originalSql);
比反射更安全、更推荐的替代方案
反射修改属于“破坏性修补”,应作为最后手段。生产环境优先采用以下正向设计:
- 使用 MyBatis 拦截器(Interceptor)拦截 Executor.query():在 SQL 执行前,基于原 BoundSql 构造新的 BoundSql,传入修改后的 SQL 和完整 ParameterMapping,完全符合框架语义
-
自定义 SqlSource 实现类:继承
StaticSqlSource或实现SqlSource接口,在getBoundSql()中动态注入条件,避免反射 - 借助 JSQLParser 等库解析并重写 SQL:在拦截器中解析 AST,安全地插入 WHERE 条件,再生成新 SQL,类型安全、可测试、易维护
-
通过 MyBatis 的
<bind>标签或 OGNL 表达式 在 XML 中预埋权限逻辑,由框架自动处理
JDK 9+ 与模块化下的现实约束
若坚持使用反射(例如在测试或受限环境中),必须面对 JDK 模块系统限制:
- 运行时抛出
InaccessibleObjectException是常态,需启动参数显式开放:--add-opens java.base/java.lang=ALL-UNNAMED(针对核心类)--add-opens org.apache.ibatis/mybatis=ALL-UNNAMED(若自定义模块) - 这些参数在容器化、云原生或受管平台(如某些 PaaS)中往往不可配置,导致方案失效
- Spring Boot 3+ 默认启用严格模块检查,反射路径更易断裂

















