Feature Flag代码实现


本文介绍Feature Flag的代码实现及其具体细节。

框架设计

灰度框架设计

关于算法逻辑详见《Feature Flag灰度算法设计》

代码实现

代码动态编译

使用arthas-memorycompiler实现源码的动态编译,实现运行中动态编译灰度实例的效果,详细信息查看《Arthas实现动态编译》

灰度实例懒加载

考虑到多个应用会同时配置灰度信息,如果应用都需要定义一个灰度实例的类将很麻烦,因此在设计的时候就在考虑如何更简单让业务接入,例如只要通过静态方法就可以直接使用,而不用额外的编码工作。通过一番思考,最终决定通过动态编译的方式,在使用的时候如果对应$flagName的灰度实例还未生成,就先编译类并生成实例注册到Spring容器;如果已生成,则直接使用判断是否开启新特性。

对应的代码实现如下:

package cn.zzq0324.feature.flag;

import cn.zzq0324.feature.flag.spring.SpringContextHolder;
import cn.zzq0324.feature.flag.support.JdkCompiler;
import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StreamUtils;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

/**
 * description: feature flag实例生成器 <br>
 * date: 2021/6/11 12:55 下午 <br>
 * author: zzq0324 <br>
 * version: 1.0 <br>
 */
public class FeatureFlagInstanceRegister {

    private static Logger logger = LoggerFactory.getLogger(FeatureFlagInstanceRegister.class);

    // 模板类路径
    private static String FEATURE_FLAG_TPL_PATH = "generator/FeatureFlagInstance.tpl";

    /**
     * 如果flag对应的bean不存在,则生成
     *
     * @param flagName flag名称
     * @return 返回开关实例对象
     */
    public static FeatureFlagInstance registerIfNotExist(String flagName) {
        if (!isContainsBean(flagName)) {
            try {
                dynamicLoadClassAndRegisterBean(flagName);
            } catch (Exception e) {
                throw new IllegalArgumentException("dynamicLoadClassAndRegisterBean error", e);
            }
        }

        FeatureFlagInstance instance = getInstanceFromSpringContext(flagName);

        Preconditions.checkNotNull(instance);

        return instance;
    }

    /**
     * 动态生成FeatureFlag类并注册成Spring Bean
     *
     * @param flagName 开关名称
     */
    private synchronized static void dynamicLoadClassAndRegisterBean(String flagName)
        throws IOException, ClassNotFoundException {
        // 二次确认,避免并发下重复生成导致可能的冲突或者报错
        if (isContainsBean(flagName)) {
            return;
        }

        // 编译并加载class
        Class<? extends FeatureFlagInstance> beanType = compileAndLoadClass(flagName);

        // 注册Bean到Spring
        registerBean(flagName, beanType);
        logger.info("register bean for flag: {} successfully.", flagName);
    }

    protected static Class<? extends FeatureFlagInstance> compileAndLoadClass(String flagName)
        throws IOException, ClassNotFoundException {
        // 获取模板内容
        String sourceTemplate = loadSourceTemplate();
        String className = flagNameToClassName(flagName);

        // 替换模板变量
        String source = sourceTemplate.replace("$flagName", flagName);
        source = source.replace("$className", className);
        logger.info("flag: {} source as follow: \n{}", flagName, source);

        // 动态编译
        Class<? extends FeatureFlagInstance> beanType =
            JdkCompiler.compile(FeatureFlagInstance.class.getPackage().getName(), className, source);

        logger.info("compile and load class[{}] for flag: {} successfully.", beanType.getName(), flagName);

        return beanType;
    }

    private static void registerBean(String flagName, Class<? extends FeatureFlagInstance> beanType) {
        ConfigurableApplicationContext applicationContext =
            (ConfigurableApplicationContext)SpringContextHolder.getApplicationContext();

        BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry)applicationContext.getBeanFactory();

        // 注册Bean
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(beanType);
        // 设置构造器参数
        beanDefinitionBuilder.addConstructorArgValue(flagName);
        beanFactory.registerBeanDefinition(flagName, beanDefinitionBuilder.getBeanDefinition());
    }

    protected static String loadSourceTemplate() throws IOException {
        ClassPathResource classPathResource = new ClassPathResource(FEATURE_FLAG_TPL_PATH);
        try (InputStream is = classPathResource.getInputStream()) {
            return StreamUtils.copyToString(is, Charset.defaultCharset());
        }
    }

    /**
     * 将特性开关名称转为类名,将横杠转为下划线
     *
     * @param flagName 开关名称
     * @return 返回类名
     */
    protected static String flagNameToClassName(String flagName) {
        flagName = flagName.replaceAll("-", "_");

        return flagName;
    }

    protected static FeatureFlagInstance getInstanceFromSpringContext(String flagName) {
        return SpringContextHolder.getApplicationContext().getBean(flagName, FeatureFlagInstance.class);
    }

    protected static boolean isContainsBean(String flagName) {
        return SpringContextHolder.getApplicationContext().containsBean(flagName);
    }
}

灰度判断逻辑

灰度判断逻辑核心实现如下:

/**
* 判断业务id是否在灰度范围内,通过bizId计算hash值
*
* @param bizId 业务id,根据业务的具体场景设置,例如可以是用户id、员工id、城市等
* @return 返回是否灰度
*/
public boolean isFeatureOn(String bizId) {
    // 判断是否在灰度时间段,不在灰度时间段直接返回
    if (!isInTimeSection()) {
        return false;
    }

    // 判断是否在黑名单
    if (isInBlackList(bizId)) {
        return false;
    }

    // 判断是否在白名单
    if (isInWhiteList(bizId)) {
        return true;
    }

    // 判断比例,如果为0代表还未灰度
    if (getLaunchPercent() == 0) {
        return false;
    }

    // 灰度100%
    if (getLaunchPercent() == 100) {
        return true;
    }

    long hash = 0;
    // 业务id为空,由于无法计算hash值,采用随机算法
    if (bizId == null) {
        hash = ThreadLocalRandom.current().nextInt(100);
    } else {
        hash = seededHash(bizId);
    }

    // 判断是否小于灰度比例,是的话直接返回
    return hash < getLaunchPercent();
}

完整类详见FeatureFlagInstance.java

完整实现详见Github

参考资料


文章作者: zzq0324
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 zzq0324 !
  目录