曾经兼职维护过公司的压测代码一断时间,当时接手过来的那压测代码,简直不要太LOW:就是简单暴力的java -cp去运行一个main方法,里面用多线程去加压,测试结果也是不严谨的自己算出来的(如果进行反推计算可以发现很多不能自圆其说的地方)。由于测试结果的不准确,后来我基于JMeter自定义采样器的开发规范集成到JMeter中去了。然后就一有压测的事情就找我,干着干着好像也能勉强算半个压测的行家了,基础的Http接口压测、基于Tcp的私有长连接协议的压测、音视频UDP媒体中继层面的压测都搞过。然后最近公司一个RestHttpApi接口数量200+的系统要压测,又找到我。我没有功夫通过自定义采样器的方式去一个一个接口的去硬实现,也不想折腾JMeter里各种元件,于是萌生了结合低代码思路去开发一个简单易用的自定义采样器的想法。
1、编译下载
0 积分免费下载,内容包括jmeter-5.4.3及通用 HTTP 自定义采样器编译,如下图:添加采样器即可直接使用。由于使用jmeter5.4.3因此依赖java11。
https://download.csdn.net/download/camelials/87769473
2、源码地址
https://github.com/bossfriday/bossfriday-jmeter
首先介绍一下为什么选择JMeter以及如何开发JMeter自定义采样器。
压测工具很多,之所以选择JMeter的原因是LoadRunner太重了(装一下好几个G),然后基本都是在windows下使用(现在应该是出了linux版)。
JMeter使用Java语言开发,扩展自定义采样器其实十分简单,简单来说就2步:继承AbstractSampler去实现你自己的采样器、继承AbstractSamplerGui去实现采样器的UI。至于JMeter的集成方式就更加简单:直接将编译好的Jar包放到:{JMeterHome}/lib/ext目录下重启JMeter即可,代码示例及效果如下:
1)AppSampler.java
public class AppSampler extends AbstractSampler implements TestStateListener {
private static final Logger log = LogManager.getLogger(AppSampler.class);
private static BaseSampler sampler;
private static SamplerSetting setting;
public AppSampler() {
this.setName(GUI_SAMPLER_NAME);
}
@Override
public void testStarted() {
try {
setSamplerSetting(this);
setSampler(setting);
sampler.testStarted();
log.info("sampler testStarted() done.");
} catch (Exception ex) {
sampler.setStartError(true);
log.error("testStarted() error!", ex);
}
}
@Override
public SampleResult sample(Entry entry) {
try {
if (sampler.isStartError()) {
return AppSamplerUtils.buildSampleResult(setting.getSampleLabel(),
String.valueOf(CODE_TEST_STARTED_ERROR),
RESPONSE_MESSAGE_STARTED_ERROR,
false);
}
SampleResult sampleResult = sampler.sample();
sampleResult.setSampleLabel(setting.getSampleLabel());
return sampleResult;
} catch (Exception ex) {
log.error("sample() error!", ex);
return AppSamplerUtils.buildSampleResult(setting.getSampleLabel(),
String.valueOf(CODE_SAMPLE_ERROR),
ex.getMessage(),
false);
}
}
@Override
public void testStarted(String s) {
// ignore the unsupported invoke
}
@Override
public void testEnded() {
try {
sampler.testEnded();
log.info("sampler testEnded() done.");
} catch (Exception ex) {
log.error("testEnded() error!", ex);
}
}
@Override
public void testEnded(String s) {
// ignore the unsupported invoke
}
/**
* setSamplerSetting
*
* @param sampler
*/
private static synchronized void setSamplerSetting(AbstractSampler sampler) throws PocException {
String samplerType = sampler.getPropertyAsString(GUI_SAMPLER_TYPE);
String sampleLabel = sampler.getPropertyAsString(GUI_SAMPLE_LABEL);
String assertType = sampler.getPropertyAsString(GUI_ASSERT_TYPE);
boolean isLogRequest = sampler.getPropertyAsBoolean(GUI_SAMPLE_IS_LOG_REQUEST);
boolean isLogResponse = sampler.getPropertyAsBoolean(GUI_SAMPLE_IS_LOG_RESPONSE);
String sampleVar = sampler.getPropertyAsString(GUI_SAMPLE_VAR);
boolean isHttps = sampler.getPropertyAsBoolean(GUI_IS_HTTPS);
String method = sampler.getPropertyAsString(GUI_METHOD);
String url = sampler.getPropertyAsString(GUI_URL);
String headerData = sampler.getPropertyAsString(GUI_HEADER_DATA);
String bodyData = sampler.getPropertyAsString(GUI_BODY_DATA);
String variables = sampler.getPropertyAsString(GUI_VARIABLES);
String jsonPath = sampler.getPropertyAsString(GUI_JSON_PATHS);
String defaultValues = sampler.getPropertyAsString(GUI_DEFAULT_VALUES);
String manualDoc = sampler.getPropertyAsString(GUI_MANUAL_DOC);
if (StringUtils.isEmpty(sampleLabel)) {
throw new PocException("sampleLabel is empty!");
}
if (StringUtils.isEmpty(samplerType)) {
throw new PocException("samplerType is empty!");
}
setting = SamplerSetting.builder()
.samplerType(samplerType)
.sampleLabel(sampleLabel)
.assertType(assertType)
.isLogRequest(isLogRequest)
.isLogResponse(isLogResponse)
.sampleVar(sampleVar)
.isHttps(isHttps)
.method(method)
.url(url)
.headerData(headerData)
.bodyData(bodyData)
.variables(variables)
.jsonPath(jsonPath)
.defaultValues(defaultValues)
.manualDoc(manualDoc)
.build();
log.info("Current SamplerSetting is: {}", setting);
}
/**
* setSampler
*
* @param setting
* @throws PocException
* @throws InvocationTargetException
* @throws NoSuchMethodException
* @throws InstantiationException
* @throws IllegalAccessException
*/
private static synchronized void setSampler(SamplerSetting setting) throws PocException,
InvocationTargetException,
NoSuchMethodException,
InstantiationException,
IllegalAccessException {
sampler = AppSamplerBuilder.getSampler(setting);
}
}
2)AppSamplerGui.java
public class AppSamplerGui extends AbstractSamplerGui {
private JTextField sampleLabel;
private JLabeledChoice samplerType;
private JLabeledChoice asserterType;
private JCheckBox isLogRequest;
private JCheckBox isLogResponse;
private JCheckBox isHttps;
private JLabeledChoice method;
private JTextField url;
private JSyntaxTextArea sampleVar = JSyntaxTextArea.getInstance(5, 80);
JTextScrollPane sampleVarScrollPane = JTextScrollPane.getInstance(this.sampleVar);
private JSyntaxTextArea headerData = JSyntaxTextArea.getInstance(5, 80);
JTextScrollPane headerDataScrollPane = JTextScrollPane.getInstance(this.headerData);
private JSyntaxTextArea bodyData = JSyntaxTextArea.getInstance(10, 80);
JTextScrollPane bodyDataScrollPane = JTextScrollPane.getInstance(this.bodyData);
private JSyntaxTextArea manualDoc = JSyntaxTextArea.getInstance(38, 80);
JTextScrollPane manualDocScrollPane = JTextScrollPane.getInstance(this.manualDoc);
private JTextField variables;
private JTextField jsonPath;
private JTextField defaultValues;
public AppSamplerGui() {
super();
JPanel settingPanel = new VerticalPanel(10, 10);
settingPanel.add(this.createBaseSetting());
settingPanel.add(this.createRequestSetting());
settingPanel.add(this.createResponseJsonExtractorSetting());
settingPanel.add(this.createManualDoc());
JPanel dataPanel = new JPanel(new BorderLayout(10, 10));
dataPanel.add(settingPanel, BorderLayout.NORTH);
this.setLayout(new BorderLayout(10, 10));
this.setBorder(this.makeBorder());
this.add(this.makeTitlePanel(), BorderLayout.NORTH);
this.add(dataPanel, BorderLayout.CENTER);
}
@Override
public String getLabelResource() {
throw new IllegalStateException("This shouldn't be called");
}
@Override
public TestElement createTestElement() {
AppSampler sampler = new AppSampler();
this.modifyTestElement(sampler);
return sampler;
}
@Override
public void modifyTestElement(TestElement testElement) {
testElement.clear();
this.configureTestElement(testElement);
testElement.setProperty(GUI_SAMPLE_LABEL, this.sampleLabel.getText());
testElement.setProperty(new BooleanProperty(GUI_IS_HTTPS, this.isHttps.isSelected()));
testElement.setProperty(GUI_SAMPLER_TYPE, this.samplerType.getText());
testElement.setProperty(GUI_ASSERT_TYPE, this.asserterType.getText());
testElement.setProperty(new BooleanProperty(GUI_SAMPLE_IS_LOG_REQUEST, this.isLogRequest.isSelected()));
testElement.setProperty(new BooleanProperty(GUI_SAMPLE_IS_LOG_RESPONSE, this.isLogResponse.isSelected()));
testElement.setProperty(GUI_SAMPLE_VAR, this.sampleVar.getText());
testElement.setProperty(GUI_METHOD, this.method.getText());
testElement.setProperty(GUI_URL, this.url.getText());
testElement.setProperty(GUI_HEADER_DATA, this.headerData.getText());
testElement.setProperty(GUI_BODY_DATA, this.bodyData.getText());
testElement.setProperty(GUI_VARIABLES, this.variables.getText());
testElement.setProperty(GUI_JSON_PATHS, this.jsonPath.getText());
testElement.setProperty(GUI_DEFAULT_VALUES, this.defaultValues.getText());
}
@Override
public String getStaticLabel() {
return GUI_SAMPLER_NAME;
}
@Override
public void clearGui() {
super.clearGui();
this.manualDoc.setText(AppSamplerManual.getDocument());
}
@Override
public void configure(TestElement element) {
super.configure(element);
this.sampleLabel.setText(element.getPropertyAsString(GUI_SAMPLE_LABEL));
this.isHttps.setSelected(element.getPropertyAsBoolean(GUI_IS_HTTPS));
this.samplerType.setText(element.getPropertyAsString(GUI_SAMPLER_TYPE));
this.asserterType.setText(element.getPropertyAsString(GUI_ASSERT_TYPE));
this.isLogRequest.setSelected(element.getPropertyAsBoolean(GUI_SAMPLE_IS_LOG_REQUEST));
this.isLogResponse.setSelected(element.getPropertyAsBoolean(GUI_SAMPLE_IS_LOG_RESPONSE));
this.sampleVar.setText(element.getPropertyAsString(GUI_SAMPLE_VAR));
this.method.setText(element.getPropertyAsString(GUI_METHOD));
this.url.setText(element.getPropertyAsString(GUI_URL));
this.headerData.setText(element.getPropertyAsString(GUI_HEADER_DATA));
this.bodyData.setText(element.getPropertyAsString(GUI_BODY_DATA));
this.variables.setText(element.getPropertyAsString(GUI_VARIABLES));
this.jsonPath.setText(element.getPropertyAsString(GUI_JSON_PATHS));
this.defaultValues.setText(element.getPropertyAsString(GUI_DEFAULT_VALUES));
}
protected Component createBaseSetting() {
this.sampleLabel = new JTextField(20);
JLabel label1 = new JLabel("SampleLabel: ");
label1.setLabelFor(this.sampleLabel);
this.samplerType = new JLabeledChoice("", AppSamplerBuilder.getSamplerTypes(), true, false);
this.samplerType.setPreferredSize(new Dimension(200, 30));
this.asserterType = new JLabeledChoice("", AppSamplerAsserter.getAsserters(), false, false);
this.asserterType.setPreferredSize(new Dimension(200, 30));
JLabel label2 = new JLabel(" SamplerType: ");
label2.setLabelFor(this.samplerType);
JLabel label3 = new JLabel(" AsserterType: ");
label3.setLabelFor(this.asserterType);
this.isLogRequest = new JCheckBox("IsLogRequest ");
this.isLogRequest.setFont(null);
this.isLogRequest.setSelected(false);
this.isLogResponse = new JCheckBox("IsLogResponse");
this.isLogResponse.setFont(null);
this.isLogResponse.setSelected(false);
JPanel panel1 = new HorizontalPanel();
panel1.setLayout(new FlowLayout(0, 0, 0));
panel1.add(label1, BorderLayout.WEST);
panel1.add(this.sampleLabel, BorderLayout.WEST);
panel1.setLayout(new FlowLayout(0, 0, 0));
panel1.add(label2, BorderLayout.WEST);
panel1.add(this.samplerType, BorderLayout.WEST);
panel1.add(label3, BorderLayout.WEST);
panel1.add(this.asserterType, BorderLayout.WEST);
panel1.add(this.isLogRequest, BorderLayout.WEST);
panel1.add(this.isLogResponse, BorderLayout.WEST);
JPanel panel3 = new HorizontalPanel();
JPanel vertPanel3 = new VerticalPanel();
JPanel httpHeaderDataPanel3 = new JPanel(new BorderLayout());
httpHeaderDataPanel3.add(this.sampleVarScrollPane, BorderLayout.CENTER);
vertPanel3.add(httpHeaderDataPanel3);
vertPanel3.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.gray), "Sample Variable ( Fun Supported )"));
panel3.add(vertPanel3);
return this.getTitledPanel("Global Setting", panel1, panel3);
}
protected Component createRequestSetting() {
this.method = new JLabeledChoice("", AppSamplerUtils.getHttpMethodNames(), false, false);
this.url = new JTextField(83);
JLabel pathLabel = new JLabel("URL: ");
pathLabel.setLabelFor(this.url);
this.isHttps = new JCheckBox("IsHttps");
this.isHttps.setFont(null);
this.isHttps.setSelected(false);
JPanel panel1 = new HorizontalPanel();
panel1.setLayout(new FlowLayout(0, 0, 0));
panel1.add(new JLabel("Method:"));
panel1.add(this.method, BorderLayout.WEST);
panel1.add(pathLabel, BorderLayout.WEST);
panel1.add(this.url, BorderLayout.WEST);
panel1.add(this.isHttps, BorderLayout.WEST);
JPanel panel2 = new HorizontalPanel();
JPanel headerDataContentPanel = new VerticalPanel();
JPanel httpHeaderDataPanel = new JPanel(new BorderLayout());
httpHeaderDataPanel.add(this.headerDataScrollPane, BorderLayout.CENTER);
headerDataContentPanel.add(httpHeaderDataPanel);
headerDataContentPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.gray), "Header Data"));
panel2.add(headerDataContentPanel);
JPanel panel3 = new HorizontalPanel();
JPanel bodyDataContentPanel = new VerticalPanel();
JPanel bodyDataPanel = new JPanel(new BorderLayout());
bodyDataPanel.add(this.bodyDataScrollPane, BorderLayout.CENTER);
bodyDataContentPanel.add(bodyDataPanel);
bodyDataContentPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.gray), "Body Data"));
panel3.add(bodyDataContentPanel);
return this.getTitledPanel("Request Setting (Fun & Var Supported)", panel1, panel2, panel3);
}
protected Component createResponseJsonExtractorSetting() {
this.variables = new JTextField(90);
JLabel label1 = new JLabel("Name Of Variables: ");
label1.setLabelFor(this.sampleLabel);
label1.setPreferredSize(new Dimension(110, 30));
this.jsonPath = new JTextField(90);
JLabel label2 = new JLabel("JSON Path: ");
label2.setLabelFor(this.jsonPath);
label2.setPreferredSize(new Dimension(110, 30));
this.defaultValues = new JTextField(90);
JLabel label3 = new JLabel("Default Values: ");
label3.setLabelFor(this.defaultValues);
label3.setPreferredSize(new Dimension(110, 30));
JPanel panel1 = new HorizontalPanel();
panel1.setLayout(new FlowLayout(0, 0, 0));
panel1.add(label1, BorderLayout.WEST);
panel1.add(this.variables, BorderLayout.WEST);
JPanel panel2 = new HorizontalPanel();
panel2.setLayout(new FlowLayout(0, 0, 0));
panel2.add(label2, BorderLayout.WEST);
panel2.add(this.jsonPath, BorderLayout.WEST);
panel2.add(new JLabel("(Var Spt)"), BorderLayout.WEST);
JPanel panel3 = new HorizontalPanel();
panel3.setLayout(new FlowLayout(0, 0, 0));
panel3.add(label3, BorderLayout.WEST);
panel3.add(this.defaultValues, BorderLayout.WEST);
JPanel titledPanel = new VerticalPanel();
JPanel borderedPanel = new VerticalPanel();
borderedPanel.add(panel1);
borderedPanel.add(panel2);
borderedPanel.add(panel3);
borderedPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
titledPanel.add(borderedPanel);
titledPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.gray), "Response JSON Extractor Setting"));
return titledPanel;
}
protected Component createManualDoc() {
this.manualDoc.setEditable(false);
JPanel panel = new HorizontalPanel();
JPanel bodyDataContentPanel = new VerticalPanel();
JPanel bodyDataPanel = new JPanel(new BorderLayout());
bodyDataPanel.add(this.manualDocScrollPane, BorderLayout.CENTER);
bodyDataContentPanel.add(bodyDataPanel);
panel.add(bodyDataContentPanel);
return this.getTitledPanel("Manual Document", panel);
}
/**
* getTitledPanel
*
* @param title
* @param panels
* @return
*/
private Component getTitledPanel(String title, JPanel... panels) {
JPanel titledPanel = new VerticalPanel();
JPanel borderedPanel = new VerticalPanel();
for (JPanel panel : panels) {
borderedPanel.add(panel);
}
borderedPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
titledPanel.add(borderedPanel);
titledPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.gray), title));
return titledPanel;
}
}
从上面的UI展示,对于Jmeter相对熟悉的同学肯定可以发现:使用Jmeter自带HTTP采样器也能进行HTTP接口的压力测试,那么为什么要重复造轮子呢?重复造轮子的事情如果没有充足的理由,那么坚决是不应该干的。Jmeter自带HTTP采样器存在以下不足:
下面是一个用于压测验证码登录场景的使用Jmeter自身元件配置出来的测试计划:
结合上面的实例,是不是有上面的说的这些弊端呢?因此我这里闭门造车的初衷为:
1、使用简单,降低Jmeter学习成本(像使用PostMan一样的进行HTTP接口压力测试,再也不需要去学习Jmeter各种插件/元件的使用方式,以及不用去到处找各种非官方插件)。
2、一眼就能看明白整个测试计划(整个测试计划在一个界面中配置完成,再也不用去推敲各种无序Jmeter元件的在某个测试计划中的用途)。
3、自定义函数及采样断言的扩展简单快捷(后续介绍方法)。
上面说的这些太空泛,那么看实际例子吧(前两天压了一下我们系统会议服务主要场景的测试计划):
1、创建会议:
2、加入会议
创建会议时候总采样数是5万,这里使用自定义函数modCsvData保障5万次加入采样只会往5000个会议里加,这样就可以控制每个会议的成员为10人,很多时候这种类似的要求经常碰到:例如:创建N个百人群,或者其他一对多的关系的时候,那么这个时候可以使用这种方法进行精准的控制
3、离开会议
4、结束会议
相信这些测试计划其中意思大家容易看懂,是不是:使用简单、一眼就能看到全貌?
另外,也可以把它当成接口批量调用的工具去使用,例如:
上面的这测试计划的用途是把指定订单的结束时间往后延期30天,指定的订单数据可以直接从库里用sql查出来之后导出为csv。
开发这玩意大部分的时间是处于边做边想的状态(毕竟设计开发共用了9天),以下是当时动手编码之前做的一些简单考虑的记录,大家简单了解下在哪些简单想法下诞生了这个玩意即可。
要求:简单快速扩展新的采样器
实现要求:抽象束行之后结合自定义注解,启动时反射扫描以实现自动装载。新增采样器实现约束即可使用。(兼容一些特殊场景的压测无法使用通用采样器进行配置的情况,例如:真实媒体文件,然后以一定码率向某个会议发送媒体流)。
两种选择:(实现阶段酌情选择,目前倾向开发难度小的函数Map方式)
基本上首选JSON PATH。
在经过一定的本工具压测实战之后,基本可以确定本工具可用且基本定型,那么工具自身执行效率还是需要验证一下的。因为表达式执行如果自身效率不高则会对打压结果造成干扰。设计之初我就有所考虑(3.6章节中执行优化)以下是非严谨验证方法:
测试计划:2个变量、3个函数;
CommonHttpSampler临时改造:将应答由请求接口获取改为直接hardcode为一个正确应答:
sampleResult.sampleStart();
int currentSampleIndex = this.sampleIndex.getAndIncrement();
Map<String, Object> args = this.executor.getVariableMap(currentSampleIndex);
String url = this.executor.applyExpression(this.executor.getUrlParser(), args);
String header = this.executor.applyExpression(this.executor.getHeaderParser(), args);
String body = this.executor.applyExpression(this.executor.getBodyParser(), args);
httpRequest = this.executor.getHttpRequest(url, header, body);
ResponseHandler<String> responseHandler = new BasicResponseHandler();
// sampleResult.sampleStart()
// String responseBody = httpClient.execute(httpRequest, responseHandler);
String responseBody = "{\n" +
" \"code\": 0,\n" +
" \"msg\": \"success\",\n" +
" \"data\": {\n" +
" \"msgExpireTime\": 300\n" +
" }\n" +
"}";
sampleResult.sampleEnd();
测试结果(测试机器:自用笔记本,DELL 灵越 17年老款):平均延时<1ms,秒吞吐量:10W+
从上面的测试结果基本上可以认为:使用本压测工具进行压力测试,工具自身性能损耗对压测结果干扰基本可以忽略。
1、继承BaseFunction
2、增加Fun注解设置函数名称。
3、实现apply及getArgsMap方法。
4、将编译jar拷贝至{jmeterHome}/lib/ext/目录下(可通过使用说明是否自动生成判断新增函数是否自动加载)。
自定义函数示例:
@FunctionExecutor.Fun(name = Const.FUNCTION_APPEND_STRING)
public class AppendString extends BaseFunction {
public AppendString(String funName) {
super(funName);
}
@Override
public Object apply(Object... args) throws PocException {
if (ArrayUtils.isEmpty(args)) {
return "";
}
StringBuilder sb = new StringBuilder();
for (Object arg : args) {
sb.append(arg.toString());
}
return sb.toString();
}
@Override
public Map<String, Integer> getArgsMap() {
return Collections.emptyMap();
}
@Override
public String getDocument() {
StringBuilder sb = new StringBuilder();
sb.append(this.funName);
sb.append("(#{str}...)");
return sb.toString();
}
}
目前提供多种函数,一般情况下的压力测试基本够用了,这里列出部分:
appendString(#{str}…):字符串拼接
circleCsvData(#{csvFileName}, #{varName}, #{sampleIndex}):顺序转圈获取CSV数据;
getCsvData(#{csvFileName}, #{varName}, #{sampleIndex}):顺序获取CSV数据(如果数据不够抛出异常);
getDeptName(#{sampleIndex}):获取部门中文名称(造相对真实好看的数据用);
getGroupName(#{sampleIndex}):获取群中文名称(造相对真实好看的数据用);
getMobile(#{mobileSegment}, #{sampleIndex}):自增获取手机号;
getDate(#{date}, #{pattern}, #{field}, #{amount}):获取服务指定格式时间字符串(支持按amount进行时间加减);
getTime(#{amount}):获取服务器时间戳(支持按amount进行时间加减);
mathAdd(#{x}, #{y}):加法运算;
mathDivide(#{x}, #{y}):除法运算;
mathMultiply(#{x}, #{y}):乘法运算;
mathSubtract(#{x}, #{y}):减法运算;
modCsvData(#{csvFileName}, #{varName}, #{sampleIndex}, #{mod}):sampleIndex取模获取CSV数据(例如:构建100人群时使用)
randomChineseName(#{firstNameLength}):获取随机中文姓名(造相对真实好看的数据用);
randomCsvData(#{csvFileName}, #{varName}):随机获取CSV数据;
randomMobile(#{mobileSegment}, #{start}, #{end}):随机获取手机号;
leftString(#{content}, #{length}):类似Excel中的Left函数
rightString(#{content}, #{length}):类似Excel中的Right函数
大家可以阅读:cn.bossfriday.jmeter.fuction.impl下源码了解详细实现,这些函数的用途相信从函数名称就跟知道个大概。
方法和自定函数类似,示例:
@AppSamplerAsserter.Asserter(name = Const.ASSERTER_CODE_MSG_ASSERT)
public class CodeMsgAsserter implements ISamplerAsserter {
@Override
public Combo3<Boolean, String, String> isSuccess(String response) {
if (StringUtils.isEmpty(response)) {
return new Combo3<>(false, "501", "response is empty");
}
ApiResponse<Void> apiResponse = JSON.parseObject(response, new TypeReference<ApiResponse<Void>>() {
});
return new Combo3<>(apiResponse.isSuccess(), String.valueOf(apiResponse.getCode()), apiResponse.getMsg());
}
}
备注:
ApiResponse定义:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private int code;
private String msg;
@JSONField(name = "data")
private T data;
/**
* isSuccess
*
* @return
*/
public boolean isSuccess() {
return CODE_SUCCESS == this.code;
}
}
为了使用方便,在采样器UI的最下面会将自动生成的帮助信息给列出来(毕竟没有功夫折腾UI下的脚本提示,对于前端好多年没有搞过了,现在含有提示功能的编辑器控件应该很多了),这样可以方便的去复制粘贴了:
当前发布版本的内容如下:
1. Sample Variable Example:
lang: en
mobile: getMobile(138)
2. URL Example:
localhost:9033/baseCheck
localhost:9033/baseCheck?id=#{id}
localhost:9033/baseCheck?mobile=getMobile(138)
3. Header Example:
content-type: application/json
uid: #{uid}
mobile: getMobile(138)
4. Body Example:
mobile=13810494632®ion=86&vCode=966966
mobile=#{mobile}®ion=86&deviceCode=1
mobile=getMobile(138)®ion=86&deviceCode=1
5. Name Of Variable / Default Values Example:
uid;appToken;imToken
6. JSON Path Example:
$.data.uid;$.data.appToken;$.data.imToken
#{mobile};$.data.uid;$.data.appToken;$.data.imToken
getMobile(138);$.data.uid;$.data.appToken;$.data.imToken
7. Implemented Functions:
appendString(#{str}...)
circleCsvData(#{csvFileName}, #{varName})
getCsvData(#{csvFileName}, #{varName})
getDate(#{date}, #{pattern}, #{field}, #{amount}) // field:Calendar.XXField; 1:Year; 2:Mon; 5:Day; 10:Hour; 12:Min; 13:Sec;
getDeptName()
getGroupName()
getMobile(#{mobileSegment})
getTime(#{amount})
leftString(#{content}, #{length})
mathAdd(#{x}, #{y})
mathDivide(#{x}, #{y})
mathMultiply(#{x}, #{y})
mathSubtract(#{x}, #{y})
modCsvData(#{csvFileName}, #{varName}, #{mod})
offsetLimitCsvData(#{csvFileName}, #{varName}, #{offset}, #{limit})
randomChineseName(#{firstNameLength})
randomCsvData(#{csvFileName}, #{varName})
randomMobile(#{mobileSegment}, #{start}, #{end})
rightString(#{content}, #{length})
对应JMeter采样标签,大家可以理解为给你的用例取一个可读的名字:
目前只提供了一个通用HTTP采样器,如果碰到非HTTP接口压测,或者其他需要,大家可以自己去扩展,方法很简单:继承BaseSampler去实现即可,大家可以参考CommonHttpSampler的实现。采样器自动装载方式如下:
public class AppSamplerBuilder {
@Documented
@Target({ElementType.TYPE})
@Retention(RUNTIME)
public @interface SamplerType {
/**
* 采样器类型名称
*
* @return
*/
String name() default "";
/**
* ignore
*
* @return
*/
boolean ignore() default false;
}
private static final Logger log = LogManager.getLogger(AppSamplerBuilder.class);
private static ConcurrentHashMap<String, Class<?>> clazzMap = new ConcurrentHashMap<>();
private static String[] samplerTypes;
static {
// init clazzMap
Set<Class<? extends BaseSampler>> classes = new Reflections().getSubTypesOf(BaseSampler.class);
for (Class<?> clazz : classes) {
SamplerType samplerType = clazz.getAnnotation(SamplerType.class);
if (samplerType.ignore()) {
continue;
}
if (!clazzMap.containsKey(samplerType.name())) {
clazzMap.put(samplerType.name(), clazz);
log.info("load sampler done: {}", samplerType.name());
} else {
log.warn("duplicated sampler: {}", samplerType.name());
}
}
// init samplerTypeNames
samplerTypes = new String[clazzMap.size()];
int x = 0;
for (String key : clazzMap.keySet()) {
samplerTypes[x] = key;
x++;
}
// sort
int size = samplerTypes.length;
for (int i = 0; i < size - 1; i++) {
for (int j = i + 1; j < samplerTypes.length; j++) {
if (samplerTypes[i].compareTo(samplerTypes[j]) > 0) {
String temp = samplerTypes[i];
samplerTypes[i] = samplerTypes[j];
samplerTypes[j] = temp;
}
}
}
}
/**
* getSampler
*/
public static BaseSampler getSampler(SamplerSetting config)
throws PocException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
String samplerTypeName = config.getSamplerType();
if (!clazzMap.containsKey(config.getSamplerType())) {
throw new PocException("invalid samplerTypeName!(" + samplerTypeName + ")");
}
return (BaseSampler) clazzMap.get(samplerTypeName).getConstructor(SamplerSetting.class).newInstance(config);
}
/**
* getSamplerTypes
*
* @return
*/
public static String[] getSamplerTypes() {
return samplerTypes;
}
}
目前就内置了一个断言器,通过阅读:5.2 采样器断言扩展 大家应该就能明白其前后。不过现在很多系统的应答都是基于以下形式,不同的是可能各家的成功码有所差异,那么大家实际使用的时候改下断言逻辑或者扩展自己的断言器都是可以的。
{
"code": 0,
"msg": "success",
"data": {
xxxxxxx
}
}
jmeter.log是否记录请求,目的是辅助问题排查。
jmeter.log是否记录应答,目的是辅助问题排查。日志处理代码如下(通过阅读下面代码,你会发现如果采样失败,不管你是否配置了IsLogRequest,IsLogResponse,都会日志输出请求及应答的):
protected String getSampleLog(int sampleIndex, SampleResult result, String url, String header, String body, String response) {
StringBuilder sb = new StringBuilder();
if (result.isSuccessful()) {
sb.append("sample success, sampleIndex=" + sampleIndex + ", time=" + result.getTime());
} else {
sb.append("sample failed, sampleIndex=" + sampleIndex + ", time=" + result.getTime());
}
if (this.setting.isLogRequest() || !result.isSuccessful()) {
if (!StringUtils.isEmpty(url)) {
sb.append(", url=" + url);
}
if (!StringUtils.isEmpty(header)) {
sb.append(", header=" + header.replace('\n', ' '));
}
if (!StringUtils.isEmpty(body)) {
sb.append(", body=" + body.replace('\n', ' '));
}
}
boolean isLogResponse = (this.setting.isLogResponse() || !result.isSuccessful()) && !StringUtils.isEmpty(response);
if (isLogResponse) {
sb.append(", response=" + response.replace('\n', ' '));
}
return sb.toString();
}
从帮助示例可以看到,采样变量支持常量和函数:
lang: en
mobile: getMobile(138, #{sampleIndex})
从帮助示例可以看到,URL支持常量、变量、函数:
localhost:9033/baseCheck
localhost:9033/baseCheck?id=#{id}
localhost:9033/baseCheck?mobile=getMobile(138, #{sampleIndex})
从帮助示例可以看到,Header支持常量、变量、函数:
content-type: application/json
uid: #{sampleIndex}
mobile: getMobile(138, #{sampleIndex})
从帮助示例可以看到,Body支持常量、变量、函数:
mobile=13810494632®ion=86&vCode=966966
mobile=#{mobile}®ion=86&deviceCode=1
mobile=getMobile(138, #{sampleIndex})®ion=86&deviceCode=1
目前就一个#{sampleIndex},直接使用即可,无需显式声明;
从帮助示例可以看到,应答提取器变量名和默认值都是用分号切分:
uid;appToken;imToken
从帮助示例可以看到,应答提取器JsonPath为标准JsonPath写法和采样变量及函数支持:
$.data.uid;$.data.appToken;$.data.imToken
#{mobile};$.data.uid;$.data.appToken;$.data.imToken
getMobile(138, #{sampleIndex});$.data.uid;$.data.appToken;$.data.imToken
由于对于采样变量的处理是用的无序Map处理(后续有空改为有序Map解决该问题),因此下面的用法不支持(会出错) :
--采样变量
now: getTs()
end: mathAdd(#{now}, 3700000)
--Header
192.168.100.62:9030/calendar/v1/event/#{now}/#{end}
解决方案:不声明变量
--采样变量
now: getTs()
--Header
192.168.100.62:9030/calendar/v1/event/#{now}/mathAdd(#{now}, 3700000)
由于工具开发时间较短(设计,开发时间一共不到10天),脚本处理部分极度简化(300行代码左右)。后续有时间再考虑替换为比较健壮的脚本引擎方式(考虑:分词,语法,四元组,执行器方式的脚本处理引擎),可以参考:遵循编译原理主要过程实现“打印1+1结果”:https://blog.csdn.net/camelials/article/details/123415475
下面的用法目前不支持(函数及参数嵌套):
--采样变量
end: mathAdd(getTs(), 3700000)
--Header
192.168.100.62:9030/calendar/v1/event/getTs()/#{end}
1、源码地址:https://github.com/bossfriday/bossfriday-jmeter
2、源码经过了sonar和阿里规约的扫描,大家阅读起来应该还好。想吐槽的是标准的doubleCheck单例写法sonar扫描不过,因此我也就不改成其他的实现方式了,算是对它的抗争:
3、由于初版的开发时间较短,这玩意前后搞了9天(包括设计、开发、自测试),因此脚本表达式部分只能先极度的简化处理(300行代码吧),之前搞过一个低代码平台,里面的表达式处理那是十分强大,不过要适配至Jmeter里还是需要静下心来花费一定的时间,这方面大家有兴趣可以看下我低代码专栏里的相关文章。后续可能考虑替换表达引擎,不过现在看来,这非常精简的表达式处理也好像够用了,基本上也能达到预期。
短时间闭门造车出来的东西,各方面难免不周,欢迎指正/交流。
原创不易,写文档不易,国庆原创写文章不易,动动小手,点赞+关注,大家的支持和关注是我原创和分享的最大动力。
全称 Application Performance Index,是由 Apdex 联盟开放的用于评估应用性能的工业标准。Apdex 联盟起源于 2004 年,由 PeterSevcik发起。Apdex 标准从用户的角度出发,将对应用响应时间的表现,转为用户对于应用性能的可量化为范围为 0-1 的满意度评价。
T:满意阈值,小于或等于该值,表示满意。
F:失败阈值,大于或等于该值,表示不满意。
吞吐量是指系统在单位时间内处理请求的数量。对于无并发的应用系统而言,吞吐量与响应时间成严格的反比关系,实际上此时吞吐量就是响应时间的倒数。前面已经说过,对于单用户的系统,响应时间(或者系统响应时间和应用延迟时间)可以很好地度量系统的性能,但对于并发系统,通常需要用吞吐量作为性能指标。
并发用户数是指系统可以同时承载的正常使用系统功能的用户的数量。与吞吐量相比,并发用户数是一个更直观但也更笼统的性能指标。现实中很难模拟出在同一时刻(同一毫秒)发出大量的请求(线程越多CPU线程切换损失也越大)。
最佳打压线程数,任何系统的任何接口一定存在一个吞吐量上限,也可以称之为冒烟点。一般情况下任何系统的任何接口打压线程与吞吐量关系为:
所有Request的成功比例,OK表示成功,KO表示不成功。
数据分析。Samples:采样数量。KO:失败数量。Error:失败率。Average:平均耗时。Min:最小耗时。Max:最长耗时。90th/95th/99thpct:90%、95%、99%的线程耗时。Throughput:每秒钟发送的请求数量。
随时间变化,每个时间节点上的最长/最短/90%/95%/99%的线程响应时间
响应时间与百分比的对应关系,即有百分之多少的线程花费了某一响应时间。