JSQLParser 4.x 中 IN 表达式缺少括号的问题排查与解决
问题背景
在使用 MyBatis Plus 拦截器动态拼接数据权限 SQL 时,使用 JSQLParser 构造IN表达式,运行后发现生成的 SQL 语法错误:
-- 期望SELECT*FROMsdk_projectWHEREdeleted=0ANDsales_user_idIN('aaa','bbb','ccc')-- 实际SELECT*FROMsdk_projectWHEREdeleted=0ANDsales_user_idIN'aaa','bbb','ccc'Druid 随即抛出异常:
com.alibaba.druid.sql.parser.ParserException: not supported. pos 116, line 1, column 116, token ,问题代码
InExpressioninExpression=newInExpression();inExpression.setLeftExpression(newColumn("sales_user_id"));inExpression.setRightExpression(newExpressionList<>(ids.stream().map(StringValue::new).collect(Collectors.toList())));根因分析
环境
| 依赖 | 版本 |
|---|---|
| JSQLParser | 4.9(通过 MyBatis-Plus 3.5.7 传递引入) |
关键结论
InExpression在调用toString()时,括号完全由rightExpression.toString()自身决定,InExpression本身不负责添加括号。
ExpressionList.toString() 不带括号
通过反编译jsqlparser-4.9.jar验证:
// ExpressionList.toString() 等价源码@OverridepublicStringtoString(){StringBuildersb=newStringBuilder();appendTo(sb);// 自行拼接,不加括号returnsb.toString();// 输出:'aaa', 'bbb', 'ccc'}ParenthesedExpressionList.toString() 带括号
// ParenthesedExpressionList.toString() 等价源码@OverridepublicStringtoString(){returnPlainSelect.getStringList(this,true,true);// ^^^^ ^^^^// useBrackets=true ─┘ │// useComma=true ─────────┘// 输出:('aaa', 'bbb', 'ccc')}两个true是源码内硬编码的,不是外部调用者传入的。通过字节码可以直接验证:
ParenthesedExpressionList.toString(): 0: aload_0 ← this 1: iconst_1 ← 硬编码 true(useBrackets) 2: iconst_1 ← 硬编码 true(useComma) 3: invokestatic PlainSelect.getStringList:(List, Z, Z)三种写法实测对比
List<String>ids=Arrays.asList("aaa","bbb","ccc");// A: Parenthesis 包裹 ExpressionListInExpressioninA=newInExpression();inA.setLeftExpression(newColumn("user_id"));inA.setRightExpression(newParenthesis(newExpressionList<>(ids.stream().map(StringValue::new).collect(Collectors.toList()))));// 输出:user_id IN ('aaa', 'bbb', 'ccc') ✓// B: ParenthesedExpressionListInExpressioninB=newInExpression();inB.setLeftExpression(newColumn("user_id"));inB.setRightExpression(newParenthesedExpressionList<>(ids.stream().map(StringValue::new).collect(Collectors.toList())));// 输出:user_id IN ('aaa', 'bbb', 'ccc') ✓// C: 裸 ExpressionList(原始问题写法)InExpressioninC=newInExpression();inC.setLeftExpression(newColumn("user_id"));inC.setRightExpression(newExpressionList<>(ids.stream().map(StringValue::new).collect(Collectors.toList())));// 输出:user_id IN 'aaa', 'bbb', 'ccc' ✗ SQL 语法错误A 和 B 的 SQL 输出完全相同,两种写法均可解决问题。
Parenthesis 的原理
Parenthesis是一个装饰器,toString()直接硬拼括号:
// Parenthesis.toString() 等价源码@OverridepublicStringtoString(){return"("+expression+")";}字节码验证:
Parenthesis.toString(): 7: ldc "(" ← 硬拼左括号 13: append(expression) 31: ldc ")" ← 硬拼右括号 39: areturntoString() 的触发时机
setRightExpression()只存引用,不触发toString()。整个序列化发生在最后调用statement.toString()时:
// 构造阶段:只存引用,无任何输出newParenthesedExpressionList<>(list)// 构造对象inExpression.setRightExpression(...)// 存引用// 序列化阶段:statement.toString() 触发整棵 AST 递归序列化returnstatement.toString();// └─ InExpression.toString()// └─ StringBuilder.append("IN ")// └─ StringBuilder.append(rightExpression) ← 此处才调用 rightExpression.toString()// └─ ParenthesedExpressionList.toString()// └─ PlainSelect.getStringList(this, true, true)// └─ 输出 ('aaa', 'bbb', 'ccc')解决方案
将ExpressionList替换为ParenthesedExpressionList:
// 修复前inExpression.setRightExpression(newExpressionList<>(ids.stream().map(StringValue::new).collect(Collectors.toList())));// 修复后inExpression.setRightExpression(newParenthesedExpressionList<>(ids.stream().map(StringValue::new).collect(Collectors.toList())));或者用Parenthesis包裹也可以(两者 SQL 输出等价):
inExpression.setRightExpression(newParenthesis(newExpressionList<>(ids.stream().map(StringValue::new).collect(Collectors.toList()))));总结
| 写法 | SQL 输出 | 说明 |
|---|---|---|
ExpressionList<>(...) | IN 'a', 'b'✗ | 不带括号,SQL 语法错误 |
ParenthesedExpressionList<>(...) | IN ('a', 'b')✓ | 内部硬编码useBrackets=true |
Parenthesis(ExpressionList<>(...)) | IN ('a', 'b')✓ | 装饰器强制加括号 |
注意:此问题仅出现在 JSQLParser 4.x。若项目升级至 5.x,建议重新验证 API 兼容性。