关于Seata的一个小issue...😯

引言

某一天,笔者在逛着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 自定义异常

这样做的好处是:

  1. 异常处理统一 - 所有不支持的SQL语法都抛出 NotSupportYetException

  2. 测试逻辑一致 - 所有数据库类型使用相同的测试断言

  3. 兼容性保持 - DruidIsolationTest 等其他测试正常运行

  4. 用户体验改善 - 提供统一、友好的错误信息

最终,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!

相关推荐
葫芦和十三6 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp6 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑7 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯8 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan10 小时前
多Agent之间的区别
后端
青石路11 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充12 小时前
1.面向对象设计思想
后端
IT_陈寒12 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro13 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗13 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端