当前位置: 首页 > news >正文

从配置到运行时:Forge Admin 的动态 API 配置管理是怎么做的

问题:同一个接口,今天要加认证、明天要加加密、后天要限流,这些行为散落在拦截器、过滤器、注解里,改一次牵一发动全身,怎么集中管理和动态刷新?

1. 这个问题在企业后台里为什么常见

在企业后台开发中,API 行为控制的需求随着业务发展不断变化。一个看似简单的接口可能涉及多种横切关注点:

  • 认证鉴权:哪些接口需要登录?哪些接口可以匿名访问?
  • 报文加解密:敏感数据传输是否需要加密?
  • 租户隔离:是否自动追加tenant_id条件?
  • 限流保护:是否开启接口限流?
  • 脱敏处理:哪些字段需要脱敏后返回?

传统做法有三种:

做法

代码形式

问题

硬编码

拦截器里写if (path.contains("/public")) return true

条件散落,难以维护

注解标注

每个 Controller 方法加@ApiEncrypt@SaCheckPermission

接口多了注解满天飞,修改要改源码

配置文件

YAML 里写auth.excludePaths: /login,/register

无法运行时动态调整,重启才能生效

真实场景更复杂:

  • 某接口上线时允许匿名访问,运营后发现数据泄露风险,紧急要求加认证
  • 某接口原本不限流,用户量暴涨后触发雪崩,需要立即开启限流
  • 新增租户后,部分接口需要关闭租户隔离(如公共查询接口)
  • 不同模块的接口可能有不同的默认策略

核心痛点:API 行为配置散落在代码各处,无法集中管理、无法运行时动态刷新、无法细粒度控制。

2. Forge Admin 是怎么解决的

Forge Admin 提供了forge-starter-api-config模块,实现"配置驱动 + 两级缓存 + 事件刷新"的动态 API 配置管理。

整体架构

┌─────────────────────────────────────────────────────────────────┐ │ API 配置管理架构 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────┐ ┌───────────────┐ │ │ │ sys_api_config│ │ 拦截器链 │ │ │ │ (数据库表) │───────>│ AuthInterceptor│ │ │ └───────────────┘ │ CryptoInterceptor│ │ │ │ │ TenantInterceptor│ │ │ ▼ │ LimitInterceptor │ │ │ ┌───────────────┐ └───────────────┘ │ │ │ ApiConfigManager│ │ │ │ │ (核心决策引擎) │ ▼ │ │ │ - Ant路径匹配 │ ┌───────────────┐ │ │ │ - 两级缓存 │ │ ThreadLocal │ │ │ │ - 事件刷新 │ │ (请求上下文) │ │ │ └───────────────┘ └───────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌───────────────┐ ┌───────────────┐ │ │ │ Caffeine (L1) │ │ 业务Service │ │ │ │ Redis (L2) │ │ (调用决策API) │ │ │ └───────────────┘ └───────────────┘ │ │ │ │ 配置变更 ─────> ApiConfigRefreshEvent ─────> 异步刷新缓存 │ │ │ └─────────────────────────────────────────────────────────────────┘

核心能力表

能力

描述

优势

集中配置

所有接口行为存储在sys_api_config

可通过后台页面管理,无需改代码

Ant路径匹配

支持/api/user/**/api/{id}等模式

精确匹配和模糊匹配结合

两级缓存

Caffeine 本地缓存 + Redis 分布式缓存

高性能 + 集群一致性

事件刷新

配置变更发布事件,异步刷新缓存

运行时生效,无需重启

ThreadLocal 上下文

请求级配置缓存,避免重复查询

拦截器和业务层共享配置

模块结构

forge-starter-api-config/ ├── config/ │ ├── ApiConfigAutoConfiguration.java # 自动配置(引入即生效) │ └── ApiConfigProperties.java # 属性配置(缓存大小、过期时间) ├── domain/ │ ├── entity/SysApiConfig.java # 数据库实体 │ ├── dto/ApiConfigInfo.java # 缓存和传输 DTO │ ├── event/ApiConfigRefreshEvent.java # 配置刷新事件 │ └── dto/ApiConfigQuery.java # 查询参数 ├── service/ │ ├── IApiConfigManager.java # 核心决策引擎接口 │ ├── impl/ApiConfigManagerImpl.java # 实现类(缓存 + 匹配 + 刷新) │ ├── ISysApiConfigService.java # CRUD 服务接口 │ └── impl/SysApiConfigServiceImpl.java # CRUD 实现 ├── context/ │ └── ApiConfigContextHolder.java # ThreadLocal 上下文持有者 ├── listener/ │ └── ApiConfigRefreshListener.java # 事件监听器 ├── controller/ │ ├── SysApiConfigController.java # CRUD 接口 │ └── ApiConfigManageController.java # 管理接口(刷新缓存) ├── mapper/ │ └── SysApiConfigMapper.java # MyBatis Mapper └── registry/ ├── ApiConfigScanner.java # 接口扫描器 └── ApiConfigAutoRegistrar.java # 自动注册器

3. 核心数据结构 / 配置协议

3.1 数据库表:sys_api_config

CREATE TABLE sys_api_config ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', api_name VARCHAR(100) COMMENT '接口名称', api_code VARCHAR(100) COMMENT '接口编码(程序引用)', req_method VARCHAR(10) COMMENT '请求方式(GET/POST/PUT/DELETE/ALL)', url_path VARCHAR(500) COMMENT '接口路径(支持Ant风格)', api_version VARCHAR(20) COMMENT '接口版本号', module_code VARCHAR(50) COMMENT '所属模块', service_id VARCHAR(50) COMMENT '微服务ID', auth_flag TINYINT DEFAULT 1 COMMENT '是否需要认证(1-需要, 0-不需要)', encrypt_flag TINYINT DEFAULT 0 COMMENT '是否需要加解密(1-需要, 0-不需要)', tenant_flag TINYINT DEFAULT 1 COMMENT '是否启用租户隔离(1-启用, 0-不启用)', limit_flag TINYINT DEFAULT 0 COMMENT '是否开启限流(1-开启, 0-关闭)', sensitive_fields VARCHAR(500) COMMENT '需脱敏字段(JSON数组)', status TINYINT DEFAULT 1 COMMENT '状态(1-正常, 0-停用)', remark VARCHAR(500) COMMENT '备注说明', create_by BIGINT COMMENT '创建人', create_time DATETIME COMMENT '创建时间', update_by BIGINT COMMENT '更新人', update_time DATETIME COMMENT '更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API配置管理表';

3.2 核心字段说明

字段

类型

示例

用途

api_name

String

"查询用户信息"

人工可读的接口名称,便于管理

api_code

String

"user_query"

程序引用编码,可用于业务逻辑判断

req_method

String

"GET" / "ALL"

请求方式,ALL表示所有方法

url_path

String

/api/user/**

Ant 风格路径,支持***{变量}

auth_flag

Integer

1

0=不需要认证,1=需要认证

encrypt_flag

Integer

0

0=不需要加解密,1=需要

tenant_flag

Integer

1

0=不启用租户隔离,1=启用

limit_flag

Integer

0

0=不限流,1=开启限流

sensitive_fields

String

["phone","id_card"]

JSON 数组,指定脱敏字段

3.3 配置示例

精确匹配配置

{ "apiName": "登录接口", "apiCode": "auth_login", "reqMethod": "POST", "urlPath": "/auth/login", "authFlag": 0, "encryptFlag": 1, "tenantFlag": 0, "limitFlag": 1, "moduleCode": "auth" }

模糊匹配配置

{ "apiName": "公共查询接口", "apiCode": "public_query", "reqMethod": "GET", "urlPath": "/api/public/**", "authFlag": 0, "encryptFlag": 0, "tenantFlag": 0, "moduleCode": "common" }

3.4 DTO:ApiConfigInfo

@Data public class ApiConfigInfo implements Serializable { private Long id; private String apiName; private String apiCode; private String reqMethod; private String urlPath; private String apiVersion; private String moduleCode; private String serviceId; // 行为标志位(Boolean 类型,便于判断) private Boolean needAuth; private Boolean needEncrypt; private Boolean needTenant; private Boolean needLimit; private List<String> sensitiveFields; private Boolean enabled; private String remark; private Long cacheTime; // 缓存时间戳 // 构建缓存 Key public String buildCacheKey() { return urlPath + ":" + reqMethod; } // 从实体转换 public static ApiConfigInfo fromEntity(SysApiConfig entity) { ApiConfigInfo info = new ApiConfigInfo(); info.setNeedAuth(entity.getAuthFlag() == 1); info.setNeedEncrypt(entity.getEncryptFlag() == 1); // ... 其他字段转换 return info; } }

3.5 配置属性:ApiConfigProperties

forge: api-config: enabled: true # 是否启用 API 配置管理 auto-register: true # 是否自动注册接口 cache-warm-up: true # 是否预热缓存 scan-packages: # 扫描的包路径 - com.mdframe.forge cache: local: max-size: 1000 # 本地缓存最大容量 expire-minutes: 10 # 本地缓存过期时间(分钟) redis: enabled: true # 是否启用 Redis 缓存 expire-seconds: 1800 # Redis 缓存过期时间(秒) key-prefix: "api:config:" # Redis Key 前缀

4. 核心实现链路

4.1 启动阶段:自动配置和缓存预热

入口ApiConfigAutoConfiguration.java

@Configuration @ConditionalOnWebApplication @ConditionalOnProperty(prefix = "forge.api-config", name = "enabled", havingValue = "true") @EnableAsync public class ApiConfigAutoConfiguration { @Bean public ApiConfigScanner apiConfigScanner(RequestMappingHandlerMapping mapping) { return new ApiConfigScanner(mapping); // 扫描已注册的接口 } @Bean @ConditionalOnProperty(prefix = "forge.api-config", name = "auto-register") public ApiConfigAutoRegistrar apiConfigAutoRegistrar( ApiConfigScanner scanner, SysApiConfigMapper mapper, ApiConfigProperties props, IApiConfigManager manager) { return new ApiConfigAutoRegistrar(scanner, mapper, props, manager); } @Bean public ApiConfigRefreshListener apiConfigRefreshListener(IApiConfigManager manager) { return new ApiConfigRefreshListener(manager); // 配置刷新监听器 } }

缓存预热ApiConfigManagerImpl.warmUpCache()

@Override public void warmUpCache() { log.info("开始预热API配置缓存..."); long startTime = System.currentTimeMillis(); List<SysApiConfig> configs = apiConfigMapper.selectAllEnabled(); for (SysApiConfig entity : configs) { ApiConfigInfo config = ApiConfigInfo.fromEntity(entity); String cacheKey = buildCacheKey(entity.getUrlPath(), entity.getReqMethod()); // 写入 L2 缓存(Redis) putToRedis(cacheKey, config); // 写入 L1 缓存(Caffeine) localCache.put(cacheKey, config); } // 初始化全量配置列表缓存(用于 Ant 匹配) allEnabledConfigsCache = configs.stream() .map(ApiConfigInfo::fromEntity) .collect(Collectors.toList()); log.info("API配置缓存预热完成,共{}条配置,耗时{}ms", configs.size(), elapsed); }

4.2 请求阶段:配置获取和上下文设置

拦截器链调用:假设有ApiConfigInterceptor

public class ApiConfigInterceptor implements HandlerInterceptor { @Autowired private IApiConfigManager apiConfigManager; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String urlPath = request.getRequestURI(); String method = request.getMethod(); // 获取 API 配置 ApiConfigInfo config = apiConfigManager.getApiConfig(urlPath, method); // 设置到 ThreadLocal 上下文 ApiConfigContextHolder.setConfig(config); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 清除上下文 ApiConfigContextHolder.clear(); } }

核心决策逻辑ApiConfigManagerImpl.getApiConfig()

@Override public ApiConfigInfo getApiConfig(String urlPath, String method) { if (!configProperties.isEnabled()) { return null; } // 使用 Ant 路径匹配查找最匹配的配置 ApiConfigInfo config = findBestMatchConfig(urlPath, method); if (config != null) { log.debug("API配置匹配成功: {} {} -> {}", method, urlPath, config.getUrlPath()); } return config; } private ApiConfigInfo findBestMatchConfig(String urlPath, String method) { ApiConfigInfo bestMatch = null; int bestScore = -1; for (ApiConfigInfo config : allEnabledConfigsCache) { // 检查请求方法是否匹配 if (!method.equalsIgnoreCase(config.getReqMethod())) { continue; } // 使用 Ant 路径匹配 if (matcher.match(config.getUrlPath(), urlPath)) { // 计算匹配分数:路径越精确,分数越高 int score = calculateMatchScore(urlPath, config.getUrlPath()); if (score > bestScore) { bestScore = score; bestMatch = config; } } } return bestMatch; } private int calculateMatchScore(String requestPath, String configPath) { // 精确匹配:完全相等 -> 100分 if (requestPath.equals(configPath)) { return 100; } // 通配符匹配:根据匹配段数计算分数 if (configPath.contains("*") || configPath.contains("**")) { int matchCount = calculateMatchSegments(requestPath, configPath); return matchCount * 20; } // 前缀匹配 -> 50分 if (requestPath.startsWith(configPath)) { return 50; } return 0; }

上下文使用ApiConfigContextHolder

public class ApiConfigContextHolder { private static final ThreadLocal<ApiConfigInfo> CONTEXT_HOLDER = new ThreadLocal<>(); public static void setConfig(ApiConfigInfo config) { CONTEXT_HOLDER.set(config); } public static ApiConfigInfo getConfig() { return CONTEXT_HOLDER.get(); } // 快捷判断方法 public static boolean needAuth() { ApiConfigInfo config = CONTEXT_HOLDER.get(); return config != null && config.getNeedAuth(); } public static boolean needEncrypt() { ApiConfigInfo config = CONTEXT_HOLDER.get(); return config != null && config.getNeedEncrypt(); } public static boolean needTenant() { ApiConfigInfo config = CONTEXT_HOLDER.get(); return config != null && config.getNeedTenant(); } public static void clear() { CONTEXT_HOLDER.remove(); } }

4.3 配置变更:事件发布和缓存刷新

Controller 发布事件SysApiConfigController

@PostMapping("/edit") public RespInfo<Void> edit(@RequestBody SysApiConfig config) { boolean result = apiConfigService.updateConfig(config); if (result) { // 发布刷新事件(异步处理) eventPublisher.publishEvent(new ApiConfigRefreshEvent(this, ApiConfigRefreshEvent.RefreshType.SINGLE, config.getId(), "修改API配置")); } return result ? RespInfo.success() : RespInfo.error("修改失败"); }

事件监听器ApiConfigRefreshListener

@Component public class ApiConfigRefreshListener { private final IApiConfigManager apiConfigManager; @Async @EventListener public void onApiConfigRefresh(ApiConfigRefreshEvent event) { log.info("收到API配置刷新事件: type={}, reason={}", event.getRefreshType(), event.getReason()); switch (event.getRefreshType()) { case SINGLE: // 刷新单个接口配置 if (event.getConfigId() != null) { apiConfigManager.refreshApiConfigById(event.getConfigId()); } break; case ALL: // 刷新所有接口配置 apiConfigManager.refreshAllApiConfig(); break; case MODULE: // 刷新指定模块的配置 apiConfigManager.refreshApiConfigByModule(event.getModuleCode()); break; } } }

缓存刷新实现ApiConfigManagerImpl

@Override public void refreshApiConfigById(Long configId) { SysApiConfig config = apiConfigMapper.selectById(configId); if (config != null) { String cacheKey = buildCacheKey(config.getUrlPath(), config.getReqMethod()); // 清除 L1 缓存 localCache.invalidate(cacheKey); // 清除 L2 缓存(Redis) deleteFromRedis(cacheKey); // 重新加载配置到全量缓存 reloadAllEnabledConfigsCache(); log.info("刷新API配置缓存: id={}, key={}", configId, cacheKey); } } @Override public void refreshAllApiConfig() { // 清除 L1 缓存 localCache.invalidateAll(); // 清除 L2 缓存(Redis) clearAllFromRedis(); // 重新预热缓存 warmUpCache(); log.info("刷新所有API配置缓存"); }

4.4 完整链路图

┌─────────────────────────────────────────────────────────────────────┐ │ API 配置请求链路 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ HTTP Request │ │ (GET /api/user/123) │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ ApiConfigInterceptor│ │ │ │ - 获取 urlPath/method │ │ │ - 调用 apiConfigManager.getApiConfig() │ │ │ - 设置到 ThreadLocal │ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ IApiConfigManager │ │ │ │ - 查询 L1 缓存(Caffeine) │ │ │ - Ant 路径匹配查找最匹配配置 │ │ │ - 返回 ApiConfigInfo │ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ 后续拦截器链 │ │ │ │ - AuthInterceptor: 根据 needAuth 决定是否拦截 │ │ │ - CryptoInterceptor: 根据 needEncrypt 决定是否加解密 │ │ │ - TenantInterceptor: 根据 needTenant 决定是否追加租户条件 │ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ Controller │ │ │ │ - 业务逻辑处理 │ │ │ - 可通过 ApiConfigContextHolder 获取配置 │ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ afterCompletion │ │ │ │ - ApiConfigContextHolder.clear() │ │ └──────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────┐ │ 配置变更链路 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 用户修改配置(后台页面) │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ SysApiConfigController.edit() │ │ │ - 更新数据库 │ │ │ - 发布 ApiConfigRefreshEvent │ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ ApiConfigRefreshListener │ │ │ @Async @EventListener │ │ │ - 异步处理事件 │ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ IApiConfigManager.refreshApiConfigById() │ │ │ - 清除 L1 缓存 │ │ │ - 清除 L2 缓存 │ │ │ - 重新加载配置 │ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ 下一次请求生效(无需重启) │ │ │ └─────────────────────────────────────────────────────────────────────┘

5. 关键取舍和坑

5.1 为什么用两级缓存而不是只用 Redis?

问题:如果只用 Redis,每次请求都要网络调用,性能损耗明显。

取舍

  • L1(Caffeine):本地内存缓存,毫秒级响应,适合高频访问的配置
  • L2(Redis):分布式缓存,保证集群一致性,适合配置同步

架构优势

请求 -> L1 缓存(命中则返回) -> L2 缓存(命中则写入 L1) -> 数据库

坑点

  • L1 和 L2 的过期时间要合理设置(L1 过期时间 < L2)
  • 集群环境下,配置变更必须清除所有节点的 L1 缓存(通过 Redis Pub/Sub 或事件广播)

5.2 为什么用 Ant 路径匹配而不是精确匹配?

问题:接口数量多时,每个接口都配置一次太繁琐;有些接口有路径参数(如/api/user/{id}),精确匹配无法覆盖。

Ant 匹配规则

模式

匹配示例

/api/user/*

/api/user/123/api/user/profile

/api/user/**

/api/user/123/profile/api/user/123/roles

/api/user/{id}

/api/user/123(Spring MVC 路径参数)

/api/public/**

所有/api/public/下的路径

匹配分数机制

  • 精确匹配:100 分(优先级最高)
  • 通配符匹配:根据匹配段数计算分数(20 分/段)
  • 前缀匹配:50 分

坑点

  • 配置顺序不重要,但路径精确度决定优先级
  • /api/user/**/api/user/*同时配置时,后者更精确,优先匹配

5.3 为什么用事件刷新而不是直接刷新缓存?

问题:配置变更可能涉及多条缓存、多个节点,直接刷新会导致阻塞请求处理。

事件驱动优势

  • @Async异步处理,不影响请求性能
  • 事件广播可扩展到集群(通过 Redis Pub/Sub)
  • 配置变更审计日志可在事件监听器中统一记录

坑点

  • 异步刷新有短暂延迟(通常 < 100ms),极端情况下可能有几次请求使用旧配置
  • 如果 Redis 不可用,事件广播可能失败,需增加降级逻辑

5.4 ThreadLocal 上下文的坑

问题:ThreadLocal 在异步线程池、线程复用场景下可能污染。

解决方案

  • 拦截器afterCompletion必须调用clear()
  • 异步任务(如@Async方法)不应依赖 ThreadLocal,应从IApiConfigManager直接获取

坑点

  • 不要在 Service 层长时间持有 ThreadLocal 配置(可能在后续拦截器中已清除)
  • 测试环境需注意 ThreadLocal 泄漏(单元测试后未清除)

5.5 配置优先级的坑

优先级规则:数据库配置 > 注解配置 > 系统默认值

坑点

  • 如果数据库配置和注解配置冲突,数据库配置优先,可能覆盖注解效果
  • 新增接口时,如果未配置数据库记录,会使用注解或默认值
  • 配置状态status=0(停用)的记录不参与匹配,但数据库中仍存在

5.6 缓存预热 vs 懒加载

预热优势

  • 启动时一次性加载所有配置,避免首次请求延迟
  • 缓存统计信息准确(命中率、加载次数)

懒加载优势

  • 启动速度更快
  • 只缓存实际访问的配置

取舍:配置数量 < 1000 时,预热更合适;配置数量巨大时,懒加载更合适。

5.7 常见错误配置示例

错误 1urlPath未考虑 Spring MVC 路径参数

// ❌ 错误:无法匹配 /api/user/123 "urlPath": "/api/user/:id" // ✅ 正确:Spring MVC 路径参数用 Ant 风格 "urlPath": "/api/user/*"

错误 2reqMethod大小写不一致

// ❌ 错误:大小写不一致可能导致匹配失败 "reqMethod": "get" // ✅ 正确:统一使用大写 "reqMethod": "GET"

错误 3sensitive_fields格式错误

// ❌ 错误:非 JSON 数组格式 "sensitiveFields": "phone,id_card" // ✅ 正确:JSON 数组格式 "sensitiveFields": "[\"phone\",\"id_card\"]"

6. 如何二开

6.1 新增一个 API 配置

步骤

  1. 通过后台页面添加(推荐)
    • 访问"系统管理 > API 配置管理"
    • 新增配置,填写接口名称、路径、行为标志位
    • 保存后自动触发缓存刷新
  1. 通过数据库脚本添加(适合批量导入)
INSERT INTO sys_api_config ( api_name, api_code, req_method, url_path, auth_flag, encrypt_flag, tenant_flag, limit_flag, module_code, status, tenant_id ) VALUES ( '查询用户列表', 'user_list', 'GET', '/api/user/page', 1, 0, 1, 0, 'system', 1, 1 );
    • 注意:tenant_id必须为1(默认租户)
    • 插入后需手动刷新缓存(调用/system/apiConfig/refreshAll
  1. 通过 API 接口添加
curl -X POST http://localhost:8580/system/apiConfig/add \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "apiName": "查询用户列表", "apiCode": "user_list", "reqMethod": "GET", "urlPath": "/api/user/page", "authFlag": 1, "encryptFlag": 0, "tenantFlag": 1, "moduleCode": "system" }'

6.2 扩展拦截器使用 API 配置

示例:在CryptoInterceptor中使用配置决定是否加解密

@Component public class CryptoInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 从 ThreadLocal 获取配置 if (ApiConfigContextHolder.needEncrypt()) { // 标记请求需要解密 request.setAttribute("needEncrypt", true); } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { // 从 ThreadLocal 获取配置 if (ApiConfigContextHolder.needEncrypt()) { // 标记响应需要加密 response.setHeader("X-Need-Encrypt", "true"); } } }

6.3 新增一个行为标志位

步骤

  1. 修改数据库表
ALTER TABLE sys_api_config ADD COLUMN audit_flag TINYINT DEFAULT 0 COMMENT '是否记录审计日志(1-开启, 0-关闭)';
  1. 修改实体类
@Data public class SysApiConfig { // 新增字段 @TransField(dictType = "yes_no") private Integer auditFlag; @TableField(exist = false) private String auditFlagName; }
  1. 修改 DTO
@Data public class ApiConfigInfo { private Boolean needAudit; public static ApiConfigInfo fromEntity(SysApiConfig entity) { info.setNeedAudit(entity.getAuditFlag() != null && entity.getAuditFlag() == 1); return info; } }
  1. 修改 ThreadLocal 上下文
public static boolean needAudit() { ApiConfigInfo config = CONTEXT_HOLDER.get(); return config != null && config.getNeedAudit(); }
  1. 修改拦截器
if (ApiConfigContextHolder.needAudit()) { auditLogger.log(request, response); }

6.4 自定义缓存策略

修改配置属性

forge: api-config: cache: local: max-size: 5000 # 增大本地缓存容量 expire-minutes: 30 # 增长过期时间 redis: enabled: false # 禁用 Redis 缓存(单机场景)

自定义缓存实现

替换ApiConfigManagerImpl的缓存逻辑,例如使用 Guava Cache:

private final Cache<String, ApiConfigInfo> guavaCache = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .recordStats() .build();

6.5 接入前端页面

前端页面示例(使用 AiCrudPage)

<template> <AiCrudPage :api-config="apiConfig" :columns="columns" :search-schema="searchSchema" :edit-schema="editSchema" /> </template> <script setup> const apiConfig = { pageUrl: 'GET@/system/apiConfig/page', addUrl: 'POST@/system/apiConfig/add', editUrl: 'POST@/system/apiConfig/edit', deleteUrl: 'POST@/system/apiConfig/remove', getByIdUrl: 'GET@/system/apiConfig/getById?id=:id' } const columns = computed(() => [ { title: '接口名称', key: 'apiName' }, { title: '接口编码', key: 'apiCode' }, { title: '请求方式', key: 'reqMethod' }, { title: '接口路径', key: 'urlPath' }, { title: '所属模块', key: 'moduleCode' }, { title: '认证', key: 'authFlag', render: h => h(DictTag, { dictType: 'yes_no', value: row.authFlag }) }, { title: '加解密', key: 'encryptFlag', render: h => h(DictTag, { dictType: 'yes_no', value: row.encryptFlag }) }, { title: '租户隔离', key: 'tenantFlag', render: h => h(DictTag, { dictType: 'yes_no', value: row.tenantFlag }) }, { title: '限流', key: 'limitFlag', render: h => h(DictTag, { dictType: 'yes_no', value: row.limitFlag }) }, { title: '状态', key: 'status', render: h => h(DictTag, { dictType: 'enable_disable', value: row.status }) }, ]) const searchSchema = computed(() => [ { field: 'apiName', label: '接口名称', component: 'NInput' }, { field: 'moduleCode', label: '所属模块', component: 'NInput' }, { field: 'status', label: '状态', component: 'DictSelect', dictType: 'enable_disable' }, ]) const editSchema = computed(() => [ { field: 'apiName', label: '接口名称', component: 'NInput', required: true }, { field: 'apiCode', label: '接口编码', component: 'NInput', required: true }, { field: 'reqMethod', label: '请求方式', component: 'DictSelect', dictType: 'req_method', required: true }, { field: 'urlPath', label: '接口路径', component: 'NInput', required: true }, { field: 'moduleCode', label: '所属模块', component: 'NInput' }, { field: 'authFlag', label: '是否认证', component: 'NSwitch', defaultValue: 1 }, { field: 'encryptFlag', label: '是否加解密', component: 'NSwitch', defaultValue: 0 }, { field: 'tenantFlag', label: '是否租户隔离', component: 'NSwitch', defaultValue: 1 }, { field: 'limitFlag', label: '是否限流', component: 'NSwitch', defaultValue: 0 }, { field: 'status', label: '状态', component: 'DictSelect', dictType: 'enable_disable', defaultValue: 1 }, ]) </script>

6.6 新增配置管理接口

示例:新增"刷新缓存"接口

@RestController @RequestMapping("/system/apiConfig") public class ApiConfigManageController { @Autowired private IApiConfigManager apiConfigManager; /** * 刷新所有缓存 */ @PostMapping("/refreshAll") @OperationLog(module = "API配置管理", type = OperationType.UPDATE, desc = "刷新所有缓存") public RespInfo<Void> refreshAll() { apiConfigManager.refreshAllApiConfig(); return RespInfo.success(); } /** * 刷新指定配置缓存 */ @PostMapping("/refreshById") @OperationLog(module = "API配置管理", type = OperationType.UPDATE, desc = "刷新指定缓存") public RespInfo<Void> refreshById(@RequestParam Long id) { apiConfigManager.refreshApiConfigById(id); return RespInfo.success(); } /** * 获取缓存统计信息 */ @GetMapping("/cacheStats") public RespInfo<String> getCacheStats() { return RespInfo.success(apiConfigManager.getCacheStats()); } }

7. 体验入口和下一篇预告

体验 Forge Admin

  • 在线演示:http://www.dlforgelab.com:8084/forge/login
  • 默认账号:admin / 123456
  • Gitee:https://gitee.com/ForgeLab/forge-admin
  • GitHub:https://github.com/yaomindong1996/forge-admin

验证步骤

  1. 登录后台,进入"系统管理 > API 配置管理"
  2. 新增一条配置,观察缓存刷新日志
  3. 修改配置的authFlagencryptFlag,验证拦截器行为变化
  4. 调用/system/apiConfig/cacheStats查看缓存命中率

下一篇预告

下一篇我们将继续拆解:多租户后台怎么做数据隔离?从 tenant_id 到拦截器的完整链路,介绍 Forge Admin 如何通过TenantLineInnerInterceptor实现"配置即生效"的租户隔离能力,避免业务代码散落租户判断。

http://www.jsqmd.com/news/884369/

相关文章:

  • 硕博冲刺期必看:文献阅读软件哪个好用?Scholaread多篇对比阅读实测 - nut-king
  • 从Stable Diffusion到DiT:为什么说Transformer是扩散模型的下一站?
  • 国内实力吊钩式抛丸机厂家排行:实测数据对比 - 奔跑123
  • 无锡综合实力宣传片服务商好评排行 全维度实力解析 - 奔跑123
  • 如何用YDFID-1数据集快速构建纺织缺陷检测模型:完整指南
  • 智能知识学习平台
  • 给大中小学教师同仁的AI大礼包:6款用AI减负增效提质的利器,拿走不谢! - AI论文先行者
  • 聊天机器人“越狱”频发,人工智能安全转向社交心理攻防战!
  • 天水黄金回收实测|2026本地人卖金避坑攻略 - 恒顺黄金回收
  • 2026年呼和浩特市赛罕区汽车贴膜行业趋势与选型指南白皮书 - GrowthUME
  • 邯郸装修深度解析|装修公司怎么选不踩坑?为什么更多邯郸业主认准辉煌装饰? - 博客万
  • 异地恋别称是什么 还有哪些说法
  • 华为光猫配置解密工具终极指南:快速掌握家庭网络配置管理
  • 无锡专业短视频服务机构口碑排行 2026年度版 - 奔跑123
  • 3步掌握TigerVNC:跨平台远程桌面控制的终极免费方案
  • 从零开始:Python智能体建模框架Mesa的完整指南
  • 星链引擎矩阵系统深度解析:AI驱动下的全域智能营销SaaS新范式
  • phpMyAdmin 4.8.1文件包含漏洞CVE-2018-12613实战解析
  • 重温数据库访问
  • 渗透测试信息收集四维框架:从零基础构建数字画像
  • 3步搞定iPhone USB网络共享:Apple-Mobile-Drivers-Installer终极安装指南
  • 武汉宠物医院行业发展现状解析及优质机构盘点 - 品牌评测官
  • 华为光猫配置解密工具终极指南:5分钟快速掌握网络配置解密
  • Openclaw通过图生图+数字人技能快速生成带货视频
  • 3个颠覆性技巧:重新定义Cursor AI免费使用的终极指南
  • CVE-2026-21509:Office 2016/2019预览窗格零日漏洞深度解析
  • 2026 AI Agent十大趋势:从“听话的执行者“到“自主的思考者“
  • 新用户注册Taotoken后快速获取API Key并完成首次模型调用的全过程
  • 终极微信抢红包指南:无需ROOT的智能助手完整教程
  • SMAPI星露谷物语模组框架:3步轻松安装与终极使用教程