iPaaS文档库 iPaaS文档库
00 概述
01 产品安装指南
02 快速入门指南
03 Studio使用指南
04 iPaaS使用指南
05 高级配置指南
06 接口服务说明
07 升级&数据迁移指南
08 产品集成指南
09 FAQ
10 iPaaS上线指南
运维指南
  • 网关自定义插件开发指南
  • 目录
  • 1. 概述
  • 1.1 什么是自定义插件
  • 1.2 自定义插件的三要素
  • 2. 核心概念与运行原理
  • 2.1 加载与执行全流程
  • 2.2 三个关键点(务必记住)
  • 3. 开发环境准备
  • 3.1 基础环境
  • 3.2 获取网关依赖(关键,无源码环境)
  • 4. 从零开发一个自定义插件(手把手)
  • 4.1 新建 Maven 工程
  • 4.2 编写 pom.xml
  • 4.3 编写策略配置 POJO —— CustomPolicyConfig.java
  • 4.4 编写策略缓存类 —— CustomPolicyConfigCache.java
  • 4.5 编写过滤器工厂 —— CustomPolicyGatewayFilterFactory.java
  • 4.6 上线前检查清单 ✅
  • 5. 编译打包
  • 5.1 打包注意事项
  • 6. 部署到网关生效
  • 6.1 网关目录结构
  • 6.2 放置插件 jar
  • 6.3 重启网关生效
  • 6.4 验证插件已生效
  • 6.5 让路由使用你的插件
  • 6.6 卸载插件
  • 7. 进阶:让插件真正处理业务
  • 7.1 读取请求信息
  • 7.2 前置处理:修改请求头后继续放行
  • 7.3 前置拦截:直接拒绝请求
  • 7.4 后置处理:修改响应体(例如脱敏、加解密)
  • 8. 策略配置数据说明
  • 9. 常见问题与排错
  • Q1:插件 jar 已放进 lib/,但过滤器不生效
  • Q2:启动报 ClassCastException / LinkageError
  • Q3:插件用了 spring-boot-maven-plugin 打包,启动后完全不生效
  • Q4:策略读不到(getPolicyConfig 返回 null)
  • Q5:插件代码里 @Autowired 注入的 Bean 是 null
  • Q6:Windows 下替换 jar 报"文件被占用"
  • 10. 完整代码清单
  • config/CustomPolicyConfig.java
  • config/CustomPolicyConfigCache.java
  • CustomPolicyGatewayFilterFactory.java

# 网关自定义插件开发指南

适用版本:API Gateway 9.1.0 适用对象:需要在 没有网关源码 的环境下,独立开发、打包并部署自定义网关插件的开发者。


# 目录

  1. 概述
  2. 核心概念与运行原理
  3. 开发环境准备
  4. 从零开发一个自定义插件(手把手)
  5. 编译打包
  6. 部署到网关生效
  7. 进阶:让插件真正处理业务
  8. 策略配置数据说明
  9. 常见问题与排错
  10. 完整代码清单

# 1. 概述

# 1.1 什么是自定义插件

iPaaS 网关基于 Spring Cloud Gateway 构建。自定义插件本质上就是一个 GatewayFilter(网关过滤器),以独立 JAR 的形式存在。把它放进网关的 lib/ 目录并重启网关后,插件就被加载进网关的类路径,由 Spring 自动扫描注册,随即参与请求/响应的处理链路。

你可以用自定义插件实现诸如:

  • 自定义鉴权、签名校验
  • 请求/响应日志、审计
  • 请求头改写、参数处理
  • 响应体脱敏、加解密、内容改写
  • 业务限流、灰度路由
  • 任何需要在网关层统一处理的逻辑

# 1.2 自定义插件的三要素

一个可被网关识别并使用的自定义插件,至少包含 三个核心部分:

组成 作用 是否必需
过滤器工厂 (GatewayFilterFactory) 真正的过滤逻辑入口,决定请求如何被处理 ✅ 必需
策略配置 POJO (XxxPolicyConfig) 描述"策略参数"的 Java 对象,对应你在管理端配置的那份 JSON 视插件是否需要策略而定
策略缓存类 (XxxPolicyConfigCache) 把管理端下发的策略数据缓存到内存,供过滤器运行时读取 视插件是否需要策略而定

💡 如果你的插件逻辑是固定的(不需要在管理端动态配置参数),可以只写过滤器工厂,省略策略配置相关的两个类。本文档为了完整起见,三个都讲。


# 2. 核心概念与运行原理

在动手之前,请先花 3 分钟理解下面的运行原理,它会帮你避开 90% 的坑。

# 2.1 加载与执行全流程

┌──────────────────────────────────────────────────────────────────────┐
│                         网关启动阶段                                   │
│                                                                      │
│  网关用 PropertiesLauncher (ZIP layout) 启动,启动参数带:            │
│       -Dloader.path=<网关根>/lib                                      │
│                                                                      │
│  ⇒ Spring Boot 启动类加载器把 lib/ 下【所有 jar】加入 classpath,      │
│    其中包括你放进 lib/ 的自定义插件 jar                                │
│                                                                      │
│  ⇒ Spring Boot 扫描 @SpringBootApplication 主类所在包                 │
│    (com.primeton.gateway 及其子包)下的所有 @Component / @Configuration│
│    并注册为 Bean。你的过滤器工厂、策略缓存类就是这样被自动注册的        │
│                                                                      │
│  ⇒ Spring Cloud Gateway 收集所有 GatewayFilterFactory 类型的 Bean,     │
│    供路由 filters 配置引用                                            │
└──────────────────────────────────────────────────────────────────────┘
                                ↓
┌──────────────────────────────────────────────────────────────────────┐
│                         请求处理阶段                                   │
│                                                                      │
│  每个请求进来 → 命中某条路由 → 该路由配置了你的过滤器 →                │
│  过滤器从策略缓存中读取当前请求对应的策略参数 → 执行业务逻辑 →         │
│  chain.filter(exchange) 继续往后传                                    │
└──────────────────────────────────────────────────────────────────────┘

# 2.2 三个关键点(务必记住)

  1. 插件类的包名必须以 com.primeton.gateway 开头。 网关主程序 GatewayApplication 位于 com.primeton.gateway 包,其 @SpringBootApplication 默认只扫描该包及其子包。你写的 @Component / @Configuration 只有落在 com.primeton.gateway.** 下才会被自动注册。这是网关所有内置插件都遵循的约定(例如 com.primeton.gateway.desensitizationGatewayFilter、com.primeton.gateway.custom)。

    ⚠️ 这是最容易踩的坑:插件 jar 已经放进 lib/、类也加载了,但过滤器就是不生效——99% 是包名写成了 com.example.xxx,没在扫描范围内。

  2. 策略缓存类必须加 @Component("策略类型名") 注解。 注解里的字符串就是"策略类型名",它必须三处保持完全一致:

    • @Component("这里")
    • getPolicyType() 方法的返回值
    • 过滤器里查询策略时传入的字符串
  3. 运行时依赖由网关提供,插件 jar 里不要打包这些依赖。 网关自带 Spring、Spring Cloud Gateway、gateway-core 等,且都在同一个 classloader 里。插件 pom 中这些依赖用 <scope>system</scope> 引用网关 lib/ 目录下的 jar(见 3.2 方式 C / 4.2),打包时不会进产物 jar,最终插件 jar 保持"轻量"。这一点见第 5 节,务必遵守。


# 3. 开发环境准备

# 3.1 基础环境

工具 版本要求
JDK 1.8(必须,网关运行在 1.8)
Maven 3.5+
IDE IntelliJ IDEA / Eclipse 任选

# 3.2 获取网关依赖(关键,无源码环境)

你开发的插件需要编译期依赖网关提供的几个 jar(最核心的是 gateway-core)。在 没有网关源码 的情况下,推荐用 Maven 的 system 作用域直接引用网关安装包 lib/ 目录下的 jar,完全不需要往本地 .m2 仓库安装任何东西,也不需要公司私服。

每个网关安装包解压后都有 lib/ 目录,里面有全部依赖 jar,例如:

API_GATEWAY/
└── lib/
    ├── gateway-core-9.1.0.jar         ← 提供策略缓存接口、工具类
    ├── gateway-common-9.1.0.jar       ← 提供公共工具
    ├── spring-cloud-gateway-server-3.1.6.jar
    ├── spring-boot-2.7.18.jar
    └── ...

前置操作(一次性):

  1. 从网关安装包 API_GATEWAY/lib/ 目录复制全部 jar 到插件工程下的 lib/ 子目录(工程根目录新建 lib/ 文件夹,把 jar 全部拷进去)。
  2. 在插件工程的 pom.xml 里,把需要的 jar 以 <scope>system</scope> + <systemPath> 的形式声明依赖。

原理: system 作用域告诉 Maven"这个依赖在文件系统里固定路径下,别去仓库找了"。Maven 编译时直接从 systemPath 指定的路径读 jar,打包时默认也不会把 system 依赖打进产物 jar——所以打出来的天然就是网关需要的 thin jar。

pom 里要声明哪些 jar? 只声明你代码里 import 到的类所属的 jar。下面第 4.2 节给出的示例 pom 只列了 gateway-plugin-custom 示例代码必须的 8 个 jar;后续你扩展功能时,根据新 import 的类属于哪个 jar,按同样格式自行添加即可。

📌 如何判断某个类在哪个 jar 里? 在网关 lib/ 目录下执行 for j in *.jar; do unzip -l "$j" | grep "类名" >/dev/null && echo "$j"; done(git bash 环境),或用 JD-GUI 逐个打开 jar 搜索。常见对应关系见 4.2 节注释。


# 4. 从零开发一个自定义插件(手把手)

下面以"自定义策略插件(CustomPolicy)"为例,源码:my-gateway-plugin.zip,完整演示开发过程。最终效果:过滤器在请求经过时,读取管理端下发的 customParam1 / customParam2 两个参数并打印日志。

# 4.1 新建 Maven 工程

目录结构如下。⚠️ 注意:包名以 com.primeton.gateway 开头,这是网关组件扫描的要求。

my-gateway-plugin/
├── pom.xml
├── lib/                                         ← 从网关安装包 lib/ 拷来的全部 jar(见 3.2 方式 C)
└── src/main/java/com/primeton/gateway/custom/
    ├── CustomPolicyGatewayFilterFactory.java   ← 过滤器工厂(入口)
    └── config/
        ├── CustomPolicyConfig.java             ← 策略配置 POJO
        └── CustomPolicyConfigCache.java        ← 策略缓存类

# 4.2 编写 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>my-gateway-plugin</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <!-- ===== 网关版本与 lib 目录路径(按实际环境修改)===== -->
        <gateway.version>9.1.0</gateway.version>
        <!-- lib 目录位于插件工程根目录下,内含从网关安装包拷来的全部 jar -->
        <gateway.lib>${project.basedir}/lib</gateway.lib>

        <!-- ===== 以下版本号务必与你 lib 目录里 jar 的实际文件名一致 ===== -->
        <spring.version>5.3.31</spring.version>
        <spring-cloud-gateway.version>3.1.6</spring-cloud-gateway.version>
        <reactor.version>3.4.22</reactor.version>
        <slf4j.version>1.7.36</slf4j.version>
    </properties>

    <dependencies>
        <!--
            以下 8 个 jar 是 gateway-plugin-custom 示例代码编译必须的依赖。
            采用 system scope 直接引用 lib 目录下的 jar,无需 install 到本地仓库。
            打包时 system 依赖不会被打进产物 jar,天然就是 thin jar。

            后续扩展功能时,根据你新 import 的类所属 jar,按同样格式自行添加。
        -->

        <!-- gateway-core:ApiPolicy / PolicyConfigCache / PolicyConfigCacheUtil / JsonUtils -->
        <dependency>
            <groupId>com.primeton.api</groupId>
            <artifactId>gateway-core</artifactId>
            <version>${gateway.version}</version>
            <scope>system</scope>
            <systemPath>${gateway.lib}/gateway-core-${gateway.version}.jar</systemPath>
        </dependency>

        <!-- spring-cloud-gateway-server:GatewayFilter / GatewayFilterChain / AbstractGatewayFilterFactory -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-gateway-server</artifactId>
            <version>${spring-cloud-gateway.version}</version>
            <scope>system</scope>
            <systemPath>${gateway.lib}/spring-cloud-gateway-server-${spring-cloud-gateway.version}.jar</systemPath>
        </dependency>

        <!-- spring-core:Ordered -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
            <scope>system</scope>
            <systemPath>${gateway.lib}/spring-core-${spring.version}.jar</systemPath>
        </dependency>

        <!-- spring-context:@Component -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
            <scope>system</scope>
            <systemPath>${gateway.lib}/spring-context-${spring.version}.jar</systemPath>
        </dependency>

        <!-- spring-web:ServerWebExchange -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring.version}</version>
            <scope>system</scope>
            <systemPath>${gateway.lib}/spring-web-${spring.version}.jar</systemPath>
        </dependency>

        <!-- spring-beans:Aware -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
            <scope>system</scope>
            <systemPath>${gateway.lib}/spring-beans-${spring.version}.jar</systemPath>
        </dependency>

        <!-- reactor-core:Mono -->
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-core</artifactId>
            <version>${reactor.version}</version>
            <scope>system</scope>
            <systemPath>${gateway.lib}/reactor-core-${reactor.version}.jar</systemPath>
        </dependency>

        <!-- slf4j-api:Logger / LoggerFactory -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
            <scope>system</scope>
            <systemPath>${gateway.lib}/slf4j-api-${slf4j.version}.jar</systemPath>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

📌 几个要点:

  • system 作用域告诉 Maven"依赖在 systemPath 指定的固定路径下",编译时直接从该路径读 jar,既不需要 install 到本地 .m2 仓库,也不会被打进产物 jar——所以打出来的天然就是网关需要的 thin jar。
  • gateway.lib 指向插件工程下的 lib/ 目录(第 3.2 方式 C 的前置步骤里拷贝的那份)。用 ${project.basedir}/lib 相对路径,工程挪位置不会断。
  • 版本号:<properties> 里的 gateway.version、spring.version 等务必与你 lib/ 目录里 jar 的实际文件名一致。不确定时进 lib/ 目录看一眼文件名,例如 spring-core-5.3.31.jar 对应 <spring.version>5.3.31</spring.version>。
  • 不要打 fat jar。 绝对不要加 spring-boot-maven-plugin 做 repackage。system 依赖本就不会进 jar,用默认的 maven-compiler-plugin + maven-jar-plugin 即可产出纯净的 thin jar。

# 4.3 编写策略配置 POJO —— CustomPolicyConfig.java

这个类就是"你在管理端配置的那份策略 JSON"对应的 Java 对象,字段名要和 JSON 的 key 一致。

package com.primeton.gateway.custom.config;

/**
 * 自定义策略配置。
 * 对应管理端下发的策略 JSON,例如:
 * { "customParam1": "value1", "customParam2": "value2" }
 */
public class CustomPolicyConfig {

    private String customParam1;
    private String customParam2;

    public String getCustomParam1() {
        return customParam1;
    }

    public void setCustomParam1(String customParam1) {
        this.customParam1 = customParam1;
    }

    public String getCustomParam2() {
        return customParam2;
    }

    public void setCustomParam2(String customParam2) {
        this.customParam2 = customParam2;
    }
}

# 4.4 编写策略缓存类 —— CustomPolicyConfigCache.java

实现网关提供的 PolicyConfigCache 接口,负责把管理端下发的策略存进内存、供过滤器读取。

package com.primeton.gateway.custom.config;

import com.primeton.gateway.core.model.policy.ApiPolicy;
import com.primeton.gateway.core.policy.PolicyConfigCache;
import com.primeton.gateway.util.JsonUtils;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 策略缓存类。
 *
 * 重点:
 *  1. @Component 的值 "CustomPolicy" 就是【策略类型名】,必须全局唯一。
 *  2. getPolicyType() 返回值必须与 @Component 的值完全一致。
 *  3. 过滤器中查询策略时使用的字符串也必须是同一个值。
 */
@Component("CustomPolicy")
public class CustomPolicyConfigCache implements PolicyConfigCache {

    /** 策略缓存容器,key = 策略缓存键,value = 策略配置对象 */
    private final Map<String, CustomPolicyConfig> CUSTOM_POLICY_CONFIG_MAP = new ConcurrentHashMap<>();

    /**
     * 根据缓存键读取策略(过滤器调用)
     */
    @Override
    public CustomPolicyConfig getPolicyConfig(String policyId) {
        return CUSTOM_POLICY_CONFIG_MAP.get(policyId);
    }

    /**
     * 管理端下发策略时,网关会回调此方法,把策略 JSON 反序列化后放进缓存
     */
    @Override
    public void addPolicyConfig(ApiPolicy apiPolicy) {
        // apiPolicy.getPolicyConfig() 就是管理端配置的那段 JSON 字符串
        CustomPolicyConfig config = JsonUtils.toObject(apiPolicy.getPolicyConfig(), CustomPolicyConfig.class);
        CUSTOM_POLICY_CONFIG_MAP.put(getConfigCacheKey(apiPolicy), config);
    }

    /**
     * 管理端删除策略时回调
     */
    @Override
    public void deletePolicyConfig(String policyId) {
        CUSTOM_POLICY_CONFIG_MAP.remove(policyId);
    }

    /**
     * 策略类型名,必须与 @Component 的值一致
     */
    @Override
    public String getPolicyType() {
        return "CustomPolicy";
    }
}

💡 JsonUtils、ApiPolicy、PolicyConfigCache 都来自 gateway-core,由网关在运行时提供。

# 4.5 编写过滤器工厂 —— CustomPolicyGatewayFilterFactory.java

这是插件的入口类。

package com.primeton.gateway.custom;

import com.primeton.gateway.core.cache.PolicyConfigCacheUtil;
import com.primeton.gateway.custom.config.CustomPolicyConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 自定义策略过滤器工厂。
 *
 * 继承 AbstractGatewayFilterFactory<Config>,泛型参数 Config 是
 * "路由配置内联参数"对应的类(本例不需要路由参数,留空即可)。
 *
 * @Component 让此类被网关注册为 Bean。
 */
@Component
public class CustomPolicyGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomPolicyGatewayFilterFactory.Config> {

    private static final Logger logger = LoggerFactory.getLogger(CustomPolicyGatewayFilterFactory.class);

    /** 策略类型名,必须与策略缓存类 @Component 的值一致 */
    private static final String POLICY_TYPE = "CustomPolicy";

    public CustomPolicyGatewayFilterFactory() {
        // 必须把 Config 的 Class 传给父类,否则会报 ClassCastException
        super(CustomPolicyGatewayFilterFactory.Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        // 这里返回真正的过滤器实例
        return new CustomFilter();
    }

    /**
     * 真正的过滤器逻辑写在这里
     */
    public static class CustomFilter implements GatewayFilter, Ordered {
        private static final Logger logger = LoggerFactory.getLogger(CustomFilter.class);

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            logger.info("custom filter execute");

            // 从策略缓存中读取当前请求命中的策略
            CustomPolicyConfig customPolicy = PolicyConfigCacheUtil.getPolicyConfig(
                    exchange, POLICY_TYPE, CustomPolicyConfig.class);

            if (customPolicy != null) {
                String customParam1 = customPolicy.getCustomParam1();
                String customParam2 = customPolicy.getCustomParam2();
                logger.info("Custom Param1 is {}", customParam1);
                logger.info("Custom Param2 is {}", customParam2);
            } else {
                logger.info("No custom policy matched for this request.");
            }

            // ⚠️ 务必调用 chain.filter(exchange) 把请求传给下一个过滤器,否则请求会卡住
            return chain.filter(exchange);
        }

        /**
         * 过滤器执行顺序,数字越小优先级越高。
         * 具体取值见第 7 节说明。
         */
        @Override
        public int getOrder() {
            return 0;
        }
    }

    /**
     * 路由内联参数配置类。
     * 如果路由里以 filters: [{name: CustomPolicy, args: {...}}] 形式使用,
     * args 中的字段会绑定到这里。本插件不需要,留空即可。
     */
    public static class Config {
    }
}

# 4.6 上线前检查清单 ✅

打包前,请逐条确认:

检查项 期望
所有类的包名 以 com.primeton.gateway 开头(否则 @Component 不被扫描)
@Component("X") 的值 CustomPolicy
getPolicyType() 返回值 CustomPolicy
过滤器中查询策略用的字符串 CustomPolicy
插件是否为 thin jar 是(依赖为 system 作用域,不会打进产物 jar)

# 5. 编译打包

在插件工程根目录执行:

mvn clean package

产物位于 target/my-gateway-plugin-1.0.0.jar。

# 5.1 打包注意事项

  1. 不要打 fat jar。 绝对不要使用 spring-boot-maven-plugin 把依赖一起 repackage 进来。本文 4.2 的 pom 用 system 作用域引用 lib/ 目录下的 jar,这些依赖不会进产物 jar,用默认的 maven-compiler-plugin 即可产出纯净的 thin jar,这正是网关期望的格式。

    fat jar(即用 spring-boot repackage 后的 jar)里的类位于 BOOT-INF/classes/,Spring Boot 的 loader.path 加载这种 jar 时会读不到你的类,导致插件完全不生效。

  2. 验证 jar 内容(强烈建议):

    # 应该只看到你自己写的几个 class,不应出现 spring / reactor / gateway-core 等第三方包
    jar tf target/my-gateway-plugin-1.0.0.jar | head -20
    

    预期输出大致:

    META-INF/
    META-INF/MANIFEST.MF
    com/primeton/gateway/custom/
    com/primeton/gateway/custom/CustomPolicyGatewayFilterFactory.class
    com/primeton/gateway/custom/CustomPolicyGatewayFilterFactory$Config.class
    com/primeton/gateway/custom/CustomPolicyGatewayFilterFactory$CustomFilter.class
    com/primeton/gateway/custom/config/CustomPolicyConfig.class
    com/primeton/gateway/custom/config/CustomPolicyConfigCache.class
    

    如果看到 org/springframework/... 或 BOOT-INF/,说明打包配置错了,回到 4.2 检查。


# 6. 部署到网关生效

# 6.1 网关目录结构

解压网关安装包后,典型目录结构如下:

API_GATEWAY/
├── bin/                  ← 启动/停止脚本(startup.sh / startup.bat)
├── config/               ← 配置文件(application.properties 等)
├── lib/                  ← 网关自身依赖 jar;自定义插件 jar 也放在这里
├── license/              ← 许可证
├── keystore/             ← 证书
├── logs/                 ← 日志输出
└── gateway-boot-9.1.0.jar   ← 网关主程序(ZIP layout 可执行 jar)

启动脚本通过 -Dloader.path=<网关根>/lib 让 Spring Boot 在启动时加载 lib/ 下的全部 jar。因此自定义插件 jar 放进 lib/ 目录、重启网关即可生效。

# 6.2 放置插件 jar

# 把打包好的插件 jar 放进网关 lib 目录
cp my-gateway-plugin-1.0.0.jar  API_GATEWAY/lib/

放置后的结构:

API_GATEWAY/
├── bin/
├── lib/
│   ├── gateway-core-9.1.0.jar
│   ├── ...(网关原有依赖)
│   └── my-gateway-plugin-1.0.0.jar    ← 你的自定义插件
└── ...

📌 jar 的文件名随意,Spring Boot loader 会加载 lib/ 下所有 *.jar,与文件名无关。建议用清晰的 插件名-版本.jar 命名以便管理。

# 6.3 重启网关生效

cd API_GATEWAY/bin
./shutdown.sh        # Windows 用 shutdown.bat
./startup.sh         # Windows 用 startup.bat

新增/替换插件 jar 后,必须重启网关才能生效(lib/ 目录在 JVM 启动那一刻固定下来,运行中新增 jar 不会被加载)。

# 6.4 验证插件已生效

方式一:看启动日志。 网关启动过程中,Spring 会扫描注册你的 Bean。可以在 logs/ 下的应用日志中搜索插件类名,应能看到 Bean 初始化相关的日志;过滤器里 logger.info("create custom filter") / custom filter execute 也会在请求经过时打印。

方式二:发请求实测。 给某条路由的 filters 加上你的过滤器名(见 6.5),发起一次请求,观察日志里是否出现 custom filter execute,以及策略参数是否被正确读取打印。

# 6.5 让路由使用你的插件

插件被加载后,它就像内置插件一样可用。在管理端(Governor)配置路由的 filters,加上你的过滤器名即可:

filters:
  - CustomPolicy

过滤器名取的是 GatewayFilterFactory 类名去掉 GatewayFilterFactory 后缀的部分(即 CustomPolicy)。

# 6.6 卸载插件

停止网关 → 从 lib/ 移除对应 jar → 启动网关。同时记得在路由配置中移除对该过滤器的引用,避免路由引用了不存在的过滤器。


# 7. 进阶:让插件真正处理业务

上面的示例只是在 chain.filter(exchange) 之前 打了日志。真正实用的插件通常需要做更多事情,下面给出几种常见写法。

# 7.1 读取请求信息

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    // 请求方法、URI
    String method = exchange.getRequest().getMethodValue();
    URI uri = exchange.getRequest().getURI();

    // 请求头
    String clientId = exchange.getRequest().getHeaders().getFirst("ClientId");
    String operationCode = exchange.getRequest().getHeaders().getFirst("OperationCode");

    // 业务约定:网关通过这两个请求头识别调用方和接口
    logger.info("ClientId={}, OperationCode={}", clientId, operationCode);

    return chain.filter(exchange);
}

# 7.2 前置处理:修改请求头后继续放行

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    // 给下游加上一个自定义请求头
    ServerHttpRequest request = exchange.getRequest().mutate()
            .header("X-My-Plugin", "custom-value")
            .build();
    return chain.filter(exchange.mutate().request(request).build());
}

# 7.3 前置拦截:直接拒绝请求

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    if (!checkOk(exchange)) {
        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
        // 不调用 chain.filter,直接返回,请求到此终止
        return exchange.getResponse().setComplete();
    }
    return chain.filter(exchange);
}

# 7.4 后置处理:修改响应体(例如脱敏、加解密)

响应体修改需要使用 ServerHttpResponseDecorator 装饰响应,并 return chain.filter(exchange.mutate().response(decorated).build())。这是最复杂的场景,可参考内置"脱敏插件"(DesensitizationGatewayFilterFactory)的写法,核心模式如下:

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerHttpResponse originalResponse = exchange.getResponse();
    ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
        @Override
        public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
            if (Objects.equals(getStatusCode(), HttpStatus.OK) && body instanceof Flux) {
                Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
                    // 合并所有 buffer,读取原始响应体
                    // ... 解析、改写、重新包装 ...
                    return bufferFactory().wrap(newBody.getBytes(StandardCharsets.UTF_8));
                }));
            }
            return super.writeWith(body);
        }
    };
    return chain.filter(exchange.mutate().response(decoratedResponse).build());
}

# 8. 策略配置数据说明

如果你的插件需要"动态可配置的参数"(比如本例的 customParam1/customParam2),需要配合管理端(Governor)下发策略数据:

  1. 管理端创建一个策略类型为 CustomPolicy(与代码中的策略类型名一致)的策略,填入策略配置 JSON,例如:
    { "customParam1": "hello", "customParam2": "world" }
    
  2. 将该策略绑定到某条路由 / 接口 / 客户端。
  3. 网关接收到策略后,会回调你写的 CustomPolicyConfigCache#addPolicyConfig,把 JSON 反序列化为 CustomPolicyConfig 存入缓存。
  4. 请求到达时,PolicyConfigCacheUtil.getPolicyConfig(exchange, "CustomPolicy", ...) 会根据当前请求上下文(路由、OperationCode、ClientId 等)匹配到对应策略并返回。

💡 匹配优先级:ClientId+OperationCode > OperationCode > RouteId > SystemCode。具体由 PolicyConfigCacheUtil 内部决定,插件无需关心。

如果你不需要动态策略,可以删除策略配置相关的两个类,过滤器里直接写死逻辑即可。


# 9. 常见问题与排错

# Q1:插件 jar 已放进 lib/,但过滤器不生效

最常见原因:包名不在扫描范围。

  • 网关只扫描 com.primeton.gateway.** 包。请确认你的所有类(特别是带 @Component 的过滤器工厂和策略缓存类)都在 com.primeton.gateway 或其子包下。
  • 确认路由的 filters 已加上过滤器名(去掉 GatewayFilterFactory 后缀的部分)。
  • 确认已重启网关(运行中放 jar 不生效)。

# Q2:启动报 ClassCastException / LinkageError

原因:把 spring / gateway 等依赖打进了插件 jar,与网关自身的类冲突。 解决:pom 中运行期依赖用 system 作用域引用 lib/ 目录下的 jar(见 4.2),重新打包;确认产物是 thin jar(不含 BOOT-INF/、不含第三方包)。

# Q3:插件用了 spring-boot-maven-plugin 打包,启动后完全不生效

原因:打成了 fat jar,类在 BOOT-INF/classes/,Spring Boot loader 读不到。 解决:移除 spring-boot-maven-plugin,用普通 maven-compiler-plugin + 默认 jar 打包即可(见 4.2 pom)。

# Q4:策略读不到(getPolicyConfig 返回 null)

原因:策略类型名不一致,或策略未下发。 排查:

  • 检查 @Component("X")、getPolicyType() 返回值、过滤器中查询字符串三者是否完全相同(区分大小写)。
  • 确认管理端已创建并下发策略。
  • 确认 CustomPolicyConfigCache 被注册为 Bean(包名要在 com.primeton.gateway 下)。

# Q5:插件代码里 @Autowired 注入的 Bean 是 null

原因:过滤器实例是 apply() 方法里 new 出来的普通对象,不经过 Spring 容器,无法用 @Autowired。 解决:

  • 改用 PolicyConfigCacheUtil / ApplicationBeanUtils.getBean(...) 等工具主动获取;
  • 或在过滤器工厂(它是 Bean)中注入所需依赖后,通过构造参数传给内部过滤器。

# Q6:Windows 下替换 jar 报"文件被占用"

原因:JVM 运行期间持有 jar 文件句柄。 解决:先停止网关,再替换 jar。


# 10. 完整代码清单

下面三个文件即本指南配套示例的全部源码(与 gateway-plugin-custom 模块示例对应,已做命名规范与注释增强)。

# config/CustomPolicyConfig.java

package com.primeton.gateway.custom.config;

public class CustomPolicyConfig {
    private String customParam1;
    private String customParam2;

    public String getCustomParam1() { return customParam1; }
    public void setCustomParam1(String customParam1) { this.customParam1 = customParam1; }

    public String getCustomParam2() { return customParam2; }
    public void setCustomParam2(String customParam2) { this.customParam2 = customParam2; }
}

# config/CustomPolicyConfigCache.java

package com.primeton.gateway.custom.config;

import com.primeton.gateway.core.model.policy.ApiPolicy;
import com.primeton.gateway.core.policy.PolicyConfigCache;
import com.primeton.gateway.util.JsonUtils;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component("CustomPolicy")
public class CustomPolicyConfigCache implements PolicyConfigCache {

    private final Map<String, CustomPolicyConfig> CUSTOM_POLICY_CONFIG_MAP = new ConcurrentHashMap<>();

    @Override
    public CustomPolicyConfig getPolicyConfig(String policyId) {
        return CUSTOM_POLICY_CONFIG_MAP.get(policyId);
    }

    @Override
    public void addPolicyConfig(ApiPolicy apiPolicy) {
        CustomPolicyConfig config = JsonUtils.toObject(apiPolicy.getPolicyConfig(), CustomPolicyConfig.class);
        CUSTOM_POLICY_CONFIG_MAP.put(getConfigCacheKey(apiPolicy), config);
    }

    @Override
    public void deletePolicyConfig(String policyId) {
        CUSTOM_POLICY_CONFIG_MAP.remove(policyId);
    }

    @Override
    public String getPolicyType() {
        return "CustomPolicy";
    }
}

# CustomPolicyGatewayFilterFactory.java

package com.primeton.gateway.custom;

import com.primeton.gateway.core.cache.PolicyConfigCacheUtil;
import com.primeton.gateway.custom.config.CustomPolicyConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class CustomPolicyGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomPolicyGatewayFilterFactory.Config> {

    private static final Logger logger = LoggerFactory.getLogger(CustomPolicyGatewayFilterFactory.class);
    private static final String POLICY_TYPE = "CustomPolicy";

    public CustomPolicyGatewayFilterFactory() {
        super(CustomPolicyGatewayFilterFactory.Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return new CustomFilter();
    }

    public static class CustomFilter implements GatewayFilter, Ordered {
        private static final Logger logger = LoggerFactory.getLogger(CustomFilter.class);

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            logger.info("custom filter execute");
            CustomPolicyConfig customPolicy = PolicyConfigCacheUtil.getPolicyConfig(
                    exchange, POLICY_TYPE, CustomPolicyConfig.class);
            if (customPolicy != null) {
                logger.info("Custom Param1 is {}", customPolicy.getCustomParam1());
                logger.info("Custom Param2 is {}", customPolicy.getCustomParam2());
            }
            return chain.filter(exchange);
        }

        @Override
        public int getOrder() {
            return 0;
        }
    }

    public static class Config {
    }
}

至此,你已经具备在无网关源码环境下,独立开发、打包并部署一个网关自定义插件所需的全部知识。祝开发顺利!

← 5.18 Redis Cluster集群模式ESB配置方式 06 接口服务说明 →