最近在学习使用Kotlin开发PaperMC插件,遇到了一个大坑,不吐不快。
PersistentDataType<T, Z> 接口
我们可以给物品或方块添加自定义的BNT标签,而这个接口定义的自定义标签的数据类型,泛型T表示标签中所存储的数据的类型,泛型Z表示读取后转化为的数据类型。
比如我希望添加一个标签记录最近更新标签的时间,以Long型时间戳的方式存储,可以创建一个类实现该接口,有如下代码:
object DatetimeTagType : PersistentDataType<Long, ZonedDateTime> {
override fun getPrimitiveType(): Class<Long> {
return Long::class.java
}
override fun getComplexType(): Class<ZonedDateTime> {
return ZonedDateTime::class.java
}
override fun toPrimitive(complex: ZonedDateTime, context: PersistentDataAdapterContext): Long {
return complex.toInstant().toEpochMilli()
}
override fun fromPrimitive(primitive: Long, context: PersistentDataAdapterContext): ZonedDateTime {
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(primitive), ZoneId.systemDefault())
}
}
然后通过下面的代码向主手的物品存储一条自定义标签信息:
val timeNamespace = NamespacedKey(HelloPaper.getInstance(), "updateTime")
player.inventory.itemInMainHand.itemMeta.persistentDataContainer.set(
timeNamespace,//命名空间
DatetimeTagType,//自定义NBT数据类型
ZonedDateTime.now()//当前时间
)
向物品存储自定义的时间信息时,底层会调用toPrimitive
方法,将ZonedDateTime
转换为Long类型存储起来。
读取数据也很简单:
player.inventory.itemInMainHand.itemMeta.persistentDataContainer.get(
timeNamespace,//命名空间
DatetimeTagType//自定义NBT数据类型
)
底层通过调用fromPrimitive
方法将Long型数据转换为ZonedDateTime
。
现在来测试一下存储数据。一切正常:
那么读取数据呢?
Caused by: java.lang.IllegalArgumentException: The found object is of the type Long. Expected type long
at org.bukkit.craftbukkit.v1_19_R1.persistence.CraftPersistentDataTypeRegistry.extract(CraftPersistentDataTypeRegistry.java:259) ~[paper-1.19.2.jar:git-Paper-237]
...
哦吼,完蛋!居然报错了。
错误分析
翻译一下报错信息:object的类型是Long,期望的是long
这就很奇怪了。这里的object应该就是存储的时间戳,它确实是Long类型,但是插件不知道为什么偏偏需要long类型。我尝试将Kotlin改为Java,居然正常了!
好吧,不知道Kotlin搞了什么鬼。观察DatetimeTagType
的代码,fromPrimitive
函数需要Long类型的参数,看上去没有问题。既然报错发生在 paper.jar 中,那我就尝试找找源代码。通过Google搜索CraftPersistentDataTypeRegistry
类,果然让我找到了Bukkit的一些源码。根据异常抛出的类与行号,我找到了异常发生的位置。
org/bukkit/craftbukkit/persistence/CraftPersistentDataTypeRegistry.java
public <T> T extract(Class<T> type, NBTBase tag) throws ClassCastException, IllegalArgumentException {
TagAdapter adapter = this.adapters.computeIfAbsent(type, CREATE_ADAPTER);
if (!adapter.isInstance(tag)) {
throw new IllegalArgumentException(String.format("`The found tag instance cannot store %s as it is a %s", type.getSimpleName(), tag.getClass().getSimpleName()));
}
Object foundValue = adapter.extract(tag);
if (!type.isInstance(foundValue)) {
throw new IllegalArgumentException(String.format("The found object is of the type %s. Expected type %s", foundValue.getClass().getSimpleName(), type.getSimpleName()));
}//foundValue是Long,type是long
return type.cast(foundValue);
}
foundValue
就是从NBT标签中取得的Long
型时间戳,此处是Object
类型,需要转为实际类型。type
应当是包装类型Long
的class,实际却是基本类型long
的class。因为任何对象都不可能是基本类型的实例,因此type.isInstance(foundValue)
判断为false
,进而引发异常。
那么这个type又是从哪来的呢?
org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java
@Override
public <T, Z> Z get(NamespacedKey key, PersistentDataType<T, Z> type) {
Validate.notNull(key, "The provided key for the custom value was null");
Validate.notNull(type, "The provided type for the custom value was null");
NBTBase value = this.customDataTags.get(key.toString());
if (value == null) {
return null;
}
return type.fromPrimitive(registry.extract(type.getPrimitiveType(), value), adapterContext);
}
从该方法的最后一行调用了extract
方法,extract()
第一个参数通过type.getPrimitiveType()
得到。由此推断出,我所编写的getPrimitiveType
函数,返回了long.class
而非java.lang.Long.class
。不用想,这肯定是Kotlin干的好事。谁能想到
return Long::class.java
返回的是基本类型long
的class对象而非包装类型Long
的class对象。我打开Kotlin字节码分析工具,然后反编译为java,看到这行代码被编译成这样:
return Long.TYPE;
那么这个Long.TYPE
又是什么呢?查看java的源码:
/**
* The {@code Class} instance representing the primitive type
* {@code long}.
*
* @since 1.1
*/
@SuppressWarnings("unchecked")
public static final Class<Long> TYPE = (Class<Long>) Class.getPrimitiveClass("long");
原来Long.TYPE
就是基本类型long的class对象。可是Kotlin为什么就这么做?
问题解决
根据Kotlin的中文文档,Java 的原生类型映射到了相应的 Kotlin 类型。我在Kotlin中使用非空的Long类型,也就是kotlin.Long
,实际上对应Java中的基本类型long
;而可空的kotlin.Long?
才对应java.lang.Long
。
- 那我将
DatetimeTagType
的第一个泛型参数改为Long?
是否可行呢?答案是不行(也可能是我不知道)!这样的话getPrimitiveType()
返回类型变成了Class<Long?>
,我不知道如何返回这样一个类型。还像之前那样return Long::class.java
的话,会提示类型不匹配,因为Class<Long>
不是Class<Long?>
。 - 如果写成
return Long?::class.java
呢?似乎没有这种写法,编译器还是报错(初学Kotlin,还不能完全理解所有的语法)。 - 如果第一个泛型参数明确写为
java.lang.Long
呢?这样勉强可以解决,只需要在特定的位置将kotlin.Lang
强转为java.lang.Long
或者将java.lang.Long
强转为kotlin.Lang
即可。但这样写出来的代码比较丑陋,会变成下面这幅模样:
object DatetimeTagType : PersistentDataType<java.lang.Long, ZonedDateTime> {
override fun getPrimitiveType(): Class<Long> {
return java.lang.Long::class.java
}
override fun getComplexType(): Class<ZonedDateTime> {
return ZonedDateTime::class.java
}
override fun toPrimitive(complex: ZonedDateTime, context: PersistentDataAdapterContext): Long {
return complex.toInstant().toEpochMilli() as Long
}
override fun fromPrimitive(primitive: Long, context: PersistentDataAdapterContext): ZonedDateTime {
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(primitive as kotlin.Long), ZoneId.systemDefault())
}
}
有没有更优雅的方法?回过头来看,我只要让getPrimitiveType()
函数返回JVM平台类型java.lang.Long.class
就可以了,于是我手动指定返回java.lang.Long::class.java
,这样返回类型不匹配,我在进行一次强转,最终该方法变成这样:
@Suppress("UNCHECKED_CAST")
override fun getPrimitiveType(): Class<Long> {
return java.lang.Long::class.java as Class<Long>
}
重新编译运行,终于正常了!
被这样一个小问题折磨了很久。你根本想象不到Kotlin究竟会怎样编译你的代码。
标签:插件,java,MC,糖太甜,Long,Class,return,type,class From: https://www.cnblogs.com/Fortern/p/16841383.html