背景: 有一个 Infinispan (13.0.10) 缓存,它使用自定义数据类型
Quad<String, Long, Type, ValidityPeriod>
作为键。据我所知,所有 hashCode()
和 equals(Object o)
方法要么是自动生成的,要么是正确实现的。
缓存也用于本地模式。
问题: 当我尝试使用我知道缓存包含的 Quad 来调用
cache.get(Object key)
时,我得到 null
。 cache.containsKey(Object key)
也返回 false
。
这怎么可能?
澄清: 我说我知道缓存包含
Quad
键,因为我已经使用 Eclipse IDE 调试器完成了以下操作:
cache.entrySet().stream().filter(e -> e.getKey().hashCode == Quad.of(a, b, c, d).hashCode()).collect(toList());
// this returns a non-empty list with my expected return
cache.entrySet().stream().filter(e -> e.getKey().equals(Quad.of(a, b, c, d))).collect(toList());
// this returns a non-empty list with my expected return
根据 Radim Vansa 的建议,我还尝试了以下方法:
cache.entrySet().stream().collect(Collectors.toMap(Entry::getKey, Entry::getValue, (o1,o2) -> o1, ConcurrentHashMap::new)).get(Quad.of(a, b, c, d));
// this returns my expected return
更多背景信息(如果需要): 我正在开发一个较旧的项目,我应该将其从 Infinispan 版本 10 更新到 13。我已经成功地做到了这一点,并且还集成了 ProtoStream API,而不是使用旧的 JBossMarshaller。此版本更改很重要,因为更新后检索停止工作。
有多个缓存正在使用,有些缓存使用自定义数据类型进行索引,例如
Pair<K, V>
、Triple<L, M, R>
和 Quad<A, B, C, D>
,这些都是通用的。我已经为它们编写了一些 ProtoAdpaters,它们工作得很好。
我现在遇到了一个问题,其中有一个 AdvancedCache 使用这样的 Quad 作为键:
Quad<String, Long, Type, ValidityPeriod>
Quad 覆盖
equals(Object o)
和 hashCode
,如下所示:
public class Quad<A, B, C, D> {
// other code omitted for brevity
public boolean equals(final Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof Quad<?, ?, ?, ?>) {
final Quad<?, ?, ?, ?> other = (Quad<?, ?, ?, ?>) obj;
return Objects.equals(getFirst(), other.getFirst())
&& Objects.equals(getSecond(), other.getSecond())
&& Objects.equals(getThird(), other.getThird())
&& Objects.equals(getFourth(), other.getFourth());
}
return false;
}
public int hashCode() {
return Objects.hashCode(getFirst())
^ Objects.hashCode(getSecond())
^ Objects.hashCode(getThird())
^ Objects.hashCode(getFourth());
}
}
仅供参考
Type
的结构如下:
public class Type implements Serializable {
private int fieldA;
private int fieldB;
private String fieldC;
// hashCode and equals are auto-generated
// constructor, getters and setters omitted for brevity
}
ValidityPeriod
是这样的:
public class ValidityPeriod implements Serializable {
private LocalDate validFrom;
private LocalDate invalidFrom;
// hashCode and equals are auto-generated
// constructor, getters and setters omitted for brevity
}
LocalDate
的编组器使用以下适配器:
@ProtoAdapter(LocalDate.class)
public class LocalDateAdapter {
@ProtoFactory
LocalDate create(int year, short month, short day) {
return LocalDate.of(year, month, month);
}
@ProtoField(number = 1, required = true)
int getYear(LocalDate localDate) {
return localDate.getYear();
}
@ProtoField(number = 2, required = true)
short getMonth(LocalDate localDate) {
return (short) localDate.getMonth().getValue();
}
@ProtoField(number = 3, required = true)
short getDay(LocalDate localDate) {
return (short) localDate.getDayOfMonth();
}
}
我尝试使用调试器来了解 Infinispan 的内部工作原理,但我似乎无法确定产生此错误的具体线路。 据我所知,这与
CacheImpl.get(Object, long, InvocationContext)
有关。
更新: 好的,根据 Pruivo 的建议,我尝试创建一个 reprex。然而奇怪的是,我试图复制从缓存检索对象之前发生的每个过程,但在我创建的 reprex 中它可以工作。 然而有趣的是我尝试了以下内容: 我在
ValidityPeriod
中创建了两个方法,它们执行以下操作(几乎像 Infinispan Transformers):
public String toFormString() {
return String.format("%s§%s", validFrom, invalidFrom);
}
public static ValidityPeriod fromFormString(String form) {
String[] split = form.split("§");
return from(LocalDate.parse(split[0]),LocalDate.parse(split[1]));
}
然后,我将 Quad 更改为
Quad<String, Long, Type, String>
,同时使用这些方法中的字符串而不是 ValidityPeriod 本身构建缓存的密钥。奇怪的是,这解决了我原来的问题。
然而,由于这是一个肮脏的修复,我不满足于永久保留此解决方案。我猜一定是ValidityPeriod
出了问题。
我仍然很困惑为什么缓存返回的内容与它包含的内容不同,所以我仍然会保留我原来的问题。
按照 Pruivo 的建议仔细推论后,我可以确定这个错误的原因。它是为
ProtoAdapter
编写的 LocalDate
,其中在反序列化时创建了不正确的 LocalDate
对象。
具体修复: 这种情况的具体修复如下:
@ProtoAdapter(LocalDate.class)
public class LocalDateAdapter {
@ProtoFactory
LocalDate create(int year, short month, short day) {
return LocalDate.of(year, month, day); // <= here was the error
}
@ProtoField(number = 1, required = true)
int getYear(LocalDate localDate) {
return localDate.getYear();
}
@ProtoField(number = 2, required = true)
short getMonth(LocalDate localDate) {
return (short) localDate.getMonth().getValue();
}
@ProtoField(number = 3, required = true)
short getDay(LocalDate localDate) {
return (short) localDate.getDayOfMonth();
}
}
说明: 正如 Pruivo 所说,Infinispan 使用
hashCode()
来确定密钥的所有者/段。在后台,Infinispan 使用 ConcurrentHashMap
来存储键和值。如果缓存具有相应的配置参数集,则此映射包含序列化对象。
现在来说说问题。
ConcurrentHashMap
包含调用以下方法来检索对象:
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) { // <= line 5
// this code here is not reached
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
}
return null;
}
正如您在第 5 行中看到的,有一个名为
tabAt()
的方法的检查。这验证了地图该地址处的对象引用不是null
。如果是 null
,则不采取进一步操作,并且将返回 null
。这包括不使用对象的哈希值或 equals
进行比较。您可以在 tabAt()
之后的行中看到这一点。这可以解释为什么 cache.entrySet().stream()...
可以检索对象而 get()
却不能。
现在我有根据地猜测为什么找不到对象的引用,因此是
null
:我猜原始密钥是正确序列化的对象的密钥,但在反序列化后,该密钥变得损坏,因为它通过使用错误的对象更改了值编组员。由于修改 Map
中的键可能会产生意外的行为,这可以解释奇怪的行为。 (修改密钥时 Map
意外行为的来源:https://stackoverflow.com/a/21601013/14945497)
建议/备注: 我想说的最后一点是,这个错误可能非常不明显,但会导致其他问题,因为反序列化时会创建错误的对象。这会造成很多与奇怪行为的混淆,并且需要做大量的侦探工作来消除它。考虑到这一点,我的建议是对所有编组器的定义进行双重检查。