From 268542e8ebf6819c720fe1447389766715057656 Mon Sep 17 00:00:00 2001 From: waterWang Date: Thu, 27 Aug 2026 18:00:00 +0000 Subject: [PATCH] fix: preserve nested generic element types (Byte/Short/Float) during hessian2 deserialization (#16440) hessian2 encodes Byte/Short/Integer all as int and Float/Double as double on the wire, so the narrow element types of generic collections can only be restored from the declared generic type. On the provider side the request arguments were decoded with only the erased Class, so nested generics such as Map> or List> lost the inner element type and deserialized as Integer/Double, causing ClassCastException on typed access. This change: - exposes the generic parameter types on MethodDescriptor/ ReflectionMethodDescriptor; - passes the generic parameter type through DecodeableRpcInvocation when decoding request arguments; - makes Hessian2ObjectInput.readObject(Class, Type) recursively narrow numeric elements (Byte/Short/Float/Character) inside nested generic collections to match the declared generic type. Signed-off-by: waterWang --- .../dubbo/rpc/model/MethodDescriptor.java | 14 ++ .../rpc/model/ReflectionMethodDescriptor.java | 7 + .../dubbo/DecodeableRpcInvocation.java | 12 +- .../hessian2/Hessian2ObjectInput.java | 106 +++++++++- .../hessian2/Hessian2SerializationTest.java | 184 ++++++++++++++++++ 5 files changed, 321 insertions(+), 2 deletions(-) diff --git a/dubbo-common/src/main/java/org/apache/dubbo/rpc/model/MethodDescriptor.java b/dubbo-common/src/main/java/org/apache/dubbo/rpc/model/MethodDescriptor.java index ec6bcf4c7b94..7934826cf89d 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/rpc/model/MethodDescriptor.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/rpc/model/MethodDescriptor.java @@ -48,6 +48,20 @@ public interface MethodDescriptor { Class[] getParameterClasses(); + /** + * Retrieves the generic parameter types of the method. + *

+ * For parameterized parameters like {@code List} this returns the + * {@link java.lang.reflect.ParameterizedType} instead of just the raw {@code Class}, + * which allows deserializers to preserve narrow element types (Byte/Short/Float) + * nested inside generic collections. + * + * @return the generic parameter types + */ + default Type[] getGenericParameterTypes() { + return getParameterClasses(); + } + Class getReturnClass(); Type[] getReturnTypes(); diff --git a/dubbo-common/src/main/java/org/apache/dubbo/rpc/model/ReflectionMethodDescriptor.java b/dubbo-common/src/main/java/org/apache/dubbo/rpc/model/ReflectionMethodDescriptor.java index 27f5d8e06b6d..84a5114abc65 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/rpc/model/ReflectionMethodDescriptor.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/rpc/model/ReflectionMethodDescriptor.java @@ -44,6 +44,7 @@ public class ReflectionMethodDescriptor implements MethodDescriptor { public final String methodName; private final String[] compatibleParamSignatures; private final Class[] parameterClasses; + private final Type[] genericParameterTypes; private final Class returnClass; private final Type[] returnTypes; private final String paramDesc; @@ -57,6 +58,7 @@ public ReflectionMethodDescriptor(Method method) { this.method = method; this.methodName = method.getName(); this.parameterClasses = method.getParameterTypes(); + this.genericParameterTypes = method.getGenericParameterTypes(); this.returnClass = method.getReturnType(); Type[] returnTypesResult; try { @@ -149,6 +151,11 @@ public Class[] getParameterClasses() { return parameterClasses; } + @Override + public Type[] getGenericParameterTypes() { + return genericParameterTypes; + } + @Override public String getParamDesc() { return paramDesc; diff --git a/dubbo-rpc/dubbo-rpc-dubbo/src/main/java/org/apache/dubbo/rpc/protocol/dubbo/DecodeableRpcInvocation.java b/dubbo-rpc/dubbo-rpc-dubbo/src/main/java/org/apache/dubbo/rpc/protocol/dubbo/DecodeableRpcInvocation.java index 3a7d6e84005b..318da456b505 100644 --- a/dubbo-rpc/dubbo-rpc-dubbo/src/main/java/org/apache/dubbo/rpc/protocol/dubbo/DecodeableRpcInvocation.java +++ b/dubbo-rpc/dubbo-rpc-dubbo/src/main/java/org/apache/dubbo/rpc/protocol/dubbo/DecodeableRpcInvocation.java @@ -47,6 +47,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.reflect.Type; import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -82,6 +83,8 @@ public class DecodeableRpcInvocation extends RpcInvocation implements Codec, Dec protected final transient Supplier callbackServiceCodecFactory; + private transient Type[] genericParameterTypes; + private static final boolean CHECK_SERIALIZATION = Boolean.parseBoolean(SystemPropertyConfigUtils.getSystemProperty(SERIALIZATION_SECURITY_CHECK_KEY, "true")); @@ -237,6 +240,7 @@ protected Class[] drawPts(String path, String version, String desc, Class[ MethodDescriptor methodDescriptor = serviceDescriptor.getMethod(getMethodName(), desc); if (methodDescriptor != null) { pts = methodDescriptor.getParameterClasses(); + this.genericParameterTypes = methodDescriptor.getGenericParameterTypes(); this.setReturnTypes(methodDescriptor.getReturnTypes()); // switch TCCL @@ -273,8 +277,14 @@ protected Class[] drawPts(String path, String version, String desc, Class[ protected Object[] drawArgs(ObjectInput in, Class[] pts) throws IOException, ClassNotFoundException { Object[] args; args = new Object[pts.length]; + Type[] genericTypes = this.genericParameterTypes; for (int i = 0; i < args.length; i++) { - args[i] = in.readObject(pts[i]); + Type genericType = (genericTypes != null && i < genericTypes.length) ? genericTypes[i] : null; + if (genericType != null) { + args[i] = in.readObject(pts[i], genericType); + } else { + args[i] = in.readObject(pts[i]); + } } return args; } diff --git a/dubbo-serialization/dubbo-serialization-hessian2/src/main/java/org/apache/dubbo/common/serialize/hessian2/Hessian2ObjectInput.java b/dubbo-serialization/dubbo-serialization-hessian2/src/main/java/org/apache/dubbo/common/serialize/hessian2/Hessian2ObjectInput.java index 8bb224d5452e..c15ca59e5d27 100644 --- a/dubbo-serialization/dubbo-serialization-hessian2/src/main/java/org/apache/dubbo/common/serialize/hessian2/Hessian2ObjectInput.java +++ b/dubbo-serialization/dubbo-serialization-hessian2/src/main/java/org/apache/dubbo/common/serialize/hessian2/Hessian2ObjectInput.java @@ -22,8 +22,16 @@ import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import com.alibaba.com.caucho.hessian.io.Hessian2Input; @@ -119,6 +127,7 @@ public T readObject(Class cls) throws IOException, ClassNotFoundException } @Override + @SuppressWarnings("unchecked") public T readObject(Class cls, Type type) throws IOException, ClassNotFoundException { if (!Objects.equals( mH2i.getSerializerFactory().getClassLoader(), @@ -126,7 +135,102 @@ public T readObject(Class cls, Type type) throws IOException, ClassNotFou mH2i.setSerializerFactory(hessian2FactoryManager.getSerializerFactory( Thread.currentThread().getContextClassLoader())); } - return readObject(cls); + if (type instanceof ParameterizedType && containsNarrowableType(type)) { + // hessian2 encodes Byte/Short/Integer all as int and Float/Double as double on the wire, + // so the element type of such generic collections can only be restored from the declared + // generic type. hessian's expectedTypes mechanism is single-level, so nested generics + // (e.g. List>, Map>) lose the narrow element type. Read the + // object with the erased type and then recursively narrow numeric elements to match the + // declared generic type. + Object obj = mH2i.readObject(cls); + return (T) narrowByType(obj, type); + } + return (T) mH2i.readObject(cls); + } + + /** + * Checks whether the given {@link Type} declares any narrow primitive-wrapper element type + * (Byte/Short/Float/Character) anywhere in the generic hierarchy. Only such types need the + * recursive narrowing pass, avoiding unnecessary copies for e.g. {@code List}. + */ + private boolean containsNarrowableType(Type type) { + if (type instanceof ParameterizedType) { + Type[] typeArgs = ((ParameterizedType) type).getActualTypeArguments(); + for (Type typeArg : typeArgs) { + if (containsNarrowableType(typeArg)) { + return true; + } + } + return false; + } + return type == Byte.class || type == Short.class || type == Float.class || type == Character.class; + } + + /** + * Recursively narrows widened numeric elements (Integer/Double) inside generic collections to the + * element types declared by {@code type}. Returns the input object unchanged when no element needs + * to be narrowed. + */ + @SuppressWarnings("unchecked") + private Object narrowByType(Object obj, Type type) { + if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + Type rawType = parameterizedType.getRawType(); + Type[] typeArgs = parameterizedType.getActualTypeArguments(); + if (rawType instanceof Class + && Collection.class.isAssignableFrom((Class) rawType) + && typeArgs.length == 1) { + Type elementType = typeArgs[0]; + if (obj instanceof List) { + List result = new ArrayList<>(((List) obj).size()); + for (Object element : (List) obj) { + result.add(narrowByType(element, elementType)); + } + return result; + } + if (obj instanceof Set) { + Set result = new LinkedHashSet<>(); + for (Object element : (Set) obj) { + result.add(narrowByType(element, elementType)); + } + return result; + } + if (obj instanceof Collection) { + Collection result = new ArrayList<>(); + for (Object element : (Collection) obj) { + result.add(narrowByType(element, elementType)); + } + return result; + } + } else if (rawType instanceof Class + && Map.class.isAssignableFrom((Class) rawType) + && typeArgs.length == 2) { + Type keyType = typeArgs[0]; + Type valueType = typeArgs[1]; + if (obj instanceof Map) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : ((Map) obj).entrySet()) { + result.put(narrowByType(entry.getKey(), keyType), narrowByType(entry.getValue(), valueType)); + } + return result; + } + } + } else if (type instanceof Class) { + Class clazz = (Class) type; + if (clazz == Byte.class && obj instanceof Integer) { + return Byte.valueOf(((Number) obj).byteValue()); + } + if (clazz == Short.class && obj instanceof Integer) { + return Short.valueOf(((Number) obj).shortValue()); + } + if (clazz == Float.class && obj instanceof Double) { + return Float.valueOf(((Number) obj).floatValue()); + } + if (clazz == Character.class && obj instanceof Integer) { + return (char) ((Number) obj).intValue(); + } + } + return obj; } public InputStream readInputStream() throws IOException { diff --git a/dubbo-serialization/dubbo-serialization-hessian2/src/test/java/org/apache/dubbo/common/serialize/hessian2/Hessian2SerializationTest.java b/dubbo-serialization/dubbo-serialization-hessian2/src/test/java/org/apache/dubbo/common/serialize/hessian2/Hessian2SerializationTest.java index 559ed1e3b6ad..faefae35a58e 100644 --- a/dubbo-serialization/dubbo-serialization-hessian2/src/test/java/org/apache/dubbo/common/serialize/hessian2/Hessian2SerializationTest.java +++ b/dubbo-serialization/dubbo-serialization-hessian2/src/test/java/org/apache/dubbo/common/serialize/hessian2/Hessian2SerializationTest.java @@ -28,8 +28,13 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -654,4 +659,183 @@ void testLimit5() throws IOException, ClassNotFoundException { frameworkModel.destroy(); } } + + @Test + void testReadObjectWithNestedGenericType() throws IOException, ClassNotFoundException { + // hessian2 encodes Byte/Short/Integer all as int on the wire, so narrow element types of + // generic collections can only be restored from the declared generic type (apache/dubbo#16440). + + // Map> — nested generic, inner elements must stay Byte + { + FrameworkModel frameworkModel = new FrameworkModel(); + Serialization serialization = + frameworkModel.getExtensionLoader(Serialization.class).getExtension("hessian2"); + URL url = URL.valueOf("").setScopeModel(frameworkModel); + + Map> original = new LinkedHashMap<>(); + original.put("c1", new ArrayList<>(Arrays.asList((byte) 1, (byte) 127, (byte) -1))); + original.put("c2", new ArrayList<>(Arrays.asList((byte) 5, (byte) -8))); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ObjectOutput objectOutput = serialization.serialize(url, outputStream); + objectOutput.writeObject(original); + objectOutput.flushBuffer(); + + byte[] bytes = outputStream.toByteArray(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + ObjectInput objectInput = serialization.deserialize(url, inputStream); + + Type mapOfListByte = parameterizedType(Map.class, String.class, parameterizedType(List.class, Byte.class)); + Map result = objectInput.readObject(Map.class, mapOfListByte); + + Assertions.assertEquals(2, result.size()); + for (Object value : result.values()) { + Assertions.assertInstanceOf(List.class, value); + for (Object element : (List) value) { + Assertions.assertInstanceOf( + Byte.class, element, "nested generic element must not be widened to Integer"); + } + } + Assertions.assertEquals(original.get("c1"), result.get("c1")); + Assertions.assertEquals(original.get("c2"), result.get("c2")); + + frameworkModel.destroy(); + } + + // List> — deeply nested generic + { + FrameworkModel frameworkModel = new FrameworkModel(); + Serialization serialization = + frameworkModel.getExtensionLoader(Serialization.class).getExtension("hessian2"); + URL url = URL.valueOf("").setScopeModel(frameworkModel); + + List> original = new ArrayList<>(); + original.add(new ArrayList<>(Arrays.asList((byte) 1, (byte) 2, (byte) -3))); + original.add(new ArrayList<>(Arrays.asList((byte) 9, (byte) 10))); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ObjectOutput objectOutput = serialization.serialize(url, outputStream); + objectOutput.writeObject(original); + objectOutput.flushBuffer(); + + byte[] bytes = outputStream.toByteArray(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + ObjectInput objectInput = serialization.deserialize(url, inputStream); + + Type listOfListByte = parameterizedType(List.class, parameterizedType(List.class, Byte.class)); + List result = objectInput.readObject(List.class, listOfListByte); + + Assertions.assertEquals(2, result.size()); + for (Object inner : result) { + Assertions.assertInstanceOf(List.class, inner); + for (Object element : (List) inner) { + Assertions.assertInstanceOf(Byte.class, element); + } + } + Assertions.assertEquals(original, result); + + frameworkModel.destroy(); + } + + // Map> — Float is encoded as double on the wire + { + FrameworkModel frameworkModel = new FrameworkModel(); + Serialization serialization = + frameworkModel.getExtensionLoader(Serialization.class).getExtension("hessian2"); + URL url = URL.valueOf("").setScopeModel(frameworkModel); + + Map> original = new LinkedHashMap<>(); + original.put("m", new ArrayList<>(Arrays.asList(1.5f, -2.25f, 3.0f))); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ObjectOutput objectOutput = serialization.serialize(url, outputStream); + objectOutput.writeObject(original); + objectOutput.flushBuffer(); + + byte[] bytes = outputStream.toByteArray(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + ObjectInput objectInput = serialization.deserialize(url, inputStream); + + Type mapOfListFloat = + parameterizedType(Map.class, String.class, parameterizedType(List.class, Float.class)); + Map result = objectInput.readObject(Map.class, mapOfListFloat); + + for (Object value : result.values()) { + for (Object element : (List) value) { + Assertions.assertInstanceOf(Float.class, element); + } + } + Assertions.assertEquals(original, result); + + frameworkModel.destroy(); + } + + // simple List keeps working + { + FrameworkModel frameworkModel = new FrameworkModel(); + Serialization serialization = + frameworkModel.getExtensionLoader(Serialization.class).getExtension("hessian2"); + URL url = URL.valueOf("").setScopeModel(frameworkModel); + + List original = new ArrayList<>(Arrays.asList((byte) 1, (byte) 2, (byte) -5)); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ObjectOutput objectOutput = serialization.serialize(url, outputStream); + objectOutput.writeObject(original); + objectOutput.flushBuffer(); + + byte[] bytes = outputStream.toByteArray(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + ObjectInput objectInput = serialization.deserialize(url, inputStream); + + List result = objectInput.readObject(List.class, parameterizedType(List.class, Byte.class)); + Assertions.assertEquals(original, result); + Assertions.assertInstanceOf(Byte.class, result.get(0)); + + frameworkModel.destroy(); + } + + // List is untouched by the narrowing pass + { + FrameworkModel frameworkModel = new FrameworkModel(); + Serialization serialization = + frameworkModel.getExtensionLoader(Serialization.class).getExtension("hessian2"); + URL url = URL.valueOf("").setScopeModel(frameworkModel); + + List original = new ArrayList<>(Arrays.asList("a", "b", "c")); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ObjectOutput objectOutput = serialization.serialize(url, outputStream); + objectOutput.writeObject(original); + objectOutput.flushBuffer(); + + byte[] bytes = outputStream.toByteArray(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + ObjectInput objectInput = serialization.deserialize(url, inputStream); + + List result = objectInput.readObject(List.class, parameterizedType(List.class, String.class)); + Assertions.assertEquals(original, result); + + frameworkModel.destroy(); + } + } + + private static ParameterizedType parameterizedType(Type rawType, Type... typeArguments) { + return new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return typeArguments; + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public Type getOwnerType() { + return null; + } + }; + } }