如何使用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>

这个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,将来因为某种原因要换远程调用框架时,会变得轻松一些

原文地址:https://www.cnblogs.com/nullifier/p/10742957.html

时间: 2024-10-04 04:47:04

如何使用Dubbo 2.7.0和Spring boot实现FAT测试(Feature Acceptance Test)的相关文章

Spring Boot应用的测试——Mockito

Spring Boot应用的测试——Mockito Spring Boot可以和大部分流行的测试框架协同工作:通过Spring JUnit创建单元测试:生成测试数据初始化数据库用于测试:Spring Boot可以跟BDD(Behavier Driven Development)工具.Cucumber和Spock协同工作,对应用程序进行测试. 进行软件开发的时候,我们会写很多代码,不过,再过六个月(甚至一年以上)你知道自己的代码怎么运作么?通过测试(单元测试.集成测试.接口测试)可以保证系统的可维

spring boot项目如何测试,如何部署

有很多网友会时不时的问我,spring boot项目如何测试,如何部署,在生产中有什么好的部署方案吗?这篇文章就来介绍一下spring boot 如何开发.调试.打包到最后的投产上线. 开发阶段 单元测试 在开发阶段的时候最重要的是单元测试了,springboot对单元测试的支持已经很完善了. 1.在pom包中添加spring-boot-starter-test包引用 <dependency> <groupId>org.springframework.boot</groupI

开发者测试-采用精准测试工具对Spring Boot应用进行测试

简介:本文主要介绍把现今主流的springboot框架项目和精准测试工具进行结合和应用,通过精准测试的数据穿透.数据采集.测试用例与代码的双向追溯.数据分析等一系列精准测试的特有功能,达到对项目质量的保证. 本次环境搭建分为基础环境准备.springboot环境搭建.精准测试环境搭建.精准测试与springboot环境对接等一整套完整的配置过程,用户可以通过下图中的流程图确认自己所部署过程中进行到的阶段点,从而排查部署中可能遇见的问题. 一,Spring Boot配置流程 1,使用工具 1, E

Spring Boot 1.4测试的简单理解

首先maven要引入spring-boot-starter-test这个包. 先看一段代码 @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT) public class MyTest {         @Autowired     private TestRestTemplate restTemplate;     @Test    public void test()

Spring Boot 中的测试:JUnit

官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html 原文地址:https://www.cnblogs.com/cag2050/p/11163080.html

Spring Boot集成Junit测试

添加依赖: 在测试类上添加注解: 原文地址:https://www.cnblogs.com/niwotaxuexiba/p/10849952.html

Spring Boot 1.4.0 Release Notes 更新日志-02

Spring Framework 4.3 Spring Boot 1.4 builds on and requires Spring Framework 4.3. There are a number of nice refinements in Spring Framework 4.3 including new Spring MVC @RequestMapping annotations. Refer to the Spring Framework reference documentati

spring boot 2.0.3+spring cloud (Finchley)7、微服务监控Spring Cloud Admin

参考:Spring Boot Admin 2.0 上手 Spring Boot Admin 用于管理和监控一个或多个Spring Boot程序,在 Spring Boot Actuator 的基础上提供简洁的可视化 WEB UI,提供如下功能: 显示 name/id 和版本号 显示在线状态 Logging 日志级别管理 JMX beans 管理 Threads 会话和线程管理 Trace 应用请求跟踪 应用运行参数信息,如: Java 系统属性 Java 环境变量属性 内存信息 Spring 环

Spring Boot微服务如何集成fescar解决分布式事务?

什么是fescar? 关于fescar的详细介绍,请参阅fescar wiki. 传统的2PC提交协议,会持有一个全局性的锁,所有局部事务预提交成功后一起提交,或有一个局部事务预提交失败后一起回滚,最后释放全局锁.锁持有的时间较长,会对并发造成较大的影响,死锁的风险也较高. fescar的创新之处在于,每个局部事务执行完立即提交,释放本地锁:它会去解析你代码中的sql,从数据库中获得事务提交前的事务资源即数据,存放到undo_log中,全局事务协调器在回滚的时候直接使用undo_log中的数据覆