首先介绍做一下场景介绍:
1、我们公司的测试环境比较复杂,预发环境(UAT)一套,SIT环境4套,DEV环境7套。我是负责中台模块的测试,功能类似一个订单中心,但是功能相对比较复杂。网关进来的95%以上的请求都要我负责的模块来处理(不论线上业务还是线下业务,因此所有的环境都要经过我负责模块。
2、我们公司使用的grpc微服务框架,而我负责的中台模块,都是通过grpc的微服务接口(不提供http接口),对于测试来讲,这是个不幸的消息。
那么我们中台的接口自动化测试是如何来实现的呢?
这个是完整 RPC 架构图
一个 RPC 的核心功能主要有 5 个部分组成,分别是:客户端、客户端 Stub、网络传输模块、服务端 Stub、服务端等
- 客户端(Client):服务调用方。
- 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
- 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
- 服务端(Server):服务的真正提供者。
- Network Service:底层传输,可以是 TCP 或 HTTP。
了解上面的基本知识。现在来介绍我是如何实现的。
1、创建连接到远程服务器的 channel
2、构建使用该channel的客户端stub
3、调用服务方法,执行RPC调用
4、封装成Controller
构建客户端stub
public class Client {
//样例 stub
private DemoServiceGrpc.DemoServiceBlockingStub demoServiceBlockingStub;
//原生的stub 点对点测试
public Client() {
ManagedChannel channel = null;
try {
String ip =PropertiesUtils.getValue("****.grpc.ip");
String port = PropertiesUtils.getValue("****.grpc.port");
channel = ManagedChannelBuilder.forTarget("static://" + ip + ":" + port).usePlaintext().build();
} catch (Exception e) {
e.printStackTrace();
}
demoServiceBlockingStub = DemoServiceGrpc.newBlockingStub(channel);
}
public DemoServiceGrpc.DemoServiceBlockingStub getdemoServiceBlockingStub() {
return demoServiceBlockingStub;
}
}
封装Controller:
那么简单的http 接口服务就实现了。接下来重点来了,如何实现部署一台服务访问不同环境呢????
具体实现:
基于spring提供原生的 AbstractRoutingDataSource
,参考一些文档自己实现切换
1、 为了区分不同环境的配置,采用了application-{}.yaml文件来隔离, 然后通过application.yaml文件来控制加载所有的配置文件
2、application.yaml配置
3、在Springboot的启动类上,排除掉datasource自动配置
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GrpcApplication {
public static void main(String[] args) {
SpringApplication.run(GrpcApplication.class, args);
}
}
4、新建一个EnvContext类,采用ThreadLocal的方式,对每个请求线程的环境变量进行隔离,这里容易遇到坑,springboot都是内嵌的tomcat启动模式,如果tomcat设置了链接的重用规则,那么如果env的信息没有被清除,可能会导致错误加载配置
/**
* 用来存放环境的变量,用于动态的去切换
*/
public class EnvContext {
public static ThreadLocal envThreadLocal = new InheritableThreadLocal<>();
public static String getEnv(){
return envThreadLocal.get();
}
public static void setEnv(String env){
envThreadLocal.set(env);
}
public static void clear(){
envThreadLocal.remove();
}
}
5、创建一个DynamicDataSource, 这里继承了AbstractRoutingDataSource,动态数据源类集成了Spring提供的AbstractRoutingDataSource类,AbstractRoutingDataSource 中获取数据源的方法就是 determineTargetDataSource,而此方法又通过 determineCurrentLookupKey 方法获取查询数据源的key。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return EnvContext.getEnv();
}
}
6、定义一个枚举类,放入所有的环境信息
@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum EnvEnum {
DEV1("dev1","开发环境dev1"),
DEV2("dev2","开发环境dev2"),
DEV3("dev3","开发环境dev3"),
DEV4("dev4","开发环境dev4"),
DEV5("dev5","开发环境dev5"),
DEV6("dev6","开发环境dev6"),
DEV7("dev7","开发环境dev7"),
SIT1("sit1","集成环境SIT1"),
SIT2("sit2","集成环境SIT2"),
SIT3("sit3","集成环境SIT3"),
SIT4("sit4","集成环境SIT4"),
UAT("uat","集成环境UAT");
public String env;
public String desc;
}
7、重点来了,我们通过AOP, 去拿到每次http的请求头中的header信息,来动态的切换EnvContext中的env配置
@Aspect
@Component
@Slf4j
public class EnvAop {
public ThreadLocal threadLocal = new ThreadLocal<>();
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping ) && @annotation(io.swagger.annotations.ApiOperation))")
public void ex(){}
@Around("ex()")
public Object envAop(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object result;
try {
//获取每个请求的header,拿到环境变量的参数,存入ThreadLocal中,供每个线程使用
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
// 获取请求头
Enumeration enumeration = request.getHeaderNames();
//
String env = request.getHeader("env");
if(StringUtils.isEmpty(env)){
log.info("~~~~ 拦截到http请求,环境变量信息为空,设置为默认dev1", env);
env = EnvEnum.DEV1.env;
}
log.info("~~~~ 拦截到http请求,环境变量信息为{}", env);
EnvConfig.envThreadLocal.set(env);
result = proceedingJoinPoint.proceed();
} finally {
//请求结束后,将环境变量的信息从ThreadLocal中移除
EnvConfig.clear();
log.info("~~~~ http请求结束,重置env的信息为{}" , EnvConfig.getEnv());
}
return result;
}
}
8、编写一个工具类,动态获取Spring的容器ApplicationContext
@Component
public class SpringContextUtil {
@Resource
private ApplicationContext applicationContext;
private static ConfigurableApplicationContext context;
private static BeanFactory factoryBean;
@PostConstruct
public void init() {
context = (ConfigurableApplicationContext) applicationContext;
factoryBean = context.getBeanFactory();
}
public static BeanFactory getFactoryBean() {
return factoryBean;
}
public static ConfigurableApplicationContext getApplicationContext() {
return context;
}
}
9、然后编写一个配置信息动态读取工具类,每次请求进来,env会动态切换,然后工具类会自动拼装env信息去读取
public class PropertiesUtils {
public static String getValue(String key) throws Exception {
Environment environment = (Environment) SpringContextUtil.getApplicationContext().getBean("environment");
String value = environment.getProperty(EnvConfig.getEnv() + "." + key);
if(StringUtils.isEmpty(value)){
throw new Exception("配置信息获取失败,请检查application-"+ EnvConfig.getEnv()+".yaml文件!, key = " + key + " , env = " + EnvConfig.getEnv());
}
return value;
}
}
到此 实现通过http请求 中header中配置env参数来实现 动态切换服务器(以此类推可以修改同过parame或者url中的参数来实现动态切换服务器)
重点注意:
实现client 的连接的方法不能通过Springboot 的@service @Autowired来实现 不然无法实现动态切换服务器 也就是Controller里面每次使用client的时候 都要new
因为通过Bean实现的话,启动的时候就已经加载完成了,无法实现动态加载
声明:该文章参考公司同事(章帅)的文章