Togglz 深入解析与 Spring Boot 3 实战:让功能发布变成“可控开关”

欢迎你来读这篇博客,这篇博客主要是关于 Togglz 的深入解析与工程化实战。

Togglz 是 Java 生态里的 Feature Flag / Feature Toggle 工具。它可以让我们把“代码上线”和“功能开放”拆开:代码可以先发布到生产环境,但功能是否真正对用户生效,可以通过开关动态控制。

序言

在传统开发模式里,我们经常把“发布代码”和“发布功能”绑在一起。

比如你写了一个新的结算逻辑,发版之后,这个逻辑立刻对所有用户生效。如果线上出问题,常见处理方式就是:

  1. 回滚版本;
  2. 临时改配置;
  3. 紧急修代码;
  4. 半夜重新发版;
  5. 程序员在工位上默默修仙。

但在成熟的持续交付体系里,更推荐的方式是:

代码可以先上线,功能可以后打开;功能可以灰度打开,也可以快速关闭。

这就是 Feature Flag,也叫 Feature Toggle,中文一般叫功能开关特性开关灰度开关

Togglz 正是 Java 平台上对 Feature Toggles 模式的一种实现。它可以在运行时判断某个功能是否启用,并且可以通过配置、数据库、文件、控制台、激活策略等方式动态管理功能状态。

本文会从以下几个角度深入展开:

  • Togglz 是什么;
  • Feature Flag 解决什么问题;
  • Togglz 的核心组件;
  • Spring Boot 3.x 如何集成 Togglz
  • 如何基于 JDBC 持久化开关状态;
  • 如何接入 Admin Console 和 Actuator;
  • 如何做租户级灰度;
  • 如何在业务代码里优雅使用;
  • 如何测试;
  • 生产环境如何治理 Feature Flag;
  • Togglz 和 Nacos、配置中心、权限系统、XXL-JOB 等工具的边界。

本文示例以 Spring Boot 3.x + Java 17+ + Togglz 4.6.2 为基础。

说明:Togglz 4.x 已经面向 Spring Boot 3、Spring 6、Jakarta 10 和 Java 17 生态。旧项目如果还在 Spring Boot 2.x,需要特别关注版本兼容问题。


正文

一、Togglz 是什么?

1.1 一句话理解

Togglz 是一个 Java Feature Flag 工具,用来在应用运行时动态控制某个功能是否启用。

你可以把它理解成:

1
2
普通代码发布:代码一上线,功能就生效
Togglz 模式:代码先上线,功能是否生效由开关决定

例如:

1
2
3
4
5
6
7
if (BizFeatures.NEW_SETTLEMENT_ENGINE.isActive()) {
// 新结算逻辑
return newSettlementCalculator.calculate(command);
} else {
// 老结算逻辑
return oldSettlementCalculator.calculate(command);
}

这段代码的核心含义是:

  • 开关打开:走新逻辑;
  • 开关关闭:走老逻辑;
  • 不需要重新发布代码;
  • 线上出问题可以快速关闭。

这在复杂业务系统里非常实用,尤其是订单、支付、财务、结算、会员、营销、库存这类高风险模块。


1.2 Feature Flag 到底解决什么问题?

Feature Flag 解决的不是“能不能写 if-else”的问题,而是发布风险治理问题。

它主要解决以下场景。

1.2.1 灰度发布

新功能不直接对所有用户开放,而是先对部分用户、部分租户、部分环境开放。

例如:

1
2
3
4
第一阶段:只给内部测试账号打开
第二阶段:只给 3 个试点租户打开
第三阶段:给 10% 用户打开
第四阶段:全量打开

1.2.2 快速止血

上线新逻辑后发现有问题,传统方式可能要重新发版或回滚。

使用 Togglz 后,可以直接关掉开关:

1
NEW_SETTLEMENT_ENGINE = false

功能立刻回到老逻辑。

这叫“止血开关”,在生产事故里很有价值。

1.2.3 新旧逻辑双轨运行

重构一个核心模块时,最怕一刀切。

比如你要重构结算逻辑,可以让新旧逻辑同时存在:

1
2
老逻辑:稳定可用
新逻辑:逐步灰度

如果新逻辑出问题,关开关即可。

1.2.4 A/B 测试

同一个功能可以让不同用户看到不同版本。

例如:

1
2
A 组用户:老导出页面
B 组用户:新导出页面

然后结合埋点分析转化率、点击率、错误率等指标。

1.2.5 未完成代码提前合并

有些功能开发周期较长,如果一直不合主干,分支会越来越难合。

Feature Flag 可以让未完成代码提前合入主干,但默认关闭。

1
2
3
代码已经进入 master
功能默认关闭
等开发完成后再打开

这样可以降低长期分支带来的合并地狱。


二、Togglz 不是什么?

在深入实战前,先把边界说清楚。

2.1 Togglz 不是配置中心

Togglz 和 Nacos、Apollo、Spring Cloud Config 不是同一类东西。

工具 核心职责 示例
Nacos / Apollo 管理配置值 超时时间、线程池大小、短信供应商
Togglz 管理功能是否启用 新结算逻辑是否开启、新导出是否开启
Spring Security 管理认证授权 用户是否有权限访问某接口
XXL-JOB / Quartz 管理任务调度 定时任务何时执行

配置中心当然也可以用一个 boolean 配置实现功能开关,但 Togglz 的优势在于它围绕 Feature Flag 做了更完整的抽象:

  • Feature;
  • FeatureManager;
  • StateRepository;
  • UserProvider;
  • ActivationStrategy;
  • Admin Console;
  • Actuator Endpoint;
  • Testing Support。

配置中心是“给你一堆配置”,Togglz 是“围绕功能发布做治理”。


2.2 Togglz 不是权限系统

不要把功能开关当成权限系统。

比如:

1
2
用户是否能访问应付导出:权限系统决定
新版应付导出逻辑是否启用:Togglz 决定

权限是“这个用户有没有资格用这个功能”。

功能开关是“这个功能当前是否处于开放状态”。

二者可以组合,但不要混成一坨。不然最后你会得到一个既不像权限系统、也不像灰度系统的神秘 if 森林。


2.3 Togglz 不是完整实验平台

Togglz 可以支持用户级、角色级、比例级、时间级等激活策略,但它不是完整的 A/B 实验平台。

完整 A/B 平台通常还需要:

  • 分桶稳定性;
  • 实验互斥;
  • 指标采集;
  • 统计显著性分析;
  • 实验看板;
  • 实验生命周期管理。

Togglz 更适合做 Java 应用内嵌式 Feature Flag,而不是替代专业实验平台。


三、Togglz 核心架构

Togglz 的核心架构并不复杂,可以理解为 6 个组件。

flowchart TB
    A[业务代码] --> B[FeatureManager]
    B --> C[FeatureProvider]
    B --> D[StateRepository]
    B --> E[UserProvider]
    B --> F[ActivationStrategy]

    C --> C1[Feature 枚举]
    D --> D1[application.yml]
    D --> D2[JDBC]
    D --> D3[File]
    D --> D4[Redis / MongoDB 等]
    E --> E1[当前用户]
    E --> E2[角色]
    E --> E3[租户ID]
    F --> F1[用户名策略]
    F --> F2[角色策略]
    F --> F3[比例灰度]
    F --> F4[自定义租户策略]

    G[Admin Console / Actuator] --> B

3.1 Feature

Feature 表示一个功能开关。

在 Java 中通常使用 enum 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.cybermario.togglz.feature;

import org.togglz.core.Feature;
import org.togglz.core.annotation.Label;

public enum BizFeatures implements Feature {

@Label("新版结算引擎")
NEW_SETTLEMENT_ENGINE,

@Label("新版应付导出")
NEW_PAYABLE_EXPORT,

@Label("租户灰度结算")
TENANT_GRAY_SETTLEMENT,

@Label("成本月结增强校验")
COST_MONTH_END_STRICT_CHECK
}

推荐使用枚举定义 Feature,因为:

  • 类型安全;
  • IDE 可提示;
  • 不容易写错字符串;
  • 后续清理方便;
  • 和代码审查更友好。

不推荐在业务代码里到处写字符串:

1
new NamedFeature("NEW_SETTLEMENT_ENGINE")

这种方式灵活,但也更容易写出错别字。功能开关可不是许愿池,拼错了它不会报梦提醒你。


3.2 FeatureManager

FeatureManager 是 Togglz 的核心入口,用于判断某个 Feature 是否激活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.togglz.core.manager.FeatureManager;

@Service
@RequiredArgsConstructor
public class SettlementFeatureService {

private final FeatureManager featureManager;

public boolean useNewSettlementEngine() {
return featureManager.isActive(BizFeatures.NEW_SETTLEMENT_ENGINE);
}
}

也可以在枚举里封装一个 isActive() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.togglz.core.Feature;
import org.togglz.core.context.FeatureContext;
import org.togglz.core.annotation.Label;

public enum BizFeatures implements Feature {

@Label("新版结算引擎")
NEW_SETTLEMENT_ENGINE;

public boolean isActive() {
return FeatureContext.getFeatureManager().isActive(this);
}
}

这样业务代码可以写成:

1
2
3
if (BizFeatures.NEW_SETTLEMENT_ENGINE.isActive()) {
// 新逻辑
}

不过在严肃业务代码中,我更建议注入 FeatureManager,因为它更容易测试,也更符合依赖注入风格。


3.3 StateRepository

StateRepository 用来存储功能开关状态。

常见存储方式:

存储方式 适合场景 是否适合生产
application.yml 本地开发、简单测试 不推荐
FileBasedStateRepository 单机应用、小型项目 视情况
JDBCStateRepository 后端服务、企业系统 推荐
Redis / MongoDB 等 特定基础设施场景 视团队能力
自定义 StateRepository 接入自研配置中心 可选

重点注意:

如果只在 application.yml 里配置 feature state,Spring Boot Starter 默认使用的是内存实现,运行时修改不会持久化。生产环境如果需要动态修改并持久化,建议显式配置 JDBCStateRepository 或其他持久化 StateRepository。


3.4 UserProvider

UserProvider 用来告诉 Togglz 当前用户是谁。

它主要用于两类场景:

  1. 判断某个功能是否只对特定用户启用;
  2. 判断当前用户是否可以访问 Admin Console。

例如:

1
2
3
4
@Bean
public UserProvider userProvider() {
return () -> new SimpleFeatureUser("admin", true);
}

实际项目里通常要从 Spring Security 里获取当前用户。


3.5 ActivationStrategy

ActivationStrategy 是激活策略。

功能开关不是只有 true/false,还可以有策略:

1
2
3
4
5
6
只对指定用户开启
只对指定角色开启
只对指定 IP 开启
只对指定时间后开启
只对指定比例用户开启
只对指定租户开启

比如:

1
2
3
4
5
6
7
togglz:
features:
TENANT_GRAY_SETTLEMENT:
enabled: true
strategy: tenant
param:
tenants: tenant_1001,tenant_1002

含义是:

  • 这个 Feature 总开关是打开的;
  • 但只有租户 tenant_1001tenant_1002 会真正激活;
  • 其他租户仍然走老逻辑。

3.6 Admin Console 与 Actuator

Togglz 可以通过两种方式管理开关:

  1. Admin Console:一个内嵌 Web 控制台;
  2. Actuator Endpoint:通过接口查看和修改开关状态。

生产环境建议:

  • Admin Console 必须加权限;
  • 不要暴露到公网;
  • 最好只允许内网访问;
  • 开关变更要打审计日志;
  • 重要开关变更最好走审批。

功能开关很强,但别让它变成生产环境的“任意门”。


四、版本与依赖选择

4.1 版本建议

本文示例使用:

1
2
3
Java: 17+
Spring Boot: 3.x
Togglz: 4.6.2

Maven 依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<properties>
<togglz.version>4.6.2</togglz.version>
</properties>

<dependencies>
<!-- Togglz Spring Boot Starter -->
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-spring-boot-starter</artifactId>
<version>${togglz.version}</version>
</dependency>

<!-- Togglz Admin Console,可选 -->
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-console</artifactId>
<version>${togglz.version}</version>
</dependency>

<!-- Spring Security 集成,可选;如果你要基于角色保护控制台,建议引入 -->
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-spring-security</artifactId>
<version>${togglz.version}</version>
</dependency>

<!-- 测试支持,可选 -->
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-testing</artifactId>
<version>${togglz.version}</version>
<scope>test</scope>
</dependency>

<!-- JUnit 5 支持,可选 -->
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-junit</artifactId>
<version>${togglz.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

如果你只需要最基础能力,togglz-spring-boot-starter 就够了。

如果要可视化管理开关,加入 togglz-console

如果要测试 Feature 开关状态,加入 togglz-testingtogglz-junit


4.2 Spring Boot 3 项目结构

示例项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
togglz-demo
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com.cybermario.togglz
│ │ │ ├── TogglzDemoApplication.java
│ │ │ ├── config
│ │ │ │ ├── TogglzConfig.java
│ │ │ │ └── SecurityConfig.java
│ │ │ ├── context
│ │ │ │ └── TenantContext.java
│ │ │ ├── feature
│ │ │ │ ├── BizFeatures.java
│ │ │ │ └── TenantActivationStrategy.java
│ │ │ ├── settlement
│ │ │ │ ├── SettlementCalculationService.java
│ │ │ │ ├── OldSettlementCalculator.java
│ │ │ │ ├── NewSettlementCalculator.java
│ │ │ │ ├── SettlementCommand.java
│ │ │ │ └── SettlementResult.java
│ │ │ └── web
│ │ │ └── SettlementController.java
│ │ └── resources
│ │ ├── application.yml
│ │ └── db
│ │ └── migration
│ │ └── V1__create_togglz_table.sql
│ └── test
│ └── java
│ └── com.cybermario.togglz
│ └── SettlementCalculationServiceTest.java

五、第一版实战:基于 application.yml 的最小接入

先做一个最小可运行版本。

5.1 定义 Feature 枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.cybermario.togglz.feature;

import org.togglz.core.Feature;
import org.togglz.core.annotation.Label;

public enum BizFeatures implements Feature {

@Label("新版结算引擎")
NEW_SETTLEMENT_ENGINE,

@Label("新版应付导出")
NEW_PAYABLE_EXPORT,

@Label("成本月结增强校验")
COST_MONTH_END_STRICT_CHECK
}

5.2 配置 application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
server:
port: 8080

togglz:
enabled: true

# 注册 Feature 枚举
feature-enums:
- com.cybermario.togglz.feature.BizFeatures

# 默认开关状态
features:
NEW_SETTLEMENT_ENGINE:
enabled: false
NEW_PAYABLE_EXPORT:
enabled: false
COST_MONTH_END_STRICT_CHECK:
enabled: true

console:
enabled: true
path: /togglz-console
secured: false # 本地演示可以 false,生产环境必须 true

management:
endpoints:
web:
exposure:
include: health,info,togglz

注意:

1
togglz.console.secured: false

只适合本地演示。生产环境不要这么干。

生产环境如果控制台裸奔,那不是灰度发布,是灰度自爆。


5.3 编写业务代码

以财务结算场景为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.cybermario.togglz.settlement;

import com.cybermario.togglz.feature.BizFeatures;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.togglz.core.manager.FeatureManager;

@Service
@RequiredArgsConstructor
public class SettlementCalculationService {

private final FeatureManager featureManager;
private final OldSettlementCalculator oldSettlementCalculator;
private final NewSettlementCalculator newSettlementCalculator;

public SettlementResult calculate(SettlementCommand command) {
if (featureManager.isActive(BizFeatures.NEW_SETTLEMENT_ENGINE)) {
return newSettlementCalculator.calculate(command);
}
return oldSettlementCalculator.calculate(command);
}
}

老逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.cybermario.togglz.settlement;

import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@Component
public class OldSettlementCalculator {

public SettlementResult calculate(SettlementCommand command) {
BigDecimal amount = command.storeAmount()
.subtract(command.feeAmount())
.add(command.adjustAmount());

return new SettlementResult("OLD", amount);
}
}

新逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.cybermario.togglz.settlement;

import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@Component
public class NewSettlementCalculator {

public SettlementResult calculate(SettlementCommand command) {
BigDecimal amount = command.storeAmount()
.subtract(command.feeAmount())
.subtract(command.buyoutPendingAmount())
.add(command.adjustAmount());

return new SettlementResult("NEW", amount);
}
}

请求模型:

1
2
3
4
5
6
7
8
9
10
11
package com.cybermario.togglz.settlement;

import java.math.BigDecimal;

public record SettlementCommand(
BigDecimal storeAmount,
BigDecimal feeAmount,
BigDecimal buyoutPendingAmount,
BigDecimal adjustAmount
) {
}

返回模型:

1
2
3
4
5
6
7
8
9
package com.cybermario.togglz.settlement;

import java.math.BigDecimal;

public record SettlementResult(
String engine,
BigDecimal amount
) {
}

Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.cybermario.togglz.web;

import com.cybermario.togglz.settlement.SettlementCalculationService;
import com.cybermario.togglz.settlement.SettlementCommand;
import com.cybermario.togglz.settlement.SettlementResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/settlements")
@RequiredArgsConstructor
public class SettlementController {

private final SettlementCalculationService settlementCalculationService;

@PostMapping("/calculate")
public SettlementResult calculate(@RequestBody SettlementCommand command) {
return settlementCalculationService.calculate(command);
}
}

5.4 启动后验证

请求:

1
2
3
4
5
6
7
8
curl -X POST http://localhost:8080/api/settlements/calculate \
-H "Content-Type: application/json" \
-d '{
"storeAmount": 1000,
"feeAmount": 100,
"buyoutPendingAmount": 50,
"adjustAmount": 20
}'

如果 NEW_SETTLEMENT_ENGINE.enabled=false,返回:

1
2
3
4
{
"engine": "OLD",
"amount": 920
}

如果打开新逻辑:

1
2
3
4
togglz:
features:
NEW_SETTLEMENT_ENGINE:
enabled: true

返回:

1
2
3
4
{
"engine": "NEW",
"amount": 870
}

六、第二版实战:接入 Admin Console

如果引入了:

1
2
3
4
5
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-console</artifactId>
<version>${togglz.version}</version>
</dependency>

并配置:

1
2
3
4
5
togglz:
console:
enabled: true
path: /togglz-console
secured: false

启动应用后访问:

1
http://localhost:8080/togglz-console

你可以在控制台看到所有 Feature,并手动开启或关闭。


6.1 生产环境必须加权限

本地可以:

1
2
3
togglz:
console:
secured: false

生产环境必须:

1
2
3
4
togglz:
console:
secured: true
feature-admin-authority: ROLE_FEATURE_ADMIN

配合 Spring Security:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.cybermario.togglz.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(registry -> registry
.requestMatchers("/togglz-console/**").hasRole("FEATURE_ADMIN")
.requestMatchers("/actuator/togglz/**").hasRole("FEATURE_ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.csrf(csrf -> csrf.ignoringRequestMatchers("/actuator/togglz/**"));

return http.build();
}
}

注意:

  • 是否关闭 CSRF 要看你的安全策略;
  • 如果是管理后台,建议前端通过统一网关访问;
  • 不建议直接暴露 Togglz Console;
  • 开关变更最好落审计日志。

七、第三版实战:基于 JDBC 持久化开关状态

前面的 application.yml 版本有一个核心问题:

运行时通过控制台改了开关,重启后状态可能丢失。

生产环境一般要把开关状态持久化到数据库。


7.1 创建表结构

Togglz 的 JDBCStateRepository 默认使用一张表保存状态,核心字段如下:

1
2
3
4
5
6
CREATE TABLE feature_toggle (
feature_name VARCHAR(100) PRIMARY KEY,
feature_enabled INTEGER NOT NULL,
strategy_id VARCHAR(200),
strategy_params TEXT
);

如果你使用 Flyway:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- src/main/resources/db/migration/V1__create_togglz_table.sql

CREATE TABLE IF NOT EXISTS feature_toggle (
feature_name VARCHAR(100) PRIMARY KEY,
feature_enabled INTEGER NOT NULL,
strategy_id VARCHAR(200),
strategy_params TEXT
);

COMMENT ON TABLE feature_toggle IS 'Togglz 功能开关状态表';
COMMENT ON COLUMN feature_toggle.feature_name IS '功能开关名称';
COMMENT ON COLUMN feature_toggle.feature_enabled IS '是否启用:1=启用,0=禁用';
COMMENT ON COLUMN feature_toggle.strategy_id IS '激活策略ID';
COMMENT ON COLUMN feature_toggle.strategy_params IS '激活策略参数';

PostgreSQL 中,未加双引号的字段名会转为小写,但 SQL 访问通常大小写不敏感,不影响使用。


7.2 配置 JDBCStateRepository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.cybermario.togglz.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.togglz.core.repository.StateRepository;
import org.togglz.core.repository.jdbc.JDBCStateRepository;

import javax.sql.DataSource;

@Configuration
public class TogglzConfig {

@Bean
public StateRepository stateRepository(DataSource dataSource) {
return JDBCStateRepository.newBuilder(dataSource)
.tableName("feature_toggle")
.createTable(false)
.usePostgresTextColumns(true)
.build();
}
}

解释一下:

1
.tableName("feature_toggle")

指定表名。

1
.createTable(false)

生产环境建议关闭自动建表,由 Flyway/Liquibase 管理表结构。

1
.usePostgresTextColumns(true)

PostgreSQL 场景下,策略参数可以使用 TEXT 类型。


7.3 初始化开关数据

可以初始化几条数据:

1
2
3
4
5
6
INSERT INTO feature_toggle (feature_name, feature_enabled, strategy_id, strategy_params)
VALUES
('NEW_SETTLEMENT_ENGINE', 0, NULL, NULL),
('NEW_PAYABLE_EXPORT', 0, NULL, NULL),
('COST_MONTH_END_STRICT_CHECK', 1, NULL, NULL)
ON CONFLICT (feature_name) DO NOTHING;

然后通过控制台动态修改状态。


7.4 JDBC 模式下的执行流程

sequenceDiagram
    participant User as 用户请求
    participant API as 业务接口
    participant FM as FeatureManager
    participant DB as feature_toggle
    participant Old as 老逻辑
    participant New as 新逻辑

    User->>API: 请求结算计算
    API->>FM: 判断 NEW_SETTLEMENT_ENGINE 是否激活
    FM->>DB: 查询 feature_toggle
    DB-->>FM: 返回开关状态
    alt 开关开启
        FM-->>API: true
        API->>New: 执行新结算逻辑
    else 开关关闭
        FM-->>API: false
        API->>Old: 执行老结算逻辑
    end

八、第四版实战:租户级灰度

在 SaaS 系统中,最常见的灰度单位不是“用户”,而是“租户”。

例如:

1
2
3
tenant_1001:试点租户,打开新结算逻辑
tenant_1002:试点租户,打开新结算逻辑
tenant_2001:普通租户,仍然走老逻辑

这就需要自定义 ActivationStrategy。


8.1 定义 TenantContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.cybermario.togglz.context;

public final class TenantContext {

private static final ThreadLocal<String> TENANT_ID = new ThreadLocal<>();

private TenantContext() {
}

public static void setTenantId(String tenantId) {
TENANT_ID.set(tenantId);
}

public static String getTenantId() {
return TENANT_ID.get();
}

public static void clear() {
TENANT_ID.remove();
}
}

8.2 从请求头中提取租户 ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.cybermario.togglz.context;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class TenantContextFilter implements Filter {

private static final String TENANT_HEADER = "X-Tenant-Id";

@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {

HttpServletRequest httpRequest = (HttpServletRequest) request;

try {
String tenantId = httpRequest.getHeader(TENANT_HEADER);
TenantContext.setTenantId(tenantId);
chain.doFilter(request, response);
} finally {
TenantContext.clear();
}
}
}

生产环境里,租户 ID 一般来自:

  • JWT;
  • 登录态;
  • 网关解析;
  • 租户域名;
  • 请求头;
  • RPC 上下文。

不要完全信任前端直接传来的 X-Tenant-Id,否则租户隔离会很危险。


8.3 自定义 UserProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.cybermario.togglz.config;

import com.cybermario.togglz.context.TenantContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.togglz.core.user.SimpleFeatureUser;
import org.togglz.core.user.UserProvider;

@Configuration
public class TogglzUserConfig {

@Bean
public UserProvider userProvider() {
return () -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

String username = "anonymous";
boolean featureAdmin = false;

if (authentication != null && authentication.isAuthenticated()) {
username = authentication.getName();
featureAdmin = authentication.getAuthorities().stream()
.anyMatch(authority -> "ROLE_FEATURE_ADMIN".equals(authority.getAuthority()));
}

SimpleFeatureUser user = new SimpleFeatureUser(username, featureAdmin);
user.setAttribute("tenantId", TenantContext.getTenantId());

return user;
};
}
}

这里把当前租户 ID 放到了 FeatureUser 的 attribute 中。

后面的激活策略可以从用户属性里读取它。


8.4 自定义 TenantActivationStrategy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.cybermario.togglz.feature;

import org.springframework.stereotype.Component;
import org.togglz.core.activation.Parameter;
import org.togglz.core.activation.ParameterBuilder;
import org.togglz.core.repository.FeatureState;
import org.togglz.core.spi.ActivationStrategy;
import org.togglz.core.user.FeatureUser;

import java.util.Arrays;
import java.util.Objects;

@Component
public class TenantActivationStrategy implements ActivationStrategy {

public static final String ID = "tenant";
public static final String PARAM_TENANTS = "tenants";

@Override
public String getId() {
return ID;
}

@Override
public String getName() {
return "租户灰度策略";
}

@Override
public boolean isActive(FeatureState featureState, FeatureUser user) {
if (user == null) {
return false;
}

Object tenantIdValue = user.getAttribute("tenantId");
if (tenantIdValue == null) {
return false;
}

String tenantId = tenantIdValue.toString();
String tenants = featureState.getParameter(PARAM_TENANTS);

if (tenants == null || tenants.isBlank()) {
return false;
}

return Arrays.stream(tenants.split(","))
.map(String::trim)
.filter(value -> !value.isBlank())
.anyMatch(value -> Objects.equals(value, tenantId));
}

@Override
public Parameter[] getParameters() {
return new Parameter[]{
ParameterBuilder.create(PARAM_TENANTS)
.label("租户ID列表")
.description("多个租户ID使用英文逗号分隔,例如 tenant_1001,tenant_1002")
};
}
}

Spring Boot Starter 会自动把 Spring 容器中的 ActivationStrategy Bean 加入到 FeatureManager


8.5 配置租户灰度开关

1
2
3
4
5
6
7
8
9
10
togglz:
feature-enums:
- com.cybermario.togglz.feature.BizFeatures

features:
TENANT_GRAY_SETTLEMENT:
enabled: true
strategy: tenant
param:
tenants: tenant_1001,tenant_1002

业务代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.cybermario.togglz.settlement;

import com.cybermario.togglz.feature.BizFeatures;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.togglz.core.manager.FeatureManager;

@Service
@RequiredArgsConstructor
public class SettlementCalculationService {

private final FeatureManager featureManager;
private final OldSettlementCalculator oldSettlementCalculator;
private final NewSettlementCalculator newSettlementCalculator;

public SettlementResult calculate(SettlementCommand command) {
if (featureManager.isActive(BizFeatures.TENANT_GRAY_SETTLEMENT)) {
return newSettlementCalculator.calculate(command);
}
return oldSettlementCalculator.calculate(command);
}
}

验证:

1
2
3
4
5
6
7
8
9
curl -X POST http://localhost:8080/api/settlements/calculate \
-H "Content-Type: application/json" \
-H "X-Tenant-Id: tenant_1001" \
-d '{
"storeAmount": 1000,
"feeAmount": 100,
"buyoutPendingAmount": 50,
"adjustAmount": 20
}'

返回新逻辑:

1
2
3
4
{
"engine": "NEW",
"amount": 870
}

换成普通租户:

1
2
3
4
5
6
7
8
9
curl -X POST http://localhost:8080/api/settlements/calculate \
-H "Content-Type: application/json" \
-H "X-Tenant-Id: tenant_2001" \
-d '{
"storeAmount": 1000,
"feeAmount": 100,
"buyoutPendingAmount": 50,
"adjustAmount": 20
}'

返回老逻辑:

1
2
3
4
{
"engine": "OLD",
"amount": 920
}

九、第五版实战:用 AOP 做方法级功能保护

有些功能不是“新旧逻辑切换”,而是“功能整体关闭”。

例如:

1
新版应付导出功能关闭时,接口直接不允许访问

可以用 AOP 封装一个注解。


9.1 定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.cybermario.togglz.feature;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeatureGuard {

BizFeatures value();

String message() default "当前功能暂未开放";
}

9.2 定义异常

1
2
3
4
5
6
7
8
package com.cybermario.togglz.feature;

public class FeatureDisabledException extends RuntimeException {

public FeatureDisabledException(String message) {
super(message);
}
}

9.3 编写切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.cybermario.togglz.feature;

import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.togglz.core.manager.FeatureManager;

@Aspect
@Component
@RequiredArgsConstructor
public class FeatureGuardAspect {

private final FeatureManager featureManager;

@Around("@annotation(featureGuard)")
public Object around(ProceedingJoinPoint joinPoint,
FeatureGuard featureGuard) throws Throwable {

if (featureManager.isActive(featureGuard.value())) {
return joinPoint.proceed();
}

throw new FeatureDisabledException(featureGuard.message());
}
}

9.4 使用注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.cybermario.togglz.web;

import com.cybermario.togglz.feature.BizFeatures;
import com.cybermario.togglz.feature.FeatureGuard;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PayableExportController {

@GetMapping("/api/payables/export")
@FeatureGuard(value = BizFeatures.NEW_PAYABLE_EXPORT, message = "新版应付导出功能暂未开放")
public String export() {
return "export success";
}
}

这样业务方法里就不用到处写:

1
2
3
if (!featureManager.isActive(...)) {
throw new RuntimeException(...);
}

十、Actuator Endpoint 使用

如果配置了:

1
2
3
4
5
management:
endpoints:
web:
exposure:
include: health,info,togglz

可以查看开关状态:

1
curl http://localhost:8080/actuator/togglz

返回类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[
{
"name": "NEW_SETTLEMENT_ENGINE",
"enabled": false,
"strategy": null,
"params": {}
},
{
"name": "TENANT_GRAY_SETTLEMENT",
"enabled": true,
"strategy": "tenant",
"params": {
"tenants": "tenant_1001,tenant_1002"
}
}
]

也可以通过 Actuator 修改状态。不同版本的 endpoint 细节可能略有差异,建议以当前项目暴露的 /actuator/togglz 返回为准。

生产建议:

1
2
3
4
只允许内网访问
只允许管理员角色访问
所有修改必须记录审计日志
不要让普通业务用户接触该端点

十一、开启缓存:减少数据库查询

如果每次判断开关都查数据库,高频接口会有额外压力。

可以开启 Togglz cache:

1
2
3
4
5
togglz:
cache:
enabled: true
time-to-live: 5000
time-unit: milliseconds

含义:

1
2
开关状态缓存 5 秒
5 秒内重复判断不再查数据库

生产建议:

场景 TTL 建议
风险开关,需要快速止血 1 秒 ~ 5 秒
普通灰度开关 5 秒 ~ 30 秒
很少变化的开关 30 秒 ~ 5 分钟

不要为了省几次数据库查询,把 TTL 设置得过长。

如果线上出事故,你希望开关能尽快生效,而不是等缓存慢悠悠地醒来。


十二、测试 Feature Flag

Feature Flag 最大的坑是:

开关打开测了,开关关闭没测;或者反过来。

只要业务代码里有开关,就应该测试两条路径。


12.1 使用 Togglz 测试支持

依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-testing</artifactId>
<version>${togglz.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-junit</artifactId>
<version>${togglz.version}</version>
<scope>test</scope>
</dependency>

测试示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.cybermario.togglz;

import com.cybermario.togglz.feature.BizFeatures;
import com.cybermario.togglz.settlement.SettlementCalculationService;
import com.cybermario.togglz.settlement.SettlementCommand;
import com.cybermario.togglz.settlement.SettlementResult;
import org.junit.jupiter.api.Test;
import org.togglz.junit5.AllDisabled;
import org.togglz.junit5.AllEnabled;
import org.togglz.testing.TestFeatureManager;

import java.math.BigDecimal;

import static org.assertj.core.api.Assertions.assertThat;

class SettlementCalculationServiceTest {

private final SettlementCalculationService service = TestSettlementFactory.createService();

@Test
@AllDisabled(BizFeatures.class)
void shouldUseOldSettlementEngineWhenFeatureDisabled(TestFeatureManager featureManager) {
featureManager.disable(BizFeatures.NEW_SETTLEMENT_ENGINE);

SettlementResult result = service.calculate(command());

assertThat(result.engine()).isEqualTo("OLD");
assertThat(result.amount()).isEqualByComparingTo("920");
}

@Test
@AllEnabled(BizFeatures.class)
void shouldUseNewSettlementEngineWhenFeatureEnabled(TestFeatureManager featureManager) {
featureManager.enable(BizFeatures.NEW_SETTLEMENT_ENGINE);

SettlementResult result = service.calculate(command());

assertThat(result.engine()).isEqualTo("NEW");
assertThat(result.amount()).isEqualByComparingTo("870");
}

private SettlementCommand command() {
return new SettlementCommand(
new BigDecimal("1000"),
new BigDecimal("100"),
new BigDecimal("50"),
new BigDecimal("20")
);
}
}

上面这个示例里 TestSettlementFactory 可以自己构造 service 和依赖对象,也可以用 Spring Boot Test 注入。

核心是:开关开和关都要测


12.2 测试策略建议

推荐至少覆盖:

测试类型 要测什么
Unit Test 开关打开/关闭时分支是否正确
Integration Test FeatureManager、StateRepository 是否正常
Web Test 接口在开关关闭时是否返回预期
Regression Test 删除开关前确认老逻辑已经不再需要

十三、生产环境最佳实践

13.1 命名规范

Feature 名称建议使用业务域前缀:

1
2
3
4
5
SETTLEMENT_NEW_ENGINE
PAYABLE_NEW_EXPORT
RECEIVABLE_RECONCILIATION_V2
COST_MONTH_END_STRICT_CHECK
ORDER_NEW_SPLIT_RULE

不要使用:

1
2
3
4
5
NEW_FEATURE
TEST
FLAG_1
TEMP
AAA

开关名必须一眼看懂,否则三个月后没人敢删。


13.2 开关分类

建议把开关分为几类。

类型 说明 示例
Release Toggle 发布型开关 新结算逻辑
Experiment Toggle 实验型开关 A/B 页面
Ops Toggle 运维型开关 关闭高风险导出
Permission Toggle 功能开放型开关 指定租户开放
Kill Switch 熔断止血开关 关闭第三方调用

不同类型的生命周期不同。


13.3 每个开关必须有 Owner

每个 Feature Flag 都应该有负责人。

建议维护元信息:

1
2
3
4
5
6
7
8
开关名称:SETTLEMENT_NEW_ENGINE
负责人:财务后端组 / 张三
创建时间:2026-06-12
计划删除时间:2026-07-30
开关类型:Release Toggle
默认状态:关闭
灰度范围:指定租户
回滚方案:关闭开关走老逻辑

你可以在文档里维护,也可以扩展数据库字段,也可以用代码注解约定。


13.4 开关不要永久存在

Feature Flag 最大的问题是容易变成技术债。

上线初期:

1
2
3
4
5
if (featureEnabled) {
newLogic();
} else {
oldLogic();
}

三个月后:

1
2
3
4
5
6
7
if (featureEnabled && tenantEnabled && configEnabled && userEnabled) {
// 新逻辑
} else if (...) {
// 历史兼容逻辑
} else {
// 祖传逻辑
}

最后就变成开关博物馆。

建议:

1
2
3
4
发布型开关:功能全量稳定后 1~2 个迭代内删除
实验型开关:实验结束后删除
止血开关:可长期保留,但必须明确场景
运维型开关:保留,但必须有监控和审计

13.5 核心链路必须保持幂等

Feature Flag 只能控制入口,不能替代业务幂等。

例如结算单生成:

1
2
3
开关打开后走新结算逻辑
如果请求重试、任务重跑、消息重复消费
仍然必须保证不会生成重复结算单

所以还需要:

  • 唯一索引;
  • 幂等键;
  • 状态机;
  • 乐观锁;
  • 业务流水号;
  • 重复提交保护。

别指望一个 Feature Flag 拯救所有设计问题,它是安全带,不是自动驾驶。


13.6 开关变更要有审计

建议记录:

1
2
3
4
5
6
7
8
开关名称
修改前状态
修改后状态
修改人
修改时间
修改原因
影响范围
关联工单

生产问题排查时,这类记录非常关键。

很多事故不是因为代码变了,而是因为某个开关被人“随手一点”。

随手一点,生产升天。别问,问就是血泪经验。


13.7 监控指标

建议对关键 Feature Flag 增加监控:

1
2
3
4
5
6
7
新逻辑调用次数
老逻辑调用次数
新逻辑异常率
老逻辑异常率
新逻辑耗时
老逻辑耗时
不同租户命中情况

示例埋点:

1
2
3
4
5
6
7
if (featureManager.isActive(BizFeatures.NEW_SETTLEMENT_ENGINE)) {
meterRegistry.counter("settlement.engine", "type", "new").increment();
return newSettlementCalculator.calculate(command);
}

meterRegistry.counter("settlement.engine", "type", "old").increment();
return oldSettlementCalculator.calculate(command);

灰度发布不是“开了就完事”,而是要看数据是否正常。


十四、常见坑

14.1 只用 application.yml,误以为控制台修改会持久化

本地测试没问题,生产重启后状态丢失。

解决:

1
生产环境使用 JDBCStateRepository / FileBasedStateRepository / 其他持久化 StateRepository

14.2 控制台没加权限

本地为了方便设置:

1
2
3
togglz:
console:
secured: false

结果带到生产。

解决:

1
2
3
4
生产环境 secured=true
配合 Spring Security
网关限制来源
加审计

14.3 开关判断散落在业务代码里

到处都是:

1
2
if (featureManager.isActive(...)) {
}

解决:

  • 对新旧逻辑切换,可以封装策略类;
  • 对接口级保护,可以用注解 + AOP;
  • 对大模块,可以用 Facade 隔离;
  • 不要让 Feature Flag 污染核心领域模型。

14.4 开关长期不删除

Release Toggle 全量后还不删,代码越来越复杂。

解决:

1
2
3
每个开关建立计划删除时间
每个迭代清理过期开关
代码审查时关注 Feature Flag 生命周期

14.5 把 Feature Flag 当业务规则引擎

Togglz 可以控制功能是否启用,但不适合承载复杂业务规则。

不要写成:

1
如果 A 租户 + B 角色 + C 时间 + D 金额 + E 门店 + F 商品类目,则启用功能

这种场景更适合规则引擎、策略模式、配置化规则表,而不是把 Togglz 用成万能 if 中央处理器。


十五、Togglz 与常见方案对比

15.1 Togglz vs Nacos

对比项 Togglz Nacos
核心定位 Feature Flag 配置中心/注册中心
是否有 Feature 抽象
是否有激活策略 需要自己实现
是否适合配置普通参数 不适合 适合
是否适合功能灰度 适合 可以做,但要封装
是否有控制台

如果你已经有 Nacos,也可以直接用 Nacos 配置做开关。

但如果你的需求是:

1
2
3
4
5
6
按用户灰度
按租户灰度
按角色灰度
需要 FeatureManager
需要测试支持
需要内嵌控制台

Togglz 会更贴近 Feature Flag 场景。


15.2 Togglz vs Unleash

对比项 Togglz Unleash
部署方式 Java 应用内嵌 独立 Feature Flag 平台
控制台 内嵌控制台 独立控制台
多语言支持 偏 Java 多语言 SDK
企业治理 轻量 更完整
适用场景 Java 项目内嵌式开关 多系统统一 Feature Flag 平台

如果只是单个 Java/Spring Boot 系统,Togglz 很轻。

如果公司有多个语言、多套系统,需要统一 Feature Flag 平台,可以考虑 Unleash、OpenFeature + Provider 等方案。


15.3 Togglz vs 自己写 if + 配置

自己写当然可以:

1
2
@Value("${features.new-settlement:false}")
private boolean newSettlement;

但问题是:

  • 没有统一 Feature 抽象;
  • 没有激活策略;
  • 没有控制台;
  • 没有测试支持;
  • 没有用户上下文;
  • 没有状态仓库抽象;
  • 后期会变成散落配置。

小项目可以自己写,大项目建议别手搓轮子。

手搓轮子本身没错,但轮子多了之后,车可能就不认路了。


十六、完整配置参考

16.1 application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
server:
port: 8080

spring:
application:
name: togglz-demo

togglz:
enabled: true

feature-enums:
- com.cybermario.togglz.feature.BizFeatures

features:
NEW_SETTLEMENT_ENGINE:
enabled: false

NEW_PAYABLE_EXPORT:
enabled: false

COST_MONTH_END_STRICT_CHECK:
enabled: true

TENANT_GRAY_SETTLEMENT:
enabled: true
strategy: tenant
param:
tenants: tenant_1001,tenant_1002

cache:
enabled: true
time-to-live: 5000
time-unit: milliseconds

console:
enabled: true
path: /togglz-console
secured: true
feature-admin-authority: ROLE_FEATURE_ADMIN
use-management-port: false

management:
endpoints:
web:
exposure:
include: health,info,togglz

16.2 Feature 枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.cybermario.togglz.feature;

import org.togglz.core.Feature;
import org.togglz.core.annotation.Label;

public enum BizFeatures implements Feature {

@Label("新版结算引擎")
NEW_SETTLEMENT_ENGINE,

@Label("新版应付导出")
NEW_PAYABLE_EXPORT,

@Label("成本月结增强校验")
COST_MONTH_END_STRICT_CHECK,

@Label("租户灰度结算")
TENANT_GRAY_SETTLEMENT
}

16.3 TogglzConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.cybermario.togglz.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.togglz.core.repository.StateRepository;
import org.togglz.core.repository.jdbc.JDBCStateRepository;

import javax.sql.DataSource;

@Configuration
public class TogglzConfig {

@Bean
public StateRepository stateRepository(DataSource dataSource) {
return JDBCStateRepository.newBuilder(dataSource)
.tableName("feature_toggle")
.createTable(false)
.usePostgresTextColumns(true)
.build();
}
}

16.4 租户策略配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.cybermario.togglz.feature;

import org.springframework.stereotype.Component;
import org.togglz.core.activation.Parameter;
import org.togglz.core.activation.ParameterBuilder;
import org.togglz.core.repository.FeatureState;
import org.togglz.core.spi.ActivationStrategy;
import org.togglz.core.user.FeatureUser;

import java.util.Arrays;
import java.util.Objects;

@Component
public class TenantActivationStrategy implements ActivationStrategy {

public static final String ID = "tenant";
public static final String PARAM_TENANTS = "tenants";

@Override
public String getId() {
return ID;
}

@Override
public String getName() {
return "租户灰度策略";
}

@Override
public boolean isActive(FeatureState featureState, FeatureUser user) {
if (user == null || user.getAttribute("tenantId") == null) {
return false;
}

String currentTenantId = user.getAttribute("tenantId").toString();
String enabledTenants = featureState.getParameter(PARAM_TENANTS);

if (enabledTenants == null || enabledTenants.isBlank()) {
return false;
}

return Arrays.stream(enabledTenants.split(","))
.map(String::trim)
.filter(value -> !value.isBlank())
.anyMatch(value -> Objects.equals(value, currentTenantId));
}

@Override
public Parameter[] getParameters() {
return new Parameter[]{
ParameterBuilder.create(PARAM_TENANTS)
.label("租户ID列表")
.description("多个租户ID用英文逗号分隔")
};
}
}

十七、落地到财务系统的建议

如果你在财务 SaaS 里使用 Togglz,我建议优先用于这些场景:

17.1 结算逻辑重构

1
SETTLEMENT_NEW_ENGINE

用途:

1
2
3
新旧结算逻辑切换
灰度租户试点
异常时快速回退老逻辑

17.2 应付导出重构

1
PAYABLE_NEW_EXPORT

用途:

1
2
3
新版导出性能优化
大数据量导出灰度
出问题时快速关闭新版导出

17.3 成本月结增强校验

1
COST_MONTH_END_STRICT_CHECK

用途:

1
2
3
校验规则逐步打开
先观察影响范围
避免一次性拦截大量历史脏数据

17.4 第三方接口新版适配

1
THIRD_PARTY_ADAPTER_V2

用途:

1
2
3
外部接口切换
新老供应商切换
按租户/客户维度灰度

17.5 大客户预处理性能优化

1
SETTLEMENT_PREPROCESS_OPTIMIZATION

用途:

1
2
3
新预处理逻辑只给大客户打开
观察耗时、错误率、结果一致性
稳定后全量

十八、推荐落地流程

flowchart TD
    A[定义新功能] --> B[创建 Feature Flag]
    B --> C[默认关闭]
    C --> D[代码合并到主干]
    D --> E[测试环境打开]
    E --> F[预发环境验证]
    F --> G[生产指定租户灰度]
    G --> H{指标是否正常}
    H -- 否 --> I[关闭开关回退老逻辑]
    H -- 是 --> J[扩大灰度范围]
    J --> K[全量打开]
    K --> L[观察一个迭代]
    L --> M[删除旧逻辑和开关]

十九、总结

Togglz 的核心价值不是让你少写几行配置,而是让功能发布变得更可控。

它适合这些场景:

1
2
3
4
5
6
7
新旧逻辑切换
灰度发布
租户级开放
用户级开放
快速止血
渐进式重构
持续交付

它不适合这些场景:

1
2
3
4
5
替代权限系统
替代配置中心
替代规则引擎
替代完整 A/B 实验平台
长期堆积大量历史开关

在 Spring Boot 项目中,Togglz 的最佳实践可以总结为:

1
2
3
4
5
6
7
Feature 用枚举定义
状态用 JDBC 持久化
控制台必须加权限
关键开关必须有审计
开关开关两条路径都要测试
灰度发布必须配合监控
全量稳定后及时删除 Release Toggle

一句话:

Togglz 让功能上线从“发版即梭哈”变成“开关可控、灰度可退、风险可管”。

这就是它最有价值的地方。


参考资料

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


Togglz 深入解析与 Spring Boot 3 实战:让功能发布变成“可控开关”
https://allendericdalexander.github.io/2026/06/12/devops/togglz-deep-dive-practice-blog/
作者
AtLuoFu
发布于
2026年6月12日
许可协议