xgo 原理探索

Go 单测 mock 方案

Mock 方法原理依赖优点缺点
接口 Mock为依赖项定义接口,并提供接口的 Mock 实现。需要定义接口和 Mock 实现。灵活,遵循 Go 的类型系统;易于替换实现。需要更多的样板代码来定义接口和 Mock 实现。
Monkey Patching(bouk/moneky)直接修改函数指针的内存地址来实现对函数的替换。内存保护;汇编代码。强大,可以 Mock 任何函数,甚至第三方库的函数。复杂,容易出错;线程不安全;依赖系统指令集。

bouk/monkey 弊端

bouk/monkey 🐒

monkey 的核心功能是能够在运行时替换某个函数的实现。

原理:

  1. 函数指针替换:在 Go 语言中,函数的地址存储在内存中。bouk/monkey 通过直接修改函数指针的内存地址来实现对函数的替换。
  2. 汇编代码:使用了汇编代码来实现对函数入口的跳转。这些汇编代码会在函数被调用时,将执行流重定向到新的函数实现。
  3. 内存保护:为了修改内存中的函数指针,bouk/monkey 需要临时修改内存页面的保护属性(例如,将页面设为可写)。在修改完毕后,它会恢复原来的保护属性。
  4. 反射与 unsafe 包:利用 Go 的反射机制和 unsafe 包,bouk/monkey 可以获取并操作函数的底层实现细节。

实现步骤:

  1. 保存原函数:在替换函数之前,bouk/monkey 会保存原始函数的指针,以便在需要时恢复或调用原始函数。
  2. 生成跳转代码:bouk/monkey 生成一段汇编跳转代码,这段代码会在函数调用时,将执行流跳转到新的函数实现。
  3. 修改函数指针:使用 unsafe 包,bouk/monkey 修改目标函数的入口地址,指向生成的跳转代码。
  4. 恢复内存保护:在完成上述修改后,恢复内存页面的保护属性。

有以下几个弊端:

  1. 如果启用了内联,Monkey 有时无法修补函数。尝试在禁用内联的情况下运行测试,例如: go test -gcflags=-l。同样的命令行参数也可以用于构建。
  2. Monkey 不能在一些面向安全的操作系统上工作,这些操作系统不允许同时写入和执行内存页。目前的方法并没有真正可靠的解决方案。
  3. 线程不安全的。
  4. 依赖指令集。

先看 xgo 怎么用

xgo 😈

代码结构如下:

.
├── greet.go
└── greet_test.go

现在在 greet.go 中有一个函数 greet

func greet(s string) string {return "hello " + s
}

在真实的生产环境中,greet 可能要复杂得多,它可能会依赖各种第三方 API,也可能会依赖数据库等多种外部组件。所以在测试的时候,我们希望对其进行 mock,使其返回一个固定的值,便于我们撰写单元测试。

xgo 参考了 go-monkey 的思想,但是不从 修改指令 这个途径入手,而是另辟蹊径,从 代码重写 的角度实现了 mock 的能力。

为了使用 xgo,我们需要先安装 xgo 这个命令:

go install github.com/xhd2015/xgo/cmd/xgo@latest

同时在我们的项目中需要引入 xgo 依赖:

go get "github.com/xhd2015/xgo/runtime/mock"

我们编写的 greet_test.go 如下:

package xgo_useimport ("testing""github.com/xhd2015/xgo/runtime/mock"
)func TestOriginGreet(t *testing.T) {res := greet("world")if res != "hello world" {t.Fatalf("greet() = %q; want %q", res, "hello world")}
}func TestMockGreet(t *testing.T) {mock.Patch(greet, func(s string) string {return "mock " + s})res := greet("world")if res != "mock world" {t.Fatalf("greet() = %q; want %q", res, "mock world")}
}

可以看到在 TestMockGreet 这个单元测试中,我们将 greet 进行了 mock,返回 "mock " + s

mock.Patch(greet, func(s string) string {return "mock " + s
})

为了使用 xgo 的能力,我们在执行单元测试的时候,需要运行以下命令:

xgo test -v ./

输出大致如下:

➜  xgo-use git:(master) xgo test -v ./
xgo is taking a while to setup, please wait...
=== RUN   TestOriginGreet
--- PASS: TestOriginGreet (0.00s)
=== RUN   TestMockGreet
--- PASS: TestMockGreet (0.00s)
PASS
ok      xgo-explore/xgo-use     (cached)

xgo 的核心原理

xgo 的核心原理是利用 go build -toolexec 的能力。

运行以下命令:

go help build

找到 toolexec 的相关说明:

-toolexec 'cmd args'a program to use to invoke toolchain programs like vet and asm.For example, instead of running asm, the go command will run'cmd args /path/to/asm <arguments for asm>'.The TOOLEXEC_IMPORTPATH environment variable will be set,matching 'go list -f {{.ImportPath}}' for the package being built.

一言以蔽之:-toolexec 允许对 go 工具链进行拦截,包括 vetasmcompilelink

这种技术也被称为:插桩(stubbing)、增强(instrumentation)和代码重写(rewriting)。

-toolexec 示意图(来源:https://blog.xhd2015.xyz/zh/posts/xgo-monkey-patching-in-go-using-toolexec/)

基于上述分析,xgo 提出了 代码重写 的思路,实现了 在编译过程中插入拦截器代码 的功能:

xgo 在 go build 中的作用位置(来源:https://blog.xhd2015.xyz/zh/posts/xgo-monkey-patching-in-go-using-toolexec/)

所以上述我们的 greet.go 文件中的源代码:

func greet(s string) string {return "hello " + s
}

经过 xgo 编译后最终实际编译的代码如下:

import "runtime"func greet(s string) (r0 string) {stop, post := runtime.__xgo_trap(Greet, &s, &r0)if stop {return}defer post()return "hello" + s
}

greet 函数重写变化示意图(来源:https://blog.xhd2015.xyz/zh/posts/xgo-monkey-patching-in-go-using-toolexec/)

如图所示,一旦函数被调用,它的控制流首先转移到 Trap,然后一系列拦截器将根据其目的检查当前调用是否应该被 Mock、修改、记录或停止。

如果 greet 注册了 mock 函数,那么就会在 __xgo_trap 中调用 mock 的函数,并将返回值设置到 r0 上进行返回,而跳过原始的执行逻辑。

第 1 步:死代码实现

➜  01-deadcode git:(master) tree
.
├── greet.go
├── greet_test.go
└── mock.go

我们先从最简单的实现开始,采用侵入性代码实现 xgo 的核心功能,这里我们还用不到 -toolexec

代码结构如上所示,在 mock.go 中,我们有如下代码:

var mockFuncs = sync.Map{}func RegisterMockFunc(funcName string, fun interface{}) {mockFuncs.Store(funcName, fun)
}
  • mockFuncs: 用于承载函数与 mock 函数的对应关系,其中 key 为函数名称,value 为 mock 函数。我们使用 sync.Map 来保证并发安全。
  • RegisterMockFunc 用于为指定的 funcName 注册 mock 函数。

greet.go 中,我们有一个 Greet 函数:

func Greet(s string) string {return "hello " + s
}

如果我们要对其支持 mock,那么需要修改其实现为:

func Greet(s string) string {fun, ok := mockFuncs.Load("Greet")if ok {f, ok := fun.(func(s string) string)if ok {return f(s)}}return "hello " + s
}

在修改后的代码中,我们先判断是否存在 mock 函数,如果存在,则执行 mock 函数,否则执行原始逻辑。

现在我们在 greet_test.go 中编写测试代码:

func TestMockGreet(t *testing.T) {RegisterMockFunc("Greet", func(s string) string {return "mock " + s})res := Greet("world")if res != "mock world" {t.Fatalf("Greet() = %q; want %q", res, "mock world")}
}func TestOriginGreet(t *testing.T) {res := Greet("world")if res != "hello world" {t.Fatalf("Greet() = %q; want %q", res, "hello world")}
}

执行测试:

# 单独执行 TestMockGreet
➜  01-deadcode git:(master) ✗ go test -v -run TestMockGreet
=== RUN   TestMockGreet
--- PASS: TestMockGreet (0.00s)
PASS
ok      xgo-explore/01-deadcode 0.103s# 单独执行 TestOriginGreet
➜  01-deadcode git:(master) ✗ go test -v -run TestOriginGreet
=== RUN   TestOriginGreet
--- PASS: TestOriginGreet (0.00s)
PASS
ok      xgo-explore/01-deadcode 0.102s# 一起执行
➜  01-deadcode git:(master) ✗ go test -v -run $Test$
=== RUN   TestMockGreet
--- PASS: TestMockGreet (0.00s)
=== RUN   TestOriginGreetgreet_test.go:20: Greet() = "mock world"; want "hello world"
--- FAIL: TestOriginGreet (0.00s)
FAIL
exit status 1
FAIL    xgo-explore/01-deadcode 0.102s

我们会发现单独执行都是 ok 的,不过一起执行的话 TestOriginGreet 就失败了,这是因为先执行了 TestMockGreet,这个时候已经往 mockFunc 中注册了 mock 函数了,所以 TessOriginGreet 就执行失败了。

这里需要在协程层面上做 mock 隔离,xgo 的思路是在编译时注入 getg() 函数来获取当前协程信息从而实现在注册 mock 函数时进行协程隔离。本文将聚焦在 xgo 的核心原理 代码重写 上,故暂时不考虑这一块。

Ok,那么短短几行代码,我们就将 xgo 的最核心思想给展示出来了。可以看到,xgo 的核心思想是往源代码中加入 合法的 Go 代码,所以不涉及指令重写,故而只要你的机器能执行 Go 程序,天然就支持 mock 功能,这就天然达到了架构无关的兼容性了。同时我们也使用了 sync.Map 来保证了并发安全。

第 2 步:死代码拦截器

➜  02-deadcode-interceptor git:(master) tree
.
├── greet.go
├── greet_test.go
└── mock.go

在第 1 步中,这段代码我觉得有点冗长了:

fun, ok := mockFuncs.Load("Greet")
if ok {f, ok := fun.(func(s string) string)if ok {return f(s)}
}

参考 xgo 的函数签名,我们对其进行优化,在 mock.go 中加入一个 丐版拦截器

// mock.go
func InterceptMock(funcName string, arg string, result *string) bool {fn, ok := mockFuncs.Load(funcName)if ok {f, ok := fn.(func(s string) string)if ok {*result = f(arg)return true}}return false
}

对应 greet.goGreet 函数就修改为:

func Greet(s string) (res string) {if InterceptMock("Greet", s, &res) {return res}return "hello " + s
}

这看起来就清爽多了。再次执行测试代码,一样是可以通过的。

➜  02-deadcode-interceptor git:(master) go test -v -run TestOriginGreet
=== RUN   TestOriginGreet
--- PASS: TestOriginGreet (0.00s)
PASS
ok      xgo-explore/02-deadcode-interceptor     0.331s➜  02-deadcode-interceptor git:(master) go test -v -run TestMockGreet
=== RUN   TestMockGreet
--- PASS: TestMockGreet (0.00s)
PASS
ok      xgo-explore/02-deadcode-interceptor     0.103s

第 3 步:toolexec 初探

➜  03-toolexec-static git:(master) tree
.
├── cmd
│   └── mytool
│       └── mytool.go
├── greet.go
├── main.go
├── mock.go
└── script.sh

这里 mock.go 没有任何变化。我们期望使用 -toolexec 来修改源代码,以实现 mock 无源代码侵入的特性,所以我们在 greet.to 中将 Greet 函数恢复为只关注实际功能的样子:

func Greet(s string) (res string) {return "hello " + s
}

同时为了更好地测试使用 -toolexec 编译后的运行结果,这里将 greet_test.go 删除了并新增了 main.go 文件,内容如下:

func main() {res := Greet("world")if res != "hello world" {log.Fatalf("Greet() = %q; want %q", res, "hello world")}RegisterMockFunc("Greet", func(s string) string {return "mock " + s})res = Greet("world")if res != "mock world" {log.Fatalf("Greet() = %q; want %q", res, "mock world")}log.Println("run successfully")
}

那么 -toolexec 要执行的命令怎么实现呢?在 Google 搜索 go toolexec 你会看到官方给出的一个案例:toolexec.txt。

核心部分在最下面,参考这个示例,我们来实现自己的 toolexec

mkdir -p cmd/mytool
touch cmd/mytool/mytool.go

mytool.go 中,我们先写这么点代码,看一下会输出什么。

func main() {tool, args := os.Args[1], os.Args[2:]if len(args) > 0 && args[0] == "-V=full" {// don't do anything to infuence the version full output.} else if len(args) > 0 {fmt.Printf("tool: %s\n", tool)fmt.Printf("args: %v\n", args)}// 继续执行之前的命令cmd := exec.Command(tool, args...)cmd.Stdout = os.Stdoutcmd.Stderr = os.Stderrif err := cmd.Run(); err != nil {log.Fatalf("run command error: %v\n", err)}
}

这里我们企图输出执行的工具 tool 及传给它的参数 args。由于 -V=full 的作用是在终端输出版本信息,所以我们要跳过它,避免产生干扰。输出日志后,我们暂且先继续执行原始的命令,不对编译过程做其他的干扰。

Ok,现在就来看看这个 -toolexec 到底做了什么,在 03-toolexec-static 目录下执行以下命令:

# 清除缓存,一直使用最新的编译结果
go clean -cache -modcache -i -r
# 编译 mytool
go build ./cmd/mytool
# 编译业务程序
go build -toolexec=./mytool -o main

因为这几个命令经常会用到,所以我们可以将其封装到 script.sh 文件中:

touch script.sh
chmod +x script.sh

内容如下:

#!/bin/bashgo clean -cache -modcache -i -r
go build ./cmd/mytool
go build -toolexec=./mytool -o main

执行上述命令后,可以看到以下输出:

➜  03-toolexec-static git:(master) ./script.sh
# xgo-explore/03-toolexec-static
tool: /opt/homebrew/Cellar/go/1.22.3/libexec/pkg/tool/darwin_arm64/compile
args: [-o $WORK/b001/_pkg_.a -trimpath $WORK/b001=> -p main -lang=go1.22 -complete -buildid PcS9clqF_ny_Ds5N0i_s/PcS9clqF_ny_Ds5N0i_s -goversion go1.22.3 -c=4 -shared -nolocalimports -importcfg $WORK/b001/importcfg -pack ./greet.go ./main.go ./mock.go]
# xgo-explore/03-toolexec-static
tool: /opt/homebrew/Cellar/go/1.22.3/libexec/pkg/tool/darwin_arm64/link
args: [-o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=pie -buildid=KgnnCoU_6enHkOm-T62Z/PcS9clqF_ny_Ds5N0i_s/H80dtgGZw1L8mTtVqJBf/KgnnCoU_6enHkOm-T62Z -extld=cc $WORK/b001/_pkg_.a]

可以看到执行了 compilelink 两个工具,compile 是编译过程,将生成 {}.out 文件,而 link 是将多个 {}.out 文件链接成一个可执行文件。这是很经典的编译过程,如果对 Go 语言的编译过程感兴趣,也可以参考官方的 Go Compile Readme,或者笔者撰写的 Go1.21.0 程序编译过程。

这里我们需要重点关注的是 compile 命令,它是负责编译源代码的,涉及到的源代码文件会通过 -pack ./greet.go ./main.go ./mock.go 传递给 compile 命令。

结合 -toolexec 的帮助信息:

-toolexec 'cmd args'a program to use to invoke toolchain programs like vet and asm.For example, instead of running asm, the go command will run'cmd args /path/to/asm <arguments for asm>'.The TOOLEXEC_IMPORTPATH environment variable will be set,matching 'go list -f {{.ImportPath}}' for the package being built.

我们只需要在执行 compile 命令之前,在 cmd args 这个环节,进行 代码重写 就可以实现我们想要的功能了。

我们现在是要对 greet.go 里面的 Greet 函数进行重写,先看看之前的代码:

package mainfunc Greet(s string) (res string) {return "hello " + s
}

重写后的代码应该跟我们之前 第 2 步 是一样的:

package mainfunc Greet(s string) (res string) {if InterceptMock("Greet", s, &res) {return res}return "hello " + s
}

这里有 n 多种方式可以做到,现在笔者决定使用最暴力的方式,直接临时创建一个包含这段代码的文件 tmp.go,并替换掉传给 compile 的参数,即将 -pack ./greet.go ./main.go ./mock.go 替换为 -pack tmp.go ./main.go ./mock.go

综上,cmd/mytool/mytool/go 实现的代码如下:

func main() {tool, args := os.Args[1], os.Args[2:]if len(args) > 0 && args[0] == "-V=full" {// don't do anything to infuence the version full output.} else if len(args) > 0 {if filepath.Base(tool) == "compile" {index := findGreetFile(args)if index > -1 {f, err := os.Create("tmp.go")if err != nil {log.Fatalf("create tmp.go error: %v\n", err)}defer f.Close()defer os.Remove("tmp.go")_, _ = f.WriteString(newCode)args[index] = "tmp.go"}}fmt.Printf("tool: %s\n", tool)fmt.Printf("args: %v\n", args)}// 继续执行之前的命令cmd := exec.Command(tool, args...)cmd.Stdout = os.Stdoutcmd.Stderr = os.Stderrif err := cmd.Run(); err != nil {log.Fatalf("run command error: %v\n", err)}
}func findGreetFile(args []string) int {for i, arg := range args {if strings.Contains(arg, "greet.go") {return i}}return -1
}var newCode = `
package mainfunc Greet(s string) (res string) {if InterceptMock("Greet", s, &res) {return res}return "hello " + s
}
`

这里我先使用 findGreetFile 来查找 greet.go 文件所处的参数位置,如果找到了,则生成新的 tmp.go 文件,并替换参数,最后在 本次 compile 命令执行完毕后,删除 tmp.go,“毁尸灭迹”。

执行 ./script.sh 重新编译:

➜  03-toolexec-static git:(master) ✗ ./script.sh
# xgo-explore/03-toolexec-static
tool: /opt/homebrew/Cellar/go/1.22.3/libexec/pkg/tool/darwin_arm64/compile
args: [-o $WORK/b001/_pkg_.a -trimpath $WORK/b001=> -p main -lang=go1.22 -complete -buildid PcS9clqF_ny_Ds5N0i_s/PcS9clqF_ny_Ds5N0i_s -goversion go1.22.3 -c=4 -shared -nolocalimports -importcfg $WORK/b001/importcfg -pack tmp.go ./main.go ./mock.go]
# xgo-explore/03-toolexec-static
tool: /opt/homebrew/Cellar/go/1.22.3/libexec/pkg/tool/darwin_arm64/link
args: [-o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=pie -buildid=KgnnCoU_6enHkOm-T62Z/PcS9clqF_ny_Ds5N0i_s/H80dtgGZw1L8mTtVqJBf/KgnnCoU_6enHkOm-T62Z -extld=cc $WORK/b001/_pkg_.a]

输出的结果中可以看到已经将 compile 的参数替换为 -pack tmp.go ./main.go ./mock.go 了。

现在我们来执行生成的程序文件,可以看到是执行成功的。

➜  03-toolexec-static git:(master) ✗ ./main
2024/05/23 17:53:52 run successfully

如果我们不使用 -toolexec,是执行不成功的:

➜  03-toolexec-static git:(master) ✗ go clean -cache -modcache -i -r
➜  03-toolexec-static git:(master) ✗ go build -o main
➜  03-toolexec-static git:(master) ✗ ./main
2024/05/23 17:54:33 Greet() = "hello world"; want "mock world"

第 4 步:使用 AST 在函数前插入代码

➜  04-toolexec-ast git:(master) ✗ tree
.
├── cmd
│   └── mytool
│       └── mytool.go
├── greet.go
├── main.go
├── mock.go
└── script.sh

暴力替换源代码文件的方式可能是不太优雅哈,假如我们的 greet.go 内容改成下面这样:

package mainfunc Greet(s string) (res string) {return "hello " + s
}func Greet2(s string) (res string) {return "hello 2 " + s
}

如果我们想对 Greet2 也进行 代码重写,那就需要修改前面 newCode 字段的内容,而且它是写死的,确实不太优雅。现在我们正式来面对这件事,对比修改后的函数:

func Greet(s string) (res string) {if InterceptMock("Greet", s, &res) {return res}return "hello " + s
}

其实就是在每个函数前加上这么一段:

if InterceptMock("Greet", s, &res) {return res
}

了解过编译原理的读者应该可以想到,我们可以通过操作源代码的 AST 结构,往函数的开头插入这段代码即可。如果我们先不考虑参数和返回值的话,那这段代码我们需要替换的地方就是函数名称了,所以它的结构如下:

if InterceptMock("${funcName}", s, &res) {return res
}

这里我们需要用到几个标准库工具:

  • go/ast: 包定义了 Go 编程语言的抽象语法树(AST),核心有以下几种类型:
    • File: 表示一个 Go 源文件。
    • Decl: 表示一个声明,包括函数声明、变量声明、类型声明等。
    • Stmt: 表示一个语句。
    • Expr: 表示一个表达式。
  • go/token: 定义了处理 Go 源代码的词法元素的基础设施,包括位置、标记和标识符等。这个包提供了用于管理源代码位置的信息,可以帮助定位代码中的特定部分。
  • go/parser: 将一个 .go 文件以解析成 AST 结构。
  • go/printer: 提供了将 AST 格式化并输出为 Go 源码的功能

修改后的 cmd/mytool/mytool.go 代码如下:

func main() {tool, args := os.Args[1], os.Args[2:]if len(args) > 0 && args[0] == "-V=full" {// don't do anything to infuence the version full output.} else if len(args) > 0 {if filepath.Base(tool) == "compile" {index := findGreetFile(args)if index > -1 {filename := args[index]f, err := os.Create("tmp.go")defer f.Close()defer os.Remove("tmp.go")if err != nil {log.Fatalf("create tmp.go error: %v\n", err)}_, _ = f.WriteString(insertCode(filename))args[index] = "tmp.go"}}fmt.Printf("tool: %s\n", tool)fmt.Printf("args: %v\n", args)}// 继续执行之前的命令cmd := exec.Command(tool, args...)cmd.Stdout = os.Stdoutcmd.Stderr = os.Stderrif err := cmd.Run(); err != nil {log.Fatalf("run command error: %v\n", err)}
}func findGreetFile(args []string) int {for i, arg := range args {if strings.Contains(arg, "greet.go") {return i}}return -1
}func insertCode(filename string) string {fset := token.NewFileSet()fast, err := parser.ParseFile(fset, filename, nil, parser.AllErrors)if err != nil {log.Fatalf("parse file error: %v\n", err)}for _, decl := range fast.Decls {fun, ok := decl.(*ast.FuncDecl)if !ok {continue}f, err := os.Create("tmp2.go")if err != nil {log.Fatalf("create tmp2.go error: %v\n", err)}_, _ = f.WriteString(fmt.Sprintf(newCodeFormat, fun.Name.Name))f.Close()tmpFset := token.NewFileSet()tmpF, err := parser.ParseFile(tmpFset, "tmp2.go", nil, parser.AllErrors)if err != nil {log.Fatalf("parse tmp2.go error: %v\n", err)}fun.Body.List = append(tmpF.Decls[0].(*ast.FuncDecl).Body.List, fun.Body.List...)os.Remove("tmp2.go")}var buf bytes.Bufferprinter.Fprint(&buf, fset, fast)fmt.Println(buf.String())return buf.String()
}var newCodeFormat = `
package mainfunc TmpFunc() {if InterceptMock("%s", s, &res) {return res}
}
`

核心的修改在于 insertCode 函数:

  1. 使用 parser.ParseFile 将源代码文件解析成 AST 结构;

  2. 遍历 AST 结构,找到所有的声明(Decl)结构,并使用 decl(.ast.FuncDecl) 找到所有的函数;

    FuncDecl struct {Doc  *CommentGroup // associated documentation; or nilRecv *FieldList    // receiver (methods); or nil (functions)Name *Ident        // function/method nameType *FuncType     // function signature: type and value parameters, results, and position of "func" keywordBody *BlockStmt    // function body; or nil for external (non-Go) function
    }BlockStmt struct {Lbrace token.Pos // position of "{"List   []StmtRbrace token.Pos // position of "}", if any (may be absent due to syntax error)
    }
    
  3. 查看 ast.FuncDecl 的结构后,可以得出下一步就是往 FuncDecl.Body.List 列表前面插入一些 Stmt

  4. 笔者没找到类似 parseStmt 方法,所以取了个巧,我定义了一段代码的 format,里面的 %s 会使用 fun.Name.Name 获取函数名并进行替换。

    var newCodeFormat = `
    package mainfunc TmpFunc() {if InterceptMock("%s", s, &res) {return res}
    }
    `
    
  5. 创建一个临时文件 tmp2.go 并写入格式化后的代码,然后再次调用 parser.ParseFile 得到解析这段代码的抽象语法树结构 tmpF 了;

  6. 然后通过 tmpF.Decls[0].(*ast.FuncDecl).Body.List 就可以得到 TmpFunc 中的语句 Stmt 了;

  7. 将其加在源代码函数的前面即可:fun.Body.List = append(tmpF.Decls[0].(*ast.FuncDecl).Body.List, fun.Body.List...)

  8. 然后再使用 go/printer 将修改后的 AST 输出为新文件内容。

通过上述步骤,我们就可以为 greet.go 中的每个函数前面都插入打桩代码了。

修改 main.go 里面的内容,加入对 Greet2 的测试:

func main() {res := Greet("world")if res != "hello world" {log.Fatalf("Greet() = %q; want %q", res, "hello world")}RegisterMockFunc("Greet", func(s string) string {return "mock " + s})res = Greet("world")if res != "mock world" {log.Fatalf("Greet() = %q; want %q", res, "mock world")}log.Println("run greet 1 successfully")RegisterMockFunc("Greet2", func(s string) string {return "mock 2 " + s})res = Greet2("world")if res != "mock 2 world" {log.Fatalf("Greet2() = %q; want %q", res, "mock 2 world")}log.Println("run greet 2 successfully")
}

执行脚本:

./script.sh

输出应该还是跟之前是一样的,我们运行生成的可执行函数,得到如下结果那就说明我们又成功进了一步了~

➜  04-toolexec-ast git:(master) ✗ ./main
2024/05/23 20:03:22 run greet 1 successfully
2024/05/23 20:03:22 run greet 2 successfully

第 5 步:使用 reflect 反射动态获取参数和返回值名称

➜  05-toolexec-general git:(master) ✗ tree
.
├── cmd
│   └── mytool
│       └── mytool.go
├── greet.go
├── main.go
├── mock.go
└── script.sh

接下来我们来处理函数签名中的参数和返回值部分,我们的样板代码中,写死了参数的名称和返回值的名称,现在我们需要来动态获取函数参数的名称和返回值的名称,如果返回值没有名称,那我们还需要手动设置名称。

我们将 greet.to 修改为以下内容:

func Greet(s string) (res string) {return "hello " + s
}func Greet2(s2 string) (res2 string) {return "hello 2 " + s2
}func Greet3(s3 string) string {return "hello 3 " + s3
}

函数的信息当然都在前面获得的 ast.FuncDecl 结构中,再次观察其结构:

FuncDecl struct {Doc  *CommentGroup // associated documentation; or nilRecv *FieldList    // receiver (methods); or nil (functions)Name *Ident        // function/method nameType *FuncType     // function signature: type and value parameters, results, and position of "func" keywordBody *BlockStmt    // function body; or nil for external (non-Go) function}

通过注释就可以知道 Type 字段就包含了参数和返回值的相关信息,查看 FuncType 结构,如下:

FuncType struct {Func       token.Pos  // position of "func" keyword (token.NoPos if there is no "func")TypeParams *FieldList // type parameters; or nilParams     *FieldList // (incoming) parameters; non-nilResults    *FieldList // (outgoing) results; or nil
}
  • Params:函数参数
  • Results:函数返回值

查看 FieldList 结构,可知参数列表和返回值列表都在相应的 List 字段中,而其中的 Names 字段就是参数的名称了。

type FieldList struct {Opening token.Pos // position of opening parenthesis/brace/bracket, if anyList    []*Field  // field list; or nilClosing token.Pos // position of closing parenthesis/brace/bracket, if any
}type Field struct {Doc     *CommentGroup // associated documentation; or nilNames   []*Ident      // field/method/(type) parameter names; or nilType    Expr          // field/method/parameter type; or nilTag     *BasicLit     // field tag; or nilComment *CommentGroup // line comments; or nil
}

补充一下,这里为什么 Names 类型是 []*Ident 呢?因为函数有以下的命名方式:

func hello(s1, s2 string) (r1, r1 string) {}

那么在当下,只有 1 个参数和只有 1 个返回值的情况下,我们就可以通过 fun.Type.Params.List[0].Names[0].Name 来获取参数名称,也可以通过 fun.Type.Results.List[0].Names 来获取返回值名称,如果返回值没有名称,那我们就为其设置名称 __xgo_res_1 并写回源 AST 结构。这样就都有名称,就很好处理了。

经上分析, cmd/mytool/mytool.go 中我们只需要修改 insertCode 部分,修改的结果如下:

func insertCode(filename string) string {fset := token.NewFileSet()fast, err := parser.ParseFile(fset, filename, nil, parser.AllErrors)if err != nil {log.Fatalf("parse file error: %v\n", err)}for _, decl := range fast.Decls {fun, ok := decl.(*ast.FuncDecl)if !ok {continue}f, err := os.Create("tmp.go")if err != nil {log.Fatalf("create tmp.go error: %v\n", err)}_, _ = f.WriteString(newCode(fun))f.Close()tmpFset := token.NewFileSet()tmpF, err := parser.ParseFile(tmpFset, "tmp.go", nil, parser.AllErrors)if err != nil {log.Fatalf("parse tmp.go error: %v\n", err)}fun.Body.List = append(tmpF.Decls[0].(*ast.FuncDecl).Body.List, fun.Body.List...)os.Remove("tmp.go")}var buf bytes.Bufferprinter.Fprint(&buf, fset, fast)fmt.Println(buf.String())return buf.String()
}func newCode(fun *ast.FuncDecl) string {/*&{Doc:<nil> Names:[s] Type:string Tag:<nil> Comment:<nil>}&{Doc:<nil> Names:[res] Type:string Tag:<nil> Comment:<nil>}&{Doc:<nil> Names:[s2] Type:string Tag:<nil> Comment:<nil>}&{Doc:<nil> Names:[res2] Type:string Tag:<nil> Comment:<nil>}&{Doc:<nil> Names:[s3] Type:string Tag:<nil> Comment:<nil>}&{Doc:<nil> Names:[] Type:string Tag:<nil> Comment:<nil>}*/// 函数名称funcName := fun.Name.Name// 参数列表argName := fun.Type.Params.List[0].Names[0].Name// 返回值列表resNames := fun.Type.Results.List[0].Namesif len(resNames) == 0 {resNames = append(resNames, &ast.Ident{Name: "_xgo_res_1"})fun.Type.Results.List[0].Names = resNames}resName := resNames[0].Namereturn fmt.Sprintf(newCodeFormat, funcName, argName, resName, resName)
}var newCodeFormat = `
package mainfunc TmpFunc() {if InterceptMock("%s", %s, &%s) {return %s}
}
`

现在我们就可以动态获取参数名称和返回值名称了。

修改我们的 main.go,以测试所有的情况:

func main() {res := Greet("world")if res != "hello world" {log.Fatalf("Greet() = %q; want %q", res, "hello world")}RegisterMockFunc("Greet", func(s string) string {return "mock " + s})res = Greet("world")if res != "mock world" {log.Fatalf("Greet() = %q; want %q", res, "mock world")}log.Println("run greet 1 successfully")RegisterMockFunc("Greet2", func(s string) string {return "mock 2 " + s})res = Greet2("world")if res != "mock 2 world" {log.Fatalf("Greet2() = %q; want %q", res, "mock 2 world")}log.Println("run greet 2 successfully")RegisterMockFunc("Greet3", func(s string) string {return "mock 3 " + s})res = Greet3("world")if res != "mock 3 world" {log.Fatalf("Greet3() = %q; want %q", res, "mock 3 world")}log.Println("run greet 3 successfully")
}

执行编译脚本:

./script.sh

执行编译产生的可执行程序,输出如下就说明我们又成功进了一大步~

➜  05-toolexec-general git:(master) ✗ ./main
2024/05/23 20:15:08 run greet 1 successfully
2024/05/23 20:15:08 run greet 2 successfully
2024/05/23 20:15:08 run greet 3 successfully

第 6 步:支持多参数和多返回值

➜  06-toolexec-multi git:(master) ✗ tree
.
├── cmd
│   └── mytool
│       └── mytool.go
├── greet.go
├── main.go
├── mock.go
└── script.sh

本文的最后一步,我们来面对一下多参数和多返回值的问题。假设我们又如下函数:

func Pair1(s1, s2 string) (res string) {return "pair 1 " + s1 + " " + s2
}

这个时候我们 代码重写 后应该长什么样子呢?可以是下面这样的:

func Pair1(s1, s2 string) (res string) {if InterceptMock("Pair1", s1, s2, &res) {return res}return "pair 1 " + s1 + " " + s2
}

按照这个思路,下面这个函数呢?

func Pair2(s1, s2 string) (res1, res2 string) {return "pair 1 " + s1, "pair 2 " + s2
}

那就是这样的?

func Pair2(s1, s2 string) (res1, res2 string) {if InterceptMock("Pair2", s1, s2, &res1, &res2) {return res1, res2}return "pair 1 " + s1, "pair 2 " + s2
}

这种思路当然也能实现,换一种更优雅的思路呢?既然是一个列表,那么就可以用切片来承载,也就是可以是这样的:

func Pair2(s1, s2 string) (res1, res2 string) {if InterceptMock("Pair2", []interface{}{s1, s2}, []interface{}{&res1, &res2}) {return res1, res2}return "pair 1 " + s1, "pair 2 " + s2
}

那我们就可以抽象出插入代码的模板了:

if InterceptMock("${funcName}", []interface{}{${paramList}}, []interface{}{${returnListWith&}}) {return ${returnListWithout&}
}

为了实现这个,我们需要先修改一下 mock.go 中的 InterceptMock 函数:

func InterceptMock(funcName string, args []interface{}, results []interface{}) bool {mockFn, ok := mockFuncs.Load(funcName)if !ok {return false}in := make([]reflect.Value, len(args))for i, arg := range args {in[i] = reflect.ValueOf(arg)}mockFnValue := reflect.ValueOf(mockFn)out := mockFnValue.Call(in)if len(out) != len(results) {panic("mock function return value number is not equal to results number")}for i, result := range results {reflect.ValueOf(result).Elem().Set(out[i])}return true
}

拦截器的具体实现如下:

  1. 判断是否注册了 mock 函数,没有则直接返回;
  2. 将所有参数都放到 []refect.Value 中;
  3. 通过反射 refect.ValueOf 获取 mockFn 的值;
  4. 调用 mockFnValue.Call() 来执行函数,并返回结果列表;
  5. 遍历传进来的返回值引用列表,调用 reflect.ValueOf(result).Elem().Set(out[i]) 将返回值设置回去。

现在我们来修改我们的 -toolexec 工具,来根据函数的 AST 结构,获取参数列表和返回值列表,生成代插入的模板代码,并将其插入到每个函数的开头。这次在 cmd/mytool/mytool.go 中,我们只需修改 newCode 函数:

func insertCode(filename string) string {fset := token.NewFileSet()fast, err := parser.ParseFile(fset, filename, nil, parser.AllErrors)if err != nil {log.Fatalf("parse file error: %v\n", err)}for _, decl := range fast.Decls {fun, ok := decl.(*ast.FuncDecl)if !ok {continue}f, err := os.Create("tmp.go")if err != nil {log.Fatalf("create tmp.go error: %v\n", err)}_, _ = f.WriteString(newCode(fun))f.Close()tmpFset := token.NewFileSet()tmpF, err := parser.ParseFile(tmpFset, "tmp.go", nil, parser.AllErrors)if err != nil {log.Fatalf("parse tmp.go error: %v\n", err)}fun.Body.List = append(tmpF.Decls[0].(*ast.FuncDecl).Body.List, fun.Body.List...)os.Remove("tmp.go")}var buf bytes.Bufferprinter.Fprint(&buf, fset, fast)fmt.Println(buf.String())return buf.String()
}func newCode(fun *ast.FuncDecl) string {// 函数名称funcName := fun.Name.Name// 参数列表args := make([]string, 0)for _, arg := range fun.Type.Params.List {for _, name := range arg.Names {args = append(args, name.Name)}}// 返回值列表returns := make([]string, 0)returnRefs := make([]string, 0)returnNames := fun.Type.Results.List[0].Namesif len(returnNames) == 0 {for i := 0; i < fun.Type.Results.NumFields(); i++ {fun.Type.Results.List[0].Names = append(fun.Type.Results.List[0].Names,&ast.Ident{Name: fmt.Sprintf("_xgo_res_%d", i+1)})}}for _, re := range fun.Type.Results.List[0].Names {returns = append(returns, re.Name)returnRefs = append(returnRefs, "&"+re.Name)}return fmt.Sprintf(newCodeFormat,funcName,strings.Join(args, ","),strings.Join(returnRefs, ","),strings.Join(returns, ","))
}var newCodeFormat = `
package mainfunc TmpFunc() {if InterceptMock("%s", []interface{}{%s}, []interface{}{%s}) {return %s}
}
`

思路跟之前第 5 步大同小异,不过是用遍历的方式来支持多个参数和多个返回值罢了。

现在我们为 greet.go 添加更多的测试函数,代码如下:

func Greet(s string) (res string) {return "hello " + s
}func Greet2(s2 string) (res2 string) {return "hello 2 " + s2
}func Greet3(s3 string) string {return "hello 3 " + s3
}func Pair1(s1, s2 string) (res string) {return "pair 1 " + s1 + " " + s2
}func Pair2(s1, s2 string) (res1, res2 string) {return "pair 1 " + s1, "pair 2 " + s2
}func Other(i int, s string, f float64) string {return fmt.Sprintf("int: %d, string: %s, float: %f", i, s, f)
}

为了测试,我们再次修改 main.go,使其覆盖所有的情况:

func main() {RegisterMockFunc("Other", func(i int, s string, f float64) string {return fmt.Sprintf("mock %d %s %.2f", i, s, f)})res := Other(1, "hello", 3.14)if res != "mock 1 hello 3.14" {log.Fatalf("Other() = %q; want %q", res, "mock 1 hello 3.14")}log.Println("run other successfully")RegisterMockFunc("Pair1", func(s1, s2 string) string {return "mock 1 " + s1 + " " + s2})res = Pair1("hello", "world")if res != "mock 1 hello world" {log.Fatalf("Pair1() = %q; want %q", res, "mock 1 hello world")}log.Println("run pair1 successfully")RegisterMockFunc("Pair2", func(s1, s2 string) (string, string) {return "mock 2 " + s1, "mock 2 " + s2})res1, res2 := Pair2("hello", "world")if res1 != "mock 2 hello" || res2 != "mock 2 world" {log.Fatalf("Pair2() = %q, %q; want %q, %q", res1, res2, "mock 2 hello", "mock 2 world")}log.Println("run pair2 successfully")res = Greet("world")if res != "hello world" {log.Fatalf("Greet() = %q; want %q", res, "hello world")}RegisterMockFunc("Greet", func(s string) string {return "mock " + s})res = Greet("world")if res != "mock world" {log.Fatalf("Greet() = %q; want %q", res, "mock world")}log.Println("run greet 1 successfully")RegisterMockFunc("Greet2", func(s string) string {return "mock 2 " + s})res = Greet2("world")if res != "mock 2 world" {log.Fatalf("Greet2() = %q; want %q", res, "mock 2 world")}log.Println("run greet 2 successfully")RegisterMockFunc("Greet3", func(s string) string {return "mock 3 " + s})res = Greet3("world")if res != "mock 3 world" {log.Fatalf("Greet3() = %q; want %q", res, "mock 3 world")}log.Println("run greet 3 successfully")
}

编译代码:

./script.sh

执行生成的可执行程序,如果有以下输出,那我们就又成功进了一大大步了~

➜  06-toolexec-multi git:(master) ✗ ./main
2024/05/23 20:31:10 run other successfully
2024/05/23 20:31:10 run pair1 successfully
2024/05/23 20:31:10 run pair2 successfully
2024/05/23 20:31:10 run greet 1 successfully
2024/05/23 20:31:10 run greet 2 successfully
2024/05/23 20:31:10 run greet 3 successfully

更进一步

通过上面 6 个简单的小阶段,我们就已经把 xgo 最最核心的功能给实现了,在一些小场景下还勉强能用?🤡

我们来看看包含测试代码和样例函数,总共用了多少代码:

➜  06-toolexec-multi git:(master) ✗ tokei .
===============================================================================Language            Files        Lines         Code     Comments       Blanks
===============================================================================Go                      4          281          224           11           46Shell                   1            5            3            1            1
===============================================================================Total                   5          286          227           12           47
===============================================================================

短短 224 行代码,这是一个非常了不起的成就!

当然,优秀的读者肯定可以发现我们这个 丐版 xgo 有太多的不足和缺陷了。这是必然的,我们来看看 xgo 截止 1.0.37 版本,总共有多少行代码:

➜  xgo git:(master) tokei .
===============================================================================Language            Files        Lines         Code     Comments       Blanks
===============================================================================BASH                    1          104           81           11           12CSS                     1          153          118            5           30Go                    369        33232        26836         2588         3808JavaScript              1          170          146           10           14JSON                    2          435          435            0            0PowerShell              1           28           16            3            9Shell                   3          288          251            4           33SVG                     1           41           41            0            0Plain Text              7          192            0          174           18
-------------------------------------------------------------------------------HTML                    1           19           16            3            0|- JavaScript           1            6            6            0            0(Total)                             25           22            3            0
-------------------------------------------------------------------------------Markdown               17         1455            0         1083          372|- Go                   8          820          635           72          113|- JSON                 1           80           80            0            0(Total)                           2355          715         1155          485
===============================================================================Total                 404        36117        27940         3881         4296
===============================================================================

光 Go 代码就有 26836 行了。所以可知 xgo 的作者是做了很多的付出和努力的。不过我们用了不到百分之一的代码量,就将 xgo 最核心的原理展示得淋漓尽致了,感兴趣的读者可以进一步阅读 xgo 的源码,可以进一步探索如何抽象出更通用更简洁更易扩展的 interceptor,如何支持协程隔离,如何优化依赖管理,以及如何实现其他的 trace、coverage 功能。再次为 xgo 打 call 👏!

参考

  • xgo repo
  • xgo: 基于代码重写实现 Monkey Patch 和 Trace
  • go compile README
  • xgo: 在 go 中使用-toolexec 实现猴子补丁

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/27625.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NOSQL -- ES

第三个我们比较常用的NOSQL类型的数据库 --- ES 介绍: ES的全称(Elasticsearch) ES是一个分布式全文搜索的引擎 也就是我们平常在购物, 搜索东西的时候常用的, 就是一个ES的类型, 分布式全文搜索引擎 查询原理: 1>分词: 在查询之前, 其会将一些数据拆分开, 按照词进行拆分…

day54 动态规划 part10 300.最长递增子序列 674. 最长连续递增序列 718. 最长重复子数组

300.最长递增子序列 动规五部曲 1.dp[i]的定义 dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度 2.状态转移方程 位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 1 的最大值。 所以&#xff1a;if (nums[i] > nums[j]) dp[i] max(dp[i], dp…

样本学习:当AI遇上“少见多怪”

东汉名臣牟融在其著作《牟子》写道&#xff1a;“少所见&#xff0c;多所怪&#xff0c;睹橐驼&#xff0c;谓马肿背。”意思是见闻少的人遇到不常见的事物就觉得奇怪&#xff0c;见到骆驼也以为是背肿了的马。因此&#xff0c;后人总用“少见多怪”来嘲笑见识浅陋的人。然而&a…

七个备受欢迎的IntelliJ IDEA实用插件

有了Lombok插件&#xff0c;IntelliJ就能完全理解Lombok注解&#xff0c;使它们能如预期般工作&#xff0c;防止出现错误&#xff0c;并改善IDE的自动完成功能。 作为IntelliJ IDEA的常用用户&#xff0c;会非常喜欢使用它&#xff0c;但我们必须承认&#xff0c;有时这个IDE&…

3.多层感知机

目录 1.感知机训练感知机XOR问题&#xff08;Minsky&Papert 1969&#xff09; AI的第一个寒冬总结 2.多层感知机(MLP)学习XOR单隐藏层&#xff08;全连接层&#xff09;激活函数&#xff1a;Sigmoid激活函数&#xff1a;Tanh激活函数&#xff1a;ReLu 最常用的 因为计算速度…

openGauss学习笔记-300 openGauss AI特性-AI4DB数据库自治运维-DBMind的AI子功能-SQL Rewriter SQL语句改写

文章目录 openGauss学习笔记-300 openGauss AI特性-AI4DB数据库自治运维-DBMind的AI子功能-SQL Rewriter SQL语句改写300.1 概述300.2 使用指导300.2.1 前提条件300.2.2 使用方法示例300.3 获取帮助300.4 命令参考300.5 常见问题处理openGauss学习笔记-300 openGauss AI特性-AI…

微服务之负载均衡器

1、负载均衡介绍 负载均衡就是将负载(工作任务&#xff0c;访问请求)进行分摊到多个操作单元(服务器&#xff0c;组件)上 进行执行。 根据负载均衡发生位置的不同&#xff0c; 一般分为服务端负载均衡和客户端负载均衡。 服务端负载均衡指的是发生在服务提供者一方&#xff…

Visual Studio 使用第三方库管理工具 vcpkg

一、介绍 Windows下开发C/C程序&#xff0c;少不了用开源的第三方库。比如线性代数和矩阵分析的库eigen&#xff0c;或者图像处理的OpenCV库。虽然这些库都是开源的&#xff0c;但是由于要编译debug和release版本的&#xff0c;32位以及64位的&#xff0c;如果像FFmpeg…

基于springboot实现校园组团平台系统项目【项目源码】计算机毕业设计

基于springboot实现校园组团平台系统的设计演示 SSM框架介绍 本课题程序开发使用到的框架技术&#xff0c;英文名称缩写是SSM&#xff0c;在JavaWeb开发中使用的流行框架有SSH、SSM、SpringMVC等&#xff0c;作为一个课题程序采用SSH框架也可以&#xff0c;SSM框架也可以&…

FullCalendar日历组件集成实战(14)

背景 有一些应用系统或应用功能&#xff0c;如日程管理、任务管理需要使用到日历组件。虽然Element Plus也提供了日历组件&#xff0c;但功能比较简单&#xff0c;用来做数据展现勉强可用。但如果需要进行复杂的数据展示&#xff0c;以及互动操作如通过点击添加事件&#xff0…

每日一题——Python实现PAT乙级1028 人口普查 Keyboard(举一反三+思想解读+逐步优化)六千字好文

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 题目链接​编辑我的写法 专业点评 时间复杂度分析 空间复杂度分析 总结 我要更强…

Python基础教程(十八):MySQL - mysql-connector 驱动

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; &#x1f49d;&#x1f49…

碳化硅陶瓷膜良好的性能

碳化硅陶瓷膜是一种高性能的陶瓷材料&#xff0c;以其独特的物理和化学特性&#xff0c;在众多领域展现出了广泛的应用前景。以下是对碳化硅陶瓷膜的详细介绍&#xff1a; 一、基本特性 高强度与高温稳定性&#xff1a;碳化硅陶瓷膜是一种非晶态陶瓷材料&#xff0c;具有极高的…

从GAN到WGAN(02/2)

文章目录 一、说明二、GAN中的问题2.1 难以实现纳什均衡(Nash equilibrium)2.2 低维度支撑2.3 梯度消失2.4 模式坍缩2.5 缺乏适当的评估指标 三、改进的GAN训练四、瓦瑟斯坦&#xff08;Wasserstein&#xff09;WGAN4.1 什么是 Wasserstein 距离&#xff1f;4.2 为什么 Wassers…

53.Python-web框架-Django开始第一个应用的多语言

针对上一篇的功能&#xff0c;本次仅对页面做了多语言&#xff0c;大家可以看看效果。 51.Python-web框架-Django开始第一个应用的增删改查-CSDN博客 目录 部门列表 新增部门 编辑部门 部门列表 源码 <!DOCTYPE html> {% load static %} {% load i18n %} <html …

JAVA开发 使用Apache PDFBox库生成PDF文件,绘制表格

1. 表格位置定点 2.执行效果展示&#xff08;截取PDF文件图片&#xff09; 3.执行代码 当我们使用Apache PDFBox库在PDF文件中创建带有表格的内容&#xff0c;需要遵循几个步骤。PDFBox本身并没有直接的API来创建表格&#xff0c;但我们可以通过定位文本、绘制线条和单元格矩形…

shell编程基础(第16篇:命令是什么?有哪些注意事项)

前言 前面我们已经使用过各种各样的命令&#xff0c;那么命令到底是什么呢&#xff1f;我们又该怎么理解该术语&#xff1f; 什么是命令&#xff1f; 命令是command的中文翻译&#xff0c;能在命令行中执行的是命令。因为早期的计算机只有文字界面&#xff0c;命令是程序&#…

高速公路智能管理系统:构建安全畅通的数字大动脉

随着城市化进程的加速和交通需求的增长&#xff0c;高速公路系统作为城市交通的重要组成部分&#xff0c;正承担着越来越多的交通运输任务。为了提升高速公路的安全性、便捷性和智能化管理水平&#xff0c;高速公路智能管理系统应运而生。本文将深入探讨高速公路智能管理系统的…

Leetcode 剑指 Offer II 082.组合总和 II

题目难度: 中等 原题链接 今天继续更新 Leetcode 的剑指 Offer&#xff08;专项突击版&#xff09;系列, 大家在公众号 算法精选 里回复 剑指offer2 就能看到该系列当前连载的所有文章了, 记得关注哦~ 题目描述 给定一个可能有重复数字的整数数组 candidates 和一个目标数 tar…

能耗监控与管理平台

在当今社会&#xff0c;随着工业化、城市化的快速发展&#xff0c;能源消耗问题日益凸显&#xff0c;节能减排已成为全社会共同关注的焦点。在这个背景下&#xff0c;一款高效、智能的能耗监控与管理平台显得尤为重要。 一、HiWoo Cloud平台的概念 HiWoo Cloud是一款集数据采…