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