前言
组件介绍就不写了,看完文章就知道它是干嘛的
背景
日常研发可能会有这样的场景,对某个 method1() 方法优化后,产生一个 method2() ,如果不确定 method2 ()方法是否能稳定运行,或者想看看 method2() 方法优化后,执行时间相比于 method1() 快了多少。可以想办法就是让有的请求接口走method1,另外一些走method2()
当然我们也可以使用一些路由Gateway组件去做,它们都是从 controller
入手,今天的主角是在 service层去做 灰度 这个事
介绍
FeatureProbe是提供了一个配置页面的,以下两张图我都是在它官网找的:


它的一个架构图,表明 FeatureProbe 在你的应用中 充当的 "角色"是怎么样的:

它文档提供的一个 代码示例写的很简单
if (fpClient.boolValue(YOUR_TOGGLE_KEY, user, false)) {
// Do some new thing;
} else {
// Do the default thing;
}
当然接下来我们会用 注解 的方式对 FeatureProbe 进行封装,这样用起来更方便快捷
正文
先构建一下环境,后续都用得上:
<dependency>
<groupId>com.featureprobe</groupId>
<artifactId>server-sdk-java</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.4</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
提供一张全局的图,大致解释一些方法的作用:

正文开始了…
1.先写一些基础的东西,配置类相关的
package com.example.springdemo.grep;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GreyBusiness {
/**
* 业务唯一id, 默认取类名
*
* @return {@link String}
*/
String businessId() default "";
/**
* 子业务id, 默认取方法名
*
* @return {@link String}
*/
String subBusinessId() default "";
/**
* 业务版本
*
* @return int
*/
int version() default 0;
/**
* 是否进行空返回,true: 未找到版本,则不执行方法, null返回, false: 未找到版本,执行原有方法
*
* @return boolean
*/
boolean allowEmptyReturn() default false;
}
package com.example.springdemo.grep;
import lombok.Data;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 灰度业务配置 -> 自定义规则 > 函数版本 > 默认业务版本
*/
@Data
public class GreyBusinessConfig {
/**
* 默认业务版本
*/
private Integer version;
/**
* 自定义规则
*/
private List<GreyBusinessRuleConfig> customRules;
/**
* 灰度业务规则配置
*/
@Data
public static class GreyBusinessRuleConfig {
/**
* 版本 规则走这个版本
*/
private Integer version;
/**
* 子业务id
*/
private List<String> subBusinessIds;
/**
* 支持spEl表达式,自定义key
*/
private List<String> spElKeys;
/**
* 自动匹配企 用户ids
*/
private List<Long> userIds;
private List<Long> conditions1;
private List<Long> conditions2;
//===============你还可以写很多条件,根据你的业务背景===================
/**
* 参数Map对象集合
*/
private Map<String, List<String>> paramsMap = new HashMap<>();
}
}
package com.example.springdemo.grep;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Data
@Configuration
//@RefreshScope
@ConfigurationProperties(prefix = "grey")
public class GreyBusinessConfiguration {
private GreBusinessConfigEnum greyFrom = GreBusinessConfigEnum.NACOS;
private FeatureProbeConfig featureProbe;
/**
* 灰度业务配置 -> 自定义规则 > 函数版本 > 默认业务版本
*/
private Map<String, GreyBusinessConfig> config = new HashMap<>();
@Data
public static class FeatureProbeConfig {
private String sdkKey;
private String serverUrl;
private Integer refreshInterval = 10;
}
}
定义一个灰度配置的枚举类:
package com.example.springdemo.grep;
public enum GreBusinessConfigEnum {
NACOS, FEATUREPROBE
}
2.@GreyBusiness 这个注解,核心的一些实现
package com.example.springdemo.grep.service;
import com.example.springdemo.grep.GreBusinessConfigEnum;
import com.example.springdemo.grep.GreyBusiness;
import com.example.springdemo.grep.GreyBusinessConfig;
import com.example.springdemo.grep.GreyBusinessConfiguration;
import com.example.springdemo.grep.model.FeatureProbeCustomAroundInfo;
import com.example.springdemo.grep.model.GreyBusinessCheckInfo;
import com.featureprobe.sdk.server.FeatureProbe;
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
/**
* @author Jarvis
*/
@Slf4j
@Aspect
@Configuration
public class GreyBusinessAspect implements BeanPostProcessor {
@Autowired
private GreyBusinessConfiguration configuration;
@Autowired
private GreyBusinessVersionService greyBusinessVersionService;
private final ThreadLocal<FeatureProbeCustomAroundInfo> featureProbeThreadLocalInfo = new ThreadLocal<>();
/**
* 版本函数映射
*/
private final Map<String, GreyBusinessFunction> functionMap = new HashMap<>();
private static final List<String> IGNORE_FUNCTIONS = Lists.newArrayList("wait", "getClass", "toString", "equals",
"hashcode", "notify", "notifyAll", "hashCode");
@Autowired(required = false)
private Map<String, FeatureProbeCustomAnnounceService> customServiceMap = new HashMap<>();
/**
* 初始化前后期处理
*
* @param bean 类
* @param beanName bean名称
* @return {@link Object}
* @throws BeansException Bean异常
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
GreyBusiness classAnnotation = bean.getClass().getAnnotation(GreyBusiness.class);
for (Method method : bean.getClass().getMethods()) {
GreyBusiness functionAnnotation = method.getAnnotation(GreyBusiness.class);
if (null == functionAnnotation && null == classAnnotation) {
continue;
}
String name = method.getName();
if (IGNORE_FUNCTIONS.contains(name)) {
continue;
}
// 优先用方法注解否则用类注解
registerFunction(bean, method, name, null == functionAnnotation ? classAnnotation : functionAnnotation);
}
return bean;
}
/**
* 注册灰度版本方法
*
* @param object 对象
* @param method 方法
* @param methodName 方法名称
* @param greyBusiness 灰度业务
*/
private void registerFunction(Object object, Method method, String methodName, GreyBusiness greyBusiness) {
GreyBusinessFunction function = new GreyBusinessFunction(object, method);
Pair<String, String> businessId = getBusinessId(object.getClass(), methodName, greyBusiness);
String functionId = String.format("%s-%d-%s", businessId.getKey(), greyBusiness.version(),
businessId.getValue());
functionMap.put(functionId, function);
}
/**
* 获取业务id
*
* @param object 对象
* @param methodName 方法名称
* @param greyBusiness 灰色业务
* @return {@link Pair}<{@link String}, {@link String}>
*/
private Pair<String, String> getBusinessId(Class object, String methodName, GreyBusiness greyBusiness) {
String name;
if (StringUtils.isNotEmpty(greyBusiness.subBusinessId())) {
name = greyBusiness.subBusinessId();
} else {
name = methodName;
}
String businessId;
if (StringUtils.isEmpty(greyBusiness.businessId())) {
businessId = object.getSimpleName();
} else {
businessId = greyBusiness.businessId();
}
return Pair.of(businessId, name);
}
//你的注解在你包的位置
@Pointcut("@annotation(com.example.springdemo.grep.GreyBusiness)")
public void annotatedMethod() {
}
//你的注解在你包的位置
@Pointcut("@within(com.example.springdemo.grep.GreyBusiness)")
public void annotatedClass() {
}
@Around("annotatedMethod() || annotatedClass()")
@SneakyThrows
public Object greyBusinessClassOrMethod(ProceedingJoinPoint joinPoint) {
log.info("greyBusinessClassOrMethod");
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
GreyBusiness greyBusiness = AnnotationUtils.findAnnotation(signature.getMethod(), GreyBusiness.class);
GreyBusiness classGreyBusiness = AnnotationUtils.findAnnotation(joinPoint.getTarget().getClass(), GreyBusiness.class);
greyBusiness = null == greyBusiness ? classGreyBusiness : greyBusiness;
try {
initThreadLocal(joinPoint, greyBusiness);
beforeGreyBusiness(joinPoint, greyBusiness);
return greyBusinessExecutor(joinPoint, greyBusiness);
} finally {
afterGreyBusiness(joinPoint, greyBusiness);
featureProbeThreadLocalInfo.remove();
}
}
private void initThreadLocal(JoinPoint joinPoint, GreyBusiness greyBusiness) {
Method signature = ((MethodSignature) joinPoint.getSignature()).getMethod();
Pair<String, String> businessId = getBusinessId(joinPoint.getTarget().getClass(),
signature.getName(), greyBusiness);
FeatureProbeCustomAroundInfo customAroundInfo = new FeatureProbeCustomAroundInfo();
customAroundInfo.setGreyBusiness(greyBusiness);
customAroundInfo.setObject(joinPoint.getTarget().getClass());
customAroundInfo.setMethodName(signature.getName());
customAroundInfo.setArguments(joinPoint.getArgs());
customAroundInfo.setFeatureProbe(GreyBusinessVersionService.getFeatureProbeSingleton());
GreyBusinessCheckInfo checkInfo = new GreyBusinessCheckInfo();
checkInfo.setBusinessId(businessId.getLeft());
checkInfo.setSubBusinessId(businessId.getRight());
//todo 从登录信息里面获取到的 用户的信息
checkInfo.fillContext(1L,1L,1L);
checkInfo.fillMethod(signature, joinPoint.getArgs());
customAroundInfo.setFpUser(greyBusinessVersionService.getFeatureProbeUser(checkInfo));
featureProbeThreadLocalInfo.set(customAroundInfo);
}
/**
* 灰度业务执行前置逻辑
*
* @param joinPoint
* @param greyBusiness
*/
public void beforeGreyBusiness(JoinPoint joinPoint, GreyBusiness greyBusiness) {
log.info("beforeGreyBusiness,businessId:{},subBusinessId:{}", greyBusiness.businessId(),
greyBusiness.subBusinessId());
if (MapUtils.isNotEmpty(customServiceMap)) {
for (Map.Entry<String, FeatureProbeCustomAnnounceService> entry : customServiceMap.entrySet()) {
// 每个服务逻辑异常不影响其他服务
try {
FeatureProbeCustomAnnounceService customService = entry.getValue();
FeatureProbeCustomAroundInfo customAroundInfo = featureProbeThreadLocalInfo.get();
if (customService.support(customAroundInfo)) {
customService.beforeGray(customAroundInfo);
}
} catch (Exception e) {
log.error("beforeGreyBusiness customService error,customService:" + entry.getKey(), e);
}
}
}
}
/**
* 灰度业务执行后置逻辑
*
* @param joinPoint
* @param greyBusiness
*/
public void afterGreyBusiness(JoinPoint joinPoint, GreyBusiness greyBusiness) {
log.info("afterGreyBusiness,businessId:{},subBusinessId:{}", greyBusiness.businessId(),
greyBusiness.subBusinessId());
if (MapUtils.isNotEmpty(customServiceMap)) {
for (Map.Entry<String, FeatureProbeCustomAnnounceService> entry : customServiceMap.entrySet()) {
// 每个服务逻辑异常不影响其他服务
try {
FeatureProbeCustomAnnounceService customService = entry.getValue();
FeatureProbeCustomAroundInfo customAroundInfo = featureProbeThreadLocalInfo.get();
if (customService.support(customAroundInfo)) {
customService.afterGray(customAroundInfo);
}
} catch (Exception e) {
log.error("beforeGreyBusiness customService error,customService:" + entry.getKey(), e);
}
}
}
}
/**
* 灰色业务执行器
*
* @param joinPoint 连接点
* @param greyBusiness 灰色业务
* @return {@link Object}
*/
@SneakyThrows
public Object greyBusinessExecutor(ProceedingJoinPoint joinPoint, GreyBusiness greyBusiness) {
Pair<String, String> businessId = getBusinessId(joinPoint.getTarget().getClass(),
joinPoint.getSignature().getName(), greyBusiness);
if (Objects.equals(configuration.getGreyFrom(), GreBusinessConfigEnum.NACOS)) {
GreyBusinessConfig greyBusinessConfig = configuration.getConfig()
.get(businessId.getKey());
if (null == greyBusinessConfig) {
log.warn("业务id: {},未进行灰度配置, 调用原始方法", businessId.getKey());
return joinPoint.proceed();
}
return greyBusinessExecutor(joinPoint, greyBusiness, businessId);
} else if (Objects.equals(configuration.getGreyFrom(), GreBusinessConfigEnum.FEATUREPROBE)) {
FeatureProbe featureProbeSingleton = GreyBusinessVersionService.getFeatureProbeSingleton();
if (null != featureProbeSingleton) {
return greyBusinessExecutor(joinPoint, greyBusiness, businessId);
}
}
return joinPoint.proceed();
}
@SneakyThrows
public Object greyBusinessExecutor(ProceedingJoinPoint joinPoint, GreyBusiness greyBusiness,
Pair<String, String> businessId) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
int version = greyBusinessVersionService.getGreyBusinessVersion(businessId.getKey(), businessId.getValue(),
method, joinPoint.getArgs());
String functionId = String.format("%s-%d-%s", businessId.getKey(), version, businessId.getValue());
log.info("灰度流程: {},业务id:{} 子业务id:{} 版本:{}", configuration.getGreyFrom().name(), businessId.getKey(), businessId.getValue(), version);
return execute(functionId, joinPoint, greyBusiness);
}
@SneakyThrows
public Object execute(String functionId, ProceedingJoinPoint joinPoint, GreyBusiness greyBusiness) {
GreyBusinessFunction greyBusinessFunction = functionMap.get(functionId);
if (null == greyBusinessFunction) {
if (greyBusiness.allowEmptyReturn()) {
log.warn("灰度方法或版本未找到, id:{}, 直接返回", functionId);
return null;
}
return joinPoint.proceed();
}
Method invokeMethod = greyBusinessFunction.getMethod();
if (invokeMethod.getParameterCount() > joinPoint.getArgs().length) {
log.warn("灰度方法或版本调用方法参数过多, id:{}, 调用原始方法", functionId);
return joinPoint.proceed();
}
try {
Object[] args = Arrays.copyOf(joinPoint.getArgs(), invokeMethod.getParameterCount());
log.info("方法:{},按照灰度注解替换成:{}", joinPoint.getSignature().getName(), invokeMethod.getName());
return invokeMethod.invoke(greyBusinessFunction.getObject(), args);
} catch (InvocationTargetException e) {
if (null != e.getCause() && null == e.getTargetException()) {
throw e.getCause();
}
throw e.getTargetException();
}
}
@Data
@AllArgsConstructor
public static class GreyBusinessFunction {
private Object object;
private Method method;
}
}
package com.example.springdemo.grep.service;
import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.example.springdemo.grep.GreBusinessConfigEnum;
import com.example.springdemo.grep.GreyBusinessConfig;
import com.example.springdemo.grep.GreyBusinessConfiguration;
import com.example.springdemo.grep.model.GreyBusinessCheckInfo;
import com.featureprobe.sdk.server.DataRepository;
import com.featureprobe.sdk.server.FPConfig;
import com.featureprobe.sdk.server.FPUser;
import com.featureprobe.sdk.server.FeatureProbe;
import com.featureprobe.sdk.server.model.Condition;
import com.featureprobe.sdk.server.model.Rule;
import com.featureprobe.sdk.server.model.Toggle;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Service;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @author zengjin
* @date 2024/6/12
*/
@Slf4j
@Service
public class GreyBusinessVersionService implements DisposableBean {
@Autowired
private GreyBusinessConfiguration configuration;
private static volatile FeatureProbe featureProbe;
public final ThreadLocal<Map<String, String>> paramThreadLocal = new ThreadLocal<>();
public static FeatureProbe getFeatureProbeSingleton() {
if (featureProbe == null) {
synchronized (FeatureProbe.class) {
if (null == featureProbe) {
GreyBusinessConfiguration configuration = SpringUtil.getBean(GreyBusinessConfiguration.class);
featureProbe = featureProbe(configuration);
}
}
}
return featureProbe;
}
/**
* 获取灰度版本
*
* @param businessId 业务id
* @param subBusinessId 子业务id
* @param method 方法
* @param args args
* @return int
*/
public int getGreyBusinessVersion(String businessId, String subBusinessId, Method method, Object[] args) {
GreyBusinessCheckInfo checkInfo = new GreyBusinessCheckInfo();
checkInfo.setBusinessId(businessId);
checkInfo.setSubBusinessId(subBusinessId);
//TODO
checkInfo.fillContext(1L,1L,1L);
checkInfo.fillMethod(method, args);
return getGreyBusinessVersion(checkInfo);
}
/**
* 获取灰度版本
*
* @param checkInfo 检查信息
* @return int
*/
public int getGreyBusinessVersion(GreyBusinessCheckInfo checkInfo) {
try {
if (Objects.equals(configuration.getGreyFrom(), GreBusinessConfigEnum.NACOS)) {
return getNacosGreyBusinessVersion(configuration.getConfig().get(checkInfo.getBusinessId()), checkInfo);
} else if (Objects.equals(configuration.getGreyFrom(), GreBusinessConfigEnum.FEATUREPROBE)) {
return getFeatureProbeGreyBusinessVersion(checkInfo);
} else {
return 0;
}
} finally {
paramThreadLocal.remove();
}
}
public FPUser getFeatureProbeUser(GreyBusinessCheckInfo checkInfo) {
String personId = String.valueOf(checkInfo.getUserId());
String condition1 = String.valueOf(checkInfo.getCondition1());
String condition2 = String.valueOf(checkInfo.getCondition2());
//Map<String, Object> spelData = getSpelContextMap(checkInfo);
FPUser fpUser = new FPUser()
.with("subBusinessId", checkInfo.getSubBusinessId())
.with("personId", personId)
.with("condition1", condition1)
.with("condition1", condition2)
.with("funcName", checkInfo.getFuncName())
.with("personId", personId);
Map<String, String> paramMap = paramThreadLocal.get();
if (CollUtil.isNotEmpty(paramMap)) {
for (Map.Entry<String, String> entry : paramMap.entrySet()) {
fpUser = fpUser.with(entry.getKey(), entry.getValue());
}
}
//fpUser = fpUser.with("spel", spelData);
return fpUser;
}
public GreyBusinessConfig getGreyBusinessConfig(String businessId) {
if (Objects.equals(configuration.getGreyFrom(), GreBusinessConfigEnum.NACOS)) {
return configuration.getConfig().get(businessId);
} else if (Objects.equals(configuration.getGreyFrom(), GreBusinessConfigEnum.FEATUREPROBE)) {
return getGreyBusinessConfigByFeatureProbe(businessId);
}
return null;
}
private GreyBusinessConfig getGreyBusinessConfigByFeatureProbe(String businessId) {
FeatureProbe featureProbe = getFeatureProbeSingleton();
if (null == featureProbe) {
return null;
}
try {
Field field = featureProbe.getClass().getDeclaredField("dataRepository");
field.setAccessible(true);
DataRepository dataRepository = (DataRepository) field.get(featureProbe);
Toggle toggle = dataRepository.getToggle(businessId);
if (toggle.getEnabled()) {
GreyBusinessConfig greyBusinessConfig = new GreyBusinessConfig();
List<Object> variations = toggle.getVariations();
Object defaultVersion = variations.get(toggle.getDefaultServe().getSelect());
greyBusinessConfig.setVersion((Integer) defaultVersion);
greyBusinessConfig.setCustomRules(new ArrayList<>());
for (Rule rule : toggle.getRules()) {
GreyBusinessConfig.GreyBusinessRuleConfig ruleConfig = new GreyBusinessConfig.GreyBusinessRuleConfig();
Object ruleVersion = variations.get(rule.getServe().getSelect());
ruleConfig.setVersion((Integer) ruleVersion);
for (Condition condition : rule.getConditions()) {
if ("subBusinessId".equals(condition.getSubject())) {
ruleConfig.setSubBusinessIds(condition.getObjects());
} else if ("userId".equals(condition.getSubject())) {
List<Long> workspaceIds = CollStreamUtil.toList(condition.getObjects(), Long::valueOf);
ruleConfig.setUserIds(workspaceIds);
} else if ("condition1".equals(condition.getSubject())) {
List<Long> conditions1 = CollStreamUtil.toList(condition.getObjects(), Long::valueOf);
ruleConfig.setConditions1(conditions1);
} else if ("condition2".equals(condition.getSubject())) {
List<Long> conditions2 = CollStreamUtil.toList(condition.getObjects(), Long::valueOf);
ruleConfig.setConditions2(conditions2);
} else if ("spel".equals(condition.getSubject())) {
ruleConfig.setSpElKeys(condition.getObjects());
} else {
ruleConfig.getParamsMap().put(condition.getSubject(), condition.getObjects());
}
}
greyBusinessConfig.getCustomRules().add(ruleConfig);
}
return greyBusinessConfig;
}
} catch (Exception e) {
log.warn("获取灰度配置失败[{}]", businessId, e);
}
return null;
}
/**
* 通过FeatureProbe获取业务版本
*
* @param checkInfo 业务id
* @return int
*/
private int getFeatureProbeGreyBusinessVersion(GreyBusinessCheckInfo checkInfo) {
FPUser fpUser = getFeatureProbeUser(checkInfo);
BigDecimal version = BigDecimal.valueOf(getFeatureProbeSingleton().numberValue(checkInfo.getBusinessId(), fpUser, 0));
return version.intValue();
}
/**
* 获取灰色业务的版本
*
* @param greyBusinessConfig 灰度业务的配置
* @param checkInfo 业务id
* @return int
*/
private int getNacosGreyBusinessVersion(GreyBusinessConfig greyBusinessConfig, GreyBusinessCheckInfo checkInfo) {
if (null == greyBusinessConfig) {
return 0;
}
Integer classVersion = Optional.ofNullable(greyBusinessConfig.getVersion()).orElse(0);
if (CollUtil.isNotEmpty(greyBusinessConfig.getCustomRules())) {
for (GreyBusinessConfig.GreyBusinessRuleConfig customRule : greyBusinessConfig.getCustomRules()) {
if (null == customRule.getVersion()) {
continue;
}
Boolean returnVersion = null;
if (CollUtil.isNotEmpty(customRule.getSubBusinessIds())) {
returnVersion = customRule.getSubBusinessIds().contains(checkInfo.getSubBusinessId());
}
if (CollUtil.isNotEmpty(customRule.getUserIds())) {
returnVersion = customRule.getUserIds().contains(checkInfo.getUserId());
}
if (CollUtil.isNotEmpty(customRule.getConditions1()) && !Objects.equals(Boolean.FALSE, returnVersion)) {
returnVersion = customRule.getConditions1().contains(checkInfo.getCondition1());
}
if (CollUtil.isNotEmpty(customRule.getConditions2()) && !Objects.equals(Boolean.FALSE,
returnVersion)) {
returnVersion = customRule.getConditions2().contains(checkInfo.getCondition2());
}
if (CollUtil.isNotEmpty(customRule.getSpElKeys()) && !Objects.equals(Boolean.FALSE,
returnVersion)) {
EvaluationContext context = getSpelContext(checkInfo);
returnVersion = customRule.getSpElKeys().stream().anyMatch(spelKey -> {
try {
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
Expression expression = spelExpressionParser.parseExpression(spelKey);
return expression.getValue(context, Boolean.class);
} catch (Exception e) {
log.warn("解析spelKey失败", e);
return false;
}
});
}
if (CollUtil.isNotEmpty(customRule.getParamsMap()) && !Objects.equals(Boolean.FALSE,
returnVersion)) {
Map<String, String> paramLocalMap = paramThreadLocal.get();
if (CollUtil.isNotEmpty(paramLocalMap)) {
for (Map.Entry<String, List<String>> entry : customRule.getParamsMap().entrySet()) {
String s = paramLocalMap.get(entry.getKey());
if (null != s && entry.getValue().contains(s)) {
returnVersion = true;
break;
}
}
}
}
if (Objects.equals(Boolean.TRUE, returnVersion)) {
return customRule.getVersion();
}
}
}
return classVersion;
}
private EvaluationContext getSpelContext(GreyBusinessCheckInfo checkInfo) {
EvaluationContext context = new StandardEvaluationContext();
if (null != checkInfo.getMethod()) {
handlerFunctionArgs(context, checkInfo.getArgs(), checkInfo.getMethod());
}
Map<String, String> paramLocalMap = paramThreadLocal.get();
if (CollUtil.isNotEmpty(paramLocalMap)) {
for (Map.Entry<String, String> entry : paramLocalMap.entrySet()) {
context.setVariable(entry.getKey(), entry.getValue());
}
}
//context.setVariable("ctx", checkInfo.getContextInfo());
context.setVariable("funcName", checkInfo.getFuncName());
context.setVariable("subBusinessId", checkInfo.getSubBusinessId());
return context;
}
// private Map<String, Object> getSpelContextMap(GreyBusinessCheckInfo checkInfo) {
// Map<String, Object> spelData = new HashMap<>();
// spelData.put("ctx", checkInfo.getContextInfo());
// if (null != checkInfo.getMethod()) {
// handlerFunctionArgs(spelData, checkInfo.getArgs(), checkInfo.getMethod());
// }
// Map<String, String> paramMap = paramThreadLocal.get();
// if (CollUtil.isNotEmpty(paramMap)) {
// spelData.putAll(paramMap);
// }
// spelData.put("ctx", checkInfo.getContextInfo());
// spelData.put("funcName", checkInfo.getFuncName());
// spelData.put("subBusinessId", checkInfo.getSubBusinessId());
// return spelData;
// }
private void handlerFunctionArgs(EvaluationContext context, Object[] arguments, Method signatureMethod) {
String[] parameterNames = new LocalVariableTableParameterNameDiscoverer()
.getParameterNames(signatureMethod);
if (parameterNames == null) {
return;
}
for (int i = 0; i < arguments.length; i++) {
context.setVariable(parameterNames[i], arguments[i]);
}
}
private void handlerFunctionArgs(Map<String, Object> spelData, Object[] arguments, Method signatureMethod) {
String[] parameterNames = new LocalVariableTableParameterNameDiscoverer()
.getParameterNames(signatureMethod);
if (parameterNames == null) {
return;
}
for (int i = 0; i < arguments.length; i++) {
spelData.put(parameterNames[i], arguments[i]);
}
Map<String, String> paramMap = paramThreadLocal.get();
if (CollUtil.isNotEmpty(paramMap)) {
spelData.putAll(paramMap);
}
}
private static FeatureProbe featureProbe(GreyBusinessConfiguration properties) {
if (!Objects.equals(properties.getGreyFrom(), GreBusinessConfigEnum.FEATUREPROBE)) {
return null;
}
if (null == properties.getFeatureProbe()
|| StringUtils.isEmpty(properties.getFeatureProbe().getServerUrl())
|| StringUtils.isEmpty(properties.getFeatureProbe().getSdkKey())) {
return null;
}
FPConfig config = FPConfig.builder()
.remoteUri(properties.getFeatureProbe().getServerUrl())
.startWait(10L, TimeUnit.SECONDS)
.pollingMode(Duration.ofSeconds(properties.getFeatureProbe().getRefreshInterval()))
.build();
return new FeatureProbe(properties.getFeatureProbe().getSdkKey(), config);
}
@Override
public void destroy() throws Exception {
if (null != featureProbe) {
featureProbe.close();
}
}
}
package com.example.springdemo.grep.service;
import com.example.springdemo.grep.model.FeatureProbeCustomAroundInfo;
/**
* FeatureProbe自定义通知回调服务
*/
public interface FeatureProbeCustomAnnounceService {
/**
* 是否支持回调
*
* @param featureProbeCustomAroundInfo
* @return
*/
boolean support(FeatureProbeCustomAroundInfo featureProbeCustomAroundInfo);
/**
* 灰度功能开关调用前置逻辑
*
* @param featureProbeCustomAroundInfo
*/
void beforeGray(FeatureProbeCustomAroundInfo featureProbeCustomAroundInfo);
/**
* 灰度功能开关调用后置逻辑
*
* @param featureProbeCustomAroundInfo
*/
void afterGray(FeatureProbeCustomAroundInfo featureProbeCustomAroundInfo);
}
package com.example.springdemo.grep.service;
import com.example.springdemo.grep.model.FeatureProbeCustomAroundInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 灰度功能耗时统计
*
*/
@Component
@Slf4j
public class GreyCostTimeCustomerServiceImpl implements FeatureProbeCustomAnnounceService {
private ThreadLocal<Long> customCostTimeHolder = new ThreadLocal<>();
@Override
public boolean support(FeatureProbeCustomAroundInfo featureProbeCustomAroundInfo) {
if (null != featureProbeCustomAroundInfo.getGreyBusiness() &&
"ABA".equals(featureProbeCustomAroundInfo.getGreyBusiness().subBusinessId())) {
return true;
}
return false;
}
@Override
public void beforeGray(FeatureProbeCustomAroundInfo featureProbeCustomAroundInfo) {
log.info("GreyCostTimeCustomerService-beforeGray,method:{}", featureProbeCustomAroundInfo.getMethodName());
customCostTimeHolder.set(System.currentTimeMillis());
}
@Override
public void afterGray(FeatureProbeCustomAroundInfo featureProbeCustomAroundInfo) {
log.info("GreyCostTimeCustomerService-afterGray,method:{}", featureProbeCustomAroundInfo.getMethodName());
if (null != featureProbeCustomAroundInfo.getFeatureProbe()) {
long costTime = System.currentTimeMillis() - customCostTimeHolder.get();
String eventName = "costTime";
featureProbeCustomAroundInfo.getFeatureProbe().track(eventName, featureProbeCustomAroundInfo.getFpUser(), costTime);
}
customCostTimeHolder.remove();
}
}
3.一些封装参数的类
package com.example.springdemo.grep.model;
import lombok.Data;
import java.lang.reflect.Method;
/**
* 灰度业务查询检查信息
*
*/
@Data
public class GreyBusinessCheckInfo {
/**
* 业务id
*/
private String businessId;
/**
* 子业务id
*/
private String subBusinessId;
/**
* 用户id
*/
private Long userId;
/**
* 条件1
*/
private Long condition1;
/**
* 条件2
*/
private Long condition2;
//===========可以有更多的条件================
/**
* 方法名称
*/
private String funcName;
/**
* saas租户id
*/
private Long saasTenantId;
/**
* 方法
*/
private Method method;
/**
* args
*/
private Object[] args;
public void fillContext(Long userId,Long condition1,Long condition2) {
//todo 这里还可以加判断的
if (null == userId) {
return;
}
this.userId = userId;
this.condition1 = condition1;
this.condition2 = condition2;
}
public void fillMethod(Method method, Object[] args) {
if (null != method) {
this.method = method;
this.funcName = method.getName();
}
this.args = args;
}
}
package com.example.springdemo.grep.model;
import com.example.springdemo.grep.GreyBusiness;
import lombok.Data;
/**
* FeatureProbe自定义扩展,功能开关前后调用是否支持信息
*
*/
@Data
public class GreyBusinessCustomInfo {
/**
* 当前调用方法所在的类
*/
private Class object;
/**
* 当前调用方法
*/
private String methodName;
/**
* 当前调用方法传入的参数
*/
private Object[] arguments;
/**
* 当前灰度注解信息
*/
private GreyBusiness greyBusiness;
}
package com.example.springdemo.grep.model;
import com.featureprobe.sdk.server.FPUser;
import com.featureprobe.sdk.server.FeatureProbe;
import lombok.Data;
/**
* FeatureProbe自定义扩展,功能开关前后调用信息
*
* @author Jarvis
*/
@Data
public class FeatureProbeCustomAroundInfo extends GreyBusinessCustomInfo {
/**
* featureProbe基础信息
*/
private FeatureProbe featureProbe;
private FPUser fpUser;
}
3.使用注解实现灰度
3.1 Naocs 实现方式:
yaml配置:
grey:
greyFrom: NACOS
featureProbe:
sdkKey: server-77010a779fc5c76da4f16383e91b2de02070be1c
serverUrl: https://test-featureprobe.axzo.cn/server
config:
TEST-PROBO:
# 默认走新版本
version: 1
写两个测试方式,使用 @GreyBusiness 标记:
package com.example.springdemo.service.impl;
import cn.hutool.extra.spring.SpringUtil;
import com.example.springdemo.grep.GreyBusiness;
import com.example.springdemo.service.FeatureProboTestService;
import org.springframework.stereotype.Service;
@Service
public class FeatureProboTestServiceImpl implements FeatureProboTestService {
@Override
public void testProbo() {
FeatureProboTestServiceImpl bean = SpringUtil.getBean(this.getClass());
bean.executeOld();
}
@GreyBusiness(businessId = "TEST-PROBO", subBusinessId = "TEST-PROBO-SUB", version = 0)
public void executeOld(){
System.out.println("================execute method version:"+0);
}
@GreyBusiness(businessId = "TEST-PROBO", subBusinessId ="TEST-PROBO-SUB", version = 1)
public void executeNew(){
System.out.println("===============execute method version:"+1);
}
}
服务启动起来,调用一下:
@GetMapping("/testProbo")
public void test3(){
featureProboTestService.testProbo();
}

看来执行了我们配置的 version:1 的方法:executeNew()
3.2 FeatureProbo配置实现方式:
yaml 配置:
grey:
greyFrom: FEATUREPROBE
featureProbe:
sdkKey: server-77010a779fc5c76da4f16383e91b2de02070be1c
serverUrl: https://test-featureprobe.axzo.cn/server
(未写完的…后面还要补充)
6315

被折叠的 条评论
为什么被折叠?



