Fork me on GitHub

Bug Fix Version V1.1.0, Dubbo Plugin for Apache JMeter

首先先感谢网友 @流浪的云 提的bug,让我感觉到写这个工具没有白费还有点价值,非常感谢,

他在使用jmeter-plugin-dubbo插件时发现GUI中输入的信息无法使用Jmeter变量${var}与函数来进行参数化,以下是我修复这个问题的记录。

项目地址

jmeter-plugin-dubbo项目已经transfer到dubbo group下

github: jmeter-plugin-dubbo

码云: jmeter-plugin-dubbo

问题描述

  1. jmeter-plugin-dubbo插件GUI输入的信息无法使用${var}变量来进行参数化

问题修复

Jmeter的输出要想使用用户自定义变量、CSV变量、BeanShell、函数来进行参数化,必须将输入的参数通过JMeterProperty的子类addJmeter管理。如果使用的是SwingBean绑定机制可以很好的支持变量与函数参数化,如果是手写的GUI与Sample就需要注意这一点,可能写出来的插件不能使用变量${var}参数化。

我之前在处理参数值在GUI和Sample之间传递时,没有使用org.apache.jmeter.testelement.property.JMeterProperty系列子类来处理参数,因此变量无法支持,让我们来看一下区别。

先让我们看一下org.apache.jmeter.testelement.property.JMeterProperty都有哪些子类。

我们之前使用的参数赋值是这样的:

public String getVersion() {
    return this.getPropertyAsString(FIELD_DUBBO_VERSION, DEFAULT_VERSION);
}
public void setVersion(String version) {
    this.setProperty(FIELD_DUBBO_VERSION, version);
}

这种方式是无法支持使用${var}变量来参数化赋值的(也就是动态赋值)。

我们应该给setProperty传入JMeterProperty的子类来支持变量参数化,如下:

public String getVersion() {
    return this.getPropertyAsString(FIELD_DUBBO_VERSION, DEFAULT_VERSION);
}
public void setVersion(String version) {
    this.setProperty(new StringProperty(FIELD_DUBBO_VERSION, version));
}

ps.注意setProperty的使用不一样,这里使用的是new StringProperty

上面的参数还相对简单的普通字符串参数,当我们遇到集合或更加复杂的参数类型时如何处理?

我本以为使用JMeterProperty的子类CollectionProperty是可以让集合参数支持变量参数化的,结果测试下来没有任何用,传入的${var}变量,在运行的时候还是变量没有变成相应的值。

于是又换成MapPropertyObjectProperty一样无法支持变量参数化。

查看Jmeter PluginsHttp Sample源码,看他是如何处理的。

org.apache.jmeter.protocol.http.util.HTTPArgument源码

package org.apache.jmeter.protocol.http.util;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.LinkedList;
import java.util.List;
import org.apache.jmeter.config.Argument;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.testelement.property.BooleanProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;

public class HTTPArgument
  extends Argument
  implements Serializable
{
  private static final Logger log = ;
  private static final long serialVersionUID = 240L;
  private static final String ALWAYS_ENCODE = "HTTPArgument.always_encode";
  private static final String USE_EQUALS = "HTTPArgument.use_equals";
  private static final EncoderCache cache = new EncoderCache(1000);
  
  public HTTPArgument(String name, String value, String metadata)
  {
    this(name, value, false);
    setMetaData(metadata);
  }
  
  public void setUseEquals(boolean ue)
  {
    if (ue) {
      setMetaData("=");
    } else {
      setMetaData("");
    }
    setProperty(new BooleanProperty("HTTPArgument.use_equals", ue));
  }
  
  public boolean isUseEquals()
  {
    boolean eq = getPropertyAsBoolean("HTTPArgument.use_equals");
    if ((getMetaData().equals("=")) || ((getValue() != null) && (getValue().length() > 0)))
    {
      setUseEquals(true);
      return true;
    }
    return eq;
  }
  
  public void setAlwaysEncoded(boolean ae)
  {
    setProperty(new BooleanProperty("HTTPArgument.always_encode", ae));
  }
  
  public boolean isAlwaysEncoded()
  {
    return getPropertyAsBoolean("HTTPArgument.always_encode");
  }
  
  public HTTPArgument(String name, String value)
  {
    this(name, value, false);
  }
  
  public HTTPArgument(String name, String value, boolean alreadyEncoded)
  {
    this(name, value, alreadyEncoded, "UTF-8");
  }
  
  public HTTPArgument(String name, String value, boolean alreadyEncoded, String contentEncoding)
  {
    setAlwaysEncoded(true);
    if (alreadyEncoded) {
      try
      {
        if (log.isDebugEnabled()) {
          log.debug("Decoding name, calling URLDecoder.decode with '" + name + "' and contentEncoding:" + "UTF-8");
        }
        name = URLDecoder.decode(name, "UTF-8");
        if (log.isDebugEnabled()) {
          log.debug("Decoding value, calling URLDecoder.decode with '" + value + "' and contentEncoding:" + contentEncoding);
        }
        value = URLDecoder.decode(value, contentEncoding);
      }
      catch (UnsupportedEncodingException e)
      {
        log.error(contentEncoding + " encoding not supported!");
        throw new Error(e.toString(), e);
      }
    }
    setName(name);
    setValue(value);
    setMetaData("=");
  }
  
  public HTTPArgument(String name, String value, String metaData, boolean alreadyEncoded)
  {
    this(name, value, metaData, alreadyEncoded, "UTF-8");
  }
  
  public HTTPArgument(String name, String value, String metaData, boolean alreadyEncoded, String contentEncoding)
  {
    this(name, value, alreadyEncoded, contentEncoding);
    setMetaData(metaData);
  }
  
  public HTTPArgument(Argument arg)
  {
    this(arg.getName(), arg.getValue(), arg.getMetaData());
  }
  
  public HTTPArgument() {}
  
  public void setName(String newName)
  {
    if ((newName == null) || (!newName.equals(getName()))) {
      super.setName(newName);
    }
  }
  
  public String getEncodedValue()
  {
    try
    {
      return getEncodedValue("UTF-8");
    }
    catch (UnsupportedEncodingException e)
    {
      throw new Error("Should not happen: " + e.toString());
    }
  }
  
  public String getEncodedValue(String contentEncoding)
    throws UnsupportedEncodingException
  {
    if (isAlwaysEncoded()) {
      return cache.getEncoded(getValue(), contentEncoding);
    }
    return getValue();
  }
  
  public String getEncodedName()
  {
    if (isAlwaysEncoded()) {
      return cache.getEncoded(getName());
    }
    return getName();
  }
  
  public static void convertArgumentsToHTTP(Arguments args)
  {
    List<Argument> newArguments = new LinkedList();
    for (JMeterProperty jMeterProperty : args.getArguments())
    {
      Argument arg = (Argument)jMeterProperty.getObjectValue();
      if (!(arg instanceof HTTPArgument)) {
        newArguments.add(new HTTPArgument(arg));
      } else {
        newArguments.add(arg);
      }
    }
    args.removeAllArguments();
    args.setArguments(newArguments);
  }
}

org.apache.jmeter.protocol.http.gui.HTTPArgumentsPanel源码

package org.apache.jmeter.protocol.http.gui;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Iterator;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JTable;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.config.Argument;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.config.gui.ArgumentsPanel;
import org.apache.jmeter.protocol.http.util.HTTPArgument;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.gui.GuiUtils;
import org.apache.jorphan.gui.ObjectTableModel;
import org.apache.jorphan.reflect.Functor;

public class HTTPArgumentsPanel
  extends ArgumentsPanel
{
  private static final long serialVersionUID = 240L;
  private static final String ENCODE_OR_NOT = "encode?";
  private static final String INCLUDE_EQUALS = "include_equals";
  
  protected void initializeTableModel()
  {
    this.tableModel = new ObjectTableModel(new String[] { "name", "value", "encode?", "include_equals" }, HTTPArgument.class, new Functor[] { new Functor("getName"), new Functor("getValue"), new Functor("isAlwaysEncoded"), new Functor("isUseEquals") }, new Functor[] { new Functor("setName"), new Functor("setValue"), new Functor("setAlwaysEncoded"), new Functor("setUseEquals") }, new Class[] { String.class, String.class, Boolean.class, Boolean.class });
  }
  
  public static boolean testFunctors()
  {
    HTTPArgumentsPanel instance = new HTTPArgumentsPanel();
    instance.initializeTableModel();
    return instance.tableModel.checkFunctors(null, instance.getClass());
  }
  
  protected void sizeColumns(JTable table)
  {
    GuiUtils.fixSize(table.getColumn("include_equals"), table);
    GuiUtils.fixSize(table.getColumn("encode?"), table);
  }
  
  protected HTTPArgument makeNewArgument()
  {
    HTTPArgument arg = new HTTPArgument("", "");
    arg.setAlwaysEncoded(false);
    arg.setUseEquals(true);
    return arg;
  }
  
  public HTTPArgumentsPanel()
  {
    super(JMeterUtils.getResString("paramtable"));
    init();
  }
  
  public TestElement createTestElement()
  {
    Arguments args = getUnclonedParameters();
    super.configureTestElement(args);
    return (TestElement)args.clone();
  }
  
  public Arguments getParameters()
  {
    Arguments args = getUnclonedParameters();
    return (Arguments)args.clone();
  }
  
  private Arguments getUnclonedParameters()
  {
    stopTableEditing();
    
    Iterator<HTTPArgument> modelData = this.tableModel.iterator();
    Arguments args = new Arguments();
    while (modelData.hasNext())
    {
      HTTPArgument arg = (HTTPArgument)modelData.next();
      args.addArgument(arg);
    }
    return args;
  }
  
  public void configure(TestElement el)
  {
    super.configure(el);
    if ((el instanceof Arguments))
    {
      this.tableModel.clearData();
      HTTPArgument.convertArgumentsToHTTP((Arguments)el);
      for (JMeterProperty jMeterProperty : ((Arguments)el).getArguments())
      {
        HTTPArgument arg = (HTTPArgument)jMeterProperty.getObjectValue();
        this.tableModel.addRow(arg);
      }
    }
    checkButtonsStatus();
  }
  
  protected boolean isMetaDataNormal(HTTPArgument arg)
  {
    return (arg.getMetaData() == null) || (arg.getMetaData().equals("=")) || ((arg.getValue() != null) && (arg.getValue().length() > 0));
  }
  
  protected Argument createArgumentFromClipboard(String[] clipboardCols)
  {
    HTTPArgument argument = makeNewArgument();
    argument.setName(clipboardCols[0]);
    if (clipboardCols.length > 1)
    {
      argument.setValue(clipboardCols[1]);
      if (clipboardCols.length > 2)
      {
        argument.setAlwaysEncoded(Boolean.parseBoolean(clipboardCols[2]));
        if (clipboardCols.length > 3)
        {
          Boolean useEqual = BooleanUtils.toBooleanObject(clipboardCols[3]);
          
          argument.setUseEquals(useEqual != null ? useEqual.booleanValue() : true);
        }
      }
    }
    return argument;
  }
  
  private void init()
  {
    JTable table = getTable();
    JPopupMenu popupMenu = new JPopupMenu();
    JMenuItem variabilizeItem = new JMenuItem(JMeterUtils.getResString("transform_into_variable"));
    variabilizeItem.addActionListener(new ActionListener()
    {
      public void actionPerformed(ActionEvent e)
      {
        HTTPArgumentsPanel.this.transformNameIntoVariable();
      }
    });
    popupMenu.add(variabilizeItem);
    table.setComponentPopupMenu(popupMenu);
  }
  
  private void transformNameIntoVariable()
  {
    int[] rowsSelected = getTable().getSelectedRows();
    for (int selectedRow : rowsSelected)
    {
      String name = (String)this.tableModel.getValueAt(selectedRow, 0);
      if (StringUtils.isNotBlank(name))
      {
        name = name.trim();
        name = name.replaceAll("\\$", "_");
        name = name.replaceAll("\\{", "_");
        name = name.replaceAll("\\}", "_");
        this.tableModel.setValueAt("${" + name + "}", selectedRow, 1);
      }
    }
  }
}

能发现它使用的是继承Argument来处理和GUI之间的参数传递,使用继承ArgumentsPanel来处理GUI页面,这个就是我们上面说的,通过SwingBean绑定机制来进行开发,很遗憾我们没有使用这种方式,如果要改成这种方式,整个代码结构都要修改,成本太大。

但是我们发现像StringInteger等这种普通类型的参数通过使用JMeterProperty的子类可以很好的支持变量参数化,那我们能不能将集合参数拉平来直接使用普通类型的参数来处理,我承认这种方式有点恶心。

解决方式

首先我们的集合参数有索引下标和总行数,每一行有两列,那就修改集合参数的赋值,代码如下:

//标记集合参数前缀
public static String FIELD_DUBBO_METHOD_ARGS = "FIELD_DUBBO_METHOD_ARGS";
//集合参数总数
public static String FIELD_DUBBO_METHOD_ARGS_SIZE = "FIELD_DUBBO_METHOD_ARGS_SIZE";

public List<MethodArgument> getMethodArgs() {
	int paramsSize = this.getPropertyAsInt(FIELD_DUBBO_METHOD_ARGS_SIZE, 0);
	List<MethodArgument> list = new ArrayList<MethodArgument>();
	for (int i = 1; i <= paramsSize; i++) {
		String paramType = this.getPropertyAsString(FIELD_DUBBO_METHOD_ARGS + "_PARAM_TYPE" + i);
		String paramValue = this.getPropertyAsString(FIELD_DUBBO_METHOD_ARGS + "_PARAM_VALUE" + i);
		MethodArgument args = new MethodArgument(paramType, paramValue);
		list.add(args);
	}
	return list;
}
public void setMethodArgs(List<MethodArgument> methodArgs) {
	int size = methodArgs == null ? 0 : methodArgs.size();
	this.setProperty(new IntegerProperty(FIELD_DUBBO_METHOD_ARGS_SIZE, size));
	if (size > 0) {
		for (int i = 1; i <= methodArgs.size(); i++) {
			this.setProperty(new StringProperty(FIELD_DUBBO_METHOD_ARGS + "_PARAM_TYPE" + i, methodArgs.get(i-1).getParamType()));
			this.setProperty(new StringProperty(FIELD_DUBBO_METHOD_ARGS + "_PARAM_VALUE" + i, methodArgs.get(i-1).getParamValue()));
		}
	}
}

上面的代码就是将集合参数拉平来进行传递,大致的结果如下:

FIELD_DUBBO_METHOD_ARGS_SIZE = 2
FIELD_DUBBO_METHOD_ARGS_SIZE_PARAM_TYPE_1 = xx${var1}xx 
FIELD_DUBBO_METHOD_ARGS_SIZE__PARAM_VALUE_1 = xx${var2}xx 
FIELD_DUBBO_METHOD_ARGS_SIZE_PARAM_TYPE_2 = xx${var3}xx 
FIELD_DUBBO_METHOD_ARGS_SIZE__PARAM_VALUE_2 = xx${var4}xx 

让我们测试一下是否可用。

测试结果GUI上所有的输入框均可以支持Jmeter变量${var}参数化.

我觉得应该还是更加完美的解决办法只不过我没有找到,有空了再细致研究一下Jmeter的插件开发的细节看看能否找到突破口。

再次感谢网友 @流浪的云 提的bug,非常感谢!感谢使用插件的朋友多提rp和bug,让我们来一起完善起来,感谢这个开放的世界,最后还是一句老话:世界和平,Keep Real!

Comments