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

从配置到运行时: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_configCREATE 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 更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTAPI配置管理表;3.2 核心字段说明字段类型示例用途api_nameString查询用户信息人工可读的接口名称便于管理api_codeStringuser_query程序引用编码可用于业务逻辑判断req_methodStringGET / ALL请求方式ALL表示所有方法url_pathString/api/user/**Ant 风格路径支持*、**、{变量}auth_flagInteger10不需要认证1需要认证encrypt_flagInteger00不需要加解密1需要tenant_flagInteger10不启用租户隔离1启用limit_flagInteger00不限流1开启限流sensitive_fieldsString[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 DTOApiConfigInfoData 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 ListString 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 配置属性ApiConfigPropertiesforge: 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.javaConfiguration 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(); ListSysApiConfig 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 请求阶段配置获取和上下文设置拦截器链调用假设有ApiConfigInterceptorpublic 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; }上下文使用ApiConfigContextHolderpublic class ApiConfigContextHolder { private static final ThreadLocalApiConfigInfo 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 发布事件SysApiConfigControllerPostMapping(/edit) public RespInfoVoid 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(修改失败); }事件监听器ApiConfigRefreshListenerComponent 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; } } }缓存刷新实现ApiConfigManagerImplOverride 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每次请求都要网络调用性能损耗明显。取舍L1Caffeine本地内存缓存毫秒级响应适合高频访问的配置L2Redis分布式缓存保证集群一致性适合配置同步架构优势请求 - 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/123Spring 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 配置优先级的坑优先级规则数据库配置 注解配置 系统默认值坑点如果数据库配置和注解配置冲突数据库配置优先可能覆盖注解效果新增接口时如果未配置数据库记录会使用注解或默认值配置状态status0停用的记录不参与匹配但数据库中仍存在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 配置步骤通过后台页面添加推荐访问系统管理 API 配置管理新增配置填写接口名称、路径、行为标志位保存后自动触发缓存刷新通过数据库脚本添加适合批量导入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通过 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 新增一个行为标志位步骤修改数据库表ALTER TABLE sys_api_config ADD COLUMN audit_flag TINYINT DEFAULT 0 COMMENT 是否记录审计日志1-开启, 0-关闭;修改实体类Data public class SysApiConfig { // 新增字段 TransField(dictType yes_no) private Integer auditFlag; TableField(exist false) private String auditFlagName; }修改 DTOData public class ApiConfigInfo { private Boolean needAudit; public static ApiConfigInfo fromEntity(SysApiConfig entity) { info.setNeedAudit(entity.getAuditFlag() ! null entity.getAuditFlag() 1); return info; } }修改 ThreadLocal 上下文public static boolean needAudit() { ApiConfigInfo config CONTEXT_HOLDER.get(); return config ! null config.getNeedAudit(); }修改拦截器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 Cacheprivate final CacheString, ApiConfigInfo guavaCache CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .recordStats() .build();6.5 接入前端页面前端页面示例使用 AiCrudPagetemplate AiCrudPage :api-configapiConfig :columnscolumns :search-schemasearchSchema :edit-schemaeditSchema / /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 }, ]) /script6.6 新增配置管理接口示例新增刷新缓存接口RestController RequestMapping(/system/apiConfig) public class ApiConfigManageController { Autowired private IApiConfigManager apiConfigManager; /** * 刷新所有缓存 */ PostMapping(/refreshAll) OperationLog(module API配置管理, type OperationType.UPDATE, desc 刷新所有缓存) public RespInfoVoid refreshAll() { apiConfigManager.refreshAllApiConfig(); return RespInfo.success(); } /** * 刷新指定配置缓存 */ PostMapping(/refreshById) OperationLog(module API配置管理, type OperationType.UPDATE, desc 刷新指定缓存) public RespInfoVoid refreshById(RequestParam Long id) { apiConfigManager.refreshApiConfigById(id); return RespInfo.success(); } /** * 获取缓存统计信息 */ GetMapping(/cacheStats) public RespInfoString getCacheStats() { return RespInfo.success(apiConfigManager.getCacheStats()); } }7. 体验入口和下一篇预告体验 Forge Admin在线演示http://www.dlforgelab.com:8084/forge/login默认账号admin / 123456Giteehttps://gitee.com/ForgeLab/forge-adminGitHubhttps://github.com/yaomindong1996/forge-admin验证步骤登录后台进入系统管理 API 配置管理新增一条配置观察缓存刷新日志修改配置的authFlag或encryptFlag验证拦截器行为变化调用/system/apiConfig/cacheStats查看缓存命中率下一篇预告下一篇我们将继续拆解多租户后台怎么做数据隔离从 tenant_id 到拦截器的完整链路介绍 Forge Admin 如何通过TenantLineInnerInterceptor实现配置即生效的租户隔离能力避免业务代码散落租户判断。