Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions dubbo-serialization/dubbo-serialization-hessian2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ limitations under the License.
<artifactId>log4j-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
<!-- Test only: supplies the kotlin.Metadata annotation so a Kotlin `object` can be
represented by a Java fixture without adding a Kotlin compiler to the build. -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>2.4.20</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-native</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@

import java.io.InputStream;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

import com.alibaba.com.caucho.hessian.io.Deserializer;
import com.alibaba.com.caucho.hessian.io.InputStreamDeserializer;
Expand All @@ -34,6 +37,9 @@

public class Hessian2SerializerFactory extends SerializerFactory {

private static final String KOTLIN_METADATA_ANNOTATION = "kotlin.Metadata";
private static final String KOTLIN_OBJECT_INSTANCE_FIELD = "INSTANCE";

private final DefaultSerializeClassChecker defaultSerializeClassChecker;

public Hessian2SerializerFactory(
Expand Down Expand Up @@ -80,6 +86,11 @@ protected Deserializer getDefaultDeserializer(Class cl) {

checkSerializable(cl);

Object kotlinObject = kotlinObjectInstance(cl);
if (kotlinObject != null) {
return new KotlinObjectDeserializer(cl, kotlinObject);
}

if (RecordUtil.isRecord(cl)) {
return new RecordDeserializer(cl, getFieldDeserializerFactory());
} else {
Expand All @@ -89,6 +100,45 @@ protected Deserializer getDefaultDeserializer(Class cl) {
}
}

/**
* Returns the singleton held by a Kotlin {@code object} declaration, or {@code null} if this is
* not one. A Kotlin {@code object} compiles to a final class with a private constructor and a
* {@code public static final INSTANCE} field of its own type.
*
* <p>The {@code kotlin.Metadata} annotation is matched by name so that Dubbo needs no
* dependency on kotlin-stdlib. Requiring it also keeps the behaviour change scoped to Kotlin:
* a hand-written Java singleton with the same shape continues to deserialize as before.
*/
private static Object kotlinObjectInstance(Class<?> cl) {
if (!isKotlinClass(cl)) {
return null;
}
try {
Field instance = cl.getDeclaredField(KOTLIN_OBJECT_INSTANCE_FIELD);
int modifiers = instance.getModifiers();
if (!Modifier.isStatic(modifiers) || !Modifier.isFinal(modifiers) || instance.getType() != cl) {
return null;
}
return instance.get(null);
} catch (NoSuchFieldException | IllegalAccessException | RuntimeException e) {
return null;
}
}

private static boolean isKotlinClass(Class<?> cl) {
try {
for (Annotation annotation : cl.getAnnotations()) {
if (KOTLIN_METADATA_ANNOTATION.equals(
annotation.annotationType().getName())) {
return true;
}
}
} catch (RuntimeException e) {
// Annotations that cannot be resolved are not Kotlin metadata.
}
return false;
}

private void checkSerializable(Class<?> cl) {
// If class is Serializable => ok
// If class has not implement Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.dubbo.common.serialize.hessian2;

import java.io.IOException;

import com.alibaba.com.caucho.hessian.io.AbstractDeserializer;
import com.alibaba.com.caucho.hessian.io.AbstractHessianInput;

/**
* Deserializer for a Kotlin {@code object} (and {@code data object}) declaration.
*
* <p>Kotlin guarantees that such a declaration has exactly one instance per class loader, exposed as
* a {@code public static final INSTANCE} field, and gives it a private constructor so that no other
* instance can be created. The default bean deserializers do not honour that: they allocate a fresh
* instance (bypassing the private constructor) and populate its fields, so a value that crosses the
* wire is no longer reference-equal to the singleton. For a plain {@code object}, whose
* {@code equals} is inherited identity comparison, that also breaks {@code ==} and therefore
* silently changes which branch a Kotlin {@code when} selects.
*
* <p>This deserializer resolves the declared singleton instead, mirroring how
* {@code EnumDeserializer} resolves an enum constant by name: the serialized field data is read from
* the stream so that the stream position stays correct, then discarded, and the singleton is
* returned. Discarding any transmitted state is deliberate and matches enum behaviour — the identity
* guarantee is the property being preserved.
*/
public class KotlinObjectDeserializer extends AbstractDeserializer {

private final Class<?> type;
private final Object instance;

public KotlinObjectDeserializer(Class<?> type, Object instance) {
this.type = type;
this.instance = instance;
}

@Override
public Class<?> getType() {
return type;
}

@Override
public Object readMap(AbstractHessianInput in) throws IOException {
while (!in.isEnd()) {
// Read and discard: the singleton's state is authoritative, as for an enum constant.
in.readObject();
in.readObject();
}
in.readMapEnd();
in.addRef(instance);
return instance;
}

@Override
public Object readObject(AbstractHessianInput in, Object[] fields) throws IOException {
String[] fieldNames = (String[]) fields;
for (int i = 0; i < fieldNames.length; i++) {
in.readObject();
}
in.addRef(instance);
return instance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.dubbo.common.serialize.hessian2;

import java.io.Serializable;

/**
* Same shape as {@link KotlinStyleObject} but without {@code kotlin.Metadata}: an ordinary
* hand-written Java singleton. Used to prove the Kotlin handling does not change how these
* deserialize, since silently returning the singleton here would be a behaviour change.
*/
public final class JavaStyleSingleton implements Serializable {

private static final long serialVersionUID = 1L;

public static final JavaStyleSingleton INSTANCE = new JavaStyleSingleton();

private JavaStyleSingleton() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.dubbo.common.serialize.hessian2;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.serialize.ObjectInput;
import org.apache.dubbo.common.serialize.ObjectOutput;
import org.apache.dubbo.common.serialize.Serialization;
import org.apache.dubbo.common.utils.DefaultSerializeClassChecker;
import org.apache.dubbo.rpc.model.FrameworkModel;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.function.Consumer;

import com.alibaba.com.caucho.hessian.io.Hessian2Input;
import com.alibaba.com.caucho.hessian.io.Hessian2Output;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
* A Kotlin {@code object} declares a singleton, and the default bean deserializers break that
* guarantee by allocating a fresh instance. For a plain {@code object}, whose {@code equals} is
* inherited identity comparison, that also silently changes which branch a Kotlin {@code when}
* selects.
*/
class KotlinObjectDeserializerTest {

/** A Kotlin class that is not an object declaration: metadata present, but no INSTANCE field. */
@kotlin.Metadata
static final class KotlinStyleClass implements Serializable {
private static final long serialVersionUID = 1L;
private String value = "v";
}

/** Metadata present and an INSTANCE field, but not of this class's own type. */
@kotlin.Metadata
static final class KotlinStyleForeignInstance implements Serializable {
private static final long serialVersionUID = 1L;
public static final String INSTANCE = "not-a-self-reference";
private String value = "v";
}

private byte[] write(Consumer<ObjectOutput> body) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutput objectOutput = serialization().serialize(url(), out);
body.accept(objectOutput);
objectOutput.flushBuffer();
return out.toByteArray();
}

private ObjectInput read(byte[] bytes) throws IOException {
return serialization().deserialize(url(), new ByteArrayInputStream(bytes));
}

private Object roundTrip(Object value) throws IOException, ClassNotFoundException {
return read(write(o -> {
try {
o.writeObject(value);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}))
.readObject();
}

private Serialization serialization() {
return new FrameworkModel().getExtensionLoader(Serialization.class).getExtension("hessian2");
}

private URL url() {
return URL.valueOf("").setScopeModel(new FrameworkModel());
}

@Test
void testKotlinObjectKeepsSingletonIdentity() throws Exception {
Object result = roundTrip(KotlinStyleObject.INSTANCE);

Assertions.assertSame(
KotlinStyleObject.INSTANCE,
result,
"a Kotlin object must deserialize to its declared INSTANCE, not a new instance");
}

/**
* The singleton is authoritative, as an enum constant is: its serialized field data is read so
* that the stream stays aligned, then discarded rather than written back over shared state.
*/
@Test
void testTransmittedStateIsDiscardedAndStreamStaysAligned() throws Exception {
KotlinStyleObject.INSTANCE.setState("written-to-the-wire");
byte[] bytes = write(out -> {
try {
out.writeObject(KotlinStyleObject.INSTANCE);
out.writeUTF("sentinel");
} catch (IOException e) {
throw new IllegalStateException(e);
}
});
KotlinStyleObject.INSTANCE.setState("changed-after-writing");

ObjectInput in = read(bytes);
Object first = in.readObject();

Assertions.assertSame(KotlinStyleObject.INSTANCE, first);
Assertions.assertEquals(
"changed-after-writing",
KotlinStyleObject.INSTANCE.getState(),
"the singleton's own state must win; transmitted state is discarded");
Assertions.assertEquals(
"sentinel",
in.readUTF(),
"the serialized fields must still be consumed, or the next value in the stream desyncs");
}

/**
* A peer may send the map encoding rather than the field encoding — Hessian2Input dispatches
* those to readMap, and the inherited implementation rejects them. The singleton must be
* resolved there too.
*/
@Test
void testKotlinObjectViaMapEncoding() throws Exception {
Hessian2SerializerFactory factory =
new Hessian2SerializerFactory(getClass().getClassLoader(), DefaultSerializeClassChecker.getInstance());

ByteArrayOutputStream bytes = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(bytes);
out.setSerializerFactory(factory);
out.writeMapBegin(KotlinStyleObject.class.getName());
out.writeString("state");
out.writeString("written-to-the-wire");
out.writeMapEnd();
out.flush();

Hessian2Input in = new Hessian2Input(new ByteArrayInputStream(bytes.toByteArray()));
in.setSerializerFactory(factory);

Assertions.assertSame(KotlinStyleObject.INSTANCE, in.readObject());
}

@Test
void testJavaSingletonBehaviourIsUnchanged() throws Exception {
Object result = roundTrip(JavaStyleSingleton.INSTANCE);

Assertions.assertInstanceOf(JavaStyleSingleton.class, result);
Assertions.assertNotSame(
JavaStyleSingleton.INSTANCE,
result,
"an ordinary Java singleton must keep deserializing to a new instance; "
+ "the Kotlin handling is gated on kotlin.Metadata to avoid changing this");
}

/**
* Kotlin emits its metadata annotation on every class, not only on object declarations, so the
* shape of the INSTANCE field decides. Neither of these is a singleton.
*/
@Test
void testKotlinClassesThatAreNotObjectDeclarations() throws Exception {
for (Object value : Arrays.asList(new KotlinStyleClass(), new KotlinStyleForeignInstance())) {
Object result = roundTrip(value);

Assertions.assertInstanceOf(value.getClass(), result);
Assertions.assertNotSame(value, result, value.getClass().getSimpleName() + " is not an object declaration");
}
}
}
Loading
Loading