«1. Обзор

В этом руководстве мы рассмотрим, как можно использовать библиотеку Togglz с приложением Spring Boot.

2. Togglz

Библиотека Togglz обеспечивает реализацию шаблона проектирования Feature Toggles. Этот шаблон относится к наличию механизма, который позволяет определить во время выполнения приложения, включена ли определенная функция или нет, на основе переключателя.

Отключение функции во время выполнения может быть полезно в различных ситуациях, например, при работе над новой функцией, которая еще не завершена, при желании разрешить доступ к функции только для подмножества пользователей или при проведении A/B-тестирования.

В следующих разделах мы создадим аспект, который перехватывает методы с аннотацией, предоставляющей имя функции, и определим, следует ли продолжать выполнение методов в зависимости от того, включена функция или нет.

3. Зависимости Maven

Наряду с зависимостями Spring Boot библиотека Togglz предоставляет jar Spring Boot Starter:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.0</version>
</parent>

<dependency>
    <groupId>org.togglz</groupId>
    <artifactId>togglz-spring-boot-starter</artifactId>
    <version>2.4.1</version>
<dependency>
    <groupId>org.togglz</groupId>
    <artifactId>togglz-spring-security</artifactId>
    <version>2.4.1</version>
</dependency>
</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>1.4.194</version>
</dependency>

Последние версии togglz-spring-boot-starter, togglz-spring-security , spring-boot-starter-web, spring-boot-starter-data-jpa, spring-boot-starter-test, h2 можно загрузить с Maven Central.

4. Конфигурация Togglz

Библиотека togglz-spring-boot-starter содержит автоконфигурацию для создания необходимых bean-компонентов, таких как FeatureManager. Единственный bean-компонент, который нам нужно предоставить, — это bean-компонент featureProvider.

Во-первых, давайте создадим перечисление, которое реализует интерфейс Feature и содержит список имен функций:

public enum MyFeatures implements Feature {

    @Label("Employee Management Feature")
    EMPLOYEE_MANAGEMENT_FEATURE;

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

Перечисление также определяет метод isActive(), который проверяет, включена ли определенная функция.

Затем мы можем определить bean-компонент типа EnumBasedFeatureProvider в классе конфигурации Spring Boot:

@Configuration
public class ToggleConfiguration {

    @Bean
    public FeatureProvider featureProvider() {
        return new EnumBasedFeatureProvider(MyFeatures.class);
    }
}

5. Создание аспекта

Далее мы создадим аспект, который перехватывает пользовательскую аннотацию AssociatedFeature и проверяет функцию. предоставленный в параметре аннотации, чтобы определить, активен он или нет:

@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;
        }
    }
}

Давайте также определим пользовательскую аннотацию под названием FeatureAssociation, которая будет иметь параметр value() типа перечисления MyFeatures:

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface FeatureAssociation {
    MyFeatures value();
}

Если функция активен, аспект продолжит выполнение метода; в противном случае он зарегистрирует сообщение без запуска кода метода.

6. Активация функции

Функция в Togglz может быть как активной, так и неактивной. Это поведение контролируется включенным флагом и опциональной стратегией активации.

Чтобы установить для флага enable значение true, мы можем использовать аннотацию @EnabledByDefault в определении значения перечисления.

Библиотека Togglz также предоставляет различные стратегии активации, которые можно использовать для определения того, включена ли функция на основе определенного условия.

В нашем примере давайте используем SystemPropertyActivationStrategy для нашего EMPLOYEE_MANAGEMENT_FEATURE, который оценивает состояние функции на основе значения системного свойства. Требуемое имя и значение свойства можно указать с помощью аннотации @ActivationParameter:

public enum MyFeatures implements Feature {

    @Label("Employee Management Feature") 
    @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;
    //...
}

Мы установили, что наша функция будет включена только в том случае, если свойство employee.feature имеет значение true.

Другие типы стратегий активации, предоставляемые библиотекой Togglz:

    UsernameActivationStrategy — позволяет функции быть активной для указанного списка пользователей. feature ReleaseDateActivationStrategy — автоматически активирует функцию в определенную дату и время GradualActivationStrategy — включает функцию для определенного процента пользователей ScriptEngineActivationStrategy — позволяет использовать пользовательский сценарий, написанный на языке, поддерживаемом ScriptEngine JVM чтобы определить, активна функция или нет ServerIpActivationStrategy — функция включается на основе IP-адресов сервера

7. Тестирование аспекта

7.1. Пример приложения

Чтобы увидеть наш аспект в действии, давайте создадим простой пример, содержащий функцию управления сотрудниками организации.

«По мере разработки этой функции мы можем добавлять методы и классы, аннотированные нашей аннотацией @AssociatedFeature со значением EMPLOYEE_MANAGEMENT_FEATURE. Это гарантирует, что они будут доступны только в том случае, если функция активна.

Во-первых, давайте определим класс сущности и репозиторий Employee на основе Spring Data:

@Entity
public class Employee {

    @Id
    private long id;
    private double salary;
    
    // standard constructor, getters, setters
}
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);
    }
}

Затем добавим EmployeeService с методом для увеличения зарплаты сотрудника. Мы добавим аннотацию @AssociatedFeature к методу с параметром EMPLOYEE_MANAGEMENT_FEATURE:

@Controller
public class SalaryController {

    @Autowired
    SalaryService salaryService;

    @PostMapping("/increaseSalary")
    @ResponseBody
    public void increaseSalary(@RequestParam long id) {
        salaryService.increaseSalary(id);
    }
}

Метод будет вызываться из конечной точки /increaseSalary, которую мы будем вызывать для тестирования:

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("salary incorrect", 2000, emp.getSalary(), 0.5);
}

Во-первых, давайте добавим тест, в котором мы вызываем наше сопоставление POST после установки для свойства employee.feature значения false. В этом случае функция не должна быть активной и значение зарплаты сотрудника не должно меняться:

@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("salary incorrect", 2200, emp.getSalary(), 0.5);
}

Далее добавим тест, в котором мы выполняем вызов после установки свойства в true. В этом случае значение зарплаты должно быть увеличено:

8. Выводы

В этом уроке мы показали, как мы можем интегрировать библиотеку Togglz со Spring Boot с помощью аспекта.