引言
某一天,笔者在逛着Github的时候,突然看到seata有个有趣的issue,是一个task。
相关描述:
While running the DruidSQLRecognizerFactoryTest.testIsSqlSyntaxSupports
test, a ParserException
is thrown when attempting to parse a SQL statement that starts with REPLACE
. This indicates that the Druid SQL parser does not support the REPLACE
SQL syntax.
简单来说,就是issue主人在做另一个pr的时候,发现DruidSQLRecognizerFactoryTest测试类的方法testIsSqlSyntaxSupports,当解析REPLACE语句的时候,会抛出ParserException。这会导致某个模块的maven构建失败,所以他希望有人可以解决这个问题。
原因🤓
笔者研究了一下,这是因为底层的Druid SQL解析器本来就不支持解析相关的REPLACE语法,而相关测试类的处理方法则是抛出异常并打印相关日志,并没有处理该异常或者转换该异常
异常堆栈:
css
com.alibaba.druid.sql.parser.ParserException: not supported.pos 7, line 1, column 1, token REPLACE
at com.alibaba.druid.sql.parser.SQLStatementParser.parseStatementList(SQLStatementParser.java:616)
at com.alibaba.druid.sql.SQLUtils.parseStatements(SQLUtils.java:603)
at org.apache.seata.sqlparser.druid.DruidSQLRecognizerFactoryImpl.create(DruidSQLRecognizerFactoryImpl.java:41)
at org.apache.seata.sqlparser.druid.DruidSQLRecognizerFactoryTest.testIsSqlSyntaxSupports(DruidSQLRecognizerFactoryTest.java:172)
解决方法😵
在 Seata 中将 Druid 抛出的 ParserException
转换为 Seata 自定义异常
这样做的好处是:
-
异常处理统一 - 所有不支持的SQL语法都抛出 NotSupportYetException
-
测试逻辑一致 - 所有数据库类型使用相同的测试断言
-
兼容性保持 - DruidIsolationTest 等其他测试正常运行
-
用户体验改善 - 提供统一、友好的错误信息
最终,GitHub Action构建成功 - 不再因为stderr输出而失败
但是,你以为这样就结束了吗?不会!!!
修改一个类往往会有蝴蝶效应。
笔者在修改之后进行maven构建时,发现测试类依然报错,但是明明笔者已经测试过了该类的测试类是否正确运行,那为什么还会有测试类报错呢?答案是因为修改了DruidSQLRecognizerFactoryImpl
,而maven构建检查的时候,其他测试类也会一起运行,所以修改的类不能将全部异常都抛出是NotSupportYetException
,对只是ParserException
才进行转换抛出NotSupportYetException
具体修改:
1.在DruidSQLRecognizerFactoryImpl
新建立一个私有方法,用来确定是否是ParserException
,如果是,才转换抛出NotSupportYetException
,否则按原有异常抛出
2.只对会抛出ParserException
的方法进行try-catch,避免用一个大的try-catch来包裹住整个方法,提高可读性
3.将asts等名字转换成sqlStatements,提高可读性
4.使用反射方式检查异常类型,避免直接引用 ParserException(因为ParserException 的直接引用导致类加载时的依赖问题。例如在 DruidIsolationTest 中,类隔离机制会阻止某些Druid类的加载,包括 ParserException)
修改的源码如下:
java
/**
* DruidSQLRecognizerFactoryImpl
*
*/
class DruidSQLRecognizerFactoryImpl implements SQLRecognizerFactory {
@Override
public List<SQLRecognizer> create(String sql, String dbType) {
List<SQLStatement> sqlStatements;
try {
sqlStatements = SQLUtils.parseStatements(sql, DruidDbTypeAdapter.getAdaptiveDbType(dbType));
} catch (RuntimeException e) {
if (isParserException(e)) {
throw new NotSupportYetException("not support the sql syntax: " + sql +
"\nplease see the doc about SQL restrictions https://seata.apache.org/zh-cn/docs/user/sqlreference/dml", e);
}
throw e;
}
if (CollectionUtils.isEmpty(sqlStatements)) {
throw new UnsupportedOperationException("Unsupported SQL: " + sql);
}
if (sqlStatements.size() > 1 && !(sqlStatements.stream().allMatch(statement -> statement instanceof SQLUpdateStatement)
|| sqlStatements.stream().allMatch(statement -> statement instanceof SQLDeleteStatement))) {
throw new UnsupportedOperationException("ONLY SUPPORT SAME TYPE (UPDATE OR DELETE) MULTI SQL -" + sql);
}
List<SQLRecognizer> recognizers = null;
SQLRecognizer recognizer = null;
for (SQLStatement sqlStatement : sqlStatements) {
SQLOperateRecognizerHolder recognizerHolder =
SQLOperateRecognizerHolderFactory.getSQLRecognizerHolder(dbType.toLowerCase());
if (sqlStatement instanceof SQLInsertStatement) {
recognizer = recognizerHolder.getInsertRecognizer(sql, sqlStatement);
} else if (sqlStatement instanceof SQLUpdateStatement) {
recognizer = recognizerHolder.getUpdateRecognizer(sql, sqlStatement);
} else if (sqlStatement instanceof SQLDeleteStatement) {
recognizer = recognizerHolder.getDeleteRecognizer(sql, sqlStatement);
} else if (sqlStatement instanceof SQLSelectStatement) {
recognizer = recognizerHolder.getSelectForUpdateRecognizer(sql, sqlStatement);
}
if (sqlStatement instanceof SQLReplaceStatement) {
throw new NotSupportYetException("not support the sql syntax with ReplaceStatement:" + sqlStatement +
"\nplease see the doc about SQL restrictions https://seata.apache.org/zh-cn/docs/user/sqlreference/dml");
}
if (recognizer != null && recognizer.isSqlSyntaxSupports()) {
if (recognizers == null) {
recognizers = new ArrayList<>();
}
recognizers.add(recognizer);
}
}
return recognizers;
}
/**
* Check if the exception is a Druid ParserException
* Use class name comparison to avoid directly referencing the ParserException class
*/
private boolean isParserException(Throwable e) {
if (e == null) {
return false;
}
String className = e.getClass().getName();
return "com.alibaba.druid.sql.parser.ParserException".equals(className);
}
}
总结❤️
如果你看了这篇文章有收获可以点赞+关注+收藏🤩,这是对笔者更新的最大鼓励!如果你有更多方案或者文章中有错漏之处,请在评论区提出帮助笔者勘误,祝你拿到更好的offer!