This is an automated email from the ASF dual-hosted git repository.
alexstocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git
The following commit(s) were added to refs/heads/develop by this push:
new e12d8c5f9 fix(generic): support variadic method invocation via generic
call (#3284)
e12d8c5f9 is described below
commit e12d8c5f97885075361b468356703b1d91058167
Author: 承潜 <[email protected]>
AuthorDate: Mon Apr 13 13:20:09 2026 +0800
fix(generic): support variadic method invocation via generic call (#3284)
* fix(generic): support variadic method invocation via generic call
* fix(generic): support variadic invocation across reflection paths
---
common/constant/key.go | 69 +++----
common/rpc_service.go | 5 +
filter/generic/service_filter.go | 331 ++++++++++++++++++++++++++++++++--
filter/generic/service_filter_test.go | 265 +++++++++++++++++++++++++++
protocol/triple/server.go | 85 ++++++---
protocol/triple/server_test.go | 28 +++
proxy/proxy_factory/default.go | 39 +++-
proxy/proxy_factory/default_test.go | 32 ++++
proxy/proxy_factory/invoker_test.go | 41 +++++
proxy/proxy_factory/pass_through.go | 2 +-
proxy/proxy_factory/utils.go | 11 +-
proxy/proxy_factory/utils_test.go | 71 ++++++--
server/server.go | 29 ++-
server/server_test.go | 26 +++
14 files changed, 933 insertions(+), 101 deletions(-)
diff --git a/common/constant/key.go b/common/constant/key.go
index 2be7ffc88..aba807ced 100644
--- a/common/constant/key.go
+++ b/common/constant/key.go
@@ -28,40 +28,41 @@ const (
)
const (
- GroupKey = "group"
- VersionKey = "version"
- InterfaceKey = "interface"
- MessageSizeKey = "message_size"
- PathKey = "path"
- ServiceKey = "service"
- MethodsKey = "methods"
- TimeoutKey = "timeout"
- CategoryKey = "category"
- CheckKey = "check"
- EnabledKey = "enabled"
- SideKey = "side"
- OverrideProvidersKey = "providerAddresses"
- BeanNameKey = "bean.name"
- GenericKey = "generic"
- ClassifierKey = "classifier"
- TokenKey = "token"
- LocalAddr = "local-addr"
- RemoteAddr = "remote-addr"
- DefaultRemotingTimeout = 1000
- ReleaseKey = "release"
- AnyhostKey = "anyhost"
- PortKey = "port"
- ProtocolKey = "protocol"
- PathSeparator = "/"
- DotSeparator = "."
- CommaSeparator = ","
- SslEnabledKey = "ssl-enabled"
- ParamsTypeKey = "parameter-type-names" // key used in pass
through invoker factory, to define param type
- MetadataTypeKey = "metadata-type"
- MaxCallSendMsgSize = "max-call-send-msg-size"
- MaxServerSendMsgSize = "max-server-send-msg-size"
- MaxCallRecvMsgSize = "max-call-recv-msg-size"
- MaxServerRecvMsgSize = "max-server-recv-msg-size"
+ GroupKey = "group"
+ VersionKey = "version"
+ InterfaceKey = "interface"
+ MessageSizeKey = "message_size"
+ PathKey = "path"
+ ServiceKey = "service"
+ MethodsKey = "methods"
+ TimeoutKey = "timeout"
+ CategoryKey = "category"
+ CheckKey = "check"
+ EnabledKey = "enabled"
+ SideKey = "side"
+ OverrideProvidersKey = "providerAddresses"
+ BeanNameKey = "bean.name"
+ GenericKey = "generic"
+ GenericVariadicCallSliceKey = "generic-variadic-call-slice" // internal
marker from generic filter to variadic reflection dispatchers
+ ClassifierKey = "classifier"
+ TokenKey = "token"
+ LocalAddr = "local-addr"
+ RemoteAddr = "remote-addr"
+ DefaultRemotingTimeout = 1000
+ ReleaseKey = "release"
+ AnyhostKey = "anyhost"
+ PortKey = "port"
+ ProtocolKey = "protocol"
+ PathSeparator = "/"
+ DotSeparator = "."
+ CommaSeparator = ","
+ SslEnabledKey = "ssl-enabled"
+ ParamsTypeKey = "parameter-type-names" // key used in
pass through invoker factory, to define param type
+ MetadataTypeKey = "metadata-type"
+ MaxCallSendMsgSize = "max-call-send-msg-size"
+ MaxServerSendMsgSize = "max-server-send-msg-size"
+ MaxCallRecvMsgSize = "max-call-recv-msg-size"
+ MaxServerRecvMsgSize = "max-server-recv-msg-size"
// TODO: remove KeepAliveInterval and KeepAliveInterval in version 4.0.0
KeepAliveInterval = "keep-alive-interval"
diff --git a/common/rpc_service.go b/common/rpc_service.go
index 84da0cd66..cc936d3c1 100644
--- a/common/rpc_service.go
+++ b/common/rpc_service.go
@@ -132,6 +132,11 @@ func (m *MethodType) ReplyType() reflect.Type {
return m.replyType
}
+// IsVariadic returns true if the method has a variadic (...T) final parameter.
+func (m *MethodType) IsVariadic() bool {
+ return m.method.Type.IsVariadic()
+}
+
// SuiteContext transfers @ctx to reflect.Value type or get it from @m.ctxType.
func (m *MethodType) SuiteContext(ctx context.Context) reflect.Value {
if ctxV := reflect.ValueOf(ctx); ctxV.IsValid() {
diff --git a/filter/generic/service_filter.go b/filter/generic/service_filter.go
index 16925f771..0a986a9c8 100644
--- a/filter/generic/service_filter.go
+++ b/filter/generic/service_filter.go
@@ -19,6 +19,8 @@ package generic
import (
"context"
+ "reflect"
+ "strings"
"sync"
)
@@ -35,7 +37,9 @@ import (
"dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/common/extension"
"dubbo.apache.org/dubbo-go/v3/filter"
+ "dubbo.apache.org/dubbo-go/v3/filter/generic/generalizer"
"dubbo.apache.org/dubbo-go/v3/protocol/base"
+ dubboHessian "dubbo.apache.org/dubbo-go/v3/protocol/dubbo/hessian2"
"dubbo.apache.org/dubbo-go/v3/protocol/invocation"
"dubbo.apache.org/dubbo-go/v3/protocol/result"
)
@@ -78,44 +82,339 @@ func (f *genericServiceFilter) Invoke(ctx context.Context,
invoker base.Invoker,
`, mtdName, types, args)
// get the type of the argument
- ivkUrl := invoker.GetURL()
- svc := common.ServiceMap.GetServiceByServiceKey(ivkUrl.Protocol,
ivkUrl.ServiceKey())
+ ivkURL := invoker.GetURL()
+ svc := common.ServiceMap.GetServiceByServiceKey(ivkURL.Protocol,
ivkURL.ServiceKey())
method := svc.Method()[mtdName]
if method == nil {
return &result.RPCResult{
- Err: perrors.Errorf("\"%s\" method is not found,
service key: %s", mtdName, ivkUrl.ServiceKey()),
+ Err: perrors.Errorf("\"%s\" method is not found,
service key: %s", mtdName, ivkURL.ServiceKey()),
}
}
+
argsType := method.ArgsType()
+ if err := validateGenericArgs(method.IsVariadic(), len(argsType),
len(args), mtdName); err != nil {
+ return &result.RPCResult{Err: err}
+ }
// get generic info from attachments of invocation, the default value
is "true"
generic := inv.GetAttachmentWithDefaultValue(constant.GenericKey,
constant.GenericSerializationDefault)
// get generalizer according to value in the `generic`
- g := getGeneralizer(generic)
+ // realize
+ newArgs, err := realizeInvocationArgs(getGeneralizer(generic),
argsType, args, method.IsVariadic(), types)
+ if err != nil {
+ return &result.RPCResult{Err: err}
+ }
- if len(args) != len(argsType) {
- return &result.RPCResult{
- Err: perrors.Errorf("the number of args(=%d) is not
matched with \"%s\" method", len(args), mtdName),
+ newIvc := invocation.NewRPCInvocation(mtdName, newArgs,
inv.Attachments())
+ newIvc.SetReply(inv.Reply())
+ if method.IsVariadic() {
+ newIvc.SetAttribute(constant.GenericVariadicCallSliceKey, true)
+ }
+
+ return invoker.Invoke(ctx, newIvc)
+}
+
+// validateGenericArgs checks the generic arg count against the method
signature.
+// Variadic methods accept any count >= the fixed parameter count.
+func validateGenericArgs(isVariadic bool, argsTypeCount, argCount int,
methodName string) error {
+ if isVariadic {
+ if argCount >= argsTypeCount-1 {
+ return nil
}
+ } else if argCount == argsTypeCount {
+ return nil
}
- // realize
+ return perrors.Errorf("the number of args(=%d) is not matched with
\"%s\" method", argCount, methodName)
+}
+
+// realizeInvocationArgs realizes generic args and packs a variadic tail into
the declared slice type.
+func realizeInvocationArgs(g generalizer.Generalizer, argsType []reflect.Type,
args []hessian.Object, isVariadic bool, types any) ([]any, error) {
+ if !isVariadic {
+ return realizeFixedArgs(g, args, argsType)
+ }
+
+ newArgs, err := realizeFixedArgs(g, args[:len(argsType)-1],
argsType[:len(argsType)-1])
+ if err != nil {
+ return nil, err
+ }
+
+ variadicArg, err := realizeVariadicArg(g, args[len(argsType)-1:],
argsType[len(argsType)-1], variadicTypeName(types))
+ if err != nil {
+ return nil, err
+ }
+
+ return append(newArgs, variadicArg), nil
+}
+
+// realizeFixedArgs realizes non-variadic parameters one by one.
+func realizeFixedArgs(g generalizer.Generalizer, args []hessian.Object,
argsType []reflect.Type) ([]any, error) {
newArgs := make([]any, len(argsType))
- for i := 0; i < len(argsType); i++ {
+ for i := range argsType {
newArg, err := g.Realize(args[i], argsType[i])
if err != nil {
- return &result.RPCResult{
- Err: perrors.Errorf("realization failed, %v",
err),
- }
+ return nil, perrors.Errorf("realization of arg[%d]
failed: %v", i, err)
}
newArgs[i] = newArg
}
- // build a normal invocation
- newIvc := invocation.NewRPCInvocation(mtdName, newArgs,
inv.Attachments())
- newIvc.SetReply(inv.Reply())
+ return newArgs, nil
+}
- return invoker.Invoke(ctx, newIvc)
+// realizeVariadicArg realizes the variadic tail into the declared slice type.
+// It unwraps a single arg only when its declared generic type matches the
variadic slice.
+func realizeVariadicArg(g generalizer.Generalizer, args []hessian.Object,
variadicSliceType reflect.Type, variadicType string) (any, error) {
+ variadicArgs := normalizeVariadicArgs(args, variadicSliceType,
variadicType)
+ slice := reflect.MakeSlice(variadicSliceType, len(variadicArgs),
len(variadicArgs))
+ elemType := variadicSliceType.Elem()
+
+ for i, arg := range variadicArgs {
+ realized, err := g.Realize(arg, elemType)
+ if err != nil {
+ return nil, perrors.Errorf("realization of variadic
arg[%d] failed: %v", i, err)
+ }
+ realizedValue, err := assignableValue(realized, elemType)
+ if err != nil {
+ return nil, perrors.Errorf("realization of variadic
arg[%d] failed: %v", i, err)
+ }
+ slice.Index(i).Set(realizedValue)
+ }
+
+ return slice.Interface(), nil
+}
+
+// assignableValue fits a realized value into the target type without
panicking on Set.
+func assignableValue(value any, targetType reflect.Type) (reflect.Value,
error) {
+ if value == nil {
+ if canBeNil(targetType) {
+ return reflect.Zero(targetType), nil
+ }
+ return reflect.Value{}, perrors.Errorf("nil is not assignable
to %s", targetType)
+ }
+
+ realizedValue := reflect.ValueOf(value)
+ if realizedValue.Type().AssignableTo(targetType) {
+ return realizedValue, nil
+ }
+ if realizedValue.Type().ConvertibleTo(targetType) {
+ return realizedValue.Convert(targetType), nil
+ }
+
+ return reflect.Value{}, perrors.Errorf("type %s is not assignable to
%s", realizedValue.Type(), targetType)
+}
+
+func canBeNil(typ reflect.Type) bool {
+ switch typ.Kind() {
+ case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
reflect.Ptr, reflect.Slice:
+ return true
+ default:
+ return false
+ }
+}
+
+// variadicTypeName returns the last generic type name when the caller
provides $invoke type metadata.
+func variadicTypeName(types any) string {
+ switch typeNames := types.(type) {
+ case []string:
+ if len(typeNames) == 0 {
+ return ""
+ }
+ return typeNames[len(typeNames)-1]
+ case []any:
+ if len(typeNames) == 0 {
+ return ""
+ }
+ if typeName, ok := typeNames[len(typeNames)-1].(string); ok {
+ return typeName
+ }
+ }
+
+ return ""
+}
+
+// normalizeVariadicArgs unwraps one packed array only when the generic type
says it
+// is the variadic slice itself; otherwise the single arg stays as one
variadic value.
+func normalizeVariadicArgs(args []hessian.Object, variadicSliceType
reflect.Type, variadicType string) []hessian.Object {
+ if len(args) != 1 {
+ return args
+ }
+ if !shouldUnwrapPackedVariadicArg(variadicType, variadicSliceType) {
+ if variadicType != "" {
+ return args
+ }
+ return normalizeVariadicArgsWithoutType(args[0],
variadicSliceType)
+ }
+ if args[0] == nil {
+ return nil
+ }
+
+ return unwrapToSlice(args[0])
+}
+
+// normalizeVariadicArgsWithoutType keeps dubbo-go compatibility when generic
callers omit `types`.
+// A real single variadic value stays packed as one element, while a packed
tail slice is unwrapped once.
+func normalizeVariadicArgsWithoutType(arg hessian.Object, variadicSliceType
reflect.Type) []hessian.Object {
+ if arg == nil {
+ return nil
+ }
+
+ v := reflect.ValueOf(arg)
+ if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
+ return []hessian.Object{arg}
+ }
+
+ elemType := variadicSliceType.Elem()
+ argType := v.Type()
+ if elemType.Kind() != reflect.Interface &&
(argType.AssignableTo(elemType) || argType.ConvertibleTo(elemType)) {
+ return []hessian.Object{arg}
+ }
+
+ return unwrapToSlice(arg)
+}
+
+// shouldUnwrapPackedVariadicArg matches the declared variadic slice against
the
+// generic tail type, including Java names and JVM array descriptors.
+func shouldUnwrapPackedVariadicArg(variadicType string, variadicSliceType
reflect.Type) bool {
+ if variadicType == "" {
+ return false
+ }
+
+ for _, typeName := range javaTypeNamesForType(variadicSliceType) {
+ if variadicType == typeName {
+ return true
+ }
+ }
+
+ elemType := variadicSliceType.Elem()
+ if elemType.Kind() == reflect.Interface && (variadicType ==
"[Ljava.lang.Object;" || variadicType == "java.lang.Object[]") {
+ return true
+ }
+
+ return false
+}
+
+// javaTypeNamesForType returns the generic type spellings we accept for the
variadic slice.
+func javaTypeNamesForType(typ reflect.Type) []string {
+ zero := reflect.Zero(typ)
+ if !zero.IsValid() {
+ return nil
+ }
+
+ names := make([]string, 0, 2)
+ if name, err := dubboHessian.GetJavaName(zero.Interface()); err == nil
&& name != "" {
+ names = append(names, name)
+ }
+ if desc := dubboHessian.GetClassDesc(zero.Interface()); desc != "" &&
desc != "V" {
+ names = appendUniqueString(names, desc)
+ }
+ if desc := jvmArrayDescriptorForType(typ); desc != "" {
+ names = appendUniqueString(names, desc)
+ }
+
+ return names
+}
+
+// jvmArrayDescriptorForType builds descriptors like [B, [[B or
[[Ljava.lang.String;.
+func jvmArrayDescriptorForType(typ reflect.Type) string {
+ if typ.Kind() != reflect.Slice && typ.Kind() != reflect.Array {
+ return ""
+ }
+
+ depth := 0
+ for typ.Kind() == reflect.Slice || typ.Kind() == reflect.Array {
+ depth++
+ typ = typ.Elem()
+ }
+
+ leaf := jvmLeafDescriptorForType(typ)
+ if leaf == "" {
+ return ""
+ }
+
+ return strings.Repeat("[", depth) + leaf
+}
+
+func jvmLeafDescriptorForType(typ reflect.Type) string {
+ switch typ.Kind() {
+ case reflect.Bool:
+ return "Z"
+ case reflect.Int8, reflect.Uint8:
+ return "B"
+ case reflect.Int16:
+ return "S"
+ case reflect.Uint16:
+ return "C"
+ case reflect.Int, reflect.Int64:
+ return "J"
+ case reflect.Int32:
+ return "I"
+ case reflect.Float32:
+ return "F"
+ case reflect.Float64:
+ return "D"
+ case reflect.String:
+ return "Ljava.lang.String;"
+ case reflect.Interface:
+ return "Ljava.lang.Object;"
+ case reflect.Map:
+ return "Ljava.util.Map;"
+ case reflect.Struct:
+ if typ.PkgPath() == "time" && typ.Name() == "Time" {
+ return "Ljava.util.Date;"
+ }
+ return "Ljava.lang.Object;"
+ default:
+ zero := reflect.New(typ).Elem().Interface()
+ desc := dubboHessian.GetClassDesc(zero)
+ switch desc {
+ case "", "V", "java.util.List":
+ return ""
+ case "java.lang.String":
+ return "Ljava.lang.String;"
+ case "java.lang.Object":
+ return "Ljava.lang.Object;"
+ case "java.util.Date":
+ return "Ljava.util.Date;"
+ case "java.util.Map":
+ return "Ljava.util.Map;"
+ }
+ if len(desc) == 1 {
+ return desc
+ }
+ if strings.HasPrefix(desc, "L") && strings.HasSuffix(desc, ";")
{
+ return desc
+ }
+ if strings.Contains(desc, ".") {
+ return "L" + strings.ReplaceAll(desc, ".", "/") + ";"
+ }
+ return ""
+ }
+}
+
+func appendUniqueString(values []string, value string) []string {
+ for _, existing := range values {
+ if existing == value {
+ return values
+ }
+ }
+ return append(values, value)
+}
+
+// unwrapToSlice returns slice/array elements, or keeps obj as one variadic
element.
+func unwrapToSlice(obj hessian.Object) []hessian.Object {
+ if obj == nil {
+ return []hessian.Object{nil}
+ }
+ v := reflect.ValueOf(obj)
+ if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
+ out := make([]hessian.Object, v.Len())
+ for i := 0; i < v.Len(); i++ {
+ out[i] = v.Index(i).Interface()
+ }
+ return out
+ }
+ // not a collection — treat as single variadic element
+ return []hessian.Object{obj}
}
func (f *genericServiceFilter) OnResponse(_ context.Context, result
result.Result, _ base.Invoker, inv base.Invocation) result.Result {
diff --git a/filter/generic/service_filter_test.go
b/filter/generic/service_filter_test.go
index b28d25e05..84756f42c 100644
--- a/filter/generic/service_filter_test.go
+++ b/filter/generic/service_filter_test.go
@@ -71,6 +71,22 @@ func (s *MockHelloService) HelloPB(req
*generalizer.RequestType) (*generalizer.R
return nil, perrors.Errorf("people not found")
}
+func (s *MockHelloService) HelloVariadic(prefix string, names ...string)
(string, error) {
+ return prefix, nil
+}
+
+func (s *MockHelloService) EchoVariadic(args ...any) ([]any, error) {
+ return args, nil
+}
+
+func (s *MockHelloService) BytesVariadic(args ...[]byte) ([][]byte, error) {
+ return args, nil
+}
+
+func (s *MockHelloService) NestedStringVariadic(args ...[]string) ([][]string,
error) {
+ return args, nil
+}
+
func TestServiceFilter_Invoke(t *testing.T) {
filter := &genericServiceFilter{}
@@ -214,3 +230,252 @@ func TestServiceFilter_OnResponse(t *testing.T) {
response := filter.OnResponse(context.Background(), rpcResult, nil,
invocation1)
assert.Equal(t, "result", response.Result())
}
+
+func TestServiceFilter_InvokeVariadic(t *testing.T) {
+ filter := &genericServiceFilter{}
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockInvoker := mock.NewMockInvoker(ctrl)
+ service := &MockHelloService{}
+ ivkURL := common.NewURLWithOptions(
+ common.WithProtocol("test-variadic"),
+ common.WithParams(url.Values{}),
+ common.WithParamsValue(constant.InterfaceKey,
service.Reference()),
+ common.WithParamsValue(constant.GenericKey,
constant.GenericSerializationDefault),
+ )
+ _, err :=
common.ServiceMap.Register(ivkURL.GetParam(constant.InterfaceKey, ""),
+ ivkURL.Protocol,
+ "",
+ "",
+ service)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ _ =
common.ServiceMap.UnRegister(ivkURL.GetParam(constant.InterfaceKey, ""),
ivkURL.Protocol, ivkURL.ServiceKey())
+ })
+
+ mockInvoker.EXPECT().GetURL().Return(ivkURL).AnyTimes()
+
+ cases := []struct {
+ name string
+ inv *invocation.RPCInvocation
+ assertInvocation func(t *testing.T, inv base.Invocation)
+ }{
+ {
+ name: "fixed plus discrete variadic args",
+ inv: invocation.NewRPCInvocation(constant.Generic,
[]any{
+ "HelloVariadic",
+ []string{"java.lang.String",
"java.lang.String", "java.lang.String"},
+ []hessian.Object{"hello", "alice", "bob"},
+ }, map[string]any{
+ constant.GenericKey:
constant.GenericSerializationDefault,
+ }),
+ assertInvocation: func(t *testing.T, inv
base.Invocation) {
+ assert.Equal(t, "HelloVariadic",
inv.MethodName())
+ assert.Equal(t, []any{"hello",
[]string{"alice", "bob"}}, inv.Arguments())
+ },
+ },
+ {
+ name: "fixed plus single scalar variadic arg",
+ inv: invocation.NewRPCInvocation(constant.Generic,
[]any{
+ "HelloVariadic",
+ []string{"java.lang.String",
"java.lang.String"},
+ []hessian.Object{"hello", "alice"},
+ }, map[string]any{
+ constant.GenericKey:
constant.GenericSerializationDefault,
+ }),
+ assertInvocation: func(t *testing.T, inv
base.Invocation) {
+ assert.Equal(t, "HelloVariadic",
inv.MethodName())
+ assert.Equal(t, []any{"hello",
[]string{"alice"}}, inv.Arguments())
+ },
+ },
+ {
+ name: "fixed plus packed variadic array",
+ inv: invocation.NewRPCInvocation(constant.Generic,
[]any{
+ "HelloVariadic",
+ []string{"java.lang.String",
"[Ljava.lang.String;"},
+ []hessian.Object{"hello", []any{"alice",
"bob"}},
+ }, map[string]any{
+ constant.GenericKey:
constant.GenericSerializationDefault,
+ }),
+ assertInvocation: func(t *testing.T, inv
base.Invocation) {
+ assert.Equal(t, "HelloVariadic",
inv.MethodName())
+ assert.Equal(t, []any{"hello",
[]string{"alice", "bob"}}, inv.Arguments())
+ },
+ },
+ {
+ name: "fixed plus packed variadic array without types",
+ inv: invocation.NewRPCInvocation(constant.Generic,
[]any{
+ "HelloVariadic",
+ nil,
+ []hessian.Object{"hello", []string{"alice",
"bob"}},
+ }, map[string]any{
+ constant.GenericKey:
constant.GenericSerializationDefault,
+ }),
+ assertInvocation: func(t *testing.T, inv
base.Invocation) {
+ assert.Equal(t, "HelloVariadic",
inv.MethodName())
+ assert.Equal(t, []any{"hello",
[]string{"alice", "bob"}}, inv.Arguments())
+ },
+ },
+ {
+ name: "fixed plus packed nil variadic array",
+ inv: invocation.NewRPCInvocation(constant.Generic,
[]any{
+ "HelloVariadic",
+ []string{"java.lang.String",
"[Ljava.lang.String;"},
+ []hessian.Object{"hello", nil},
+ }, map[string]any{
+ constant.GenericKey:
constant.GenericSerializationDefault,
+ }),
+ assertInvocation: func(t *testing.T, inv
base.Invocation) {
+ assert.Equal(t, "HelloVariadic",
inv.MethodName())
+ assert.Equal(t, []any{"hello", []string{}},
inv.Arguments())
+ },
+ },
+ {
+ name: "zero variadic args",
+ inv: invocation.NewRPCInvocation(constant.Generic,
[]any{
+ "HelloVariadic",
+ []string{"java.lang.String"},
+ []hessian.Object{"hello"},
+ }, map[string]any{
+ constant.GenericKey:
constant.GenericSerializationDefault,
+ }),
+ assertInvocation: func(t *testing.T, inv
base.Invocation) {
+ assert.Equal(t, "HelloVariadic",
inv.MethodName())
+ assert.Equal(t, []any{"hello", []string{}},
inv.Arguments())
+ },
+ },
+ {
+ name: "interface variadic keeps nil element",
+ inv: invocation.NewRPCInvocation(constant.Generic,
[]any{
+ "EchoVariadic",
+ []string{"java.lang.Object",
"java.lang.Object"},
+ []hessian.Object{nil, "tail"},
+ }, map[string]any{
+ constant.GenericKey:
constant.GenericSerializationDefault,
+ }),
+ assertInvocation: func(t *testing.T, inv
base.Invocation) {
+ assert.Equal(t, "EchoVariadic",
inv.MethodName())
+ assert.Equal(t, []any{[]any{nil, "tail"}},
inv.Arguments())
+ },
+ },
+ {
+ name: "interface variadic keeps a single nil element",
+ inv: invocation.NewRPCInvocation(constant.Generic,
[]any{
+ "EchoVariadic",
+ []string{"java.lang.Object"},
+ []hessian.Object{nil},
+ }, map[string]any{
+ constant.GenericKey:
constant.GenericSerializationDefault,
+ }),
+ assertInvocation: func(t *testing.T, inv
base.Invocation) {
+ assert.Equal(t, "EchoVariadic",
inv.MethodName())
+ assert.Equal(t, []any{[]any{nil}},
inv.Arguments())
+ },
+ },
+ {
+ name: "slice variadic keeps a single slice element",
+ inv: invocation.NewRPCInvocation(constant.Generic,
[]any{
+ "BytesVariadic",
+ []string{"[B"},
+ []hessian.Object{[]byte("tail")},
+ }, map[string]any{
+ constant.GenericKey:
constant.GenericSerializationDefault,
+ }),
+ assertInvocation: func(t *testing.T, inv
base.Invocation) {
+ assert.Equal(t, "BytesVariadic",
inv.MethodName())
+ assert.Equal(t,
[]any{[][]byte{[]byte("tail")}}, inv.Arguments())
+ },
+ },
+ {
+ name: "slice variadic unwraps packed values without
types",
+ inv: invocation.NewRPCInvocation(constant.Generic,
[]any{
+ "BytesVariadic",
+ nil,
+ []hessian.Object{[][]byte{[]byte("a"),
[]byte("b")}},
+ }, map[string]any{
+ constant.GenericKey:
constant.GenericSerializationDefault,
+ }),
+ assertInvocation: func(t *testing.T, inv
base.Invocation) {
+ assert.Equal(t, "BytesVariadic",
inv.MethodName())
+ assert.Equal(t, []any{[][]byte{[]byte("a"),
[]byte("b")}}, inv.Arguments())
+ },
+ },
+ {
+ name: "slice variadic unwraps nested byte descriptor",
+ inv: invocation.NewRPCInvocation(constant.Generic,
[]any{
+ "BytesVariadic",
+ []string{"[[B"},
+ []hessian.Object{[][]byte{[]byte("a"),
[]byte("b")}},
+ }, map[string]any{
+ constant.GenericKey:
constant.GenericSerializationDefault,
+ }),
+ assertInvocation: func(t *testing.T, inv
base.Invocation) {
+ assert.Equal(t, "BytesVariadic",
inv.MethodName())
+ assert.Equal(t, []any{[][]byte{[]byte("a"),
[]byte("b")}}, inv.Arguments())
+ },
+ },
+ {
+ name: "nested string variadic unwraps nested string
descriptor",
+ inv: invocation.NewRPCInvocation(constant.Generic,
[]any{
+ "NestedStringVariadic",
+ []string{"[[Ljava.lang.String;"},
+ []hessian.Object{[][]string{{"a", "b"}, {"c"}}},
+ }, map[string]any{
+ constant.GenericKey:
constant.GenericSerializationDefault,
+ }),
+ assertInvocation: func(t *testing.T, inv
base.Invocation) {
+ assert.Equal(t, "NestedStringVariadic",
inv.MethodName())
+ assert.Equal(t, []any{[][]string{{"a", "b"},
{"c"}}}, inv.Arguments())
+ },
+ },
+ {
+ name: "interface variadic unwraps a packed object
array",
+ inv: invocation.NewRPCInvocation(constant.Generic,
[]any{
+ "EchoVariadic",
+ []string{"[Ljava.lang.Object;"},
+ []hessian.Object{[]any{"alice", "bob"}},
+ }, map[string]any{
+ constant.GenericKey:
constant.GenericSerializationDefault,
+ }),
+ assertInvocation: func(t *testing.T, inv
base.Invocation) {
+ assert.Equal(t, "EchoVariadic",
inv.MethodName())
+ assert.Equal(t, []any{[]any{"alice", "bob"}},
inv.Arguments())
+ },
+ },
+ {
+ name: "interface variadic keeps packed nil object array
empty",
+ inv: invocation.NewRPCInvocation(constant.Generic,
[]any{
+ "EchoVariadic",
+ []string{"[Ljava.lang.Object;"},
+ []hessian.Object{nil},
+ }, map[string]any{
+ constant.GenericKey:
constant.GenericSerializationDefault,
+ }),
+ assertInvocation: func(t *testing.T, inv
base.Invocation) {
+ assert.Equal(t, "EchoVariadic",
inv.MethodName())
+ assert.Equal(t, []any{[]any{}}, inv.Arguments())
+ },
+ },
+ }
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ mockInvoker.EXPECT().Invoke(gomock.Any(),
gomock.Any()).DoAndReturn(
+ func(_ context.Context, inv base.Invocation)
result.Result {
+ marked, ok :=
inv.GetAttribute(constant.GenericVariadicCallSliceKey)
+ require.True(t, ok)
+ useCallSlice, ok := marked.(bool)
+ require.True(t, ok)
+ assert.True(t, useCallSlice)
+ tt.assertInvocation(t, inv)
+ return &result.RPCResult{}
+ },
+ ).Times(1)
+
+ invokeResult := filter.Invoke(context.Background(),
mockInvoker, tt.inv)
+ require.NoError(t, invokeResult.Error())
+ })
+ }
+}
diff --git a/protocol/triple/server.go b/protocol/triple/server.go
index 0b31f3281..90e72bd6d 100644
--- a/protocol/triple/server.go
+++ b/protocol/triple/server.go
@@ -644,32 +644,7 @@ func buildMethodInfoWithReflection(methodType
reflect.Method) *common.MethodInfo
return params
},
MethodFunc: func(ctx context.Context, args []any, handler any)
(any, error) {
- in := []reflect.Value{reflect.ValueOf(handler)}
- in = append(in, reflect.ValueOf(ctx))
- for _, arg := range args {
- in = append(in, reflect.ValueOf(arg))
- }
- returnValues := method.Func.Call(in)
- if len(returnValues) == 1 {
- if isReflectValueNil(returnValues[0]) {
- return nil, nil
- }
- if err, ok :=
returnValues[0].Interface().(error); ok {
- return nil, err
- }
- return nil, nil
- }
- var result any
- var err error
- if !isReflectValueNil(returnValues[0]) {
- result = returnValues[0].Interface()
- }
- if len(returnValues) > 1 &&
!isReflectValueNil(returnValues[1]) {
- if e, ok :=
returnValues[1].Interface().(error); ok {
- err = e
- }
- }
- return result, err
+ return callMethodByReflection(ctx, method, handler,
args)
},
}
}
@@ -700,6 +675,64 @@ func isReflectValueNil(v reflect.Value) bool {
}
}
+func callMethodByReflection(ctx context.Context, method reflect.Method,
handler any, args []any) (any, error) {
+ in := []reflect.Value{reflect.ValueOf(handler)}
+ in = append(in, reflect.ValueOf(ctx))
+ for _, arg := range args {
+ in = append(in, reflect.ValueOf(arg))
+ }
+
+ var returnValues []reflect.Value
+ if shouldUseGenericVariadicCallSlice(ctx, method, args) {
+ returnValues = method.Func.CallSlice(in)
+ } else {
+ returnValues = method.Func.Call(in)
+ }
+
+ if len(returnValues) == 1 {
+ if isReflectValueNil(returnValues[0]) {
+ return nil, nil
+ }
+ if err, ok := returnValues[0].Interface().(error); ok {
+ return nil, err
+ }
+ return nil, nil
+ }
+ var result any
+ var err error
+ if !isReflectValueNil(returnValues[0]) {
+ result = returnValues[0].Interface()
+ }
+ if len(returnValues) > 1 && !isReflectValueNil(returnValues[1]) {
+ if e, ok := returnValues[1].Interface().(error); ok {
+ err = e
+ }
+ }
+ return result, err
+}
+
+// shouldUseGenericVariadicCallSlice mirrors the ServiceInfo reflection gate
for
+// Triple's reflection-based method dispatch.
+func shouldUseGenericVariadicCallSlice(ctx context.Context, method
reflect.Method, args []any) bool {
+ if !method.Type.IsVariadic() || len(args) == 0 || len(args) !=
method.Type.NumIn()-2 {
+ return false
+ }
+
+ value, ok :=
ctx.Value(constant.DubboCtxKey(constant.GenericVariadicCallSliceKey)).(bool)
+ if !ok || !value {
+ return false
+ }
+
+ lastArg := args[len(args)-1]
+ if lastArg == nil {
+ return false
+ }
+
+ lastArgType := reflect.TypeOf(lastArg)
+ variadicSliceType := method.Type.In(method.Type.NumIn() - 1)
+ return lastArgType.AssignableTo(variadicSliceType) ||
lastArgType.ConvertibleTo(variadicSliceType)
+}
+
// generateAttachments transfer http.Header to map[string]any and make all
keys lowercase
func generateAttachments(header http.Header) map[string]any {
attachments := make(map[string]any, len(header))
diff --git a/protocol/triple/server_test.go b/protocol/triple/server_test.go
index b42e19d00..508b21118 100644
--- a/protocol/triple/server_test.go
+++ b/protocol/triple/server_test.go
@@ -922,6 +922,12 @@ func newServerForMethodHandlerTest() *Server {
return &Server{triServer: tri.NewServer("127.0.0.1:0", nil)}
}
+type tripleVariadicReflectionServiceForTest struct{}
+
+func (s *tripleVariadicReflectionServiceForTest) HelloVariadic(ctx
context.Context, prefix string, names ...string) (string, error) {
+ return prefix + ":" + fmt.Sprint(len(names)), nil
+}
+
func TestExtractUnaryInvocationArgs(t *testing.T) {
t.Run("from non-idl argument slice", func(t *testing.T) {
name := "alice"
@@ -937,6 +943,28 @@ func TestExtractUnaryInvocationArgs(t *testing.T) {
})
}
+func TestBuildMethodInfoWithReflectionVariadic(t *testing.T) {
+ svc := &tripleVariadicReflectionServiceForTest{}
+ method, ok := reflect.TypeOf(svc).MethodByName("HelloVariadic")
+ require.True(t, ok)
+
+ methodInfo := buildMethodInfoWithReflection(method)
+ require.NotNil(t, methodInfo)
+
+ t.Run("generic packed variadic tail uses call slice", func(t
*testing.T) {
+ ctx := context.WithValue(context.Background(),
constant.DubboCtxKey(constant.GenericVariadicCallSliceKey), true)
+ res, err := methodInfo.MethodFunc(ctx, []any{"hello",
[]string{"alice", "bob"}}, svc)
+ require.NoError(t, err)
+ assert.Equal(t, "hello:2", res)
+ })
+
+ t.Run("ordinary discrete variadic call remains unchanged", func(t
*testing.T) {
+ res, err := methodInfo.MethodFunc(context.Background(),
[]any{"hello", "alice", "bob"}, svc)
+ require.NoError(t, err)
+ assert.Equal(t, "hello:2", res)
+ })
+}
+
func TestWrapTripleResponse(t *testing.T) {
resp := tri.NewResponse("already-wrapped")
assert.Same(t, resp, wrapTripleResponse(resp))
diff --git a/proxy/proxy_factory/default.go b/proxy/proxy_factory/default.go
index b74e5bef7..cc52569a4 100644
--- a/proxy/proxy_factory/default.go
+++ b/proxy/proxy_factory/default.go
@@ -130,7 +130,8 @@ func (pi *ProxyInvoker) Invoke(ctx context.Context,
invocation base.Invocation)
}
// prepare argv
- if (len(method.ArgsType()) == 1 || len(method.ArgsType()) == 2 &&
method.ReplyType() == nil) && method.ArgsType()[0].String() == "[]interface {}"
{
+ useCallSlice := shouldUseGenericVariadicCallSlice(invocation, method,
args)
+ if !useCallSlice && (len(method.ArgsType()) == 1 ||
len(method.ArgsType()) == 2 && method.ReplyType() == nil) &&
method.ArgsType()[0].String() == "[]interface {}" {
in = append(in, reflect.ValueOf(args))
} else {
for i := 0; i < len(args); i++ {
@@ -150,7 +151,7 @@ func (pi *ProxyInvoker) Invoke(ctx context.Context,
invocation base.Invocation)
var replyv reflect.Value
var retErr any
- returnValues, callErr := callLocalMethod(method.Method(), in)
+ returnValues, callErr := callLocalMethod(method.Method(), in,
useCallSlice)
if callErr != nil {
logger.Errorf("Invoke function error: %+v, service: %#v",
callErr, url)
@@ -176,6 +177,33 @@ func (pi *ProxyInvoker) Invoke(ctx context.Context,
invocation base.Invocation)
return result
}
+// shouldUseGenericVariadicCallSlice only enables CallSlice for the generic
variadic path
+// after the filter has already packed the variadic tail into the declared
slice type.
+func shouldUseGenericVariadicCallSlice(invocation base.Invocation, method
*common.MethodType, args []any) bool {
+ if !method.IsVariadic() || len(args) != len(method.ArgsType()) ||
len(args) == 0 {
+ return false
+ }
+
+ value, ok :=
invocation.GetAttribute(constant.GenericVariadicCallSliceKey)
+ if !ok {
+ return false
+ }
+
+ useCallSlice, ok := value.(bool)
+ if !ok || !useCallSlice {
+ return false
+ }
+
+ lastArg := args[len(args)-1]
+ if lastArg == nil {
+ return true
+ }
+
+ lastArgType := reflect.TypeOf(lastArg)
+ variadicSliceType := method.ArgsType()[len(method.ArgsType())-1]
+ return lastArgType.AssignableTo(variadicSliceType) ||
lastArgType.ConvertibleTo(variadicSliceType)
+}
+
func getProviderURL(url *common.URL) *common.URL {
if url.SubURL == nil {
return url
@@ -203,6 +231,13 @@ func (tpi *infoProxyInvoker) Invoke(ctx context.Context,
invocation base.Invocat
args := invocation.Arguments()
result := new(result.RPCResult)
if method, ok := tpi.methodMap[name]; ok {
+ // ServiceInfo reflection paths only receive ctx/args, so carry
the generic
+ // variadic marker through ctx for the downstream CallSlice
check.
+ if marked, ok :=
invocation.GetAttribute(constant.GenericVariadicCallSliceKey); ok {
+ if useCallSlice, ok := marked.(bool); ok &&
useCallSlice {
+ ctx = context.WithValue(ctx,
constant.DubboCtxKey(constant.GenericVariadicCallSliceKey), true)
+ }
+ }
res, err := method.MethodFunc(ctx, args, tpi.svc)
result.SetResult(res)
if err != nil {
diff --git a/proxy/proxy_factory/default_test.go
b/proxy/proxy_factory/default_test.go
index 28f48ef9e..6c7c300a7 100644
--- a/proxy/proxy_factory/default_test.go
+++ b/proxy/proxy_factory/default_test.go
@@ -18,17 +18,21 @@
package proxy_factory
import (
+ "context"
"fmt"
"testing"
)
import (
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
import (
"dubbo.apache.org/dubbo-go/v3/common"
+ "dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/protocol/base"
+ "dubbo.apache.org/dubbo-go/v3/protocol/invocation"
)
func TestGetProxy(t *testing.T) {
@@ -58,3 +62,31 @@ func TestGetInvoker(t *testing.T) {
invoker := proxyFactory.GetInvoker(url)
assert.True(t, invoker.IsAvailable())
}
+
+func TestInfoProxyInvoker_InvokePropagatesGenericVariadicMarker(t *testing.T) {
+ info := &common.ServiceInfo{
+ Methods: []common.MethodInfo{
+ {
+ Name: "HelloVariadic",
+ MethodFunc: func(ctx context.Context, args
[]any, handler any) (any, error) {
+ marked, ok :=
ctx.Value(constant.DubboCtxKey(constant.GenericVariadicCallSliceKey)).(bool)
+ require.True(t, ok)
+ assert.True(t, marked)
+ assert.Equal(t, []any{"hello",
[]string{"alice", "bob"}}, args)
+ return "ok", nil
+ },
+ },
+ },
+ }
+
+ invoker := newInfoInvoker(common.NewURLWithOptions(), info, struct{}{})
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName("HelloVariadic"),
+ invocation.WithArguments([]any{"hello", []string{"alice",
"bob"}}),
+ )
+ inv.SetAttribute(constant.GenericVariadicCallSliceKey, true)
+
+ res := invoker.Invoke(context.Background(), inv)
+ require.NoError(t, res.Error())
+ assert.Equal(t, "ok", res.Result())
+}
diff --git a/proxy/proxy_factory/invoker_test.go
b/proxy/proxy_factory/invoker_test.go
index 5757f6e53..e71791c85 100644
--- a/proxy/proxy_factory/invoker_test.go
+++ b/proxy/proxy_factory/invoker_test.go
@@ -43,6 +43,14 @@ func (s *ProxyInvokerService) Hello(_ context.Context, name
string) (string, err
return "hello:" + name, nil
}
+func (s *ProxyInvokerService) EchoVariadic(args ...any) ([]any, error) {
+ return args, nil
+}
+
+func (s *ProxyInvokerService) CountByteSlices(args ...[]byte) (int, error) {
+ return len(args), nil
+}
+
type PassThroughService struct{}
func (s *PassThroughService) Service(method string, argTypes []string, args
[][]byte, attachments map[string]any) (any, error) {
@@ -148,3 +156,36 @@ func TestPassThroughProxyInvoker_Invoke(t *testing.T) {
assert.EqualError(t, result.Error(), "the param type is not
[]byte")
})
}
+
+func TestProxyInvoker_InvokeVariadicCallSliceGating(t *testing.T) {
+ const (
+ protocol = "test-variadic-protocol"
+ interfaceName = "ProxyInvokerVariadicService"
+ )
+ registerService(t, protocol, interfaceName, &ProxyInvokerService{})
+ u := newURL(protocol, interfaceName)
+ invoker := &ProxyInvoker{BaseInvoker: *base.NewBaseInvoker(u)}
+
+ t.Run("generic variadic marker expands packed slice", func(t
*testing.T) {
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName("EchoVariadic"),
+ invocation.WithArguments([]any{[]any{"alice", "bob"}}),
+ )
+ inv.SetAttribute(constant.GenericVariadicCallSliceKey, true)
+
+ res := invoker.Invoke(context.Background(), inv)
+ require.NoError(t, res.Error())
+ assert.Equal(t, []any{"alice", "bob"}, res.Result())
+ })
+
+ t.Run("ordinary slice variadic element does not trigger call slice",
func(t *testing.T) {
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName("CountByteSlices"),
+ invocation.WithArguments([]any{[]byte("alice")}),
+ )
+
+ res := invoker.Invoke(context.Background(), inv)
+ require.NoError(t, res.Error())
+ assert.Equal(t, 1, res.Result())
+ })
+}
diff --git a/proxy/proxy_factory/pass_through.go
b/proxy/proxy_factory/pass_through.go
index 21083ac6b..edc24d0fc 100644
--- a/proxy/proxy_factory/pass_through.go
+++ b/proxy/proxy_factory/pass_through.go
@@ -112,7 +112,7 @@ func (pi *PassThroughProxyInvoker) Invoke(ctx
context.Context, invocation base.I
var replyv reflect.Value
var retErr any
- returnValues, callErr := callLocalMethod(method.Method(), in)
+ returnValues, callErr := callLocalMethod(method.Method(), in, false)
if callErr != nil {
logger.Errorf("Invoke function error: %+v, service: %#v",
callErr, url)
diff --git a/proxy/proxy_factory/utils.go b/proxy/proxy_factory/utils.go
index 44af7d28c..1d5a7dc13 100644
--- a/proxy/proxy_factory/utils.go
+++ b/proxy/proxy_factory/utils.go
@@ -26,8 +26,9 @@ import (
perrors "github.com/pkg/errors"
)
-// CallLocalMethod is used to handle invoke exception in user func.
-func callLocalMethod(method reflect.Method, in []reflect.Value)
([]reflect.Value, error) {
+// callLocalMethod invokes a local method and recovers panics.
+// useCallSlice is reserved for generic calls that already carry a packed
variadic tail.
+func callLocalMethod(method reflect.Method, in []reflect.Value, useCallSlice
bool) ([]reflect.Value, error) {
var (
returnValues []reflect.Value
retErr error
@@ -46,7 +47,11 @@ func callLocalMethod(method reflect.Method, in
[]reflect.Value) ([]reflect.Value
}
}()
- returnValues = method.Func.Call(in)
+ if useCallSlice {
+ returnValues = method.Func.CallSlice(in)
+ } else {
+ returnValues = method.Func.Call(in)
+ }
}()
if retErr != nil {
diff --git a/proxy/proxy_factory/utils_test.go
b/proxy/proxy_factory/utils_test.go
index 4afd7fdc3..95d8bccba 100644
--- a/proxy/proxy_factory/utils_test.go
+++ b/proxy/proxy_factory/utils_test.go
@@ -45,19 +45,25 @@ func (s *callLocalMethodSample) PanicUnknown() {
panic(123)
}
+func (s *callLocalMethodSample) EchoVariadic(args ...any) []any {
+ return args
+}
+
func TestCallLocalMethod(t *testing.T) {
sample := &callLocalMethodSample{}
cases := []struct {
- name string
- method string
- in []reflect.Value
- assertErr func(t *testing.T, err error)
- assertOut func(t *testing.T, out []reflect.Value)
+ name string
+ method string
+ in []reflect.Value
+ useCallSlice bool
+ assertErr func(t *testing.T, err error)
+ assertOut func(t *testing.T, out []reflect.Value)
}{
{
- name: "call success",
- method: "Sum",
- in: []reflect.Value{reflect.ValueOf(sample),
reflect.ValueOf(1), reflect.ValueOf(2)},
+ name: "call success",
+ method: "Sum",
+ in: []reflect.Value{reflect.ValueOf(sample),
reflect.ValueOf(1), reflect.ValueOf(2)},
+ useCallSlice: false,
assertErr: func(t *testing.T, err error) {
assert.NoError(t, err)
},
@@ -67,29 +73,58 @@ func TestCallLocalMethod(t *testing.T) {
},
},
{
- name: "panic with error",
- method: "PanicError",
- in: []reflect.Value{reflect.ValueOf(sample)},
+ name: "panic with error",
+ method: "PanicError",
+ in: []reflect.Value{reflect.ValueOf(sample)},
+ useCallSlice: false,
assertErr: func(t *testing.T, err error) {
assert.EqualError(t, err, "boom")
},
},
{
- name: "panic with string",
- method: "PanicString",
- in: []reflect.Value{reflect.ValueOf(sample)},
+ name: "panic with string",
+ method: "PanicString",
+ in: []reflect.Value{reflect.ValueOf(sample)},
+ useCallSlice: false,
assertErr: func(t *testing.T, err error) {
assert.EqualError(t, err, "boom str")
},
},
{
- name: "panic with unknown type",
- method: "PanicUnknown",
- in: []reflect.Value{reflect.ValueOf(sample)},
+ name: "panic with unknown type",
+ method: "PanicUnknown",
+ in: []reflect.Value{reflect.ValueOf(sample)},
+ useCallSlice: false,
assertErr: func(t *testing.T, err error) {
assert.EqualError(t, err, "invoke function
error, unknow exception: 123")
},
},
+ {
+ name: "variadic slice stays packed without call
slice",
+ method: "EchoVariadic",
+ in: []reflect.Value{reflect.ValueOf(sample),
reflect.ValueOf([]any{"alice", "bob"})},
+ useCallSlice: false,
+ assertErr: func(t *testing.T, err error) {
+ assert.NoError(t, err)
+ },
+ assertOut: func(t *testing.T, out []reflect.Value) {
+ assert.Len(t, out, 1)
+ assert.Equal(t, []any{[]any{"alice", "bob"}},
out[0].Interface())
+ },
+ },
+ {
+ name: "variadic slice expands with call slice",
+ method: "EchoVariadic",
+ in: []reflect.Value{reflect.ValueOf(sample),
reflect.ValueOf([]any{"alice", "bob"})},
+ useCallSlice: true,
+ assertErr: func(t *testing.T, err error) {
+ assert.NoError(t, err)
+ },
+ assertOut: func(t *testing.T, out []reflect.Value) {
+ assert.Len(t, out, 1)
+ assert.Equal(t, []any{"alice", "bob"},
out[0].Interface())
+ },
+ },
}
for _, tt := range cases {
@@ -98,7 +133,7 @@ func TestCallLocalMethod(t *testing.T) {
if !ok {
t.Fatalf("method %s not found", tt.method)
}
- out, err := callLocalMethod(m, tt.in)
+ out, err := callLocalMethod(m, tt.in, tt.useCallSlice)
if tt.assertErr != nil {
tt.assertErr(t, err)
}
diff --git a/server/server.go b/server/server.go
index eacb0c391..cff53f39f 100644
--- a/server/server.go
+++ b/server/server.go
@@ -204,7 +204,12 @@ func CallMethodByReflection(ctx context.Context, method
reflect.Method, handler
for _, arg := range args {
in = append(in, reflect.ValueOf(arg))
}
- returnValues := method.Func.Call(in)
+ var returnValues []reflect.Value
+ if shouldUseGenericVariadicCallSlice(ctx, method, args) {
+ returnValues = method.Func.CallSlice(in)
+ } else {
+ returnValues = method.Func.Call(in)
+ }
// Process return values
if len(returnValues) == 1 {
@@ -229,6 +234,28 @@ func CallMethodByReflection(ctx context.Context, method
reflect.Method, handler
return result, err
}
+// shouldUseGenericVariadicCallSlice is the ServiceInfo reflection-side gate
for
+// generic variadic calls whose tail has already been packed into the declared
slice type.
+func shouldUseGenericVariadicCallSlice(ctx context.Context, method
reflect.Method, args []any) bool {
+ if !method.Type.IsVariadic() || len(args) == 0 || len(args) !=
method.Type.NumIn()-2 {
+ return false
+ }
+
+ value, ok :=
ctx.Value(constant.DubboCtxKey(constant.GenericVariadicCallSliceKey)).(bool)
+ if !ok || !value {
+ return false
+ }
+
+ lastArg := args[len(args)-1]
+ if lastArg == nil {
+ return false
+ }
+
+ lastArgType := reflect.TypeOf(lastArg)
+ variadicSliceType := method.Type.In(method.Type.NumIn() - 1)
+ return lastArgType.AssignableTo(variadicSliceType) ||
lastArgType.ConvertibleTo(variadicSliceType)
+}
+
// createReflectionMethodFunc creates a MethodFunc that calls the given method
via reflection.
func createReflectionMethodFunc(method reflect.Method) func(ctx
context.Context, args []any, handler any) (any, error) {
return func(ctx context.Context, args []any, handler any) (any, error) {
diff --git a/server/server_test.go b/server/server_test.go
index edced826a..9a4e309a7 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -33,6 +33,7 @@ import (
import (
"dubbo.apache.org/dubbo-go/v3/common"
+ "dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/global"
)
@@ -315,6 +316,12 @@ func (g *greetServiceForTest) Greet(ctx context.Context,
req string) (string, er
func (g *greetServiceForTest) Reference() string { return
"greetServiceForTest" }
+type variadicReflectionServiceForTest struct{}
+
+func (s *variadicReflectionServiceForTest) HelloVariadic(ctx context.Context,
prefix string, names ...string) (string, error) {
+ return prefix + ":" + strconv.Itoa(len(names)), nil
+}
+
// TestEnhanceServiceInfoMethodFuncBackfillExactName verifies that
// enhanceServiceInfo fills in MethodFunc when the ServiceInfo method name
// matches the Go exported method name exactly (PascalCase).
@@ -353,6 +360,25 @@ func
TestEnhanceServiceInfoMethodFuncBackfillJavaStyleName(t *testing.T) {
"MethodFunc must be found via swapped-case lookup to avoid
nil-func panic on lowercase-first method names")
}
+func TestCallMethodByReflectionVariadic(t *testing.T) {
+ svc := &variadicReflectionServiceForTest{}
+ method, ok := reflect.TypeOf(svc).MethodByName("HelloVariadic")
+ require.True(t, ok)
+
+ t.Run("generic packed variadic tail uses call slice", func(t
*testing.T) {
+ ctx := context.WithValue(context.Background(),
constant.DubboCtxKey(constant.GenericVariadicCallSliceKey), true)
+ res, err := CallMethodByReflection(ctx, method, svc,
[]any{"hello", []string{"alice", "bob"}})
+ require.NoError(t, err)
+ assert.Equal(t, "hello:2", res)
+ })
+
+ t.Run("ordinary discrete variadic call remains unchanged", func(t
*testing.T) {
+ res, err := CallMethodByReflection(context.Background(),
method, svc, []any{"hello", "alice", "bob"})
+ require.NoError(t, err)
+ assert.Equal(t, "hello:2", res)
+ })
+}
+
// Test getMetadataPort with default protocol
func TestGetMetadataPortWithDefaultProtocol(t *testing.T) {
opts := defaultServerOptions()