obabichevjb opened a new issue, #16489: URL: https://github.com/apache/dubbo/issues/16489
### Pre-check - [x] I am sure that all the content I provide is in English. ### Search before asking - [x] I had searched in the [issues](https://github.com/apache/dubbo/issues?q=is%3Aissue) and found no similar issues. ### Apache Dubbo Component Java SDK (apache/dubbo) ### Dubbo Version Dubbo Java 3.3.6, OpenJDK 21 (Amazon Corretto 21.0.5), macOS 15.6 (OS-independent) Kotlin 2.4.20, Gradle 9.5, default hessian2 serialization ### Steps to reproduce this issue A Kotlin project with one Dubbo dependency. Provider and consumer run in the same JVM over a real Netty `dubbo://` transport with a direct URL and `RegistryConfig("N/A")`, so no registry or other infrastructure is needed — and because the transport is real rather than `injvm://`, values are genuinely serialized. `build.gradle.kts`: ```kotlin plugins { kotlin("jvm") version "2.4.20" application } repositories { mavenCentral() } dependencies { implementation("org.apache.dubbo:dubbo:3.3.6") implementation("io.netty:netty-all:4.1.118.Final") implementation("org.javassist:javassist:3.30.2-GA") implementation("org.slf4j:slf4j-simple:2.0.13") } kotlin { jvmToolchain(21) } application { mainClass = "repro.MainKt" } ``` `src/main/resources/security/serialize.allowlist`: ``` repro ``` `src/main/kotlin/Main.kt`: ```kotlin package repro import java.io.Serializable import org.apache.dubbo.config.* import org.apache.dubbo.config.bootstrap.DubboBootstrap sealed interface Reply : Serializable data class Found(val text: String) : Reply object NotFoundObject : Reply // equals() is inherited identity comparison data object NotFoundDataObject : Reply // compiler generates equals() enum class NotFoundEnum : Reply { INSTANCE } interface ReplyService { fun roundTrip(reply: Reply): Reply // returns its argument unchanged } class ReplyServiceImpl : ReplyService { override fun roundTrip(reply: Reply): Reply = reply } /** The dispatch a consumer actually writes. */ private fun describe(reply: Reply): String = when (reply) { NotFoundObject -> "matched NotFoundObject" NotFoundDataObject -> "matched NotFoundDataObject" NotFoundEnum.INSTANCE -> "matched NotFoundEnum.INSTANCE" is Found -> "matched Found(${reply.text})" else -> ">>> FELL THROUGH TO else <<<" } private fun report(label: String, sent: Reply, back: Reply) = println("%-12s | ===:%-5s | ==:%-5s | %s".format(label, back === sent, back == sent, describe(back))) fun main() { DubboBootstrap.getInstance() .application(ApplicationConfig("repro")) .registry(RegistryConfig("N/A")) .protocol(ProtocolConfig("dubbo", 20881)) .service(ServiceConfig<ReplyService>().apply { setInterface(ReplyService::class.java) setRef(ReplyServiceImpl()) }) .start() val svc = ReferenceConfig<ReplyService>().apply { setInterface(ReplyService::class.java) url = "dubbo://127.0.0.1:20881" }.get() report("object", NotFoundObject, svc.roundTrip(NotFoundObject)) report("data object", NotFoundDataObject, svc.roundTrip(NotFoundDataObject)) report("enum", NotFoundEnum.INSTANCE, svc.roundTrip(NotFoundEnum.INSTANCE)) report("data class", Found("hi"), svc.roundTrip(Found("hi"))) DubboBootstrap.getInstance().destroy() } ``` `./gradlew run` prints: ``` object | ===:false | ==:false | >>> FELL THROUGH TO else <<< data object | ===:false | ==:true | matched NotFoundDataObject enum | ===:true | ==:true | matched NotFoundEnum.INSTANCE data class | ===:false | ==:true | matched Found(hi) ``` ### What you expected to happen **Expected:** `NotFoundObject` should arrive at the consumer as the singleton, so the `NotFoundObject ->` branch runs — the same way an enum constant survives today. **Actual:** it arrives as a *different instance of the same class*, the `when` matches nothing, and control reaches `else`. No exception, no warning, no log line — only the wrong code path. Kotlin's `object` declares a singleton: the compiler emits a final class with a **private** constructor and a single `public static final INSTANCE`, and the language guarantees no other instance exists. The default bean deserializers do not honour that — they allocate a fresh instance, bypassing the private constructor, and populate its fields. So Dubbo produces an instance the language says cannot exist. Because a plain `object` inherits `equals` from `Any` (identity comparison), losing identity also loses equality, and Kotlin's `when` compares with `==`. That is why dispatch silently changes. Hessian2 already solves exactly this problem for the Java equivalent. `EnumDeserializer` writes the constant's name and resolves the real JVM constant on the way back, which is why the `enum` row above is intact including `===`. A Kotlin `object` is just a class with a static `INSTANCE` and gets no such treatment. ### Anything else _No response_ ### Do you have a (mini) reproduction demo? - [ ] Yes, I have a minimal reproduction demo to help resolve this issue more effectively! ### Are you willing to submit a pull request to fix on your own? - [x] Yes I am willing to submit a pull request on my own! ### Code of Conduct - [x] I agree to follow this project's [Code of Conduct](https://www.apache.org/foundation/policies/conduct) -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
