1. Обзор
В этом руководстве мы рассмотрим, как можно использовать библиотеку Togglz
с приложением Spring Boot.
2. Тогглз
Библиотека 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 Центральный.
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
может быть как активной, так и неактивной. Это поведение контролируется включенным
флагом и опциональной стратегией активации.
Чтобы установить для включенного
флага значение 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
— позволяет функции быть активной для указанного списка пользователей.- UserRoleActivationStrategy — текущая роль пользователя используется для определения состояния функции.
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>{ }
Далее добавим EmployeeService
с методом увеличения зарплаты сотрудника. Мы добавим аннотацию @AssociatedFeature
к методу с параметром EMPLOYEE_MANAGEMENT_FEATURE
:
@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);
}
}
Метод будет вызываться из конечной точки /increaseSalary
, которую мы будем вызывать для тестирования:
@Controller
public class SalaryController {
@Autowired
SalaryService salaryService;
@PostMapping("/increaseSalary")
@ResponseBody
public void increaseSalary(@RequestParam long id) {
salaryService.increaseSalary(id);
}
}
7.2. Юнит-тест
Во-первых, давайте добавим тест, в котором мы вызываем наше сопоставление POST после установки для свойства employee.feature значения
false
. При этом функция не должна быть активной и значение зарплаты сотрудника не должно меняться:
@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);
}
Далее давайте добавим тест, в котором мы выполняем вызов после установки свойства в значение true
. В этом случае размер оклада должен быть увеличен:
@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);
}
8. Выводы
В этом руководстве мы показали, как мы можем интегрировать библиотеку Togglz
с Spring Boot, используя аспект.
Полный исходный код примера можно найти на GitHub .