无Beta环境的Java项目中新功能上线的状态控制

1 需求

一般情况下,一个服务具有测试环境线上沙箱环境(beta环境)生产环境三种过度环境。对于那些需要多个服务共同合作的项目,服务双方需要在相应的环境下进行测试通过才能正式投入线上运行。常规的测试流程是:测试环境联调--> beta环境联调--> 生产环境正式上线。如下图所示:

上线步骤.png

但如果服务目前并不具备独立的线上Beta环境的话,和那些具备Beta环境的服务合作的时候,服务Beta环境的缺失会给双方联调测试带来诸多问题。服务方解决这个问题的常见方法如下所述(假设有两个服务,称为A和B,其中服务A没有Beta环境,服务B有Beta环境):

  1. 服务B调用服务A的测试环境服务
上线步骤-测试环境.png

这种情况下,如果服务B的Beta环境需要的是生产环境的数据(线上线下的数据往往是隔离的),那么服务A的测试环境提供的服务将无法满足其需求。

  1. 服务B调用服务A的生产环境服务
上线步骤-生产环境.png

这种情况下,如果服务A的生产环境新作的修改会导致当前生产环境的状态发生变化,而服务B又没有正式上线(Beta环境和生产环境是隔离的),且服务A的正常服务需要服务B的支持,那么服务A直接上线就会影响线上的正常服务。

另一方面,对于一个服务来说,如果短期内有多个项目需要上线,但是其中的一个项目因为某些原因不能在生产环境中生效,那么就会影响后续项目的上线计划。有如下两种方式解决这个问题:

  1. 该项目不上线,其他项目并行开发,优先考虑当前能够上线的功能。但是如果项目之间有共同的修改(譬如说添加了某一通用模块,该模块在后续项目中也可以使用),那么后续的项目将无法共享其修改。
  2. 简单的添加一个开关,满足条件的话采用新的修改。但是这个方法会在线上环境中引入新旧不同的版本,功能修改过程中,需要保留原始逻辑。

为了不影响后续的开发,服务中需要添加功能上线版本控制模块,该模块的目的是:

  1. 解决和拥有Beta环境的服务共同联调的困难,支撑调用发或者依赖方在Beta环境上的服务。
  2. 能够对上线的功能进行版本控制,支持特定功能延迟上线的需求

2 思路

直接引入开关变量虽然能够满足大体的需求,但是该方法没有统一的模板,如果新功能中修改的地方比较分散,比较容易出错。因此,可以在开关变量的基础上,抽象出功能上线版本控制模块

  1. 功能版本的分类

    对于一个新的项目,其所提供的功能的状态无非是原始版本新版本混合版本三种:

    • 原始版本:新功能上线前的状态
    • 新版本:新功能替换原始功能
    • 混合版本:线上同时运行新旧两种版本,只不过新版本的调用需要满足一定的条件,例如白名单

    引入混合版本后,就可以通过提供一个混合版本中的过滤条件控制线上功能的服务状态。当满足过滤条件的时候,调用新版本的服务,当不满足条件的时候,依旧调用老版本的服务。可以将此过滤条件理解为类似白名单的作用。

    上线步骤-混合版本.png
  1. 功能版本的切换

    版本的切换可以通过一个简单的状态值进行控制,控制流程如下:

    上线步骤-控制流程图.png

版本切换的逻辑在任何功能中都是一致的,因此这部分逻辑是通用逻辑,可以通过设计模式中的模板方法实现这个逻辑,也可以通过接口中的默认函数实现这个逻辑。

  1. 版本不同状态的封装

    既然需要控制新功能在线上的情况,在开发新功能的时候,就需要将原始的功能代码保留下来,与新的更新的功能一起被提取出来,作为新旧两个版本对外提供服务。
    这里的难点在于如何提供通用的入参(不同功能的参数肯定不一样)和通用的返回对象。为了尽可能的灵活性,采用Java中的泛型机制控制入参和返回。

3 实现

3.1 版本状态变量

通过枚举类定义版本控制的三种状态

public enum FunctionVersionStatusEnum {
    APPLY_ORIGINAL_VERSION(0, "应用原逻辑"),
    APPLY_MIXED_VERSION(1, "应用混合逻辑"),
    APPLY_NEW_VERSION(2, "应用新逻辑");
    // ... 省略其他
}

目前,功能的版本控制在配置文件中设置,在类中读取并转化成FunctionVersionStatusEnum对象

3.2 版本控制通用模板接口

通过接口定义功能上线版本控制模块中需要实现的方法,通过接口的默认方法模拟模板方法完成版本控制逻辑。

public interface FunctionVersionControl<T, R> {
    R applyNewVersion(T request);
    R applyOriginalVersion(T request);
    boolean checkNewVersionConditionInMixedVersionModel(T request);
    FunctionVersionStatusEnum getStatus();
    default R apply(FunctionVersionStatusEnum versionEnum, T request) {
        Objects.requireNonNull(versionEnum, "versionEnum can not be null");
        switch(versionEnum) {
            case APPLY_NEW_VERSION:
                return applyNewVersion(request);
            case APPLY_MIXED_VERSION:
                if (checkNewVersionConditionInMixedVersionModel(request)) {
                    return applyNewVersion(request);
                }
                return applyOriginalVersion(request);
            case APPLY_ORIGINAL_VERSION:
                return applyOriginalVersion(request);
            default:
                throw new AssertionError("FunctionVersionStatusEnum doesn't have type " + versionEnum);
        }
    }

}

其中,applyNewVersionapplyOriginalVersion方法分别表示功能的新旧逻辑;checkNewVersionConditionInMixedVersionModel方法为混合版本模式下能够触发新版本功能的前提条件;getStatus方法用于获取当前功能的状态;apply方法应用版本控制逻辑。

如果是新功能,则applyOriginalVersion为空方法即可
如果有白名单类似的需求,重写checkNewVersionConditionInMixedVersionModel方法,让能够调用新版本逻辑的部分结果为true

4 使用

实现功能上线版本控制的方式有两种:

  1. 直接在相关的类中实现FunctionVersionControl接口,调用apply方法
  2. 新建类实现FunctionVersionControl接口,注入该类并调用其apply