如何使用Dubbo 2.7.0和Spring boot实现FAT测试(Feature Acceptance Test)

Posted specialist

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何使用Dubbo 2.7.0和Spring boot实现FAT测试(Feature Acceptance Test)相关的知识,希望对你有一定的参考价值。

在一个调用链非常长的功能中,如果想修改其中的一个特性,并进行测试,而又不影响该环境的其他用户使用现有功能、特性,例如:

1. A、B、C、D之间通过Dubbo实现远程调用

2. 这些模块可能有一个或者多个实例

3. 此环境由多个人员(包括开发、测试)同时使用

此时若想修改B中的某个功能,增加一个特性(称为FAT1),并且也注册到此环境中,则会发生如下问题:

当其他的用户从使用此功能时,从A发起的调用可能会由于Dubbo带的负载均衡算法等原因,在带有FAT1和不带有FAT1的实例间来回切换,最后的表现可能就是某一个功能使用两次,产生的结果竟然不一样!

 

解决这个问题最简单的方法就是给每个功能特性(FAT)独立设置一个测试环境,例如这一期有20个功能特性上线,就部署20个环境好了。。。。等等,是不是哪里不对?部署20个环境?你是否感觉到你BOSS站在你座位后面,随时准备把你扔出办公室?

仔细分析这个问题,要解决的重点有两个:

1. 将不同人员进行开发/测试的特性隔离开

2. 不修改的部分尽量共享,以节省资源

 

综上,最好的解决方案应该是如下图所示:

 

1. 建立一个Baseline环境,该环境包含了应用程序所需的所有组件、数据集等

2. 对于不同的功能特性,为该特性修改的组件独立发布一个实例,称之为一个Feature,对应的测试场称之为FAT+编号,例如Feature 1的测试环境称为FAT1

3. 开发和测试某个功能特性(例如Feature 1)时,利用路由功能让上游模块自动选择正确的下游模块,便于开发人员调试以及测试人员查看效果

 

通过对Dubbo文档的探索(http://dubbo.apache.org/zh-cn/docs/user/demos/routing-rule.html),发现实现此功能的方案有如下几种:

1. 使用条件路由规则

2. 使用动态标签功能

3. 使用静态标签功能

 

经过对上述三种方法的分析,发现各自的优缺点如下:

1. 如果使用条件路由:

优点是需求明晰,如果我想设计一个FAT测试场,其中A、B是待测试组件,可以使用路由规则host != A => host !=B和host = A => host = B

缺点是:

A. 需要使用Dubbo控制台修改路由规则,对于一般的开发/测试来说,权限太大了

B. 如果组件A、B、D同时修改了,当请求从A->B->C传递时,C不一定知道这个请求是否应该传到D,使用条件路由无法实现

 

2. 如果使用动态标签,1中的问题B能够得到解决,因为标签在整个调用链路中都会以Attachment的形式被传递,但是A问题依然无法解决

 

综上,要实现此功能,最好是使用3. 静态标签功能,根据官方文档,Dubbo的标签路由功能是2.7.0开始才可用的(坑巨多,下面会一一说明),所以我们需要使用这个版本。

 

为了简化(偷)步骤(懒),我们把问题变为A->B->C这种三模块调用过程,本质上设计的调用路由问题还是一样的。

先建立三个spring boot工程:组件svcA、svcB和svcC

两个模块间调用使用的facade工程,以及他们所共享的父工程,总共六个工程如下图:

 

他们之间的关系如下:

 

其中callfromsvcA2svcB是A调用B使用的facade,而callfromsvcB2svcC是从B调用C时的facade,取名方式略暴力,品位低,敬请理解

下面进入踩坑之旅:

1. 导入Dubbo 2.7.0

因为Dubbo 2.7.0才支持tag路由功能,所以我们必须先导入它到工程,但是当你实践时,你会发现。。。。。。网上的教程(包括官方文档):都!是!骗!人!的!

 

官方的说明是:http://dubbo.apache.org/zh-cn/docs/user/versions/version-270.html

<properties>
    <dubbo.version>2.7.0</dubbo.version>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-dependencies-bom</artifactId>
            <version>${dubbo.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo</artifactId>
        <version>${dubbo.version}</version>
    </dependency>
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
    </dependency>
</dependencies>
View Code

这个bom和spring boot(2.1.4.RELEASE)是冲突的,启动时会报错:

Exception in thread "main" java.lang.AbstractMethodError: org.springframework.boot.context.config.ConfigFileApplicationListener.supportsSourceType(Ljava/lang/Class;)Z

(天哪,鬼知道这是啥错)

当然,如果不使用spring boot,可能会没有问题,不过现在建工程貌似都是用spring boot为主流

 

所以只能手动引用Dubbo。

经过反复尝试(内心:mmp),得到如下能够正常工作的pom清单:

        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>4.2.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>log4j</groupId>
                    <artifactId>log4j</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.2.0</version>
        </dependency>

 

注意:

1. 从2.7.0开始,Dubbo已经从alibaba的项目转为了apache的了,所以命名空间会发生改变,小心不要踩坑。

2. 引用com.alibaba.xxx下面的对象会因为这些对象都是Deprecated导致这些对象有删除线,解决办法就是把对应的import删掉,重新引用,你会发现有两个一摸一样的,一个在alibaba的名称空间下面,另外一个在apache里面,引用apache的那个即可。

3. 切记,千万不要在一个工程里面既引用alibaba空间下面的注解,又引用apache下面的注解,这会直接导致注解失效。

 

下面开始处理最困难的部分:给服务打上标签:

首先我们在svcA中建立两个properties文件,用于模拟普通测试和FAT测试,代码如下:

application.properties:

spring.application.name=svcB
dubbo.application.name=svcB
dubbo.registry.protocol=zookeeper
dubbo.registry.address=127.0.0.1:2181
dubbo.protocol.name=dubbo
dubbo.monitor.protocol=registry
dubbo.protocol.port=20881
server.port=55557

application-fat1.properties(请注意标红的属性):

#fat1
spring.application.name=svcB
dubbo.application.name=svcB
dubbo.registry.protocol=zookeeper
dubbo.registry.address=127.0.0.1:2181
dubbo.protocol.name=dubbo
dubbo.monitor.protocol=registry
dubbo.protocol.port=20882
server.port=55558
featuretest=fat1

 

我们假设svcA是前端,从用户处得到请求调用后续的服务的,在这个服务中,我们嵌入一个WebFilter,实现将FAT的TAG打到Dubbo调用中,代码如下:

package com.dubbotest.svcA.filters;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;

import org.apache.dubbo.common.Constants;
import org.apache.dubbo.rpc.RpcContext;
import org.springframework.beans.factory.annotation.Value;

@WebFilter
public class FatTagFilter implements Filter {
    @Value("${featuretest:#{null}}")
    private String feature;
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws ServletException, IOException {
        if (feature != null)
        {
            RpcContext.getContext().setAttachment(Constants.TAG_KEY, feature);
        }
        chain.doFilter(req, resp);
    }

}

这段代码的作用就是从环境中查找名为featuretest的变量,如果找到了,就放到Dubbo中名为TAG的attachment中。

顺便吐槽一下,Dubbo官网上的文档(http://dubbo.apache.org/zh-cn/docs/user/demos/routing-rule.html)中的范例代码:

RpcContext.getContext().setAttachment(Constants.REQUEST_TAG_KEY,"tag1");

是有问题的,2.7.0中,Constants里面已经没有名为REQUEST_TAG_KEY的常量了,只有TAG_KEY,其次,静态打标:

java -jar xxx-provider.jar -Ddubbo.provider.tag={the tag you want, may come from OS ENV}

是不起作用的,我看了下Dubbo源码,没有相关的内容

 

有了上述代码后,前端就实现了当设定了featuretest变量时,这个变量会被当成TAG存放到RPC调用的Attachment中,而根据阿里的文档,这个Attachment是能够存续在整个RPC调用过程的,但是,但是!事实证明这又是坑爹的!

还是拿前面的例子:

A->B->C

当前端传递Attachment到B时,B能够看到数据,但是不知为何,B却没能将这个数据传送到C,导致这个数据在后面调用全部失效

所以只好自己写一个过滤器放在服务B中,将这个变量传递下去:

1. 先在resources\\META-INF\\dubbo目录添加com.alibaba.dubbo.rpc.Filter,内容如下:

passFatTag=com.dubbotest.svcB.filters.PassFatTagFilter

然后再在服务B的application.properties中添加:

dubbo.provider.filter=passFatTag

最后,添加下述Java代码:

package com.dubbotest.svcB.filters;

import org.apache.dubbo.common.Constants;
import org.apache.dubbo.rpc.Filter;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Result;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.rpc.RpcException;

public class PassFatTagFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String fatTag = invocation.getAttachment(Constants.TAG_KEY);
        if (fatTag != null)
        {
            RpcContext.getContext().setAttachment(Constants.TAG_KEY, fatTag);    
        }
        Result result = invoker.invoke(invocation);
        return result;
    }

}

 请注意,这些代码在所有的下游服务器都要添加,例如本例中的B、C。如果后面还有更多的服务,也要添加,目的是让Attachment传递下去。

 

上面这些工作只是对前端到服务的调用进行了打标,下一步将进行对服务提供者进行打标:

对于application.properties的处理大同小异,无非是增加了一个FAT测试标签的变量,但是如何把这个标签弄到服务提供者上,恭喜你,遇到了史前巨坑:

前面已经说过了,下述方法对服务提供者打标是无效的:

java -jar xxx-provider.jar -Ddubbo.provider.tag={the tag you want, may come from OS ENV}

 所以要想办法,只能在Service上面想办法,例如svcB提供的服务,代码可以这么写:

package com.dubbotest.svcB.impl;

import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.dubbo.config.annotation.Reference;
import org.apache.dubbo.config.annotation.Service;

import com.facade.callfromsvcA2svcB.callfromA2B;
import com.facade.callfromsvcB2svcC.callfromB2C;

@Service(tag="fat1")
public class ServiceBimpl implements callfromA2B
{
    Logger logger = Logger.getLogger(ServiceBimpl.class.getName());
    
    @Reference
    private callfromB2C svcC;
    
    @Override
    public String getNamefromSvcB(String source) {
        logger.log(Level.INFO, "Source:"+ source);
        if (source == null)
        {
            return "no name, since source is empty";
        }
        String name = source+source.length();
        return name + " hash:"+svcC.getIDfromName(name);
    }

}

转眼你就会发现这个做法的坑爹之处:

1. FAT测试特性的代码侵入了业务逻辑

2. 无法随时修改特性测试的名称(fat1)

3. 我提供了100个服务,是不是100个服务都要添加打标的代码?如果我要修改呢?(996程序员的内心:mmp)

 

似乎问题到此陷入了僵局,不过不妨先看下打标的功能是怎么实现的:

我们先通过tag作为关键词直接搜索dubbo的jar:

我的搜索方法是这样的:用Java Search,查找All occurrences,Search for中每一个都试一遍(哪位大神如果有更好的方法,麻烦推荐)

最后找到有价值的东西:

猜想如下:Spring在加载ServiceBean的时候,通过注解拿到属性,并且调用setTag配置好,最后服务调用的时候就会使用这个tag,我们先在Service注解中放一个tag,并且对setTag打一个断点,最后启动服务,发现调用栈如下:

不出所料,果然断在了setTag上,这是调用getBean实例化对象时,对Bean对象属性填充时设定的(请看populateBean和applyPropertyValues这两个栈帧)。

这给我们了一个启发,我们可以使用一个BeanPostProcessor后处理器,在Bean实例化后对它进行设定,将tag直接设置上去,代码如下:

package com.dubbotest.svcB.postprocessors;

import org.apache.dubbo.config.spring.ServiceBean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class FeatureTestPostProcessor implements BeanPostProcessor {
    @Value("${featuretest:#{null}}")
    private String featuretest;

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (featuretest != null) {
            if (bean instanceof ServiceBean) {
                ServiceBean thebean = (ServiceBean) bean;
                thebean.setTag(featuretest);
            }
        }
        return bean;
    }
}

因为Dubbo导出服务时,先会在Spring容器中注册一个ServiceBean,所以我们可以在此ServiceBean初始化完毕后,将我们想要的属性注入。

为何不使用postProcessBeforeInitialization?如果使用Before,可能Bean本身初始化属性时又会将我们设定的属性覆盖。

逻辑很简单,无非就是当设定了featuretest时,将这个属性注入到ServiceBean的tag中。

这个代码也会造成一点问题:

如果以后Dubbo升级,可能Bean的类型会改变,属性也会改变

考虑到我们的代码并没有和业务代码耦合,如果以后发生改变,我们修改下后处理器就可以了,这不会是什么问题

 

因为B、和C都是服务提供者,所以C也应该添加上述后处理器以及用于传递消息的Dubbo过滤器

 

测试效果:

我们搭建一个基础服务器组和一个FAT测试场,命名为fat1:

其中基础服务器组入口是:127.0.0.1:8088

fat1入口是:127.0.0.1:8089

先启动基础服务组和FAT1的前端入口:

可以发现,基础服务和FAT1组使用的都是默认feature:

此时我们如果启动fat1中的某个服务,例如C:

服务启动情况如图:

运行结果:

 

可见,实现了对不同特性进行隔离的功能,fat1的使用者可以独立于Baseline环境进行开发测试。

如果此时有另外一个开发组想要开发客户提出的新需求fat2,只需要将application.properties中的featuretest改为fat2然后在本机或者服务器上发布进行测试即可,不同环境完全隔离,互不影响

上述工程的git路径:https://github.com/TTTTTAAAAAKKKKEEEENNNN/FATtestDemo

 

对于工程需要改进的地方,有如下几点思考:

1. 实现FAT使用的过滤器、后处理器需要在每个工程中独立添加,还是不够方便,如果能封装成一个jar在其他工程中引入,将会更加方便

2. 工程中引入Dubbo服务是直接使用的Dubbo注解@Service,如果能在中间嵌入一层,让工程通过Spring间接引用Dubbo,将来因为某种原因要换远程调用框架时,会变得轻松一些

 

问题(1)的解决方案(2019-04-30更新):

目前已经实现将工程中的后处理器、过滤器、拦截器打包到jar中,只需要在自己的工程引入即可,请参考:https://github.com/TTTTTAAAAAKKKKEEEENNNN/FATTest-modularization

下面是使用步骤:

1. 对于一个前端工程(使用了Spring MVC的工程)

请引入下述依赖:

<dependency>
    <groupId>com.fattest</groupId>
    <artifactId>FATtest-web</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

 

在工程的application.properties添加:featuretest=fattag,可以自己修改fattag为其他值

在Spring工程中添加:@ServletComponentScan({"com.fattest"})

 

2. 对于一个后端工程

请添加下述依赖:

<dependency>
    <groupId>com.fattest</groupId>
    <artifactId>FATtest-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

在工程的application.properties添加:dubbo.provider.filter=passFatTag

并且,在Spring工程添加:@ComponentScan({"com.fattest"})

 

请注意:

前端模块将引入:

Dubbo 2.7.0

javax.servlet-api 3.1.0(请适配自己工程合适的版本)

后端模块将会引入:Dubbo 2.7.0

以上是关于如何使用Dubbo 2.7.0和Spring boot实现FAT测试(Feature Acceptance Test)的主要内容,如果未能解决你的问题,请参考以下文章

如何使用Dubbo服务和集成Spring

如何在filter等dubbo自管理组件中注入spring的bean

Springboot 2.x 整合Dubbo 2.6.x和Dubbo 2.7.x

Spring如何解析Dubbo标签

Dubbo 和 Spring Cloud ,还需要选吗?

spring boot集成dubbo