FeatureProbe实现接口 灰度调用

前言

组件介绍就不写了,看完文章就知道它是干嘛的

背景

日常研发可能会有这样的场景,对某个 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

(未写完的…后面还要补充)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值