前言
现在要支持MS的Thrfit 协议,因为MS是集成的jmeter,所以要开发MS插件其实就是开发Jmeter插件,然后再进行封装。
一、JMeter介绍
1. JMeter
JMeter 是 Apache 基金会旗下的一款完全基于 Java 的开源软件,主要用作性能测试,通过模拟并发负载来测试并分析应用的性能状况。JMeter 最初被用于测试部署在服务器端的 Web 应用程序,现在发展到了更广泛的领域。目前 JMeter 已成为主流的性能测试工具。
1.1 功能特性
- JMeter 作为优秀的开源项目,拥有开放、强大且活跃的社区支持。
- JMeter 支持多种类型的应用、服务及协议,官方支持包括:HTTP Web 应用、HTTPS Web 应用、SOAP、REST、FTP、TCP、LDAP、Java 对象、JDBC 方式连接的数据库等。扩展后可支持更多协议。
- JMeter 具备跨平台特性,可运行于 Linux、Windows、Mac OSX 等多种操作系统平台上。测试人员通过 JMeter GUI 界面创建 JMeter 脚本,或通过录制的方式生成脚本,然后通过 GUI 界面或命令行方式来运行测试。
- JMeter 具有高可扩展性,测试人员可基于 JMeter 开发自定义的插件,由 JMeter 装载后运行。
由此可见,JMeter可以支持自定义第三方插件的。
2. 本地安装JMeter
参考链接:
https://www.jianshu.com/p/0e4daecc8122
二、开发Thrift协议插件
2.1 Thrift
Thrift 是一个软件框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++、Java、Python、PHP、Ruby、Erlang、Perl、Haskell、C#、Cocoa、JavaScript、Node.js、Smalltalk、and OCaml 等等编程语言间无缝结合的、高效的服务。
Thrift 最初由 facebook 开发,07 年四月开放源码,08 年 5 月进入 Apache 孵化器。Thrift 允许你定义一个简单的定义文件中的数据类型和服务接口。以作为输入文件,编译器生成代码用来方便地生成 RPC 客户端和服务器通信的无缝跨编程语言。
官方链接:
https://thrift.apache.org/
2.2 MAC安装Thrift环境
- Thrift 是一个rpc的框架,所以,要想编译Thrift 的IDL语言,首先要安装编辑器。
- Mac 安装还是比较方便的直接通过 brew 的方式安装
brew install thrift
- 安装成功之后看下版本信息,出现版本即为成功。
thrift -version
2.3 IDEA中编写Thrift 脚本
- 首先需要安装支持Thrift IDL语言的插件。(Thrift Support)
- 这个插件对idea的版本有限制,安装的时候需要注意下。
- 当然也可以直接在任何文本编辑器中进行编写,保存文件的时候保存为.thrift为结尾的就行。
插件地址:
https://plugins.jetbrains.com/plugin/7331-thrift-support
- 下载完之后就可以编写IDL脚本了。
2.3.1 Thrift IDL 语法对应如下:
-
基本类型:
bool:布尔值,true 或 false,对应 Java 的 boolean
byte:8 位有符号整数,对应 Java 的 byte
i16:16 位有符号整数,对应 Java 的 short
i32:32 位有符号整数,对应 Java 的 int
i64:64 位有符号整数,对应 Java 的 long
double:64 位浮点数,对应 Java 的 double
string:utf-8 编码的字符串,对应 Java 的 String -
结构体类型:
struct:定义公共的对象,类似于 C 语言中的结构体定义,在 Java 中是一个 JavaBean -
容器类型:
list:对应 Java 的 ArrayList
set:对应 Java 的 HashSet
map:对应 Java 的 HashMap -
异常类型:
exception:对应 Java 的 Exception -
服务类型:
service:对应服务的类。
2.3.2 服务端编码基本步骤
- 实现服务处理接口 impl
- 创建 TProcessor
- 创建 TServerTransport
- 创建 TProtocol
- 创建 TServer
- 启动 Server
2.3.3 客户端编码基本步骤
- 创建 Transport
- 创建 TProtocol
- 基于 TTransport 和 TProtocol 创建 Client
- 调用 Client 的相应方法
2.3.4 数据传输协议
- TBinaryProtocol 二进制格式
- TCompactProtocol 压缩格式
- TJSONProtocol JSON 格式
- TSimpleJSONProtocol 提供 JSON 只写协议,生成的文件很容易通过脚本语言解析
提示
: 客户端和服务端的协议要一致
2.3.5 编写Thrift脚本
- 创建一个maven或者java 项目都可以
- 编写Thrift脚本
namespace java com.thrift.demo // 使用java 语言定义 package
service ThriftHelloService{ // 定义一个service 相当于java当中的接口
string sayHello(1:string username) // 输入一个参数,返回string类型参数。
}
- 注意名字必须要以.thrift 结尾的。
- idea 没有.thrift文件,直接创建text修改名字即可。
2.3.6 编译Thrift脚本
- 通过 thrift -gen java 文件名.thrift 进行编译.
- 编译成功之后会在当前项目里面看到.
3. 可以看到报错不用管,因为这个项目就是我们用来使用生成java类的项目。可以看到这个类中,有很多代码,
简单分析一下这个类:生成的类主要有5个部分
-
接口类型
,默认名称都是Iface。这个接口类型被服务器和客户端共同使用。服务器端使用它来做顶层接口,编写实现类。客户端代码使用它作为生成代理的服务接口。
自动生成的接口有两个,一个是同步调用的Iface,一个是异步调用的AsyncIface。异步调用的接口多了一个回调参数。
-
客户端类型
,一个同步调用的客户端Client,一个异步调用的客户端AsyncClient -
Processor
,用来支持方法调用,每个服务的实现类都要使用Processor来注册,这样最后服务器端调用接口实现时能定位到具体的实现类。 一个同步调用Processor,一个异步调用AsyncProcessor -
方法参数的封装类
,以”方法名_args”命名 -
方法返回值的封装类
,以”方法名_result”命名
详细的源码分析参考:
https://www.kancloud.cn/digest/thrift/118986
2.3.7 编写服务器端
- 在idea中新建一个maven项目,报名要与刚刚在Thrift当中生成的namespace一样,并且把刚刚生成好的java类给复制过来
- 添加pom依赖
<dependencies>
<!-- thrift 集成-->
<dependency>
<groupId>org.apache.thrift</groupId>
<artifactId>libthrift</artifactId>
<version>0.16.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.12</version>
</dependency>
<!-- jmeter 集成-->
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_core</artifactId>
<version>5.5</version>
</dependency>
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_java</artifactId>
<version>5.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
<!-- 完整依赖打包 集成-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
- 写接口的实现类
public class ThriftHelloServiceImpl implements ThriftHelloService.Iface {
@Override
public String sayHello(String username) throws TException {
return "Hello Thrift :" + username;
}
}
- 写服务端main方法
public static void main(String[] args) throws Exception {
try {
// 实现处理接口impl
ThriftHelloServiceImpl thriftHelloService = new ThriftHelloServiceImpl();
// 创建TProcessor
TProcessor processor = new ThriftHelloService.Processor<>(thriftHelloService);
// 创建TServerTransport,非阻塞式I/O,服务端和客户端需要TFramedTransport 数据传输方式
TNonblockingServerSocket tNonblockingServerSocket = new TNonblockingServerSocket(9099);
// 创建TProtocol
TThreadedSelectorServer.Args args1 = new TThreadedSelectorServer.Args(tNonblockingServerSocket);
args1.transportFactory(new TFramedTransport.Factory());
// 二进制格式反序列化
args1.protocolFactory(new TBinaryProtocol.Factory());
args1.processor(processor);
args1.selectorThreads(16);
args1.workerThreads(32);
System.out.println("computer service server on port:" + 9099);
TThreadedSelectorServer tThreadedSelectorServer = new TThreadedSelectorServer(args1);
System.out.println("启动 Thrift 服务端");
tThreadedSelectorServer.serve();
} catch (Exception e) {
System.out.println("启动 Thrift 服务端失败" + e.getMessage());
}
}
2.3.8 编写客户端
public static void main(String[] args) {
TTransport transport = null;
try {
// 要跟服务器端的传输方式一致
transport = new TFramedTransport(new TSocket("127.0.0.1", 9099, 6000));
TProtocol protocol = new TBinaryProtocol(transport);
ThriftHelloService.Client client = new ThriftHelloService.Client(protocol);
transport.open();
String result = client.sayHello("thrift-1");
System.out.println(result);
} catch (TException e) {
e.printStackTrace();
} finally {
if (null != transport) {
transport.close();
}
}
}
- 启动验证
- 服务端
-
客户端
验证成功,说明没有问题。
2.4 编写JMeter 插件
- 因为我们目的是要写jmeter插件,所以要集成JMeter对应的依赖包,详细参考上面的pom文件。
-
两种方式:
- 一种是继承 AbstractJavaSamplerClient ,此方式集成之后,直接打包,在jmeter当中选择,java请求即可验证功能。偏向于验证功能。
- 一种是继承 AbstractSampler 和 继承AbstractSamplerGui 此方式可以通过GUI 的方式,实现JMeter的调用。
ThriftClient (封装公用client)
ThriftHelloService.Client client = null;
private TTransport tTransport = null;
public ThriftClient(String ip, int port, int timeout) {
try {
// 注意传输协议要跟服务器端一致
tTransport = new TFramedTransport(new TSocket(ip, port, timeout));
TProtocol tProtocol = new TBinaryProtocol(tTransport);
client = new ThriftHelloService.Client(tProtocol);
tTransport.open();
} catch (TTransportException e) {
e.printStackTrace();
}
}
public String getResponse(String str) {
try {
return client.sayHello(str);
} catch (TException e) {
e.printStackTrace();
return null;
}
}
public void close() {
if (tTransport != null && tTransport.isOpen()) {
tTransport.close();
}
}
2.4.1 继承AbstractJavaSamplerClient
private ThriftClient thriftClient;
/**
* 方法为性能测试时的线程运行体;
*
* @param javaSamplerContext
* @return
*/
@Override
public SampleResult runTest(JavaSamplerContext javaSamplerContext) {
SampleResult sampleResult = new SampleResult();
// 开始统计响应时间标记
sampleResult.sampleStart();
try {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
String response = thriftClient.getResponse("哈哈我是性能调试");
stopWatch.stop();
System.out.println(response + "总计花费:" + stopWatch.getTime());
if (StringUtils.isNotBlank(response)) {
sampleResult.setSuccessful(true);
sampleResult.setResponseMessage(response);
} else {
sampleResult.setSuccessful(false);
sampleResult.setResponseMessage("请求失败....");
}
} catch (Exception e) {
sampleResult.setSuccessful(false);
sampleResult.setResponseMessage("请求失败....");
} finally {
// 结束统计响应时间标记
sampleResult.sampleEnd();
}
return sampleResult;
}
/**
* 方法为初始化方法,用于初始化性能测试时的每个线程;
*
* @param context
*/
@Override
public void setupTest(JavaSamplerContext context) {
String ip = context.getParameter("ip");
String port = context.getParameter("port");
String timeout = context.getParameter("timeout");
// 初始化客户端
thriftClient = new ThriftClient(ip, Integer.valueOf(port), Integer.valueOf(timeout));
super.setupTest(context);
}
/**
* 方法为测试结束方法,用于结束性能测试中的每个线程。
*
* @param context
*/
@Override
public void teardownTest(JavaSamplerContext context) {
if (thriftClient != null) {
thriftClient.close();
}
super.teardownTest(context);
}
/**
* 方法主要用于设置传入界面的参数,初始化默认参数
*
* @return
*/
@Override
public Arguments getDefaultParameters() {
Arguments jMeterProperties = new Arguments();
jMeterProperties.addArgument("ip", "127.0.0.1");
jMeterProperties.addArgument("port", "9099");
jMeterProperties.addArgument("timeout", "6000");
return jMeterProperties;
}
- 写完之后直接打成jar包 。 mvn clean install
- 然后会看到生成两个包。 然后把对应的jar包放到 jmeter 的 lib/ext/ 下面.
- 注意: 如果打的是源码包要放源码包: 即ThriftDemo-1.0-1-jar-with-dependencies.jar
- jmeter 下面 lib 和 lib/ext 的区别: 一般我们自定义协议的包都会放到 lib/ext下面,当我们自定义里面的包有依赖第三方的依赖包,并且此时我们没有打成源码包的情况下,会将所依赖的第三方依赖包放置到 lib当中。
- 比如: 我在 lib/ext 下面放置的是 ThriftDemo-1.0-1.jar 包,这个并不是源码包。所以,还需要将项目当中pom文件所依赖的org.apache.libthrift 的jar包放置到 lib目录下面才行。否则在调用的时候,会提示找不到对应类(classNotFountException)的错误信息。
- Jmeter 效果
- 放置好jar包之后,重启jmeter,java请求,就会看到自己编写的类。
- 下面是默认的参数。
服务端启动,即可验证成功。
2.4.2 继承AbstractSampler 和AbstractSamplerGui
- 这种方式的效果如下:
就是在jmeter当中写一个自己的sampler ,并且有对应ui页面。
2.4.2.1 ThriftSamplerUI
继承AbstractSamplerGui 编写gui的页面,跟java当中swing 一样。
package com.thrift.demo.gui;
import com.thrift.demo.jmeterTest.ThriftSampler;
import org.apache.jmeter.gui.util.VerticalPanel;
import org.apache.jmeter.samplers.gui.AbstractSamplerGui;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jorphan.gui.JLabeledTextField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*;
/**
* @author fit2cloudzhao
* @date 2022/8/2 18:58
* @description:
*/
public class ThriftSamplerUI extends AbstractSamplerGui {
Logger log = LoggerFactory.getLogger(ThriftSamplerUI.class);
private final JLabeledTextField serverIp = new JLabeledTextField("ServerIp");
private final JLabeledTextField port = new JLabeledTextField("Port");
private final JLabeledTextField param = new JLabeledTextField("Param");
private void init() {
log.info("Initializing the UI.");
setLayout(new BorderLayout());
setBorder(makeBorder());
add(makeTitlePanel(), BorderLayout.NORTH);
JPanel mainPanel = new VerticalPanel();
add(mainPanel, BorderLayout.CENTER);
JPanel DPanel = new JPanel();
DPanel.setLayout(new GridLayout(3, 2));
DPanel.add(serverIp);
DPanel.add(port);
DPanel.add(param);
JPanel ControlPanel = new VerticalPanel();
ControlPanel.add(DPanel);
ControlPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.gray), "Parameters"));
mainPanel.add(ControlPanel);
}
public ThriftSamplerUI() {
super();
this.init();
}
@Override
public String getStaticLabel() {
return "Thrift Sampler";
}
@Override
public String getLabelResource() {
throw new IllegalStateException("This shouldn't be called");
}
/**
* 该方法创建一个新的Sampler,然后将界面中的数据设置到这个新的Sampler实例中。
* @return
*/
@Override
public TestElement createTestElement() {
ThriftSampler sampler = new ThriftSampler();
this.setupSamplerProperties(sampler);
return sampler;
}
private void setupSamplerProperties(ThriftSampler sampler) {
this.configureTestElement(sampler);
sampler.setServerIp(serverIp.getText());
sampler.setPort(Integer.valueOf(port.getText()));
sampler.setParam(param.getText());
}
/**
* 这个方法用于把界面的数据移到Sampler中。
* @param testElement
*/
@Override
public void modifyTestElement(TestElement testElement) {
ThriftSampler sampler = (ThriftSampler) testElement;
this.setupSamplerProperties(sampler);
}
/**
* 界面与Sampler之间的数据交换
* @param element
*/
@Override
public void configure(TestElement element) {
super.configure(element);
ThriftSampler sampler = (ThriftSampler) element;
this.serverIp.setText(sampler.getServerIp());
this.port.setText(sampler.getPort().toString());
this.param.setText(sampler.getParam());
}
/**
* 该方法会在reset新界面的时候调用,这里可以填入界面控件中需要显示的一些缺省的值(就是默认显示值)
*/
@Override
public void clearGui() {
super.clearGui();
this.serverIp.setText("服务端ip");
this.port.setText("9099");
this.param.setText("参数");
}
}
2.4.2.2 ThriftSampler
package com.thrift.demo.jmeterTest;
import com.thrift.demo.client.ThriftClient;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.jmeter.samplers.AbstractSampler;
import org.apache.jmeter.samplers.Entry;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.testelement.TestStateListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author fit2cloudzhao
* @date 2022/8/2 21:45
* @description:
*/
public class ThriftSampler extends AbstractSampler implements TestStateListener {
Logger log = LoggerFactory.getLogger(ThriftSampler.class);
private ThriftClient thriftClient;
private static final String SERVER_IP = "server_ip";
private static final String PORT = "port";
private static final String PARAM = "request_param";
public ThriftSampler() {
setName("Thrift sampler");
}
@Override
public SampleResult sample(Entry entry) {
SampleResult sampleResult = new SampleResult();
// 开始统计响应时间标记
sampleResult.sampleStart();
try {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
String param = getParam();
String response = "";
System.out.println("入参:" + param);
log.info("入参:" + param);
thriftClient = getThriftClient();
if (StringUtils.isNotBlank(param)) {
response = thriftClient.getResponse(param);
} else {
response = thriftClient.getResponse("我是空的");
}
System.out.println("response==>" + response);
log.info("response==>" + response);
stopWatch.stop();
System.out.println(response + "总计花费:" + stopWatch.getTime());
log.info(response + "总计花费:" + stopWatch.getTime());
if (StringUtils.isNotBlank(response)) {
sampleResult.setSuccessful(true);
sampleResult.setResponseMessage(response);
sampleResult.setResponseData(("请求成功:"+response).getBytes());
sampleResult.setResponseCode("200");
} else {
sampleResult.setSuccessful(false);
sampleResult.setResponseMessage("请求失败....请求参数:" + param);
sampleResult.setResponseCode("500");
sampleResult.setResponseData("请求失败".getBytes());
}
} catch (Exception e) {
sampleResult.setSuccessful(false);
sampleResult.setResponseMessage("请求失败...." + e.getMessage());
sampleResult.setResponseCode("500");
sampleResult.setResponseData(("请求失败...." + e.getMessage()).getBytes());
} finally {
// 结束统计响应时间标记
sampleResult.sampleEnd();
}
return sampleResult;
}
public ThriftClient getThriftClient() {
if (thriftClient == null) {
thriftClient = new ThriftClient(getServerIp(), getPort(), 10000);
}
return this.thriftClient;
}
public String getServerIp() {
return getPropertyAsString(SERVER_IP);
}
public Integer getPort() {
return getPropertyAsInt(PORT);
}
public void setServerIp(String serverIp) {
setProperty(SERVER_IP, serverIp);
}
public void setPort(Integer port) {
setProperty(PORT, port);
}
public void setParam(String param) {
setProperty(PARAM, param);
}
public String getParam() {
return getPropertyAsString(PARAM);
}
@Override
public void testStarted() {
}
@Override
public void testStarted(String s) {
}
@Override
public void testEnded() {
this.testEnded("local");
}
@Override
public void testEnded(String s) {
}
}
- 这里的sample 方法跟继承AbstractJavaSamplerClient 里面的runTest方法差不多,都是写执行逻辑的,请求客户端的逻辑可以写在这里。
- 写完之后,进行打包,跟之前打包一样,需要把源码包放到jmeter lib/ext下面。
- 重启jmeter ,选择自己写的sampler ,就会看到下面的效果。
- 注意:如果打包完之后,看不到自己写的sampler,需要看下jmeter日志。会有错误日志的,根据错误信息进行进一步处理。
2.5 编写MS的插件
- 因为MS集成的JMeter,所以首先必须是JMeter支持的插件,其次在封装成MS的插件。
- 前面已经写了一个JMeter支持的Thrift插件了,现在只需要把这个插件给集成到MS插件里面就好了。
- 看一下我们要实现的效果。
2.5.1 MeterSphere 接口自动化插件基础开发教程
参考链接:
https://wiki.fit2cloud.com/pages/viewpage.action?pageId=67671925
metersphere-plugin-DummySampler
2.5.2 编写MS 的 Thrift 插件
- 新建一个maven项目 ,目录结构如下:
- 然后参考以上文章直接去改自己的逻辑就行。
- 这边有一点要注意:要把自己写的Thrfit的jar 给依赖进来。一定要是源码包!!!否则集成到MS当中会报错。跟在JMeter当中错误一样。
- 在resource创建 lib文件夹,然后把集成的第三方jar放进来,并且在pom文件中添加依赖。
- 然后进行打包就行。mvn clean install 打完包之后,一个MS的插件就封装好了,然后直接在MS的插件管理上传插件就行。
- 上传完插件之后,检查下后面的小眼睛,看下自己编写的表单内容是否有添加上来。如果没有,检查打包问题。
- 注意:MS删除插件必须重启ms-server服务才行,否则依赖的还是上一次的jar包。
- 上传完成之后在接口自动化会看到自己添加的插件信息
- 这个时候客户端写好了,如果是本地部署的,本地服务端直接启动就好。因为我这个是放到服务器上的,所以在把刚才的服务端给封装成docker镜像给放上去。
- 打开刚刚写的服务端的代码,打成jar包。具体方式如下:
- 打开Project Structure
- 选择Artifacts 添加一个jar包。
选择resource之后,就会看到下面生成的MANIFEST.MF 文件,打开会看到很多class,说明选择成功。
然后再build一下
build完之后会看到当前项目当中有一个out的文件夹
找到
这个也就是我们打的jar包。到此,jar已经打包完成了,放置到有jdk的环境当中 java -jar 就可以起来了。
我们这边封装成docker 镜像的方式,方便启动。
2.5.2.1 封装Dockerfile
FROM fabric8/java-alpine-openjdk11-jre:latest // 注意arm/amd 的区别
MAINTAINER FIT2CLOUD <support@fit2cloud.com>
RUN mkdir -p /opt/apps
ADD out/artifacts/ThriftDemo_jar /opt/apps
WORKDIR /opt/apps
EXPOSE 9099
CMD ["java","-jar","ThriftDemo.jar"]
然后执行打成镜像, 并上传到服务器当中。
docker build -t ms-thrift-server:1.0 .
docker save -o ms-thrift-server.tar ms-thrift-server:1.0
上传服务器,加载镜像并启动
docker load -i ms-thrift-server.tar
docker run --name ms-thrift-server -d -p 9099:9099 ms-thrift-server:1.0
docker ps // 查看状态
docker logs -f ms-thrift-server // 出现启动Thrift 服务端,即启动成功。
2.5.3 调试
万事俱备了,只欠最后一哆嗦。
- 填写对应的服务器的地址和端口,然后随便传一个参数,可以看到返回200 即链接成功。