mirror of
https://github.com/spring-projects/spring-petclinic.git
synced 2026-02-11 00:51:11 +00:00
updated data.sql and schema.sql with feature flag table
This commit is contained in:
parent
871bccd2dc
commit
c518db81a2
10 changed files with 537 additions and 433 deletions
|
|
@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy;
|
|||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Custom annotation to mark methods protected by feature flags
|
||||
* Custom annotation to mark methods protected by feature toggle
|
||||
*
|
||||
* Usage:
|
||||
* @FeatureToggle(key = "add-new-pet")
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
package org.springframework.samples.petclinic.featureflag.aspect;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.samples.petclinic.featureflag.annotation.FeatureToggle;
|
||||
import org.springframework.samples.petclinic.featureflag.service.FeatureFlagService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
/**
|
||||
* AOP Aspect that intercepts methods annotated with @FeatureToggle
|
||||
* and checks if the feature is enabled before allowing execution
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class FeatureFlagAspect {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FeatureFlagAspect.class);
|
||||
|
||||
private final FeatureFlagService featureFlagService;
|
||||
|
||||
public FeatureFlagAspect(FeatureFlagService featureFlagService) {
|
||||
this.featureFlagService = featureFlagService;
|
||||
}
|
||||
|
||||
@Around("@annotation(org.springframework.samples.petclinic.featureflag.annotation.FeatureToggle)")
|
||||
public Object checkFeatureFlag(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
FeatureToggle featureToggle = method.getAnnotation(FeatureToggle.class);
|
||||
|
||||
String flagKey = featureToggle.key();
|
||||
String contextParam = featureToggle.contextParam();
|
||||
|
||||
// Extract context if specified
|
||||
String context = null;
|
||||
if (!contextParam.isEmpty()) {
|
||||
context = extractContextFromParams(method, joinPoint.getArgs(), contextParam);
|
||||
}
|
||||
|
||||
// Check if feature is enabled
|
||||
boolean isEnabled = featureFlagService.isFeatureEnabled(flagKey, context);
|
||||
|
||||
logger.debug("Feature flag '{}' check: enabled={}, context={}", flagKey, isEnabled, context);
|
||||
|
||||
if (isEnabled) {
|
||||
// Feature is enabled, proceed with method execution
|
||||
return joinPoint.proceed();
|
||||
} else {
|
||||
// Feature is disabled, handle based on configuration
|
||||
logger.info("Feature '{}' is disabled, blocking execution", flagKey);
|
||||
return handleDisabledFeature(joinPoint, featureToggle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract context value from method parameters
|
||||
*/
|
||||
private String extractContextFromParams(Method method, Object[] args, String paramName) {
|
||||
Parameter[] parameters = method.getParameters();
|
||||
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
// Check if parameter name matches (requires -parameters compiler flag)
|
||||
if (parameters[i].getName().equals(paramName)) {
|
||||
if (args[i] != null) {
|
||||
return args[i].toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for @RequestParam, @PathVariable annotations
|
||||
for (var annotation : parameters[i].getAnnotations()) {
|
||||
String annotationStr = annotation.toString();
|
||||
if (annotationStr.contains(paramName)) {
|
||||
if (args[i] != null) {
|
||||
return args[i].toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle disabled feature - redirect or return error view
|
||||
*/
|
||||
private Object handleDisabledFeature(ProceedingJoinPoint joinPoint, FeatureToggle featureToggle) {
|
||||
String redirect = featureToggle.disabledRedirect();
|
||||
String message = featureToggle.disabledMessage();
|
||||
|
||||
// Look for RedirectAttributes in method parameters
|
||||
Object[] args = joinPoint.getArgs();
|
||||
for (Object arg : args) {
|
||||
if (arg instanceof RedirectAttributes) {
|
||||
RedirectAttributes redirectAttrs = (RedirectAttributes) arg;
|
||||
redirectAttrs.addFlashAttribute("message", message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If redirect is specified, return redirect
|
||||
if (!redirect.isEmpty()) {
|
||||
return "redirect:" + redirect;
|
||||
}
|
||||
|
||||
// Otherwise return to a generic disabled feature page or home
|
||||
return "redirect:/oups"; // PetClinic error page
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
package org.springframework.samples.petclinic.featureflag.aspect;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.samples.petclinic.featureflag.annotation.FeatureToggle;
|
||||
import org.springframework.samples.petclinic.featureflag.service.FeatureFlagService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
|
||||
/**
|
||||
* AOP Aspect that intercepts methods annotated with @FeatureToggle and checks if the
|
||||
* feature is enabled before allowing execution
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class FeatureToggleAspect {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FeatureToggleAspect.class);
|
||||
|
||||
private final FeatureFlagService featureFlagService;
|
||||
|
||||
public FeatureToggleAspect(FeatureFlagService featureFlagService) {
|
||||
this.featureFlagService = featureFlagService;
|
||||
}
|
||||
|
||||
@Around("@annotation(org.springframework.samples.petclinic.featureflag.annotation.FeatureToggle)")
|
||||
public Object checkFeatureToggle(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
FeatureToggle featureToggle = method.getAnnotation(FeatureToggle.class);
|
||||
|
||||
String flagKey = featureToggle.key();
|
||||
String contextParam = featureToggle.contextParam();
|
||||
|
||||
// Extract context if specified
|
||||
String context = null;
|
||||
if (!contextParam.isEmpty()) {
|
||||
context = extractContextFromParams(method, joinPoint.getArgs(), contextParam);
|
||||
}
|
||||
|
||||
// Check if feature is enabled
|
||||
boolean isEnabled = featureFlagService.isFeatureEnabled(flagKey, context);
|
||||
|
||||
logger.debug("Feature toggle '{}' check: enabled={}, context={}", flagKey, isEnabled, context);
|
||||
|
||||
if (isEnabled) {
|
||||
// Feature is enabled, proceed with method execution
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
else {
|
||||
// Feature is disabled, handle based on configuration
|
||||
logger.info("Feature '{}' is disabled, blocking execution", flagKey);
|
||||
return handleDisabledFeature(joinPoint, featureToggle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract context value from method parameters
|
||||
*/
|
||||
private String extractContextFromParams(Method method, Object[] args, String paramName) {
|
||||
Parameter[] parameters = method.getParameters();
|
||||
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
// Check if parameter name matches (requires -parameters compiler flag)
|
||||
if (parameters[i].getName().equals(paramName)) {
|
||||
if (args[i] != null) {
|
||||
return args[i].toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for @RequestParam, @PathVariable annotations
|
||||
for (var annotation : parameters[i].getAnnotations()) {
|
||||
String annotationStr = annotation.toString();
|
||||
if (annotationStr.contains(paramName)) {
|
||||
if (args[i] != null) {
|
||||
return args[i].toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle disabled feature - redirect or return error view
|
||||
*/
|
||||
private Object handleDisabledFeature(ProceedingJoinPoint joinPoint, FeatureToggle featureToggle) {
|
||||
String redirect = featureToggle.disabledRedirect();
|
||||
String message = featureToggle.disabledMessage();
|
||||
|
||||
// Look for RedirectAttributes in method parameters
|
||||
Object[] args = joinPoint.getArgs();
|
||||
for (Object arg : args) {
|
||||
if (arg instanceof RedirectAttributes redirectAttrs) {
|
||||
redirectAttrs.addFlashAttribute("message", message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If redirect is specified, return redirect
|
||||
if (!redirect.isEmpty()) {
|
||||
return "redirect:" + redirect;
|
||||
}
|
||||
|
||||
// Otherwise return to a generic disabled feature page or home
|
||||
return "redirect:/oups"; // PetClinic error page
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package org.springframework.samples.petclinic.featureflag.dto;
|
|||
public class FeatureCheckRequest {
|
||||
|
||||
private String flagKey;
|
||||
|
||||
private String context;
|
||||
|
||||
public String getFlagKey() {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package org.springframework.samples.petclinic.featureflag.dto;
|
|||
|
||||
public class FeatureCheckResponse {
|
||||
private String flagKey;
|
||||
|
||||
private Boolean enabled;
|
||||
|
||||
private String context;
|
||||
|
||||
public FeatureCheckResponse(String flagKey, Boolean enabled, String context) {
|
||||
|
|
|
|||
|
|
@ -7,71 +7,89 @@ import org.springframework.samples.petclinic.featureflag.entity.FlagType;
|
|||
|
||||
public class FeatureFlagRequest {
|
||||
|
||||
private String flagKey;
|
||||
private String description;
|
||||
private FlagType flagType;
|
||||
private Boolean enabled;
|
||||
private Integer percentage;
|
||||
private Set<String> listItems;
|
||||
private String flagKey;
|
||||
|
||||
// Getters and Setters
|
||||
public String getFlagKey() {
|
||||
return flagKey;
|
||||
}
|
||||
private String description;
|
||||
|
||||
public void setFlagKey(String flagKey) {
|
||||
this.flagKey = flagKey;
|
||||
}
|
||||
private FlagType flagType;
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
private Boolean enabled;
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
private Integer percentage;
|
||||
|
||||
public FlagType getFlagType() {
|
||||
return flagType;
|
||||
}
|
||||
private Set<String> whitelist;
|
||||
|
||||
public void setFlagType(FlagType flagType) {
|
||||
this.flagType = flagType;
|
||||
}
|
||||
private Set<String> blacklist;
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
// Getters and Setters
|
||||
public String getFlagKey() {
|
||||
return flagKey;
|
||||
}
|
||||
|
||||
public void setEnabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
public void setFlagKey(String flagKey) {
|
||||
this.flagKey = flagKey;
|
||||
}
|
||||
|
||||
public Integer getPercentage() {
|
||||
return percentage;
|
||||
}
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setPercentage(Integer percentage) {
|
||||
this.percentage = percentage;
|
||||
}
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Set<String> getListItems() {
|
||||
return listItems;
|
||||
}
|
||||
public FlagType getFlagType() {
|
||||
return flagType;
|
||||
}
|
||||
|
||||
public void setListItems(Set<String> listItems) {
|
||||
this.listItems = listItems;
|
||||
}
|
||||
public void setFlagType(FlagType flagType) {
|
||||
this.flagType = flagType;
|
||||
}
|
||||
|
||||
public FeatureFlag toEntity() {
|
||||
FeatureFlag flag = new FeatureFlag();
|
||||
flag.setFlagKey(this.flagKey);
|
||||
flag.setDescription(this.description);
|
||||
flag.setFlagType(this.flagType != null ? this.flagType : FlagType.SIMPLE);
|
||||
flag.setEnabled(this.enabled != null ? this.enabled : false);
|
||||
flag.setPercentage(this.percentage);
|
||||
flag.setBlacklist(this.listItems != null ? this.listItems : Set.of());
|
||||
flag.setWhitelist(this.listItems != null ? this.listItems : Set.of());
|
||||
return flag;
|
||||
}
|
||||
public Boolean getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public Integer getPercentage() {
|
||||
return percentage;
|
||||
}
|
||||
|
||||
public void setPercentage(Integer percentage) {
|
||||
this.percentage = percentage;
|
||||
}
|
||||
|
||||
public Set<String> getWhitelist() {
|
||||
return whitelist;
|
||||
}
|
||||
|
||||
public void setWhitelist(Set<String> whitelist) {
|
||||
this.whitelist = whitelist;
|
||||
}
|
||||
|
||||
public Set<String> getBlacklist() {
|
||||
return blacklist;
|
||||
}
|
||||
|
||||
public void setBlacklist(Set<String> blacklist) {
|
||||
this.blacklist = blacklist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DTO to Entity
|
||||
*/
|
||||
public FeatureFlag toEntity() {
|
||||
FeatureFlag flag = new FeatureFlag();
|
||||
flag.setFlagKey(this.flagKey);
|
||||
flag.setDescription(this.description);
|
||||
flag.setFlagType(this.flagType != null ? this.flagType : FlagType.SIMPLE);
|
||||
flag.setEnabled(this.enabled != null ? this.enabled : false);
|
||||
flag.setPercentage(this.percentage);
|
||||
flag.setWhitelist(this.whitelist != null ? this.whitelist : Set.of());
|
||||
flag.setBlacklist(this.blacklist != null ? this.blacklist : Set.of());
|
||||
return flag;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,111 +7,121 @@ import org.springframework.samples.petclinic.featureflag.entity.FlagType;
|
|||
|
||||
public class FeatureFlagResponse {
|
||||
|
||||
private Long id;
|
||||
private String flagKey;
|
||||
private String description;
|
||||
private FlagType flagType;
|
||||
private Boolean enabled;
|
||||
private Integer percentage;
|
||||
private Set<String> whitelist;
|
||||
private Set<String> blacklist;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
private Long id;
|
||||
|
||||
public static FeatureFlagResponse fromEntity(FeatureFlag flag) {
|
||||
FeatureFlagResponse response = new FeatureFlagResponse();
|
||||
response.setId(flag.getId());
|
||||
response.setFlagKey(flag.getFlagKey());
|
||||
response.setDescription(flag.getDescription());
|
||||
response.setFlagType(flag.getFlagType());
|
||||
response.setEnabled(flag.isEnabled());
|
||||
response.setPercentage(flag.getPercentage());
|
||||
response.setWhiteList(flag.getWhitelist());
|
||||
response.setBlackList(flag.getBlacklist());
|
||||
response.setCreatedAt(flag.getCreatedAt().toString());
|
||||
response.setUpdatedAt(flag.getUpdatedAt().toString());
|
||||
return response;
|
||||
}
|
||||
private String flagKey;
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
private String description;
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
private FlagType flagType;
|
||||
|
||||
public String getFlagKey() {
|
||||
return flagKey;
|
||||
}
|
||||
private Boolean enabled;
|
||||
|
||||
public void setFlagKey(String flagKey) {
|
||||
this.flagKey = flagKey;
|
||||
}
|
||||
private Integer percentage;
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
private Set<String> whitelist;
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
private Set<String> blacklist;
|
||||
|
||||
public FlagType getFlagType() {
|
||||
return flagType;
|
||||
}
|
||||
private String createdAt;
|
||||
|
||||
public void setFlagType(FlagType flagType) {
|
||||
this.flagType = flagType;
|
||||
}
|
||||
private String updatedAt;
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
public static FeatureFlagResponse fromEntity(FeatureFlag flag) {
|
||||
FeatureFlagResponse response = new FeatureFlagResponse();
|
||||
response.setId(flag.getId());
|
||||
response.setFlagKey(flag.getFlagKey());
|
||||
response.setDescription(flag.getDescription());
|
||||
response.setFlagType(flag.getFlagType());
|
||||
response.setEnabled(flag.isEnabled());
|
||||
response.setPercentage(flag.getPercentage());
|
||||
response.setWhitelist(flag.getWhitelist());
|
||||
response.setBlacklist(flag.getBlacklist());
|
||||
response.setCreatedAt(flag.getCreatedAt().toString());
|
||||
response.setUpdatedAt(flag.getUpdatedAt().toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
public void setEnabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Integer getPercentage() {
|
||||
return percentage;
|
||||
}
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public void setPercentage(Integer percentage) {
|
||||
this.percentage = percentage;
|
||||
}
|
||||
public String getFlagKey() {
|
||||
return flagKey;
|
||||
}
|
||||
|
||||
public Set<String> getWhiteList() {
|
||||
return whitelist;
|
||||
}
|
||||
public void setFlagKey(String flagKey) {
|
||||
this.flagKey = flagKey;
|
||||
}
|
||||
|
||||
public void setWhiteList(Set<String> listItems) {
|
||||
this.whitelist = listItems;
|
||||
}
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public Set<String> getBlackList() {
|
||||
return blacklist;
|
||||
}
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public void setBlackList(Set<String> listItems) {
|
||||
this.blacklist = listItems;
|
||||
}
|
||||
public FlagType getFlagType() {
|
||||
return flagType;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
public void setFlagType(FlagType flagType) {
|
||||
this.flagType = flagType;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
public Boolean getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public String getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
public void setEnabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public Integer getPercentage() {
|
||||
return percentage;
|
||||
}
|
||||
|
||||
public void setPercentage(Integer percentage) {
|
||||
this.percentage = percentage;
|
||||
}
|
||||
|
||||
public Set<String> getWhitelist() {
|
||||
return whitelist;
|
||||
}
|
||||
|
||||
public void setWhitelist(Set<String> whitelist) {
|
||||
this.whitelist = whitelist;
|
||||
}
|
||||
|
||||
public Set<String> getBlacklist() {
|
||||
return blacklist;
|
||||
}
|
||||
|
||||
public void setBlacklist(Set<String> blacklist) {
|
||||
this.blacklist = blacklist;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(String updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(String updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,211 +22,217 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
@Transactional
|
||||
public class FeatureFlagService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FeatureFlagService.class);
|
||||
|
||||
private final FeatureFlagRepository repository;
|
||||
private static final Logger logger = LoggerFactory.getLogger(FeatureFlagService.class);
|
||||
|
||||
public FeatureFlagService(FeatureFlagRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
private final FeatureFlagRepository repository;
|
||||
|
||||
/**
|
||||
* Main helper function to check if a feature is enabled
|
||||
* Can be called from anywhere in the application
|
||||
*
|
||||
* @param flagKey The unique identifier for the feature flag
|
||||
* @return true if feature is enabled, false otherwise
|
||||
*/
|
||||
public boolean isFeatureEnabled(String flagKey) {
|
||||
return isFeatureEnabled(flagKey, null);
|
||||
}
|
||||
public FeatureFlagService(FeatureFlagRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if feature is enabled for a specific context/user
|
||||
*
|
||||
* @param flagKey The unique identifier for the feature flag
|
||||
* @param context Context identifier (e.g., userId, sessionId, email)
|
||||
* @return true if feature is enabled for this context, false otherwise
|
||||
*/
|
||||
public boolean isFeatureEnabled(String flagKey, String context) {
|
||||
try {
|
||||
Optional<FeatureFlag> flagOpt = repository.findByFlagKey(flagKey);
|
||||
|
||||
if (flagOpt.isEmpty()) {
|
||||
logger.warn("Feature flag '{}' not found, defaulting to disabled", flagKey);
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Main helper function to check if a feature is enabled
|
||||
* Can be called from anywhere in the application
|
||||
*
|
||||
* @param flagKey The unique identifier for the feature flag
|
||||
* @return true if feature is enabled, false otherwise
|
||||
*/
|
||||
public boolean isFeatureEnabled(String flagKey) {
|
||||
return isFeatureEnabled(flagKey, null);
|
||||
}
|
||||
|
||||
FeatureFlag flag = flagOpt.get();
|
||||
return evaluateFlag(flag, context);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error evaluating feature flag '{}': {}", flagKey, e.getMessage());
|
||||
// Fail safe: return false on errors
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if feature is enabled for a specific context/user
|
||||
*
|
||||
* @param flagKey The unique identifier for the feature flag
|
||||
* @param context Context identifier (e.g., userId, sessionId, email)
|
||||
* @return true if feature is enabled for this context, false otherwise
|
||||
*/
|
||||
public boolean isFeatureEnabled(String flagKey, String context) {
|
||||
try {
|
||||
Optional<FeatureFlag> flagOpt = repository.findByFlagKey(flagKey.toUpperCase());
|
||||
|
||||
/**
|
||||
* Evaluate a feature flag based on its type and configuration
|
||||
*/
|
||||
private boolean evaluateFlag(FeatureFlag flag, String context) {
|
||||
// GLOBAL_DISABLE always returns false
|
||||
if (flag.getFlagType() == FlagType.GLOBAL_DISABLE) {
|
||||
logger.debug("Flag '{}' is globally disabled", flag.getFlagKey());
|
||||
return false;
|
||||
}
|
||||
if (flagOpt.isEmpty()) {
|
||||
logger.warn("Feature flag '{}' not found, defaulting to disabled", flagKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If not enabled, return false (except for specific override cases)
|
||||
if (!flag.isEnabled()) {
|
||||
logger.debug("Flag '{}' is disabled", flag.getFlagKey());
|
||||
return false;
|
||||
}
|
||||
FeatureFlag flag = flagOpt.get();
|
||||
return evaluateFlag(flag, context);
|
||||
|
||||
switch (flag.getFlagType()) {
|
||||
case SIMPLE:
|
||||
return true; // Simple on/off
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.error("Error evaluating feature flag '{}': {}", flagKey, e.getMessage());
|
||||
// Fail safe: return false on errors
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
case WHITELIST:
|
||||
return evaluateWhitelist(flag, context);
|
||||
/**
|
||||
* Evaluate a feature flag based on its type and configuration
|
||||
* Evaluation order (highest to lowest priority):
|
||||
* 1. GLOBAL_DISABLE - always returns false
|
||||
* 2. Blacklist - if context is blacklisted, return false
|
||||
* 3. Enabled check - if not enabled, return false
|
||||
* 4. Type-specific evaluation (WHITELIST, PERCENTAGE, SIMPLE)
|
||||
*/
|
||||
private boolean evaluateFlag(FeatureFlag flag, String context) {
|
||||
// GLOBAL_DISABLE always returns false
|
||||
if (flag.getFlagType() == FlagType.GLOBAL_DISABLE) {
|
||||
logger.debug("Flag '{}' is globally disabled", flag.getFlagKey());
|
||||
return false;
|
||||
}
|
||||
|
||||
case BLACKLIST:
|
||||
return evaluateBlacklist(flag, context);
|
||||
// Check blacklist first (highest priority after GLOBAL_DISABLE)
|
||||
if (context != null && !context.trim().isEmpty()) {
|
||||
if (flag.getBlacklist().contains(context.trim())) {
|
||||
logger.debug("Flag '{}' - context '{}' is blacklisted", flag.getFlagKey(), context);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
case PERCENTAGE:
|
||||
return evaluatePercentage(flag, context);
|
||||
// If not enabled, return false (except for specific override cases)
|
||||
if (!flag.isEnabled()) {
|
||||
logger.debug("Flag '{}' is disabled", flag.getFlagKey());
|
||||
return false;
|
||||
}
|
||||
|
||||
default:
|
||||
logger.warn("Unknown flag type for '{}': {}", flag.getFlagKey(), flag.getFlagType());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
switch (flag.getFlagType()) {
|
||||
case SIMPLE:
|
||||
return true; // Simple on/off
|
||||
|
||||
/**
|
||||
* Whitelist: Only allow if context is in the list
|
||||
*/
|
||||
private boolean evaluateWhitelist(FeatureFlag flag, String context) {
|
||||
if (context == null || context.trim().isEmpty()) {
|
||||
logger.debug("Whitelist flag '{}' requires context, got null/empty", flag.getFlagKey());
|
||||
return false;
|
||||
}
|
||||
case WHITELIST:
|
||||
return evaluateWhitelist(flag, context);
|
||||
|
||||
boolean inWhitelist = flag.getWhitelist().contains(context.trim());
|
||||
logger.debug("Whitelist flag '{}' for context '{}': {}", flag.getFlagKey(), context, inWhitelist);
|
||||
return inWhitelist;
|
||||
}
|
||||
case BLACKLIST:
|
||||
// If we got here, context is not in blacklist and flag is enabled
|
||||
return true;
|
||||
|
||||
/**
|
||||
* Blacklist: Allow unless context is in the list
|
||||
*/
|
||||
private boolean evaluateBlacklist(FeatureFlag flag, String context) {
|
||||
if (context == null || context.trim().isEmpty()) {
|
||||
// No context means not blacklisted
|
||||
return true;
|
||||
}
|
||||
case PERCENTAGE:
|
||||
return evaluatePercentage(flag, context);
|
||||
|
||||
boolean inBlacklist = flag.getBlacklist().contains(context.trim());
|
||||
logger.debug("Blacklist flag '{}' for context '{}': {}", flag.getFlagKey(), context, !inBlacklist);
|
||||
return !inBlacklist;
|
||||
}
|
||||
default:
|
||||
logger.warn("Unknown flag type for '{}': {}", flag.getFlagKey(), flag.getFlagType());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Percentage: Enable for X% of requests using consistent hashing
|
||||
*/
|
||||
private boolean evaluatePercentage(FeatureFlag flag, String context) {
|
||||
if (flag.getPercentage() == null || flag.getPercentage() < 0 || flag.getPercentage() > 100) {
|
||||
logger.warn("Invalid percentage for flag '{}': {}", flag.getFlagKey(), flag.getPercentage());
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Whitelist: Only allow if context is in the whitelist
|
||||
*/
|
||||
private boolean evaluateWhitelist(FeatureFlag flag, String context) {
|
||||
if (context == null || context.trim().isEmpty()) {
|
||||
logger.debug("Whitelist flag '{}' requires context, got null/empty", flag.getFlagKey());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (flag.getPercentage() == 0) {
|
||||
return false;
|
||||
}
|
||||
boolean inWhitelist = flag.getWhitelist().contains(context.trim());
|
||||
logger.debug("Whitelist flag '{}' for context '{}': {}", flag.getFlagKey(), context, inWhitelist);
|
||||
return inWhitelist;
|
||||
}
|
||||
|
||||
if (flag.getPercentage() == 100) {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Percentage: Enable for X% of requests using consistent hashing
|
||||
*/
|
||||
private boolean evaluatePercentage(FeatureFlag flag, String context) {
|
||||
if (flag.getPercentage() == null || flag.getPercentage() < 0 || flag.getPercentage() > 100) {
|
||||
logger.warn("Invalid percentage for flag '{}': {}", flag.getFlagKey(), flag.getPercentage());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use consistent hashing to ensure same context always gets same result
|
||||
String hashInput = flag.getFlagKey() + (context != null ? context : "");
|
||||
int hash = Math.abs(hashInput.hashCode());
|
||||
int bucket = hash % 100;
|
||||
|
||||
boolean enabled = bucket < flag.getPercentage();
|
||||
logger.debug("Percentage flag '{}' for context '{}': bucket={}, percentage={}, enabled={}",
|
||||
flag.getFlagKey(), context, bucket, flag.getPercentage(), enabled);
|
||||
return enabled;
|
||||
}
|
||||
if (flag.getPercentage() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// CRUD Operations
|
||||
if (flag.getPercentage() == 100) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<FeatureFlag> getAllFlags() {
|
||||
return repository.findAll();
|
||||
}
|
||||
// Use consistent hashing to ensure same context always gets same result
|
||||
String hashInput = flag.getFlagKey() + (context != null ? context : "");
|
||||
int hash = Math.abs(hashInput.hashCode());
|
||||
int bucket = hash % 100;
|
||||
|
||||
public Optional<FeatureFlag> getFlagById(Long id) {
|
||||
return repository.findById(id);
|
||||
}
|
||||
boolean enabled = bucket < flag.getPercentage();
|
||||
logger.debug("Percentage flag '{}' for context '{}': bucket={}, percentage={}, enabled={}",
|
||||
flag.getFlagKey(), context, bucket, flag.getPercentage(), enabled);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public Optional<FeatureFlag> getFlagByKey(String flagKey) {
|
||||
return repository.findByFlagKey(flagKey);
|
||||
}
|
||||
// CRUD Operations
|
||||
|
||||
public FeatureFlag createFlag(FeatureFlag flag) {
|
||||
if (repository.existsByFlagKey(flag.getFlagKey())) {
|
||||
throw new IllegalArgumentException("Feature flag with key '" + flag.getFlagKey() + "' already exists");
|
||||
}
|
||||
|
||||
validateFlag(flag);
|
||||
return repository.save(flag);
|
||||
}
|
||||
public List<FeatureFlag> getAllFlags() {
|
||||
return repository.findAll();
|
||||
}
|
||||
|
||||
public FeatureFlag updateFlag(Long id, FeatureFlag updatedFlag) {
|
||||
FeatureFlag existingFlag = repository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Feature flag not found with id: " + id));
|
||||
public Optional<FeatureFlag> getFlagById(Long id) {
|
||||
return repository.findById(id);
|
||||
}
|
||||
|
||||
// Don't allow changing the key
|
||||
updatedFlag.setFlagKey(existingFlag.getFlagKey());
|
||||
|
||||
validateFlag(updatedFlag);
|
||||
return repository.save(updatedFlag);
|
||||
}
|
||||
public Optional<FeatureFlag> getFlagByKey(String flagKey) {
|
||||
return repository.findByFlagKey(flagKey.toUpperCase());
|
||||
}
|
||||
|
||||
public void deleteFlag(Long id) {
|
||||
if (!repository.existsById(id)) {
|
||||
throw new IllegalArgumentException("Feature flag not found with id: " + id);
|
||||
}
|
||||
repository.deleteById(id);
|
||||
}
|
||||
public FeatureFlag createFlag(FeatureFlag flag) {
|
||||
if (repository.existsByFlagKey(flag.getFlagKey().toUpperCase())) {
|
||||
throw new IllegalArgumentException("Feature flag with key '" + flag.getFlagKey() + "' already exists");
|
||||
}
|
||||
|
||||
public FeatureFlag toggleFlag(String flagKey) {
|
||||
FeatureFlag flag = repository.findByFlagKey(flagKey)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Feature flag not found: " + flagKey));
|
||||
|
||||
flag.setEnabled(!flag.isEnabled());
|
||||
return repository.save(flag);
|
||||
}
|
||||
validateFlag(flag);
|
||||
return repository.save(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate flag configuration
|
||||
*/
|
||||
private void validateFlag(FeatureFlag flag) {
|
||||
if (flag.getFlagKey() == null || flag.getFlagKey().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Flag key cannot be empty");
|
||||
}
|
||||
public FeatureFlag updateFlag(Long id, FeatureFlag updatedFlag) {
|
||||
FeatureFlag existingFlag = repository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Feature flag not found with id: " + id));
|
||||
|
||||
if (flag.getFlagType() == FlagType.PERCENTAGE) {
|
||||
if (flag.getPercentage() == null || flag.getPercentage() < 0 || flag.getPercentage() > 100) {
|
||||
throw new IllegalArgumentException("Percentage must be between 0 and 100");
|
||||
}
|
||||
}
|
||||
// Don't allow changing the key
|
||||
updatedFlag.setFlagKey(existingFlag.getFlagKey());
|
||||
|
||||
if (flag.getFlagType() == FlagType.WHITELIST ||
|
||||
flag.getFlagType() == FlagType.BLACKLIST) {
|
||||
if (flag.getWhitelist() == null || flag.getBlacklist().isEmpty()) {
|
||||
logger.warn("Flag '{}' is of type {} but has no list items",
|
||||
flag.getFlagKey(), flag.getFlagType());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
validateFlag(updatedFlag);
|
||||
return repository.save(updatedFlag);
|
||||
}
|
||||
|
||||
public void deleteFlag(Long id) {
|
||||
if (!repository.existsById(id)) {
|
||||
throw new IllegalArgumentException("Feature flag not found with id: " + id);
|
||||
}
|
||||
repository.deleteById(id);
|
||||
}
|
||||
|
||||
public FeatureFlag toggleFlag(String flagKey) {
|
||||
FeatureFlag flag = repository.findByFlagKey(flagKey.toUpperCase())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Feature flag not found: " + flagKey));
|
||||
|
||||
flag.setEnabled(!flag.isEnabled());
|
||||
return repository.save(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate flag configuration
|
||||
*/
|
||||
private void validateFlag(FeatureFlag flag) {
|
||||
if (flag.getFlagKey() == null || flag.getFlagKey().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Flag key cannot be empty");
|
||||
}
|
||||
|
||||
if (flag.getFlagType() == FlagType.PERCENTAGE) {
|
||||
if (flag.getPercentage() == null || flag.getPercentage() < 0 || flag.getPercentage() > 100) {
|
||||
throw new IllegalArgumentException("Percentage must be between 0 and 100");
|
||||
}
|
||||
}
|
||||
|
||||
if (flag.getFlagType() == FlagType.WHITELIST) {
|
||||
if (flag.getWhitelist() == null || flag.getWhitelist().isEmpty()) {
|
||||
logger.warn("Flag '{}' is of type WHITELIST but has no whitelist items", flag.getFlagKey());
|
||||
}
|
||||
}
|
||||
|
||||
if (flag.getFlagType() == FlagType.BLACKLIST) {
|
||||
if (flag.getBlacklist() == null || flag.getBlacklist().isEmpty()) {
|
||||
logger.warn("Flag '{}' is of type BLACKLIST but has no blacklist items", flag.getFlagKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -51,3 +51,44 @@ INSERT IGNORE INTO visits VALUES (1, 7, '2010-03-04', 'rabies shot');
|
|||
INSERT IGNORE INTO visits VALUES (2, 8, '2011-03-04', 'rabies shot');
|
||||
INSERT IGNORE INTO visits VALUES (3, 8, '2009-06-04', 'neutered');
|
||||
INSERT IGNORE INTO visits VALUES (4, 7, '2008-09-04', 'spayed');
|
||||
|
||||
|
||||
-- Sample data for feature flags
|
||||
|
||||
-- 1. SIMPLE flag: Add New Pet (enabled by default)
|
||||
INSERT IGNORE INTO feature_flags (flag_key, description, flag_type, enabled, percentage, created_at, updated_at)
|
||||
VALUES ('ADD_NEW_PET', 'Controls whether users can add new pets to an owner', 'SIMPLE', TRUE, NULL, NOW(), NOW());
|
||||
|
||||
-- 2. SIMPLE flag: Add Visit (enabled by default)
|
||||
INSERT IGNORE INTO feature_flags (flag_key, description, flag_type, enabled, percentage, created_at, updated_at)
|
||||
VALUES ('ADD_VISIT', 'Controls whether users can add new visits for pets', 'SIMPLE', TRUE, NULL, NOW(), NOW());
|
||||
|
||||
-- 3. WHITELIST flag: Owner Search (only specific users can search)
|
||||
INSERT IGNORE INTO feature_flags (flag_key, description, flag_type, enabled, percentage, created_at, updated_at)
|
||||
VALUES ('OWNER_SEARCH', 'Controls who can search for owners', 'WHITELIST', TRUE, NULL, NOW(), NOW());
|
||||
|
||||
-- Add whitelist items for owner-search (example user contexts)
|
||||
INSERT IGNORE INTO feature_flag_whitelist (feature_flag_id, whitelist)
|
||||
SELECT id, 'admin' FROM feature_flags WHERE flag_key = 'OWNER_SEARCH';
|
||||
|
||||
INSERT IGNORE INTO feature_flag_whitelist (feature_flag_id, whitelist)
|
||||
SELECT id, 'Ramprakash' FROM feature_flags WHERE flag_key = 'OWNER_SEARCH';
|
||||
|
||||
-- 4. PERCENTAGE flag: New UI Theme (gradually roll out to 50% of users)
|
||||
INSERT IGNORE INTO feature_flags (flag_key, description, flag_type, enabled, percentage, created_at, updated_at)
|
||||
VALUES ('NEW_UI_THEME', 'Gradually roll out new UI theme', 'PERCENTAGE', TRUE, 50, NOW(), NOW());
|
||||
|
||||
-- 5. BLACKLIST flag: Delete Owner (block specific users from deleting)
|
||||
INSERT IGNORE INTO feature_flags (flag_key, description, flag_type, enabled, percentage, created_at, updated_at)
|
||||
VALUES ('DELETE_OWNER', 'Controls who can delete owners', 'BLACKLIST', TRUE, NULL, NOW(), NOW());
|
||||
|
||||
-- Add blacklist items
|
||||
INSERT IGNORE INTO feature_flag_blacklist (feature_flag_id, blacklist)
|
||||
SELECT id, 'guest' FROM feature_flags WHERE flag_key = 'DELETE_OWNER';
|
||||
|
||||
INSERT IGNORE INTO feature_flag_blacklist (feature_flag_id, blacklist)
|
||||
SELECT id, 'readonly_user' FROM feature_flags WHERE flag_key = 'DELETE_OWNER';
|
||||
|
||||
-- 6. GLOBAL_DISABLE flag: Emergency shutdown example
|
||||
INSERT IGNORE INTO feature_flags (flag_key, description, flag_type, enabled, percentage, created_at, updated_at)
|
||||
VALUES ('EMERGENCY_SHUTDOWN', 'Emergency feature kill switch', 'GLOBAL_DISABLE', FALSE, NULL, NOW(), NOW());
|
||||
|
|
@ -53,3 +53,28 @@ CREATE TABLE IF NOT EXISTS visits (
|
|||
description VARCHAR(255),
|
||||
FOREIGN KEY (pet_id) REFERENCES pets(id)
|
||||
) engine=InnoDB;
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS feature_flags (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
flag_key VARCHAR(255) NOT NULL UNIQUE,
|
||||
description VARCHAR(500) NOT NULL,
|
||||
flag_type VARCHAR(50) NOT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
percentage INT,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
INDEX(flag_key)
|
||||
) engine=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS feature_flag_whitelist (
|
||||
feature_flag_id BIGINT NOT NULL,
|
||||
whitelist VARCHAR(255),
|
||||
FOREIGN KEY (feature_flag_id) REFERENCES feature_flags(id) ON DELETE CASCADE
|
||||
) engine=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS feature_flag_blacklist (
|
||||
feature_flag_id BIGINT NOT NULL,
|
||||
blacklist VARCHAR(255),
|
||||
FOREIGN KEY (feature_flag_id) REFERENCES feature_flags(id) ON DELETE CASCADE
|
||||
) engine=InnoDB;
|
||||
Loading…
Add table
Add a link
Reference in a new issue