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

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

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

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

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

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

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

 

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

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

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

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

 

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

 

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

相关文章
相关标签/搜索