本文介绍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();
}
完整实现详见Github