首页 > 其他分享 >Kotlin的语法糖太甜啦——开发MC插件遇到的坑

Kotlin的语法糖太甜啦——开发MC插件遇到的坑

时间:2022-10-30 17:36:17浏览次数:78  
标签:插件 java MC 糖太甜 Long Class return type class

最近在学习使用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

相关文章

  • Tomcat
    Tomcat服务器是一个免费的开放源代码的web应用服务器,属于轻量级应用服务器。是应于运行、开发Servlet技术的首选服务器。Tomcat目录结构1.bin目录:存放tomcat的命令,主要有......
  • Vscode插件推荐
    Vscode插件推荐主题/外观MaterialTheme(集成了多种主题皮肤,搭配MaterialIconTheme食用更佳)MaterialIconTheme(扁平化的主题图标库)vscode-icons(VSCode官方出......
  • 洛谷 P1789【Mc生存】插火把
    萌新写的代码,长但模块化#include<stdio.h>#defineROW100#defineCOLUMN100intmap[ROW][COLUMN];/*函数测试数据={1,1,1,0,0,1,1,0,1,0,1,1,1,0,0,0,0,0,0,0......
  • Chrome扩展插件的开发--获取网页Cookies
    Chrome扩展插件的开发--获取网页CookiesChrome浏览器在浏览器类应用软件中一直居于榜首,很多人选择Chrome浏览器不仅仅是因为它的稳定,还有它丰富的可拓展性。那么有没有想......
  • 同一个tomcat 共享session
    问题记录 最近学整理java项目session常常用来存储一些公共信息供不同页面访问,比如用户登录信息。访问同一个tomcat下的不同项目所创建的session是不一样的。自然地面临了如......
  • Tomcat目录介绍,常见的启动失败
    目录介绍:bin      专门用来存放Tomcat服务器的可执行程序conf     专门用来存放Tomcat服务器的配置文件lib       专门用来存放To......
  • Java安全之Tomcat6 Filter内存马
    Java安全之Tomcat6Filter内存马回顾Tomcat8打法先回顾下之前Tomcat789的打法这里先抛开78之间的区别,在8中,最后add到filterchain的都是一个filterconfig对象Applica......
  • 强制关闭Tomcat
    背景由于应用项目及其复杂,应用自己开启的线程在tomcat停止时没有关闭,便会导致tomcat进程没有终止。但是tomcat监听的端口却已经释放,因为执行shutdown.sh脚本时,tomcat自身......
  • React实用插件收集
    1、react-img-editor图片编辑demo:npminstallreact-img-editor-S引入和使用importReactImgEditorfrom'react-img-editor'import'react-img-editor/assets/in......
  • 米联客FMC-3G SDI视频拓展子卡全新上市!
    FMC-3GSDI子卡测试1.1概述使用FMC-3GSDI子卡来实现FPGA通过GTH高速收发器从同轴电缆接收3G-SDI信号,并通过FIFO回环导出SDI信号到同轴电缆输出。1.2外......