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; + } + }; + } }