Skip to content

Commit a20282b

Browse files
committed
feat(triple): support generic call for Triple protocol
- Add protoWrapperCodec for client-side generic call serialization - Add WrapperCodec interface for wrapper-based codecs - Enhance protoBinaryCodec to handle TripleRequestWrapper/TripleResponseWrapper - Add WithGeneric() client option to enable generic call mode - Auto-register $invoke method on server side for generic call handling - Add reflection-based method invocation in server - Enhance Go-to-Java type mapping in generic filter - Support hessian2 serialization for cross-language interoperability This enables dubbo-go to make generic calls to Java Dubbo services using Triple protocol without pre-generated stubs. Tested with: - Go client -> Java server: PASS - Java client -> Go server: PASS - Go client -> Go server: PASS Signed-off-by: TsukiKage <[email protected]>
1 parent 489ee6a commit a20282b

File tree

11 files changed

+1008
-69
lines changed

11 files changed

+1008
-69
lines changed

client/options.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,14 @@ func WithGeneric() ReferenceOption {
382382
}
383383
}
384384

385+
// WithGenericType sets the generic serialization type for generic call
386+
// Valid values: "true" (default), "gson", "protobuf"
387+
func WithGenericType(genericType string) ReferenceOption {
388+
return func(opts *ReferenceOptions) {
389+
opts.Reference.Generic = genericType
390+
}
391+
}
392+
385393
func WithSticky() ReferenceOption {
386394
return func(opts *ReferenceOptions) {
387395
opts.Reference.Sticky = true

common/constant/key.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -413,8 +413,10 @@ const (
413413

414414
// Generic Filter
415415
const (
416-
GenericSerializationDefault = "true"
417-
GenericSerializationGson = "gson"
416+
GenericSerializationDefault = "true"
417+
GenericSerializationGson = "gson"
418+
GenericSerializationProtobuf = "protobuf"
419+
GenericSerializationProtobufJson = "protobuf-json"
418420
)
419421

420422
// AdaptiveService Filter

filter/generic/filter.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,31 @@ func (f *genericFilter) Invoke(ctx context.Context, invoker base.Invoker, inv ba
9595
types,
9696
args,
9797
}
98-
newIvc := invocation.NewRPCInvocation(constant.Generic, newArgs, inv.Attachments())
99-
newIvc.SetReply(inv.Reply())
98+
99+
// For Triple protocol non-IDL mode, we need to set parameterRawValues
100+
// The format is [param1, param2, ..., paramN, reply] where the last element is the reply placeholder
101+
// Triple invoker slices as: request = inRaw[0:len-1], reply = inRaw[len-1]
102+
// So for generic call, we need [methodName, types, args, reply] to get request = [methodName, types, args]
103+
reply := inv.Reply()
104+
parameterRawValues := []any{mtdName, types, args, reply}
105+
106+
newIvc := invocation.NewRPCInvocationWithOptions(
107+
invocation.WithMethodName(constant.Generic),
108+
invocation.WithArguments(newArgs),
109+
invocation.WithParameterRawValues(parameterRawValues),
110+
invocation.WithAttachments(inv.Attachments()),
111+
invocation.WithReply(reply),
112+
)
100113
newIvc.Attachments()[constant.GenericKey] = invoker.GetURL().GetParam(constant.GenericKey, "")
101114

115+
// Copy CallType attribute from original invocation for Triple protocol support
116+
// If not present, set default to CallUnary for generic calls
117+
if callType, ok := inv.GetAttribute(constant.CallTypeKey); ok {
118+
newIvc.SetAttribute(constant.CallTypeKey, callType)
119+
} else {
120+
newIvc.SetAttribute(constant.CallTypeKey, constant.CallUnary)
121+
}
122+
102123
return invoker.Invoke(ctx, newIvc)
103124
} else if isMakingAGenericCall(invoker, inv) {
104125
inv.Attachments()[constant.GenericKey] = invoker.GetURL().GetParam(constant.GenericKey, "")

protocol/triple/client.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,18 @@ func newClientManager(url *common.URL) (*clientManager, error) {
284284

285285
triClients := make(map[string]*tri.Client)
286286

287-
if len(url.Methods) != 0 {
287+
// Check if this is a generic call - for generic call, we only need $invoke method
288+
generic := url.GetParam(constant.GenericKey, "")
289+
isGeneric := isGenericCall(generic)
290+
291+
if isGeneric {
292+
// For generic call, only register $invoke method
293+
invokeURL, err := joinPath(baseTriURL, url.Interface(), constant.Generic)
294+
if err != nil {
295+
return nil, fmt.Errorf("JoinPath failed for base %s, interface %s, method %s", baseTriURL, url.Interface(), constant.Generic)
296+
}
297+
triClients[constant.Generic] = tri.NewClient(httpClient, invokeURL, cliOpts...)
298+
} else if len(url.Methods) != 0 {
288299
for _, method := range url.Methods {
289300
triURL, err := joinPath(baseTriURL, url.Interface(), method)
290301
if err != nil {
@@ -312,6 +323,13 @@ func newClientManager(url *common.URL) (*clientManager, error) {
312323
triClient := tri.NewClient(httpClient, triURL, cliOpts...)
313324
triClients[methodName] = triClient
314325
}
326+
327+
// Register $invoke method for generic call support in non-IDL mode
328+
invokeURL, err := joinPath(baseTriURL, url.Interface(), constant.Generic)
329+
if err != nil {
330+
return nil, fmt.Errorf("JoinPath failed for base %s, interface %s, method %s", baseTriURL, url.Interface(), constant.Generic)
331+
}
332+
triClients[constant.Generic] = tri.NewClient(httpClient, invokeURL, cliOpts...)
315333
}
316334

317335
return &clientManager{

protocol/triple/server.go

Lines changed: 71 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -503,51 +503,85 @@ func createServiceInfoWithReflection(svc common.RPCService) *common.ServiceInfo
503503
if methodType.Name == "Reference" {
504504
continue
505505
}
506-
paramsNum := methodType.Type.NumIn()
507-
// the first param is receiver itself, the second param is ctx
508-
// just ignore them
509-
if paramsNum < 2 {
510-
logger.Fatalf("TRIPLE does not support %s method that does not have any parameter", methodType.Name)
511-
continue
512-
}
513-
paramsTypes := make([]reflect.Type, paramsNum-2)
514-
for j := 2; j < paramsNum; j++ {
515-
paramsTypes[j-2] = methodType.Type.In(j)
516-
}
517-
methodInfo := common.MethodInfo{
518-
Name: methodType.Name,
519-
// only support Unary invocation now
520-
Type: constant.CallUnary,
521-
ReqInitFunc: func() any {
522-
params := make([]any, len(paramsTypes))
523-
for k, paramType := range paramsTypes {
524-
params[k] = reflect.New(paramType).Interface()
525-
}
526-
return params
527-
},
506+
methodInfo := buildMethodInfoWithReflection(methodType)
507+
if methodInfo != nil {
508+
methodInfos = append(methodInfos, *methodInfo)
528509
}
529-
methodInfos = append(methodInfos, methodInfo)
530510
}
531511

532-
// only support no-idl mod call unary
533-
genericMethodInfo := common.MethodInfo{
534-
Name: "$invoke",
535-
Type: constant.CallUnary,
512+
// Add $invoke method for generic call support
513+
methodInfos = append(methodInfos, buildGenericMethodInfo())
514+
515+
info.Methods = methodInfos
516+
return &info
517+
}
518+
519+
// buildMethodInfoWithReflection creates MethodInfo for a single method using reflection.
520+
func buildMethodInfoWithReflection(methodType reflect.Method) *common.MethodInfo {
521+
paramsNum := methodType.Type.NumIn()
522+
// the first param is receiver itself, the second param is ctx
523+
if paramsNum < 2 {
524+
logger.Fatalf("TRIPLE does not support %s method that does not have any parameter", methodType.Name)
525+
return nil
526+
}
527+
528+
// Extract parameter types (skip receiver and context)
529+
paramsTypes := make([]reflect.Type, paramsNum-2)
530+
for j := 2; j < paramsNum; j++ {
531+
paramsTypes[j-2] = methodType.Type.In(j)
532+
}
533+
534+
// Capture method for closure
535+
method := methodType
536+
return &common.MethodInfo{
537+
Name: methodType.Name,
538+
Type: constant.CallUnary, // only support Unary invocation now
536539
ReqInitFunc: func() any {
537-
params := make([]any, 3)
538-
// params must be pointer
539-
params[0] = func(s string) *string { return &s }("methodName") // methodName *string
540-
params[1] = &[]string{} // argv type *[]string
541-
params[2] = &[]hessian.Object{} // argv *[]hessian.Object
540+
params := make([]any, len(paramsTypes))
541+
for k, paramType := range paramsTypes {
542+
params[k] = reflect.New(paramType).Interface()
543+
}
542544
return params
543545
},
546+
MethodFunc: func(ctx context.Context, args []any, handler any) (any, error) {
547+
in := []reflect.Value{reflect.ValueOf(handler)}
548+
in = append(in, reflect.ValueOf(ctx))
549+
for _, arg := range args {
550+
in = append(in, reflect.ValueOf(arg))
551+
}
552+
returnValues := method.Func.Call(in)
553+
if len(returnValues) == 1 {
554+
if returnValues[0].IsNil() {
555+
return nil, nil
556+
}
557+
return nil, returnValues[0].Interface().(error)
558+
}
559+
var result any
560+
var err error
561+
if !returnValues[0].IsNil() {
562+
result = returnValues[0].Interface()
563+
}
564+
if len(returnValues) > 1 && !returnValues[1].IsNil() {
565+
err = returnValues[1].Interface().(error)
566+
}
567+
return result, err
568+
},
544569
}
570+
}
545571

546-
methodInfos = append(methodInfos, genericMethodInfo)
547-
548-
info.Methods = methodInfos
549-
550-
return &info
572+
// buildGenericMethodInfo creates MethodInfo for $invoke generic call method.
573+
func buildGenericMethodInfo() common.MethodInfo {
574+
return common.MethodInfo{
575+
Name: constant.Generic,
576+
Type: constant.CallUnary,
577+
ReqInitFunc: func() any {
578+
return []any{
579+
func(s string) *string { return &s }(""), // methodName *string
580+
&[]string{}, // types *[]string
581+
&[]hessian.Object{}, // args *[]hessian.Object
582+
}
583+
},
584+
}
551585
}
552586

553587
// generateAttachments transfer http.Header to map[string]any and make all keys lowercase

protocol/triple/triple.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package triple
1919

2020
import (
2121
"context"
22+
"strings"
2223
"sync"
2324
)
2425

@@ -110,9 +111,16 @@ func (tp *TripleProtocol) Refer(url *common.URL) base.Invoker {
110111
IDLMode := url.GetParam(constant.IDLMode, "")
111112
// for now, we do not need to use this info
112113
_, ok := url.GetAttribute(constant.ClientInfoKey)
113-
// isIDL is NONIDL means new triple non-IDL mode
114-
if ok || IDLMode == constant.NONIDL {
115-
// stub code generated by new protoc-gen-go-triple
114+
// Check if this is a generic call
115+
generic := url.GetParam(constant.GenericKey, "")
116+
isGenericCall := isGenericCall(generic)
117+
118+
// Use NewTripleInvoker for:
119+
// 1. New protoc-gen-go-triple stub code (has ClientInfoKey)
120+
// 2. Non-IDL mode (IDLMode == NONIDL)
121+
// 3. Generic call (generic=true/gson/protobuf/protobuf-json)
122+
if ok || IDLMode == constant.NONIDL || isGenericCall {
123+
// new triple invoker supporting $invoke for generic calls
116124
invoker, err = NewTripleInvoker(url)
117125
} else {
118126
// stub code generated by old protoc-gen-go-triple
@@ -141,6 +149,18 @@ func (tp *TripleProtocol) Destroy() {
141149
tp.BaseProtocol.Destroy()
142150
}
143151

152+
// isGenericCall checks if the generic parameter indicates a generic call
153+
func isGenericCall(generic string) bool {
154+
if generic == "" {
155+
return false
156+
}
157+
lowerGeneric := strings.ToLower(generic)
158+
return lowerGeneric == constant.GenericSerializationDefault ||
159+
lowerGeneric == constant.GenericSerializationGson ||
160+
lowerGeneric == constant.GenericSerializationProtobuf ||
161+
lowerGeneric == constant.GenericSerializationProtobufJson
162+
}
163+
144164
func NewTripleProtocol() *TripleProtocol {
145165
return &TripleProtocol{
146166
BaseProtocol: base.NewBaseProtocol(),

0 commit comments

Comments
 (0)