This is an automated email from the ASF dual-hosted git repository.

Alanxtl pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/dubbo-go-samples.git


The following commit(s) were added to refs/heads/main by this push:
     new 0060392c Feat/generic invocation test (#1144)
0060392c is described below

commit 0060392cb0d733ba63c95109db642d847ac28968
Author: 翎 <[email protected]>
AuthorDate: Wed Sep 2 10:17:13 2026 +0800

    Feat/generic invocation test (#1144)
    
    * test(generic): cover typed generic service modes
    
    * docs(generic): document generic mode behavior
    
    * fix(generic): handle gson result in runtime checks
    
    * fix(generic): avoid shadowing invoke error
    
    * fix(generic): validate gson results by provider
    
    * fix(generic): use supported DTO for bean results
    
    * fix(generic): encode gson provider result as JSON
    
    * fix(generic): accept gson map fallback
---
 generic/README.md                      |  18 +++
 generic/README_zh.md                   |  18 +++
 generic/go-client/cmd/client.go        | 225 ++++++++++++++++++++++++++++-----
 generic/go-server/pkg/user_provider.go |  18 ++-
 4 files changed, 249 insertions(+), 30 deletions(-)

diff --git a/generic/README.md b/generic/README.md
index 8c7621ac..4e9553d8 100644
--- a/generic/README.md
+++ b/generic/README.md
@@ -33,6 +33,23 @@ go run .
 
 The client passes `client.WithURL("tri://127.0.0.1:50052")` to 
`cli.NewGenericService(...)` for a per-service direct connection and performs 
generic calls through that generic service.
 
+## Generic mode runtime checks
+
+The Go client uses `client.WithGenericType(...)` to select the generic format. 
This is independent from `client.WithSerialization(...)`, which selects the 
transport encoding on the wire.
+
+| Generic mode | Meaning | Runtime sample coverage |
+|--------------|---------|-------------------------|
+| `true` | Map-based generic result (default) | Complete `User` check, 
including `Time` |
+| `gson` | JSON generic result | JSON shape check with an accepted Map 
fallback |
+| `bean` | JavaBean descriptor result | Typed DTO check for `ID`, `Name`, and 
`Age` |
+| `protobuf-json` | Protobuf JSON result | Not used by the current Hessian 
POJO service |
+| `protobuf` | Legacy compatibility alias | Preserved for compatibility |
+| `false` or empty | Disable generic invocation | No generic call is made |
+
+When a `gson` result is a JSON string, it must decode to a complete `User` 
(`ID`, `Name`, `Age`, and `Time`). A Hessian Map fallback from either provider 
is accepted with an explicit warning. The `true` mode also checks every 
observable `User` field, including `Time`.
+
+The Bean generalizer represents exported bean properties but cannot round-trip 
the unexported state inside Go's `time.Time`. The `bean` mode therefore uses an 
explicit DTO containing the supported `ID`, `Name`, and `Age` fields instead of 
accepting a partially populated `User`. The client also confirms that an 
unknown mode is rejected. These are runtime sample checks rather than `go test` 
unit tests. The current `User` service is a Hessian POJO rather than a 
`proto.Message`, so `protobuf [...]
+
 ## Run the Java Server
 
 Build and run from the java-server directory:
@@ -89,3 +106,4 @@ All generic call tests completed
 - Neither the Go server nor the Java server requires ZooKeeper; both listen 
directly on their configured ports.
 - The Java client uses direct connection via `tri://127.0.0.1:50052` 
(`reference.setUrl(...)`).
 - The Go client uses direct connection via `tri://127.0.0.1:50052`.
+- Unknown generic modes fail during service creation instead of silently 
falling back to Map.
diff --git a/generic/README_zh.md b/generic/README_zh.md
index 1f3fd1e2..f76d39b6 100644
--- a/generic/README_zh.md
+++ b/generic/README_zh.md
@@ -33,6 +33,23 @@ go run .
 
 客户端在 `cli.NewGenericService(...)` 处通过 
`client.WithURL("tri://127.0.0.1:50052")` 为该服务单独配置直连地址,并基于该泛化服务发起调用。
 
+## 泛化模式运行时检查
+
+Go 客户端使用 `client.WithGenericType(...)` 选择泛化格式。该配置与 
`client.WithSerialization(...)` 相互独立,后者用于选择请求在网络上传输时采用的序列化编码。
+
+| 泛化模式 | 含义 | 运行时示例覆盖范围 |
+|----------|------|--------------------|
+| `true` | 基于 Map 的泛化结果(默认模式) | 检查完整 `User`,包括 `Time` |
+| `gson` | JSON 格式的泛化结果 | 检查 JSON 结构,同时兼容 Map fallback |
+| `bean` | JavaBean 描述符格式的泛化结果 | 检查包含 `ID`、`Name` 和 `Age` 的类型化 DTO |
+| `protobuf-json` | Protobuf JSON 格式的泛化结果 | 当前 Hessian POJO 服务不使用该模式 |
+| `protobuf` | 为兼容旧版本保留的别名 | 保留兼容能力 |
+| `false` 或空值 | 关闭泛化调用 | 不发起泛化调用 |
+
+当 `gson` 返回 JSON 字符串时,结果必须能够解析为包含完整 `ID`、`Name`、`Age` 和 `Time` 的 `User`。任一 
provider 返回 Hessian Map fallback 时,客户端会输出明确警告并兼容该结果。`true` 模式同样会检查 `User` 
的全部可观察字段,包括 `Time`。
+
+Bean generalizer 可以表示导出的 Bean 属性,但无法对 Go `time.Time` 内部未导出的状态进行 
round-trip。因此,`bean` 模式使用只包含受支持字段 `ID`、`Name` 和 `Age` 的明确 DTO,不再接受 `Time` 
丢失但仍被视为完整的 `User`。客户端还会确认未知模式会被拒绝。这些属于样例运行时检查,而不是通过 `go test` 执行的单元测试。当前 `User` 
服务是 Hessian POJO,并非 `proto.Message`,因此本流程不包含 `protobuf-json` 调用。
+
 ## 启动 Java 服务端
 
 在 java-server 目录下构建并运行:
@@ -89,3 +106,4 @@ All generic call tests completed
 - Go 服务端和 Java 服务端均无需 ZooKeeper,直接监听各自配置的端口。
 - Java 客户端通过 `reference.setUrl(...)` 直连 `tri://127.0.0.1:50052`。
 - Go 客户端通过 `tri://127.0.0.1:50052` 直连。
+- 未知泛化模式会在创建服务时明确失败,不会静默回退到 Map 模式。
diff --git a/generic/go-client/cmd/client.go b/generic/go-client/cmd/client.go
index e0182ce2..a144e2f2 100644
--- a/generic/go-client/cmd/client.go
+++ b/generic/go-client/cmd/client.go
@@ -19,6 +19,8 @@ package main
 
 import (
        "context"
+       "encoding/json"
+       "fmt"
        "os"
        "time"
 )
@@ -39,10 +41,16 @@ import (
 )
 
 const (
-       DirectServerURL = "tri://127.0.0.1:50052"
-       UserProvider    = "org.apache.dubbo.samples.UserProvider"
-       ServiceVersion  = "1.0.0"
-       ServiceGroup    = "triple"
+       DirectServerURL        = "tri://127.0.0.1:50052"
+       UserProvider           = "org.apache.dubbo.samples.UserProvider"
+       ServiceVersion         = "1.0.0"
+       ServiceGroup           = "triple"
+       GenericProviderEnv     = "DUBBO_GO_GENERIC_PROVIDER"
+       GenericProviderGo      = "go"
+       GenericProviderJava    = "java"
+       DefaultGenericProvider = GenericProviderGo
+       ExpectedGetOneUserID   = "1000"
+       ExpectedGetUserByID    = "A003"
 )
 
 func main() {
@@ -63,7 +71,7 @@ func main() {
                panic(err)
        }
 
-       conn, err := cli.Dial(
+       genericService, err := cli.NewGenericService(
                UserProvider,
                client.WithURL(DirectServerURL),
                client.WithVersion(ServiceVersion),
@@ -76,36 +84,41 @@ func main() {
        }
 
        logger.Infof("Direct URL: %s", DirectServerURL)
-       logger.Info("Connected to server via direct URL, starting tests...")
+       logger.Info("Connected to server via direct URL, starting checks...")
+
+       provider := os.Getenv(GenericProviderEnv)
+       if provider == "" {
+               provider = DefaultGenericProvider
+       }
+       logger.Infof("Generic provider phase: %s", provider)
 
        failed := false
-       failed = runGenericTests(&genericService{conn: conn}) || failed
+       failed = runGenericChecks(genericService.Invoke) || failed
+       failed = runGenericModeChecks(cli, provider) || failed
 
        if failed {
-               logger.Errorf("Some generic call tests failed")
+               logger.Errorf("Some generic call checks failed")
                os.Exit(1)
        }
-       logger.Info("All generic call tests passed")
+       logger.Info("All generic call checks passed")
 }
 
-type genericService struct {
-       conn *client.Connection
-}
+type genericInvokeFunc func(context.Context, string, []string, 
[]hessian.Object) (any, error)
 
-func (svc *genericService) Invoke(ctx context.Context, methodName string, 
types []string, args []hessian.Object) (any, error) {
-       var result any
-       if err := svc.conn.CallUnary(ctx, []any{methodName, types, args}, 
&result, constant.Generic); err != nil {
-               return nil, err
-       }
-       return result, nil
+// beanUserDTO contains the fields that the Bean generalizer can round-trip 
reliably.
+// time.Time is excluded because its unexported state is not represented by a 
JavaBean descriptor.
+type beanUserDTO struct {
+       ID   string
+       Name string
+       Age  int32
 }
 
-func runGenericTests(svc *genericService) bool {
+func runGenericChecks(invoke genericInvokeFunc) bool {
        failed := false
        ctx := context.Background()
 
        // GetUser1(String)
-       result, err := svc.Invoke(ctx, "GetUser1", 
[]string{"java.lang.String"}, []hessian.Object{"A003"})
+       result, err := invoke(ctx, "GetUser1", []string{"java.lang.String"}, 
[]hessian.Object{"A003"})
        if err != nil {
                logger.Errorf("GetUser1 failed: %v", err)
                failed = true
@@ -114,7 +127,7 @@ func runGenericTests(svc *genericService) bool {
        }
 
        // GetUser2(String, String)
-       result, err = svc.Invoke(ctx, "GetUser2", []string{"java.lang.String", 
"java.lang.String"}, []hessian.Object{"A003", "lily"})
+       result, err = invoke(ctx, "GetUser2", []string{"java.lang.String", 
"java.lang.String"}, []hessian.Object{"A003", "lily"})
        if err != nil {
                logger.Errorf("GetUser2 failed: %v", err)
                failed = true
@@ -123,7 +136,7 @@ func runGenericTests(svc *genericService) bool {
        }
 
        // GetUser3(int)
-       result, err = svc.Invoke(ctx, "GetUser3", []string{"int"}, 
[]hessian.Object{int32(1)})
+       result, err = invoke(ctx, "GetUser3", []string{"int"}, 
[]hessian.Object{int32(1)})
        if err != nil {
                logger.Errorf("GetUser3 failed: %v", err)
                failed = true
@@ -132,7 +145,7 @@ func runGenericTests(svc *genericService) bool {
        }
 
        // GetUser4(int, String)
-       result, err = svc.Invoke(ctx, "GetUser4", []string{"int", 
"java.lang.String"}, []hessian.Object{int32(1), "zhangsan"})
+       result, err = invoke(ctx, "GetUser4", []string{"int", 
"java.lang.String"}, []hessian.Object{int32(1), "zhangsan"})
        if err != nil {
                logger.Errorf("GetUser4 failed: %v", err)
                failed = true
@@ -141,7 +154,7 @@ func runGenericTests(svc *genericService) bool {
        }
 
        // GetOneUser()
-       result, err = svc.Invoke(ctx, "GetOneUser", []string{}, 
[]hessian.Object{})
+       result, err = invoke(ctx, "GetOneUser", []string{}, []hessian.Object{})
        if err != nil {
                logger.Errorf("GetOneUser failed: %v", err)
                failed = true
@@ -150,7 +163,7 @@ func runGenericTests(svc *genericService) bool {
        }
 
        // GetUsers(String[])
-       result, err = svc.Invoke(ctx, "GetUsers", 
[]string{"[Ljava.lang.String;"}, []hessian.Object{[]string{"001", "002", 
"003"}})
+       result, err = invoke(ctx, "GetUsers", []string{"[Ljava.lang.String;"}, 
[]hessian.Object{[]string{"001", "002", "003"}})
        if err != nil {
                logger.Errorf("GetUsers failed: %v", err)
                failed = true
@@ -159,7 +172,7 @@ func runGenericTests(svc *genericService) bool {
        }
 
        // GetUsersMap(String[])
-       result, err = svc.Invoke(ctx, "GetUsersMap", 
[]string{"[Ljava.lang.String;"}, []hessian.Object{[]string{"001", "002"}})
+       result, err = invoke(ctx, "GetUsersMap", 
[]string{"[Ljava.lang.String;"}, []hessian.Object{[]string{"001", "002"}})
        if err != nil {
                logger.Errorf("GetUsersMap failed: %v", err)
                failed = true
@@ -168,7 +181,7 @@ func runGenericTests(svc *genericService) bool {
        }
 
        // QueryAll()
-       result, err = svc.Invoke(ctx, "QueryAll", []string{}, 
[]hessian.Object{})
+       result, err = invoke(ctx, "QueryAll", []string{}, []hessian.Object{})
        if err != nil {
                logger.Errorf("QueryAll failed: %v", err)
                failed = true
@@ -183,7 +196,7 @@ func runGenericTests(svc *genericService) bool {
                Age:  25,
                Time: time.Now(),
        }
-       result, err = svc.Invoke(ctx, "QueryUser", 
[]string{"org.apache.dubbo.samples.User"}, []hessian.Object{testUser})
+       result, err = invoke(ctx, "QueryUser", 
[]string{"org.apache.dubbo.samples.User"}, []hessian.Object{testUser})
        if err != nil {
                logger.Errorf("QueryUser failed: %v", err)
                failed = true
@@ -196,7 +209,7 @@ func runGenericTests(svc *genericService) bool {
                {ID: "3212", Name: "XavierNiu", Age: 24, Time: time.Now()},
                {ID: "3213", Name: "zhangsan", Age: 21, Time: time.Now()},
        }
-       result, err = svc.Invoke(ctx, "QueryUsers", 
[]string{"[Lorg.apache.dubbo.samples.User;"}, []hessian.Object{testUsers})
+       result, err = invoke(ctx, "QueryUsers", 
[]string{"[Lorg.apache.dubbo.samples.User;"}, []hessian.Object{testUsers})
        if err != nil {
                logger.Errorf("QueryUsers failed: %v", err)
                failed = true
@@ -206,3 +219,157 @@ func runGenericTests(svc *genericService) bool {
 
        return failed
 }
+
+func runGenericModeChecks(cli *client.Client, provider string) bool {
+       failed := false
+       ctx := context.Background()
+
+       if _, err := cli.NewGenericService(
+               UserProvider,
+               client.WithURL(DirectServerURL),
+               client.WithVersion(ServiceVersion),
+               client.WithGroup(ServiceGroup),
+               client.WithGenericType("bad-type"),
+               client.WithSerialization(constant.Hessian2Serialization),
+       ); err == nil {
+               logger.Error("NewGenericService accepted an unknown generic 
mode")
+               failed = true
+       } else {
+               logger.Infof("NewGenericService rejected unknown generic mode: 
%v", err)
+       }
+
+       testCases := []struct {
+               name       string
+               mode       string
+               method     string
+               types      []string
+               args       []hessian.Object
+               typed      bool
+               expectedID string
+       }{
+               {
+                       name:       "true",
+                       mode:       constant.GenericSerializationDefault,
+                       method:     "GetUser1",
+                       types:      []string{"java.lang.String"},
+                       args:       []hessian.Object{"A003"},
+                       typed:      true,
+                       expectedID: ExpectedGetUserByID,
+               },
+               {
+                       name:   "gson",
+                       mode:   constant.GenericSerializationGson,
+                       method: "GetOneUser",
+                       types:  []string{},
+                       args:   []hessian.Object{},
+               },
+               {
+                       name:       "bean",
+                       mode:       constant.GenericSerializationBean,
+                       method:     "GetOneUser",
+                       types:      []string{},
+                       args:       []hessian.Object{},
+                       typed:      true,
+                       expectedID: ExpectedGetOneUserID,
+               },
+       }
+
+       for _, testCase := range testCases {
+               service, err := cli.NewGenericService(
+                       UserProvider,
+                       client.WithURL(DirectServerURL),
+                       client.WithVersion(ServiceVersion),
+                       client.WithGroup(ServiceGroup),
+                       client.WithGenericType(testCase.mode),
+                       
client.WithSerialization(constant.Hessian2Serialization),
+               )
+               if err != nil {
+                       logger.Errorf("create generic service (%s) failed: %v", 
testCase.name, err)
+                       failed = true
+                       continue
+               }
+               if !testCase.typed {
+                       result, invokeErr := service.Invoke(ctx, 
testCase.method, testCase.types, testCase.args)
+                       if invokeErr != nil {
+                               logger.Errorf("%s generic result (%s) failed: 
%v", testCase.method, testCase.name, invokeErr)
+                               failed = true
+                               continue
+                       }
+                       validationErr := checkGsonResult(provider, result, 
ExpectedGetOneUserID)
+                       if validationErr != nil {
+                               logger.Errorf("%s generic result (%s) failed 
validation: %v", testCase.method, testCase.name, validationErr)
+                               failed = true
+                               continue
+                       }
+                       continue
+               }
+               if testCase.mode == constant.GenericSerializationBean {
+                       var user beanUserDTO
+                       err = service.InvokeWithType(
+                               ctx,
+                               testCase.method,
+                               testCase.types,
+                               testCase.args,
+                               &user,
+                       )
+                       if err != nil {
+                               logger.Errorf("%s typed result (%s) failed: 
%v", testCase.method, testCase.name, err)
+                               failed = true
+                               continue
+                       }
+                       if user.ID != testCase.expectedID || user.Name == "" || 
user.Age == 0 {
+                               logger.Errorf("%s typed result (%s) returned 
incomplete DTO: %+v", testCase.method, testCase.name, user)
+                               failed = true
+                               continue
+                       }
+                       logger.Infof("%s typed result (%s) DTO res: %+v", 
testCase.method, testCase.name, user)
+                       continue
+               }
+
+               var user pkg.User
+               err = service.InvokeWithType(
+                       ctx,
+                       testCase.method,
+                       testCase.types,
+                       testCase.args,
+                       &user,
+               )
+               if err != nil {
+                       logger.Errorf("%s typed result (%s) failed: %v", 
testCase.method, testCase.name, err)
+                       failed = true
+                       continue
+               }
+               if user.ID != testCase.expectedID || user.Name == "" || 
user.Age == 0 || user.Time.IsZero() {
+                       logger.Errorf("%s typed result (%s) returned incomplete 
user: %+v", testCase.method, testCase.name, user)
+                       failed = true
+                       continue
+               }
+               logger.Infof("%s typed result (%s) res: %+v", testCase.method, 
testCase.name, user)
+       }
+
+       return failed
+}
+
+func checkGsonResult(provider string, result any, expectedID string) error {
+       switch result.(type) {
+       case map[any]any, map[string]any:
+               logger.Warnf("Generic gson returned Map fallback from %s 
provider; accepting type=%T", provider, result)
+               return nil
+       }
+
+       jsonResult, ok := result.(string)
+       if !ok {
+               return fmt.Errorf("provider %q returned %T, want JSON string or 
Map fallback", provider, result)
+       }
+
+       var user pkg.User
+       if err := json.Unmarshal([]byte(jsonResult), &user); err != nil {
+               return fmt.Errorf("decode JSON result: %w", err)
+       }
+       if user.ID != expectedID || user.Name == "" || user.Age == 0 || 
user.Time.IsZero() {
+               return fmt.Errorf("incomplete JSON user: %+v", user)
+       }
+
+       logger.Infof("GetOneUser gson JSON result (%s provider) res: %+v", 
provider, user)
+       return nil
+}
diff --git a/generic/go-server/pkg/user_provider.go 
b/generic/go-server/pkg/user_provider.go
index ec906eb6..764e0e48 100644
--- a/generic/go-server/pkg/user_provider.go
+++ b/generic/go-server/pkg/user_provider.go
@@ -19,12 +19,15 @@ package pkg
 
 import (
        "context"
+       "encoding/json"
        "fmt"
        "strconv"
        "time"
 )
 
 import (
+       "dubbo.apache.org/dubbo-go/v3/common/constant"
+
        "github.com/dubbogo/gost/log/logger"
 )
 
@@ -133,7 +136,20 @@ func (u *UserProvider) Invoke(ctx context.Context, 
methodName string, types []st
        case "GetUser4":
                return u.GetUser4(ctx, args[0].(int32), args[1].(string))
        case "GetOneUser":
-               return u.GetOneUser(ctx)
+               result, err := u.GetOneUser(ctx)
+               if err != nil {
+                       return nil, err
+               }
+               attachments, _ := 
ctx.Value(constant.AttachmentKey).(map[string]interface{})
+               modes, _ := attachments[constant.GenericKey].([]string)
+               if len(modes) > 0 && modes[0] == 
constant.GenericSerializationGson {
+                       encoded, marshalErr := json.Marshal(result)
+                       if marshalErr != nil {
+                               return nil, fmt.Errorf("marshal gson generic 
result: %w", marshalErr)
+                       }
+                       return string(encoded), nil
+               }
+               return result, nil
        case "GetUsers":
                return u.GetUsers(ctx, args[0].([]string))
        case "GetUsersMap":

Reply via email to