4、插件开发入门:编写第一个网关插件

好,正式进入动手环节。前面聊了网关扩展的架构和原理,今天咱们直接撸一个插件出来。不管你是 Java 派还是 Go 派,这条主线都适用——理解插件模型,然后写代码、打包、跑起来。

4.1 插件到底长什么样?

你想想看,网关本质上就是个过滤器链。请求进来,经过一堆插件(或者说过滤器),最后到达后端服务。每个插件都可以在请求被转发前、后,或者出错时做点事。

以我个人的习惯,我会先画这么一条线:

  • Pre 阶段——修改请求头、鉴权、限流、日志。
  • Route 阶段——决定路由到哪里(通常框架做了,插件不需要动)。
  • Post 阶段——修改响应体、记录耗时、缓存。
  • Error 阶段——自定义错误页面、熔断降级。

每个插件只需要实现一个接口,告诉网关:我在哪个阶段被调用,我要做什么。就这么简单。

小提示:别一开始就想着写大而全的插件。先从最简单的“打印请求路径”开始,确认能跑通,再往上堆逻辑。我在第一次写网关插件时,就因为直接嵌了复杂的认证逻辑,最后排查了半天才发现是插件没注册成功。

4.2 Java 还是 Go?先看对比

很多团队问过我这个问题。其实选型主要看你们已有的技术栈和网关产品。我用一张表帮你快速决策:

维度 Java(基于 Spring Cloud Gateway / Zuul) Go(基于 Kong / Caddy / 自研)
开发门槛 熟悉 Spring 生态即可,上手快 需要理解 Go 的 interface 和回调
部署方式 打 jar/war 包,或容器镜像 编译成二进制,体积小
热加载 大多需重启网关(部分实现 SPI 可热替换) Kong 用 Lua 插件,Caddy 用 Go 插件编译时加载
性能 依赖 JVM,有一定调优空间 原生编译,启动快,资源低
社区和生态 Spring Cloud 生态完善,文档多 Go 中间件库丰富,但具体网关插件示例较少

我个人推荐:如果你们团队 Java 偏多且网关基于 Spring Cloud,先用 Java 写第一个插件,快速验证;如果追求极致性能或已有 Go 微服务,就选 Go。

4.3 用 Java 写第一个插件

我拿 Spring Cloud Gateway 举例。它有一个核心接口叫 GatewayFilter,你只需要实现它,再注册成 Bean 就行了。

// 1. 实现 GatewayFilter 接口
@Component
public class SimpleLogFilter implements GatewayFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Pre 阶段:打印请求路径
        String path = exchange.getRequest().getURI().getPath();
        System.out.println("[Pre] 请求路径: " + path);

        // 添加自定义请求头,用于下游服务识别
        exchange.getRequest().mutate()
                .header("X-Gateway-Version", "1.0");

        // 继续执行过滤器链,获取响应
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            // Post 阶段:打印响应状态码
            Integer status = exchange.getResponse().getStatusCode() != null ?
                    exchange.getResponse().getStatusCode().value() : -1;
            System.out.println("[Post] 响应状态码: " + status);
        }));
    }

    @Override
    public int getOrder() {
        return 0; // 数字越小越先执行
    }
}

写完之后,在你的配置类里把这个过滤器加到路由上:

@Bean
public RouteLocator customRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route("log_route", r -> r.path("/api/**")
            .filters(f -> f.filter(new SimpleLogFilter()))
            .uri("lb://backend-service"))
        .build();
}

嗯,这里要注意:Ordered 接口决定了执行顺序。如果你有多个插件,记得规划好 order 值,避免出现先 post 后 pre 这种混乱情况。我曾经就因为两个 Filter 的 order 写反了,导致日志打印的顺序完全颠倒,查了大半天。

4.4 用 Go 写一个中间件插件

Go 的网关插件写法更接地气。我用 Caddy 的中间件模式举例(很多自研网关也类似)。

Caddy 的中间件就是一个 func(http.Handler) http.Handler 的函数,简洁到让人感动。

// logMiddleware 打印请求和响应
func logMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Pre 阶段
        log.Printf("[Go-Gateway] 请求: %s %s", r.Method, r.URL.Path)

        // 包装 ResponseWriter,用来捕获状态码
        lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: 200}
        next.ServeHTTP(lrw, r)

        // Post 阶段
        log.Printf("[Go-Gateway] 响应: %d", lrw.statusCode)
    })
}

// 辅助结构,用于记录状态码
type loggingResponseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (lrw *loggingResponseWriter) WriteHeader(code int) {
    lrw.statusCode = code
    lrw.ResponseWriter.WriteHeader(code)
}

然后注册到路由上:

mux := http.NewServeMux()
mux.Handle("/api/", logMiddleware(http.HandlerFunc(backendHandler)))

说白了,Go 的插件就是闭包套闭包,你可以在外层加各种逻辑。注意一下 ResponseWriter 的包装——原生接口不暴露状态码,必须自己抓。

4.5 打包与部署

写完了怎么让它跑起来?我列两个典型路径:

  1. Java 项目:mvn package → 生成 jar/war → 放到网关的 lib 目录或用容器挂载 → 重启网关。
  2. Go 二进制:go build → 生成可执行文件 → 覆盖原网关程序 → 重启(或 SIGHUP 热重启)。

注意:生产环境不要直接复制文件覆盖!推荐用 CI/CD 构建新镜像,蓝绿部署。我曾经在测试环境手滑了,把错误的插件二进制丢到生产网关,导致所有请求都返回 500,差点被领导约谈。

4.6 调试插件的小窍门

插件最难调试的地方是:你改了代码,但网关没重启,或者重启了但插件没生效。我的经验是:

  • 在插件里加一个独立标识头,比如 X-Debug-Plugin: v1.2.3,响应里能看到就说明插件确实跑起来了。
  • 用本机网关 + 本地后端调试,不要连线上。本地用 Docker 跑一个 Echo 服务,看请求头/响应体是否正确。
  • Java 插件的日志别只打 stdout,最好输出到独立文件,方便 grep。
  • Go 插件可以加 pprof,看中间件有没有内存泄漏。

4.7 完整插件开发流程

最后,我画一张图来总结从零到一开发一个网关插件的全流程,帮你把脑中的节点串起来。

网关插件开发全流程 1. 环境搭建 2. 编写插件代码 3. 本地测试 4. 打包/编译 5. 部署到网关 6. 线上验证 调试反馈循环 若测试不通过 → 返回步骤2或3 用X-Debug-Plugin头辅助定位 不通过

这张图你保存好。每次写插件的时候拿出来对照,就知道自己卡在哪一步了。很多人写插件只写代码,忽略了本地测试和调试反馈,结果线上炸了才反应过来。

4.8 避坑指南(我踩过的坑)

这里列几条我亲自掉进去过的坑,你遇到了直接跳过:

  • 插件执行顺序搞反——特别是 Java 里多个 Filter 的 order 冲突。我曾经在两个 Filter 上不小心写了相同 order,结果行为随机,后来强制给每个 Filter 赋唯一 order 才解决。
  • Go 插件忘记捕获 ResponseWriter——直接 log.Println(w.Header()) 拿不到状态码。必须用 http.ResponseWriter 包装一下。
  • 热加载的幻觉——很多团队以为放个 jar 到 lib 目录就能热加载,实际 Spring Cloud Gateway 大部分版本不支持,必须重启。我建议你在 CI 里踢掉旧 Pod 再拉新的,别想着优雅重启。
  • 插件里开了协程/线程没管理——网关插件是高并发场景,每来一个请求就开一个 goroutine 或者 new Thread() 是灾难。务必用线程池或协程池复用。

4.9 小结

这一章没有花哨的理论,就是写代码、跑起来、看结果。Java 和 Go 的核心逻辑完全一样:选一个入口,干一件事,然后交给下一个插件。你现在可以拿我给出的代码片段,改两行,放到你的开发环境试试。记住,第一个插件别追求完美,打印一行日志就算成功。