1. 概述
本文将演示如何在 Spring Boot 应用中集成 Togglz 库,通过切面编程实现功能开关(Feature Toggles)的优雅控制。功能开关模式允许在运行时动态控制特定功能的启用状态,这在渐进式功能开发、灰度发布和 A/B 测试等场景中非常实用。
2. Togglz 核心概念
Togglz 是一个功能开关模式的实现库。它的核心价值在于:
- ✅ 运行时控制:无需重启应用即可动态开启/关闭功能
- ✅ 灵活的策略:支持多种激活策略(如用户角色、时间窗口、系统属性等)
- ⚠️ 生产安全:新功能开发时可默认关闭,避免影响线上稳定
典型应用场景包括:
- 未完成功能的渐进式发布
- 特定用户群的功能灰度测试
- A/B 测试中的流量分配
- 紧急情况下的快速功能降级
3. Maven 依赖
在 Spring Boot 项目中集成 Togglz 只需添加以下依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
</parent>
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-spring-boot-starter</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-spring-security</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
</dependency>
依赖说明:
togglz-spring-boot-starter
:提供自动配置togglz-spring-security
:集成 Spring Security 的用户策略- 其他为 Spring Boot 常用组件
4. Togglz 配置
Togglz 的 Spring Boot Starter 会自动创建 FeatureManager
等核心 Bean,我们只需提供 FeatureProvider
实现:
4.1 定义功能枚举
public enum MyFeatures implements Feature {
@Label("员工管理功能")
EMPLOYEE_MANAGEMENT_FEATURE;
public boolean isActive() {
return FeatureContext.getFeatureManager().isActive(this);
}
}
关键点:
- 实现
Feature
接口 @Label
提供可读描述isActive()
方法检查功能状态
4.2 配置 FeatureProvider
@Configuration
public class ToggleConfiguration {
@Bean
public FeatureProvider featureProvider() {
return new EnumBasedFeatureProvider(MyFeatures.class);
}
}
简单粗暴:通过 EnumBasedFeatureProvider
将枚举类注册为功能提供者。
5. 创建功能切面
接下来创建切面,通过自定义注解拦截方法执行:
5.1 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface FeatureAssociation {
MyFeatures value();
}
5.2 切面实现
@Aspect
@Component
public class FeaturesAspect {
private static final Logger LOG = Logger.getLogger(FeaturesAspect.class);
@Around(
"@within(featureAssociation) || @annotation(featureAssociation)"
)
public Object checkAspect(ProceedingJoinPoint joinPoint,
FeatureAssociation featureAssociation) throws Throwable {
if (featureAssociation.value().isActive()) {
return joinPoint.proceed();
} else {
LOG.info(
"Feature " + featureAssociation.value().name() + " is not enabled!");
return null;
}
}
}
执行逻辑:
- ✅ 功能启用:正常执行目标方法
- ❌ 功能关闭:记录日志并返回 null
- ⚠️ 注意:返回 null 可能导致 NPE,实际项目中需根据业务调整
6. 功能激活策略
Togglz 提供多种激活策略,我们以系统属性策略为例:
public enum MyFeatures implements Feature {
@Label("员工管理功能")
@EnabledByDefault
@DefaultActivationStrategy(id = SystemPropertyActivationStrategy.ID,
parameters = {
@ActivationParameter(
name = SystemPropertyActivationStrategy.PARAM_PROPERTY_NAME,
value = "employee.feature"),
@ActivationParameter(
name = SystemPropertyActivationStrategy.PARAM_PROPERTY_VALUE,
value = "true") })
EMPLOYEE_MANAGEMENT_FEATURE;
//...
}
配置说明:
@EnabledByDefault
:默认启用状态SystemPropertyActivationStrategy
:基于 JVM 系统属性- 当
-Demployee.feature=true
时功能启用
其他常用策略:
| 策略名称 | 适用场景 | 示例参数 |
|---------|---------|---------|
| UsernameActivationStrategy
| 按用户控制 | users=admin,user1
|
| UserRoleActivationStrategy
| 按角色控制 | roles=ADMIN,MANAGER
|
| ReleaseDateActivationStrategy
| 定时发布 | date=2023-12-31
|
| GradualActivationStrategy
| 灰度发布 | percentage=10
|
| ServerIpActivationStrategy
| 按服务器控制 | serverIp=192.168.1.*
|
7. 切面测试
7.1 示例应用
创建员工薪资调整功能作为测试案例:
实体类:
@Entity
public class Employee {
@Id
private long id;
private double salary;
// 标准构造器/getter/setter
}
Repository:
public interface EmployeeRepository extends CrudRepository<Employee, Long>{ }
业务服务(带功能注解):
@Service
public class SalaryService {
@Autowired
EmployeeRepository employeeRepository;
@FeatureAssociation(value = MyFeatures.EMPLOYEE_MANAGEMENT_FEATURE)
public void increaseSalary(long id) {
Employee employee = employeeRepository.findById(id).orElse(null);
employee.setSalary(employee.getSalary() +
employee.getSalary() * 0.1);
employeeRepository.save(employee);
}
}
控制器接口:
@Controller
public class SalaryController {
@Autowired
SalaryService salaryService;
@PostMapping("/increaseSalary")
@ResponseBody
public void increaseSalary(@RequestParam long id) {
salaryService.increaseSalary(id);
}
}
7.2 JUnit 测试
功能关闭测试:
@Test
public void givenFeaturePropertyFalse_whenIncreaseSalary_thenNoIncrease()
throws Exception {
Employee emp = new Employee(1, 2000);
employeeRepository.save(emp);
System.setProperty("employee.feature", "false");
mockMvc.perform(post("/increaseSalary")
.param("id", emp.getId() + ""))
.andExpect(status().is(200));
emp = employeeRepository.findOne(1L);
assertEquals("薪资未变化", 2000, emp.getSalary(), 0.5);
}
功能启用测试:
@Test
public void givenFeaturePropertyTrue_whenIncreaseSalary_thenIncrease()
throws Exception {
Employee emp = new Employee(1, 2000);
employeeRepository.save(emp);
System.setProperty("employee.feature", "true");
mockMvc.perform(post("/increaseSalary")
.param("id", emp.getId() + ""))
.andExpect(status().is(200));
emp = employeeRepository.findById(1L).orElse(null);
assertEquals("薪资应增长10%", 2200, emp.getSalary(), 0.5);
}
测试要点:
- 通过
System.setProperty
动态切换功能状态 - 验证功能关闭时业务逻辑不执行
- 验证功能启用时业务逻辑正常执行
8. 总结
本文展示了在 Spring Boot 中整合 Togglz 的完整方案,核心要点:
- 声明式控制:通过注解+切面实现功能开关
- 策略灵活:支持多种激活策略满足不同场景
- 生产友好:无需重启即可动态控制功能
踩坑提醒:
- ⚠️ 切面返回 null 可能导致 NPE,需根据业务调整
- ⚠️ 功能枚举修改后需重启应用
- ⚠️ 复杂策略(如脚本引擎)需注意性能影响
完整示例代码可参考 GitHub 仓库。