Loading... ## 背景 近日笔者在为 Apache Doris 支持字典表功能,主要专注于 FE 的管理模块和优化器相关的查询计划生成。期间有一个 bug 由于笔者对 GSON 的机制毫无了解,花费很长时间未解决,遂尝试使用 Windsurf(调用模型为 Sonnet)进行分析。结果令笔者大为震撼,现记录如下,或许对当前大模型在大型 codebase 下能力边界的认知,有一定帮助。 ## 问题 这里有一个核心类 `Dictionary`,它继承自 `Table`,who has 成员 `rwLock`。这个 `Dictionary` 和基类在现版本都是通过 GSON 序列化完成持久化的。也就是为名字添加了 `@SerializedName` 注解,然后利用GsonUtils 完成 read 和 write 对象。 基类 `Table` 中, `rwLock` 对象在每一个构造函数中都进行了初始化。然而问题来了,我的 FE 在重启之后,Dictionary 对象通过基类拥有的 `rwLock` 却总是失效的。也就是说序列化与反序列化出了问题。 ```java public class Dictionary extends Table { @SerializedName(value = "dbName") private final String dbName; @SerializedName(value = "sourceCtlName") private final String sourceCtlName; @SerializedName(value = "sourceDbName") private final String sourceDbName; @SerializedName(value = "sourceTableName") private final String sourceTableName; ...... @Override public void write(DataOutput out) throws IOException { String json = GsonUtils.GSON.toJson(this); Text.writeString(out, json); } public static Dictionary read(DataInput in) throws IOException { String json = Text.readString(in); return GsonUtils.GSON.fromJson(json, Dictionary.class); } } public abstract class Table extends MetaObject implements Writable, TableIf, GsonPostProcessable { protected MonitoredReentrantReadWriteLock rwLock; public Table(TableType type) { this.rwLock = new MonitoredReentrantReadWriteLock(true); ... } public Table(long id, String tableName, TableType type, List<Column> fullSchema) { this.rwLock = new MonitoredReentrantReadWriteLock(true); ... } ...... } ``` ## Sonnet Works 接下来通过多轮问答,Sonnet 阅读代码库并不断产生猜测,并由我来验证。让我们看看它做得如何。 ### Question 1 我的 `@Dictionary` 继承自 `@Table.java` ,在Table的每一个构造函数中rwLock都会被赋值,但是我在将一个Dictionary作为TableIf执行.readLock()却报错rwLock为null,请浏览上下文以及任何可能的有关文件,帮我尝试找到这是为什么 ### Answer 1 让我帮你调查这个问题。首先,我需要查看Dictionary类和Table类的实现来了解继承关系和rwLock的初始化情况。 首先让我查看Dictionary类: > Analyzed code item Dictionary.java:Dictionary 现在让我查看Table类: > Analyzed Table.java:(1-101) 让我继续查看Table类的其他部分: > Analyzed Table.java:(101-201) 从代码中我发现了一些重要信息: 1. Table类中的rwLock是在构造函数中初始化的,有两个构造函数: - Table(TableType type) - Table(long id, String tableName, TableType type, List fullSchema) 2. 在这两个构造函数中都有初始化rwLock的代码: ```java this.rwLock = new MonitoredReentrantReadWriteLock(true); ``` 3. 从Dictionary类的代码中可以看到,它确实继承自Table类,并且在其构造函数中调用了父类的构造函数: ```java public Dictionary(CreateDictionaryInfo info, long uniqueId) { super(uniqueId, info.getDictName(), TableType.DICTIONARY, info.getColumns().stream() .map(DictionaryColumnDefinition::getOriginColumn).collect(Collectors.toList())); // ... } ``` 让我查看一下Dictionary是如何被序列化和反序列化的,因为这可能是导致rwLock为null的原因: > Searched in fe/fe-core/src/main/java/org/apache/doris/dictionary 1 result 我发现问题了。在Dictionary类中,我看到它使用了GSON进行序列化和反序列化: ```java @Override public void write(DataOutput out) throws IOException { String json = GsonUtils.GSON.toJson(this); Text.writeString(out, json); } public static Dictionary read(DataInput in) throws IOException { String json = Text.readString(in); return GsonUtils.GSON.fromJson(json, Dictionary.class); } ``` 这就是问题所在: 1. rwLock是一个非序列化字段(没有@SerializedName注解),这意味着它在序列化时会被忽略。 2. 当使用GSON反序列化时,它会创建一个新的Dictionary实例,但不会调用构造函数,而是直接设置那些带有@SerializedName注解的字段。 3. 由于rwLock没有被序列化,且在反序列化时没有调用构造函数,所以rwLock保持为null。 解决方案有两个: 1. 在Dictionary类中实现GsonPostProcessable接口,并在gsonPostProcess方法中初始化rwLock: ```java @Override public void gsonPostProcess() { super.gsonPostProcess(); this.rwLock = new MonitoredReentrantReadWriteLock(true); } ``` 2. 或者修改序列化/反序列化逻辑,不使用GSON,而是手动序列化所需的字段,并在反序列化时调用适当的构造函数。 我建议采用第一种方案,因为它更简单且不需要改变现有的序列化逻辑。这样在GSON反序列化后,会自动调用gsonPostProcess方法来初始化rwLock。 你想要我帮你实现哪种解决方案? ### Question 2 但是看起来 @OlapTable.java 和 @Table.java 的gsonPostProcess也并没有对rwLock进行处理啊?为什么就没问题呢 ### Answer 2 让我检查一下Table和OlapTable的序列化和反序列化逻辑: > Analyzed Table.java:(501-601) 让我看看Table的read方法: > Analyzed Table.java:(401-501) 我明白了问题所在! 在Table类中,有两种反序列化的方式: 1. 在版本136之前(Env.getCurrentEnvJournalVersion() < FeMetaVersion.VERSION_136),使用readFields方法,这个方法会创建一个新的Table实例并初始化rwLock。 2. 在版本136及之后,使用GSON反序列化: ```java return GsonUtils.GSON.fromJson(Text.readString(in), Table.class); ``` 而Dictionary类覆盖了read和write方法,总是使用GSON进行序列化和反序列化: ```java @Override public void write(DataOutput out) throws IOException { String json = GsonUtils.GSON.toJson(this); Text.writeString(out, json); } public static Dictionary read(DataInput in) throws IOException { String json = Text.readString(in); return GsonUtils.GSON.fromJson(json, Dictionary.class); } ``` 这就导致了Dictionary总是使用GSON反序列化,而GSON反序列化不会调用构造函数,所以rwLock没有被初始化。 解决方案: 1. Dictionary类应该遵循和Table类相同的版本控制逻辑,在版本136之前使用readFields,之后才使用GSON。 2. 或者在Dictionary的gsonPostProcess方法中初始化rwLock: ```java @Override public void gsonPostProcess() throws IOException { super.gsonPostProcess(); if (this.rwLock == null) { this.rwLock = new MonitoredReentrantReadWriteLock(true); } if (this.commitLock == null) { this.commitLock = new MonitoredReentrantLock(true); } } ``` 我建议使用第一种方案,让Dictionary遵循和Table相同的版本控制逻辑,这样可以保持代码的一致性。你觉得哪种方案更好? ### Question 3 但是我出现问题的version超过了VERSION_136,是在现在最新的版本上发生的,这个推论对吗? ### Answer 3 你说得对,让我重新检查一下。如果是在VERSION_136之后,那么Table类确实也是使用GSON进行反序列化的: ```java } else { // VERSION_136及以后 return GsonUtils.GSON.fromJson(Text.readString(in), Table.class); } ``` 这意味着Table类在新版本中也是通过GSON反序列化的,它应该也会遇到同样的rwLock为null的问题才对。让我检查一下Table类是如何处理这个问题的。 > Searched in fe/fe-core/src/main/java/org/apache/doris/catalog 22 results 我注意到Table类和一些子类(如OlapTable、View、EsTable等)都实现了GsonPostProcessable接口。让我看看这些类是如何处理反序列化后的初始化的: > Analyzed Table.java:(1-101) 让我看看Table类的gsonPostProcess方法: > Analyzed code item Table.java:gsonPostProcess 啊,我发现问题了!你说得对。在VERSION_136之后,Table类确实也是使用GSON反序列化的,但是它的gsonPostProcess方法并没有初始化rwLock。这意味着rwLock在反序列化后应该也是null才对。 让我看看其他表类型是如何处理这个问题的: > Analyzed code item OlapTable.java:gsonPostProcess 这很奇怪。OlapTable和其他表类型也没有在gsonPostProcess中初始化rwLock,但它们似乎都能正常工作。 让我再检查一下Table类的构造函数是否在某个地方被调用: > Searched in fe/fe-core/src/main/java/org/apache/doris/catalog 36 results 我明白问题了。我之前的推论是错误的。在VERSION_136之后,Table类确实使用GSON反序列化,但是GSON的反序列化机制会调用类的无参构造函数。这意味着: 1. 当使用GSON反序列化时,它会先创建一个对象实例(调用无参构造函数),然后再设置序列化的字段。 2. Table类是一个抽象类,它没有无参构造函数,但它的子类必须有。 3. Dictionary类继承自Table,但它没有提供无参构造函数,这就是问题所在! 解决方案: 在Dictionary类中添加一个无参构造函数: ```java public Dictionary() { super(TableType.DICTIONARY); } ``` 这样,当GSON反序列化Dictionary时,会先调用这个无参构造函数,这个构造函数会调用Table的构造函数,从而正确初始化rwLock。 这也解释了为什么其他表类型(如OlapTable)能正常工作 - 它们都有无参构造函数。 你想要我帮你实现这个解决方案吗? ## 总结 Java 对于 cppboy 来说确实过于 magic 了,它需要无参构造函数,而当我不提供的时候并不是报错,而是提供非预期的行为。我想这也是所谓”面向约定编程“的一部分吧X...Xb,可惜我对 GSON 的约定一无所知,but Sonnet does! 我觉得让我比较惊讶的是,在 > 啊,我发现问题了!你说得对。在VERSION_136之后,Table类确实也是使用GSON反序列化的,但是它的gsonPostProcess方法并没有初始化rwLock。这意味着rwLock在反序列化后应该也是null才对。 这段上下文附近。Sonnet的确在尝试验证自己的猜想,并且在猜想与其他背景不符的时候进行反思,而不是出现”幻觉“。这证明目前的这套工具链(Windsurf/Cursor + Sonnet/4o)在独立解决问题方面,也许的确行得通。 © 允许规范转载 打赏 赞赏作者 赞 如果觉得我的文章对你有用,请随意赞赏