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 的完整方案,核心要点:

  1. 声明式控制:通过注解+切面实现功能开关
  2. 策略灵活:支持多种激活策略满足不同场景
  3. 生产友好:无需重启即可动态控制功能

踩坑提醒:

  • ⚠️ 切面返回 null 可能导致 NPE,需根据业务调整
  • ⚠️ 功能枚举修改后需重启应用
  • ⚠️ 复杂策略(如脚本引擎)需注意性能影响

完整示例代码可参考 GitHub 仓库


原始标题:Spring Boot and Togglz Aspect | Baeldung

» 下一篇: Spring Roo