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":