feat(接口测试): JMeter格式导入支持插件

--story=1015890 --user=陈建星 接口定义-导入(Postman、JMX、HAR、MeterSphere)& 导出(MeterSphere) https://www.tapd.cn/55049933/s/1578307
This commit is contained in:
AgAngle 2024-09-13 15:36:14 +08:00 committed by Craftsman
parent 3f8c1da5e1
commit a8833729e5
19 changed files with 1039 additions and 20 deletions

View File

@ -8,12 +8,35 @@ import org.pf4j.ExtensionPoint;
/** /**
* @author jianxing * @author jianxing
* @createTime 2021-10-30 10:07 * @createTime 2021-10-30 10:07
* MsTestElement 具体实现类转换为 HashTree * HashTree 转换为 MsTestElement
* 接口导入 jmx 格式时解析扩展的 HashTree
* 要支持导入插件需要做以下修改
* 1. 编写 com.thoughtworks.xstream.converters.Converter 的实现类以实现 xstream xml 反序列化解析
* 实现类示例
* public class TCPXStreamConverter extends TestElementConverter {
* public TCPXStreamConverter(Mapper mapper) {
* super(mapper);
* }
* @Override
* public boolean canConvert(Class clazz) {
* return TCPSampler.class.isAssignableFrom(clazz);
* }
* }
*
* 2. 在插件的 resource 下新建 META-INF/services/com.thoughtworks.xstream.converters.Converter 文件
* 并添加实现类的全限定名已便于SPI的服务发现
*
* 3. 在插件的 resource 下新建 jmeter_element_alias.properties 配置文件用于配置元素的别名
* 文件内容示例如果是jmeter官方支持的组件可以省略这步
* TCPSampler=org.apache.jmeter.protocol.tcp.sampler.TCPSampler
* TCPSamplerGui=org.apache.jmeter.protocol.tcp.control.gui.TCPSamplerGui
*
* 4. 编写 MsElementConverter 的实现类 HashTree 转换为 MsTestElement
*/ */
public interface MsElementConverter<T extends TestElement> extends ExtensionPoint { public interface MsElementConverter<T extends TestElement> extends ExtensionPoint {
/** /**
* MsTestElement 具体实现类转换为 HashTree * HashTree 转换为 MsTestElement
*/ */
void toMsElement(AbstractMsTestElement parent, T element, HashTree hashTree); void toMsElement(AbstractMsTestElement parent, T element, HashTree hashTree);
} }

View File

@ -11,7 +11,10 @@ public class MsPluginManager extends DefaultPluginManager {
@Override @Override
protected ExtensionFinder createExtensionFinder() { protected ExtensionFinder createExtensionFinder() {
DefaultExtensionFinder extensionFinder = (DefaultExtensionFinder) super.createExtensionFinder(); DefaultExtensionFinder extensionFinder = (DefaultExtensionFinder) super.createExtensionFinder();
// 添加 jdbc 驱动支持
extensionFinder.add(new JdbcDriverServiceProviderExtensionFinder(this)); extensionFinder.add(new JdbcDriverServiceProviderExtensionFinder(this));
// 添加 SPI 支持
extensionFinder.addServiceProviderExtensionFinder();
return extensionFinder; return extensionFinder;
} }

View File

@ -9,6 +9,7 @@ import io.metersphere.api.dto.definition.ApiTestCaseDTO;
import io.metersphere.api.dto.request.ImportRequest; import io.metersphere.api.dto.request.ImportRequest;
import io.metersphere.api.dto.request.http.MsHTTPElement; import io.metersphere.api.dto.request.http.MsHTTPElement;
import io.metersphere.api.parser.ApiDefinitionImportParser; import io.metersphere.api.parser.ApiDefinitionImportParser;
import io.metersphere.api.parser.jmeter.xstream.MsSaveService;
import io.metersphere.api.parser.ms.MsTestElementParser; import io.metersphere.api.parser.ms.MsTestElementParser;
import io.metersphere.api.utils.ApiDefinitionImportUtils; import io.metersphere.api.utils.ApiDefinitionImportUtils;
import io.metersphere.plugin.api.spi.AbstractMsProtocolTestElement; import io.metersphere.plugin.api.spi.AbstractMsProtocolTestElement;
@ -21,7 +22,6 @@ import io.metersphere.system.service.ApiPluginService;
import io.metersphere.system.uid.IDGenerator; import io.metersphere.system.uid.IDGenerator;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.save.SaveService;
import org.apache.jorphan.collections.HashTree; import org.apache.jorphan.collections.HashTree;
import java.io.InputStream; import java.io.InputStream;
@ -35,7 +35,7 @@ public class JmeterParserApiDefinition implements ApiDefinitionImportParser<ApiI
@Override @Override
public ApiImportFileParseResult parse(InputStream inputSource, ImportRequest request) throws Exception { public ApiImportFileParseResult parse(InputStream inputSource, ImportRequest request) throws Exception {
try { try {
Object scriptWrapper = SaveService.loadElement(inputSource); Object scriptWrapper = MsSaveService.loadElement(inputSource);
HashTree hashTree = this.getHashTree(scriptWrapper); HashTree hashTree = this.getHashTree(scriptWrapper);
MsTestElementParser parser = new MsTestElementParser(); MsTestElementParser parser = new MsTestElementParser();
AbstractMsTestElement msTestElement = parser.parse(hashTree); AbstractMsTestElement msTestElement = parser.parse(hashTree);

View File

@ -63,6 +63,15 @@ public class JmeterElementConverterRegister {
} }
} }
/**
* 注销 MsTestElement 对应的转换器
*
* @param elementConverterClass 转换器的类
*/
public static void unRegister(Class<? extends AbstractJmeterElementConverter<? extends MsTestElement>> elementConverterClass) {
parserMap.remove(elementConverterClass);
}
/** /**
* 获取对应组件的转换器 * 获取对应组件的转换器
* *

View File

@ -0,0 +1,125 @@
package io.metersphere.api.parser.jmeter.xstream;
import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.ConverterLookup;
import com.thoughtworks.xstream.converters.ConverterRegistry;
import com.thoughtworks.xstream.core.Caching;
import com.thoughtworks.xstream.core.util.Cloneables;
import com.thoughtworks.xstream.mapper.Mapper;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author jianxing
* 代码参考 {@link com.thoughtworks.xstream.core.DefaultConverterLookup}
* 增加了 removeConverter 实现动态移除转换器
*/
public class MsJmeterConverterLookup implements ConverterLookup, ConverterRegistry, Caching {
private MsPrioritizedList converters = new MsPrioritizedList();
private transient Map typeToConverterMap;
private Map serializationMap = null;
public MsJmeterConverterLookup() {
this(new HashMap());
}
/**
* Constructs a DefaultConverterLookup with a provided map.
*
* @param map the map to use
* @throws NullPointerException if map is null
* @since 1.4.11
*/
public MsJmeterConverterLookup(Map map) {
typeToConverterMap = map;
typeToConverterMap.clear();
}
/**
* @deprecated As of 1.3, use {@link #MsJmeterConverterLookup()}
*/
public MsJmeterConverterLookup(Mapper mapper) {
this();
}
public Converter lookupConverterForType(Class type) {
Converter cachedConverter = type != null ? (Converter)typeToConverterMap.get(type.getName()) : null;
if (cachedConverter != null) {
return cachedConverter;
}
final Map errors = new LinkedHashMap();
Iterator iterator = converters.iterator();
while (iterator.hasNext()) {
Converter converter = (Converter)iterator.next();
try {
if (converter.canConvert(type)) {
if (type != null) {
typeToConverterMap.put(type.getName(), converter);
}
return converter;
}
} catch (final RuntimeException e) {
errors.put(converter.getClass().getName(), e.getMessage());
} catch (final LinkageError e) {
errors.put(converter.getClass().getName(), e.getMessage());
}
}
final ConversionException exception = new ConversionException(errors.isEmpty()
? "No converter specified"
: "No converter available");
exception.add("type", type != null ? type.getName() : "null");
iterator = errors.entrySet().iterator();
while (iterator.hasNext()) {
final Map.Entry entry = (Map.Entry)iterator.next();
exception.add("converter", entry.getKey().toString());
exception.add("message", entry.getValue().toString());
}
throw exception;
}
public void registerConverter(Converter converter, int priority) {
typeToConverterMap.clear();
converters.add(converter, priority);
}
public void removeConverter(Class<? extends Converter> converterClass) {
flushCache();
Iterator iterator = this.converters.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
if (converterClass.equals(next.getClass())) {
iterator.remove();
}
}
}
public void flushCache() {
typeToConverterMap.clear();
Iterator iterator = converters.iterator();
while (iterator.hasNext()) {
Converter converter = (Converter)iterator.next();
if (converter instanceof Caching) {
((Caching)converter).flushCache();
}
}
}
private Object writeReplace() {
serializationMap = (Map)Cloneables.cloneIfPossible(typeToConverterMap);
serializationMap.clear();
return this;
}
private Object readResolve() {
typeToConverterMap = serializationMap == null ? new HashMap() : serializationMap;
serializationMap = null;
return this;
}
}

View File

@ -0,0 +1,70 @@
package io.metersphere.api.parser.jmeter.xstream;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
/**
* @Author: jianxing
* @CreateTime: 2024-09-13 14:18
* 参考 {@link com.thoughtworks.xstream.core.util.PrioritizedList}
* 主要修改 PrioritizedItemIterator remove 方式支持移除转换器
*/
public class MsPrioritizedList {
private final Set set = new TreeSet();
private int lowestPriority = Integer.MAX_VALUE;
private int lastId = 0;
public void add(Object item, int priority) {
if (this.lowestPriority > priority) {
this.lowestPriority = priority;
}
this.set.add(new MsPrioritizedList.PrioritizedItem(item, priority, ++this.lastId));
}
public Iterator iterator() {
return new MsPrioritizedList.PrioritizedItemIterator(this.set.iterator());
}
private static class PrioritizedItemIterator implements Iterator {
private Iterator iterator;
public PrioritizedItemIterator(Iterator iterator) {
this.iterator = iterator;
}
public void remove() {
this.iterator.remove();
}
public boolean hasNext() {
return this.iterator.hasNext();
}
public Object next() {
return ((MsPrioritizedList.PrioritizedItem)this.iterator.next()).value;
}
}
private static class PrioritizedItem implements Comparable {
final Object value;
final int priority;
final int id;
public PrioritizedItem(Object value, int priority, int id) {
this.value = value;
this.priority = priority;
this.id = id;
}
public int compareTo(Object o) {
MsPrioritizedList.PrioritizedItem other = (MsPrioritizedList.PrioritizedItem)o;
return this.priority != other.priority ? other.priority - this.priority : other.id - this.id;
}
public boolean equals(Object obj) {
return this.id == ((MsPrioritizedList.PrioritizedItem)obj).id;
}
}
}

View File

@ -0,0 +1,600 @@
package io.metersphere.api.parser.jmeter.xstream;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import com.thoughtworks.xstream.converters.*;
import com.thoughtworks.xstream.core.ClassLoaderReference;
import com.thoughtworks.xstream.core.util.CompositeClassLoader;
import com.thoughtworks.xstream.mapper.CachingMapper;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.CommonBeanFactory;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.service.PluginLoadService;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.jmeter.reporters.ResultCollectorHelper;
import org.apache.jmeter.samplers.SampleEvent;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jmeter.util.NameUpdater;
import org.apache.jorphan.collections.HashTree;
import org.apache.jorphan.util.JMeterError;
import org.apache.jorphan.util.JOrphanUtils;
import org.pf4j.PluginWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
import com.thoughtworks.xstream.io.xml.XppDriver;
import com.thoughtworks.xstream.mapper.CannotResolveClassException;
import com.thoughtworks.xstream.mapper.Mapper;
import com.thoughtworks.xstream.mapper.MapperWrapper;
/**
* @Author: jianxing
* 代码参考 {@link org.apache.jmeter.save.SaveService}
* 主要为了接口导入支持动态插件 jmx 文件转换成 HashTree
* 修改点
* 1. 因为 ScriptWrapper 不是 public 所以替换成 MsScriptWrapper
* 2. StreamWrapper 替换为 MsXStreamWrapper使用 MsJmeterConverterLookup实现动态维护 converter
* 3. 增加 registerConverter removeConverter 方法动态维护 converter
* 4. 增加 pluginAliasMap 记录插件中 jmeter 元素的别名
*/
public class MsSaveService {
private static final Logger log = LoggerFactory.getLogger(MsSaveService.class);
// Names of DataHolder entries for JTL processing
public static final String SAMPLE_EVENT_OBJECT = "SampleEvent"; // $NON-NLS-1$
public static final String RESULTCOLLECTOR_HELPER_OBJECT = "ResultCollectorHelper"; // $NON-NLS-1$
// Names of DataHolder entries for JMX processing
public static final String TEST_CLASS_NAME = "TestClassName"; // $NON-NLS-1$
public static final MsJmeterConverterLookup JMETER_CONVERTER_LOOKUP = new MsJmeterConverterLookup();
private static final class MsXStreamWrapper extends XStream {
private MsXStreamWrapper(ReflectionProvider reflectionProvider) {
super(reflectionProvider, new XppDriver(), new ClassLoaderReference(new CompositeClassLoader()), null,
type -> JMETER_CONVERTER_LOOKUP.lookupConverterForType(type),
(converter, priority) -> JMETER_CONVERTER_LOOKUP.registerConverter(converter, priority));
}
// Override wrapMapper in order to insert the Wrapper in the chain
@Override
protected MapperWrapper wrapMapper(MapperWrapper next) {
// Provide our own aliasing using strings rather than classes
return new MapperWrapper(next) {
// Translate alias to classname and then delegate to wrapped class
@Override
public Class<?> realClass(String alias) {
String fullName = aliasToClass(alias);
if (fullName != null) {
fullName = NameUpdater.getCurrentName(fullName);
}
if (fullName == null) {
fullName = pluginAliasMap.get(alias);
}
try {
return super.realClass(fullName == null ? alias : fullName);
} catch (CannotResolveClassException e) {
PluginLoadService pluginLoadService = CommonBeanFactory.getBean(PluginLoadService.class);
LogUtils.info(e.getMessage());
for (PluginWrapper plugin : pluginLoadService.getMsPluginManager().getPlugins()) {
try {
Class<?> aClass = plugin.getPluginClassLoader().loadClass(fullName);
if (aClass != null) {
return aClass;
}
} catch (ClassNotFoundException ex) {
LogUtils.info(e.getMessage());
}
}
}
throw new MSException("无法解析:" + alias);
}
// Translate to alias and then delegate to wrapped class
@Override
public String serializedClass(@SuppressWarnings("rawtypes") // superclass does not use types
Class type) {
if (type == null) {
return super.serializedClass(null); // was type, but that caused FindBugs warning
}
String alias = classToAlias(type.getName());
return alias == null ? super.serializedClass(type) : alias;
}
};
}
}
private static final XStream JMXSAVER = new MsXStreamWrapper(new PureJavaReflectionProvider());
private static final XStream JTLSAVER = new MsXStreamWrapper(new PureJavaReflectionProvider());
static {
JTLSAVER.setMode(XStream.NO_REFERENCES); // This is needed to stop XStream keeping copies of each class
JMeterUtils.setupXStreamSecurityPolicy(JMXSAVER);
JMeterUtils.setupXStreamSecurityPolicy(JTLSAVER);
}
// The XML header, with placeholder for encoding, since that is controlled by property
private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"<ph>\"?>"; // $NON-NLS-1$
// Default file name
private static final String SAVESERVICE_PROPERTIES_FILE = "saveservice.properties"; // $NON-NLS-1$
// Property name used to define file name
private static final String SAVESERVICE_PROPERTIES = "saveservice_properties"; // $NON-NLS-1$
private static final String JMETER_ELEMENT_ALIAS_PROPERTIES = "jmeter_element_alias.properties"; // $NON-NLS-1$
private static final Map<String, String> pluginAliasMap = new HashMap<>();
// Define file format versions
private static final String VERSION_2_2 = "2.2"; // $NON-NLS-1$
// Holds the mappings from the saveservice properties file
// Key: alias Entry: full class name
// There may be multiple aliases which map to the same class
private static final Properties aliasToClass = new Properties();
// Holds the reverse mappings
// Key: full class name Entry: primary alias
private static final Properties classToAlias = new Properties();
// Version information for test plan header
// This is written to JMX files by ScriptWrapperConverter
// Also to JTL files by ResultCollector
private static final String VERSION = "1.2"; // $NON-NLS-1$
// This is written to JMX files by ScriptWrapperConverter
private static String propertiesVersion = "";// read from properties file; written to JMX files
// Must match _version property value in saveservice.properties
// used to ensure saveservice.properties and SaveService are updated simultaneously
static final String PROPVERSION = "5.0";// Expected version $NON-NLS-1$
// Internal information only
private static String fileVersion = ""; // computed from saveservice.properties file// $NON-NLS-1$
private static String fileEncoding = ""; // read from properties file// $NON-NLS-1$
static {
log.info("Testplan (JMX) version: {}. Testlog (JTL) version: {}", VERSION_2_2, VERSION_2_2);
initProps();
checkVersions();
}
public static Converter registerConverter(Class<? extends Converter> converterClass) {
try {
Converter converter = converterClass.getConstructor(Mapper.class).newInstance(getJMXMapper());
JMETER_CONVERTER_LOOKUP.registerConverter(converter, 0);
return converter;
} catch (Exception e) {
LogUtils.error(e);
}
return null;
}
public static void unRegisterConverter(Class<? extends Converter> converterClass) {
JMETER_CONVERTER_LOOKUP.removeConverter(converterClass);
if (getJMXMapper() instanceof CachingMapper cachingMapper) {
// 有插件卸载则清空缓存否则插件重新上传无效
cachingMapper.flushCache();
}
if (getJTLMapper() instanceof CachingMapper cachingMapper) {
// 有插件卸载则清空缓存否则插件重新上传无效
cachingMapper.flushCache();
}
}
public static Mapper getJMXMapper() {
return JMXSAVER.getMapper();
}
public static Mapper getJTLMapper() {
return JTLSAVER.getMapper();
}
// Helper method to simplify alias creation from properties
private static void makeAlias(String aliasList, String clazz) {
String[] aliases = aliasList.split(","); // Can have multiple aliases for same target classname
String alias = aliases[0];
for (String a : aliases) {
Object old = aliasToClass.setProperty(a, clazz);
if (old != null) {
log.error("Duplicate class detected for {}: {} & {}", alias, clazz, old);
}
}
Object oldval = classToAlias.setProperty(clazz, alias);
if (oldval != null) {
log.error("Duplicate alias detected for {}: {} & {}", clazz, alias, oldval);
}
}
private static File getSaveServiceFile() {
String saveServiceProps = JMeterUtils.getPropDefault(SAVESERVICE_PROPERTIES, SAVESERVICE_PROPERTIES_FILE); //$NON-NLS-1$
if (saveServiceProps.length() > 0) { //$NON-NLS-1$
return JMeterUtils.findFile(saveServiceProps);
}
throw new IllegalStateException("Could not find file configured in saveservice_properties property set to:" + saveServiceProps);
}
public static Properties loadProperties() throws IOException {
Properties nameMap = new Properties();
File saveServiceFile = getSaveServiceFile();
if (saveServiceFile.canRead()) {
try (FileInputStream fis = new FileInputStream(saveServiceFile)) {
nameMap.load(fis);
}
}
return nameMap;
}
public static Properties loadPluginAliasProperties(ClassLoader classLoader) throws IOException {
Properties nameMap = new Properties();
InputStream resourceAsStream = classLoader.getResourceAsStream(JMETER_ELEMENT_ALIAS_PROPERTIES);
nameMap.load(resourceAsStream);
nameMap.forEach((k, v) -> pluginAliasMap.put((String) k, (String) v));
return nameMap;
}
private static String checksum(Properties nameMap) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
// This checksums the actual entries, and it ignores comments and blank lines
nameMap.entrySet().stream().sorted(
Comparator.comparing((Map.Entry<Object, Object> e) -> e.getKey().toString())
.thenComparing(e -> e.getValue().toString())
).forEachOrdered(e -> {
md.update(e.getKey().toString().getBytes(StandardCharsets.UTF_8));
md.update(e.getValue().toString().getBytes(StandardCharsets.UTF_8));
});
return JOrphanUtils.baToHexString(md.digest());
}
private static void initProps() {
// Load the alias properties
try {
Properties nameMap = loadProperties();
try {
fileVersion = checksum(nameMap);
} catch (NoSuchAlgorithmException e) {
log.error("Can't compute checksum for saveservice properties file", e);
throw new JMeterError("JMeter requires the checksum of saveservice properties file to continue", e);
}
// now create the aliases
for (Map.Entry<Object, Object> me : nameMap.entrySet()) {
String key = (String) me.getKey();
String val = (String) me.getValue();
if (!key.startsWith("_")) { // $NON-NLS-1$
makeAlias(key, val);
} else {
// process special keys
if (key.equalsIgnoreCase("_version")) { // $NON-NLS-1$
propertiesVersion = val;
log.info("Using SaveService properties version {}", propertiesVersion);
} else if (key.equalsIgnoreCase("_file_version")) { // $NON-NLS-1$
log.info("SaveService properties file version is now computed by a checksum,"
+ "the property _file_version is not used anymore and can be removed.");
} else if (key.equalsIgnoreCase("_file_encoding")) { // $NON-NLS-1$
fileEncoding = val;
log.info("Using SaveService properties file encoding {}", fileEncoding);
} else {
key = key.substring(1);// Remove the leading "_"
registerConverter(key, val);
}
}
}
} catch (IOException e) {
log.error("Bad saveservice properties file", e);
throw new JMeterError("JMeter requires the saveservice properties file to continue");
}
}
private static void registerConverter(String key, String val) {
try {
final String trimmedValue = val.trim();
boolean useMapper = "collection".equals(trimmedValue) || "mapping".equals(trimmedValue); // $NON-NLS-1$ $NON-NLS-2$
registerConverter(key, JMXSAVER, useMapper);
registerConverter(key, JTLSAVER, useMapper);
} catch (IllegalAccessException | InstantiationException | ClassNotFoundException | IllegalArgumentException |
SecurityException | InvocationTargetException | NoSuchMethodException e1) {
log.warn("Can't register a converter: {}", key, e1);
}
}
/**
* Register converter.
*
* @param key
* @param jmxsaver
* @param useMapper
* @throws InstantiationException
* @throws IllegalAccessException
* @throws InvocationTargetException
* @throws NoSuchMethodException
* @throws ClassNotFoundException
*/
private static void registerConverter(String key, XStream jmxsaver, boolean useMapper)
throws InstantiationException, IllegalAccessException,
InvocationTargetException, NoSuchMethodException,
ClassNotFoundException {
final Class<? extends Converter> clazz = Class.forName(key).asSubclass(Converter.class);
if (useMapper) {
jmxsaver.registerConverter(clazz.getConstructor(Mapper.class).newInstance(jmxsaver.getMapper()));
} else {
jmxsaver.registerConverter(clazz.getDeclaredConstructor().newInstance());
}
}
// For converters to use
public static String aliasToClass(String s) {
String r = aliasToClass.getProperty(s);
return r == null ? s : r;
}
// For converters to use
public static String classToAlias(String s) {
String r = classToAlias.getProperty(s);
return r == null ? s : r;
}
// Called by Save function
public static void saveTree(HashTree tree, OutputStream out) throws IOException {
// Get the OutputWriter to use
OutputStreamWriter outputStreamWriter = getOutputStreamWriter(out);
writeXmlHeader(outputStreamWriter);
// Use deprecated method, to avoid duplicating code
MsScriptWrapper wrapper = new MsScriptWrapper();
wrapper.testPlan = tree;
JMXSAVER.toXML(wrapper, outputStreamWriter);
outputStreamWriter.write('\n');// Ensure terminated properly
outputStreamWriter.close();
}
// Used by Test code
public static void saveElement(Object el, OutputStream out) throws IOException {
// Get the OutputWriter to use
OutputStreamWriter outputStreamWriter = getOutputStreamWriter(out);
writeXmlHeader(outputStreamWriter);
// Use deprecated method, to avoid duplicating code
JMXSAVER.toXML(el, outputStreamWriter);
outputStreamWriter.close();
}
// Used by Test code
public static Object loadElement(InputStream in) throws IOException {
// Get the InputReader to use
InputStreamReader inputStreamReader = getInputStreamReader(in);
// Use deprecated method, to avoid duplicating code
Object element = JMXSAVER.fromXML(inputStreamReader);
inputStreamReader.close();
return element;
}
/**
* Save a sampleResult to an XML output file using XStream.
*
* @param evt sampleResult wrapped in a sampleEvent
* @param writer output stream which must be created using {@link #getFileEncoding(String)}
* @throws IOException when writing data to output fails
*/
// Used by ResultCollector.sampleOccurred(SampleEvent event)
public static synchronized void saveSampleResult(SampleEvent evt, Writer writer) throws IOException {
DataHolder dh = JTLSAVER.newDataHolder();
dh.put(SAMPLE_EVENT_OBJECT, evt);
// This is effectively the same as saver.toXML(Object, Writer) except we get to provide the DataHolder
// Don't know why there is no method for this in the XStream class
try {
JTLSAVER.marshal(evt.getResult(), new XppDriver().createWriter(writer), dh);
} catch (RuntimeException e) {
throw new IllegalArgumentException("Failed marshalling:" + (evt.getResult() != null ? showDebuggingInfo(evt.getResult()) : "null"), e);
}
writer.write('\n');
}
/**
* @param result SampleResult
* @return String debugging information
*/
private static String showDebuggingInfo(SampleResult result) {
try {
return "class:" + result.getClass() + ",content:" + ToStringBuilder.reflectionToString(result);
} catch (Exception e) {
return "Exception occurred creating debug from event, message:" + e.getMessage();
}
}
// Routines for TestSaveService
static String getPropertyVersion() {
return MsSaveService.propertiesVersion;
}
static String getFileVersion() {
return MsSaveService.fileVersion;
}
// Allow test code to check for spurious class references
static List<String> checkClasses() {
final ClassLoader classLoader = MsSaveService.class.getClassLoader();
List<String> missingClasses = new ArrayList<>();
for (Object clazz : classToAlias.keySet()) {
String name = (String) clazz;
if (!NameUpdater.isMapped(name)) {// don't bother checking class is present if it is to be updated
try {
Class.forName(name, false, classLoader);
} catch (ClassNotFoundException e) {
log.error("Unexpected entry in saveservice.properties; class does not exist and is not upgraded: {}", name);
missingClasses.add(name);
}
}
}
return missingClasses;
}
private static void checkVersions() {
if (!PROPVERSION.equalsIgnoreCase(propertiesVersion)) {
log.warn("Bad _version - expected {}, found {}.", PROPVERSION, propertiesVersion);
}
}
/**
* Read results from JTL file.
*
* @param reader of the file
* @param resultCollectorHelper helper class to enable TestResultWrapperConverter to deliver the samples
* @throws IOException if an I/O error occurs
*/
public static void loadTestResults(InputStream reader, ResultCollectorHelper resultCollectorHelper) throws IOException {
// Get the InputReader to use
InputStreamReader inputStreamReader = getInputStreamReader(reader);
DataHolder dh = JTLSAVER.newDataHolder();
dh.put(RESULTCOLLECTOR_HELPER_OBJECT, resultCollectorHelper); // Allow TestResultWrapper to feed back the samples
// This is effectively the same as saver.fromXML(InputStream) except we get to provide the DataHolder
// Don't know why there is no method for this in the XStream class
JTLSAVER.unmarshal(new XppDriver().createReader(reader), null, dh);
inputStreamReader.close();
}
/**
* Load a Test tree (JMX file)
*
* @param file the JMX file
* @return the loaded tree
* @throws IOException if there is a problem reading the file or processing it
*/
public static HashTree loadTree(File file) throws IOException {
log.info("Loading file: {}", file);
try (InputStream inputStream = new FileInputStream(file);
BufferedInputStream bufferedInputStream =
new BufferedInputStream(inputStream)) {
return readTree(bufferedInputStream, file);
}
}
/**
* @param inputStream {@link InputStream}
* @param file the JMX file used only for debug, can be null
* @return the loaded tree
* @throws IOException if there is a problem reading the file or processing it
*/
private static HashTree readTree(InputStream inputStream, File file)
throws IOException {
MsScriptWrapper wrapper = null;
try {
// Get the InputReader to use
InputStreamReader inputStreamReader = getInputStreamReader(inputStream);
wrapper = (MsScriptWrapper) JMXSAVER.fromXML(inputStreamReader);
inputStreamReader.close();
if (wrapper == null) {
log.error("Problem loading XML: see above.");
return null;
}
return wrapper.testPlan;
} catch (CannotResolveClassException | ConversionException | NoClassDefFoundError e) {
if (file != null) {
throw new IllegalArgumentException("Problem loading XML from:'" + file.getAbsolutePath() + "'. \nCause:\n" +
ExceptionUtils.getRootCauseMessage(e) + "\n\n Detail:" + e, e);
} else {
throw new IllegalArgumentException("Problem loading XML. \nCause:\n" +
ExceptionUtils.getRootCauseMessage(e) + "\n\n Detail:" + e, e);
}
}
}
private static InputStreamReader getInputStreamReader(InputStream inStream) {
// Check if we have a encoding to use from properties
Charset charset = getFileEncodingCharset();
return new InputStreamReader(inStream, charset);
}
private static OutputStreamWriter getOutputStreamWriter(OutputStream outStream) {
// Check if we have a encoding to use from properties
Charset charset = getFileEncodingCharset();
return new OutputStreamWriter(outStream, charset);
}
/**
* Returns the file Encoding specified in saveservice.properties or the default
*
* @param dflt value to return if file encoding was not provided
* @return file encoding or default
*/
// Used by ResultCollector when creating output files
public static String getFileEncoding(String dflt) {
if (fileEncoding != null && fileEncoding.length() > 0) {
return fileEncoding;
} else {
return dflt;
}
}
// @NotNull
private static Charset getFileEncodingCharset() {
// Check if we have a encoding to use from properties
if (fileEncoding != null && fileEncoding.length() > 0) {
return Charset.forName(fileEncoding);
} else {
// We use the default character set encoding of the JRE
log.info("fileEncoding not defined - using JRE default");
return Charset.defaultCharset();
}
}
private static void writeXmlHeader(OutputStreamWriter writer) throws IOException {
// Write XML header if we have the charset to use for encoding
Charset charset = getFileEncodingCharset();
// We do not use getEncoding method of Writer, since that returns
// the historical name
String header = XML_HEADER.replaceAll("<ph>", charset.name());
writer.write(header);
writer.write('\n');
}
// Normal output
// ---- Debugging information ----
// required-type : org.apache.jorphan.collections.ListedHashTree
// cause-message : WebServiceSampler : WebServiceSampler
// class : org.apache.jmeter.save.ScriptWrapper
// message : WebServiceSampler : WebServiceSampler
// line number : 929
// path : /jmeterTestPlan/hashTree/hashTree/hashTree[4]/hashTree[5]/WebServiceSampler
// cause-exception : com.thoughtworks.xstream.alias.CannotResolveClassException
// -------------------------------
/**
* Simplify getMessage() output from XStream ConversionException
*
* @param ce - ConversionException to analyse
* @return string with details of error
*/
public static String CEtoString(ConversionException ce) {
return "XStream ConversionException at line: " + ce.get("line number") + "\n" + ce.get("message")
+ "\nPerhaps a missing jar? See log file.";
}
public static String getPropertiesVersion() {
return propertiesVersion;
}
public static String getVERSION() {
return VERSION;
}
}

View File

@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.metersphere.api.parser.jmeter.xstream;
import org.apache.jorphan.collections.HashTree;
class MsScriptWrapper {
// Used by ScriptWrapperConverter
String version = "";
HashTree testPlan;
}

View File

@ -51,8 +51,6 @@ public class MsElementConverterRegister {
register(BeanShellPreProcessConverter.class); register(BeanShellPreProcessConverter.class);
register(JDBCPreProcessConverter.class); register(JDBCPreProcessConverter.class);
register(JSR223PreProcessConverter.class); register(JSR223PreProcessConverter.class);
} }
/** /**
@ -71,6 +69,15 @@ public class MsElementConverterRegister {
} }
} }
/**
* 注销 TestElement 对应的转换器
*
* @param elementConverterClass 转换器的类
*/
public static void unRegister(Class<? extends AbstractMsElementConverter<? extends TestElement>> elementConverterClass) {
parserMap.remove(elementConverterClass);
}
/** /**
* 获取对应组件的转换器 * 获取对应组件的转换器
* *

View File

@ -1,16 +1,17 @@
package io.metersphere.api.service; package io.metersphere.api.service;
import com.thoughtworks.xstream.converters.Converter;
import io.metersphere.api.parser.jmeter.xstream.MsSaveService;
import io.metersphere.api.parser.ms.MsElementConverterRegister;
import io.metersphere.api.utils.ApiDataUtils; import io.metersphere.api.utils.ApiDataUtils;
import io.metersphere.api.parser.jmeter.JmeterElementConverterRegister; import io.metersphere.api.parser.jmeter.JmeterElementConverterRegister;
import io.metersphere.plugin.api.spi.AbstractApiPlugin; import io.metersphere.plugin.api.spi.*;
import io.metersphere.plugin.api.spi.AbstractJmeterElementConverter;
import io.metersphere.plugin.api.spi.JmeterElementConverter;
import io.metersphere.plugin.api.spi.MsTestElement;
import io.metersphere.sdk.plugin.MsPluginManager; import io.metersphere.sdk.plugin.MsPluginManager;
import io.metersphere.sdk.util.CommonBeanFactory; import io.metersphere.sdk.util.CommonBeanFactory;
import io.metersphere.sdk.util.LogUtils; import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.service.PluginChangeService; import io.metersphere.system.service.PluginChangeService;
import io.metersphere.system.service.PluginLoadService; import io.metersphere.system.service.PluginLoadService;
import org.apache.jmeter.testelement.TestElement;
import org.pf4j.Plugin; import org.pf4j.Plugin;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -35,12 +36,46 @@ public class ApiPluginChangeService implements PluginChangeService {
extensionClasses.forEach(clazzList::add); extensionClasses.forEach(clazzList::add);
ApiDataUtils.setResolvers(clazzList); ApiDataUtils.setResolvers(clazzList);
// 注册插件元素解析器 // 注册插件 JmeterElementConverter 解析器
msPluginManager.getExtensionClasses(JmeterElementConverter.class, pluginId) msPluginManager.getExtensionClasses(JmeterElementConverter.class, pluginId)
.forEach(item -> JmeterElementConverterRegister.register((Class<? extends AbstractJmeterElementConverter<? extends MsTestElement>>) item)); .forEach(item -> JmeterElementConverterRegister.register((Class<? extends AbstractJmeterElementConverter<? extends MsTestElement>>) item));
// 注册插件 MsElementConverter 解析器
msPluginManager.getExtensionClasses(MsElementConverter.class, pluginId)
.forEach(item -> MsElementConverterRegister.register((Class<? extends AbstractMsElementConverter<? extends TestElement>>) item));
// 注册插件 xstream xml 序列化解析器
msPluginManager.getExtensionClasses(Converter.class, pluginId)
.forEach(MsSaveService::registerConverter);
// 加载插件 jmeter 元素别名
MsSaveService.loadPluginAliasProperties(msPluginManager.getPluginClassLoader(pluginId));
} }
} catch (Exception e) { } catch (Exception e) {
LogUtils.error("注册接口插件实现类失败:{}", e); LogUtils.error("注册接口插件实现类失败:{}", e);
} }
} }
@Override
public void handlePluginUnLoad(String pluginId) {
MsPluginManager msPluginManager = CommonBeanFactory.getBean(PluginLoadService.class).getMsPluginManager();
Plugin plugin = msPluginManager.getPlugin(pluginId).getPlugin();
try {
if (plugin instanceof AbstractApiPlugin) {
// 注销插件 JmeterElementConverter 解析器
msPluginManager.getExtensionClasses(JmeterElementConverter.class, pluginId)
.forEach(item -> JmeterElementConverterRegister.unRegister((Class<? extends AbstractJmeterElementConverter<? extends MsTestElement>>) item));
// 注销插件 MsElementConverter 解析器
msPluginManager.getExtensionClasses(MsElementConverter.class, pluginId)
.forEach(item -> MsElementConverterRegister.unRegister((Class<? extends AbstractMsElementConverter<? extends TestElement>>) item));
// 注销插件 xstream xml 序列化解析器
msPluginManager.getExtensionClasses(Converter.class, pluginId)
.forEach(MsSaveService::unRegisterConverter);
}
} catch (Exception e) {
LogUtils.error("注销接口插件实现类失败:{}", e);
}
}
} }

View File

@ -0,0 +1,45 @@
package io.metersphere.api.service;
import io.metersphere.api.parser.jmeter.xstream.MsSaveService;
import io.metersphere.system.base.BaseApiPluginTestService;
import jakarta.annotation.Resource;
import org.apache.jorphan.collections.HashTree;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Field;
/**
* @Author: jianxing
* @CreateTime: 2024-08-01 14:04
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@AutoConfigureMockMvc
public class MsSaveServiceTests {
@Resource
private BaseApiPluginTestService baseApiPluginTestService;
@Test
public void getBlankJon() throws Exception {
baseApiPluginTestService.addPlugin("file/tcp-sampler-xstream.jar");
File tcpJmx = new File(
this.getClass().getClassLoader().getResource("file/tcp.jmx")
.getPath()
);
Object scriptWrapper = MsSaveService.loadElement(new FileInputStream(tcpJmx));
getHashTree(scriptWrapper);
}
private HashTree getHashTree(Object scriptWrapper) throws Exception {
Field field = scriptWrapper.getClass().getDeclaredField("testPlan");
field.setAccessible(true);
return (HashTree) field.get(scriptWrapper);
}
}

View File

@ -1,9 +1,9 @@
package io.metersphere.api.utils; package io.metersphere.api.utils;
import io.metersphere.api.parser.jmeter.xstream.MsSaveService;
import io.metersphere.api.parser.ms.MsTestElementParser; import io.metersphere.api.parser.ms.MsTestElementParser;
import io.metersphere.plugin.api.spi.AbstractMsProtocolTestElement; import io.metersphere.plugin.api.spi.AbstractMsProtocolTestElement;
import io.metersphere.plugin.api.spi.AbstractMsTestElement; import io.metersphere.plugin.api.spi.AbstractMsTestElement;
import org.apache.jmeter.save.SaveService;
import org.apache.jorphan.collections.HashTree; import org.apache.jorphan.collections.HashTree;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.MethodOrderer;
@ -39,7 +39,7 @@ public class MsTestElementParserTest {
.getPath() .getPath()
); );
Object scriptWrapper = SaveService.loadElement(new FileInputStream(httpJmx)); Object scriptWrapper = MsSaveService.loadElement(new FileInputStream(httpJmx));
HashTree hashTree = getHashTree(scriptWrapper); HashTree hashTree = getHashTree(scriptWrapper);
MsTestElementParser parser = new MsTestElementParser(); MsTestElementParser parser = new MsTestElementParser();
AbstractMsTestElement msTestElement = parser.parse(hashTree); AbstractMsTestElement msTestElement = parser.parse(hashTree);
@ -51,7 +51,7 @@ public class MsTestElementParserTest {
this.getClass().getClassLoader().getResource("file/import/jmeter/single.jmx") this.getClass().getClassLoader().getResource("file/import/jmeter/single.jmx")
.getPath() .getPath()
); );
scriptWrapper = SaveService.loadElement(new FileInputStream(httpJmx)); scriptWrapper = MsSaveService.loadElement(new FileInputStream(httpJmx));
hashTree = getHashTree(scriptWrapper); hashTree = getHashTree(scriptWrapper);
parser = new MsTestElementParser(); parser = new MsTestElementParser();
msTestElement = parser.parse(hashTree); msTestElement = parser.parse(hashTree);

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" enabled="true">
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments">
<collectionProp name="Arguments.arguments"/>
</elementProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="172474118045900001" enabled="true">
<intProp name="ThreadGroup.num_threads">1</intProp>
<intProp name="ThreadGroup.ramp_time">1</intProp>
<longProp name="ThreadGroup.delay">0</longProp>
<longProp name="ThreadGroup.duration">0</longProp>
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<boolProp name="ThreadGroup.scheduler">false</boolProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="LoopController" enabled="true">
<intProp name="LoopController.loops">1</intProp>
<boolProp name="LoopController.continue_forever">false</boolProp>
</elementProp>
</ThreadGroup>
<hashTree>
<TCPSampler guiclass="TCPSamplerGui" testclass="TCPSampler" testname="12312312" enabled="true">
<stringProp name="TCPSampler.classname">TCPClientImpl</stringProp>
<stringProp name="TCPSampler.ctimeout">5000</stringProp>
<boolProp name="TCPSampler.reUseConnection">false</boolProp>
<boolProp name="TCPSampler.nodelay">false</boolProp>
<stringProp name="TCPSampler.closeConnection">false</stringProp>
<stringProp name="TCPSampler.timeout">5000</stringProp>
<stringProp name="TCPSampler.request">123123</stringProp>
<stringProp name="MS_RESOURCE_ID">913573930622976</stringProp>
<stringProp name="MS_STEP_ID">913573930622976</stringProp>
<stringProp name="MS_REPORT_ID">172474118045900001</stringProp>
<stringProp name="PROJECT_ID">718255970852864</stringProp>
<stringProp name="MS_CUSTOM_URL">TCP://null:null</stringProp>
<stringProp name="xxx">{&quot;reportId&quot;:&quot;172474118045900001&quot;,&quot;parseDisabledElement&quot;:false,&quot;enableGlobalCookie&quot;:true,&quot;envConfig&quot;:{&quot;id&quot;:&quot;2160780881534976&quot;,&quot;projectId&quot;:&quot;718255970852864&quot;,&quot;name&quot;:&quot;百度云&quot;,&quot;config&quot;:{&quot;commonParams&quot;:{&quot;requestTimeout&quot;:60000,&quot;responseTimeout&quot;:60000},&quot;commonVariables&quot;:[{&quot;key&quot;:&quot;&quot;,&quot;value&quot;:&quot;&quot;,&quot;id&quot;:&quot;aa0d9ff3-e0da-4f24-bf0c-51728e37bcd4&quot;,&quot;paramType&quot;:&quot;CONSTANT&quot;,&quot;enable&quot;:true,&quot;description&quot;:&quot;&quot;,&quot;tags&quot;:[],&quot;notBlankValue&quot;:false,&quot;valid&quot;:false}],&quot;httpConfig&quot;:[{&quot;id&quot;:&quot;4062c143-b73c-4109-bceb-6a60a8067f34&quot;,&quot;protocol&quot;:&quot;https&quot;,&quot;hostname&quot;:&quot;www.baidu.com&quot;,&quot;url&quot;:&quot;https://www.baidu.com&quot;,&quot;type&quot;:&quot;NONE&quot;,&quot;pathMatchRule&quot;:{&quot;condition&quot;:&quot;CONTAINS&quot;,&quot;path&quot;:&quot;&quot;},&quot;moduleMatchRule&quot;:{&quot;modules&quot;:[]},&quot;headers&quot;:[],&quot;description&quot;:&quot;&quot;,&quot;order&quot;:1,&quot;authConfig&quot;:{&quot;authType&quot;:&quot;NONE&quot;,&quot;basicAuth&quot;:{&quot;userName&quot;:&quot;&quot;,&quot;password&quot;:&quot;&quot;,&quot;valid&quot;:false},&quot;digestAuth&quot;:{&quot;userName&quot;:&quot;&quot;,&quot;password&quot;:&quot;&quot;,&quot;valid&quot;:false},&quot;httpauthValid&quot;:false},&quot;moduleMatchRuleOrder&quot;:2}],&quot;dataSources&quot;:[{&quot;id&quot;:&quot;1ab74c5f-eea2-4d35-9bbc-e8e17e96185e&quot;,&quot;dataSource&quot;:&quot;DM&quot;,&quot;driver&quot;:&quot;dm.jdbc.driver.DmDriver&quot;,&quot;driverId&quot;:&quot;dm.jdbc.driver.DmDriver&amp;dm.jdbc.driver.DmDriver&quot;,&quot;dbUrl&quot;:&quot;jdbc:dm://123.56.8.132:5236/SYS&quot;,&quot;username&quot;:&quot;SYSDBA&quot;,&quot;password&quot;:&quot;SYSDBA001&quot;,&quot;poolMax&quot;:1,&quot;timeout&quot;:1000}],&quot;hostConfig&quot;:{&quot;enable&quot;:false,&quot;hosts&quot;:[{&quot;ip&quot;:null,&quot;domain&quot;:null,&quot;description&quot;:null}]},&quot;preProcessorConfig&quot;:{&quot;apiProcessorConfig&quot;:{&quot;planProcessorConfig&quot;:{&quot;processors&quot;:[]},&quot;scenarioProcessorConfig&quot;:{&quot;processors&quot;:[]},&quot;requestProcessorConfig&quot;:{&quot;processors&quot;:[]}}},&quot;postProcessorConfig&quot;:{&quot;apiProcessorConfig&quot;:{&quot;planProcessorConfig&quot;:{&quot;processors&quot;:[]},&quot;scenarioProcessorConfig&quot;:{&quot;processors&quot;:[]},&quot;requestProcessorConfig&quot;:{&quot;processors&quot;:[]}}},&quot;assertionConfig&quot;:{&quot;assertions&quot;:[]},&quot;pluginConfigMap&quot;:{}},&quot;mock&quot;:false,&quot;description&quot;:null},&quot;globalParams&quot;:null,&quot;retryOnFail&quot;:false,&quot;retryConfig&quot;:null,&quot;testElementClassPluginIdMap&quot;:{&quot;io.metersphere.plugin.sofa.rpc.SofaRpcSamplerModule&quot;:&quot;sofa-rpc-sampler&quot;,&quot;io.metersphere.plugin.sampler.MsSpxTradeSampler&quot;:&quot;SpxSampler&quot;,&quot;io.metersphere.plugin.redis.mode.RedisSamplerModule&quot;:&quot;redis-sampler&quot;,&quot;io.metersphere.plugin.mongo.mode.MongoSamplerModule&quot;:&quot;metersphere-plugin-mongo&quot;,&quot;io.metersphere.plugin.tcp.TCPSamplerModule&quot;:&quot;tcp-sampler&quot;},&quot;testElementClassProtocolMap&quot;:{&quot;io.metersphere.plugin.sofa.rpc.SofaRpcSamplerModule&quot;:&quot;MongoDB&quot;,&quot;io.metersphere.plugin.sampler.MsSpxTradeSampler&quot;:&quot;MongoDB&quot;,&quot;io.metersphere.plugin.redis.mode.RedisSamplerModule&quot;:&quot;MongoDB&quot;,&quot;io.metersphere.plugin.mongo.mode.MongoSamplerModule&quot;:&quot;MongoDB&quot;,&quot;io.metersphere.plugin.tcp.TCPSamplerModule&quot;:&quot;MongoDB&quot;}}</stringProp>
</TCPSampler>
<hashTree>
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="脚本名称" enabled="true">
<boolProp name="cacheKey">false</boolProp>
<stringProp name="PROJECT_ID">718255970852864</stringProp>
<stringProp name="script">log.info(&quot;=======&quot;);</stringProp>
<stringProp name="scriptLanguage">beanshell</stringProp>
</JSR223PostProcessor>
<hashTree/>
</hashTree>
<DebugSampler guiclass="TestBeanGUI" testclass="DebugSampler" testname="RunningDebugSampler" enabled="true">
<boolProp name="displayJMeterProperties">false</boolProp>
<boolProp name="displayJMeterVariables">true</boolProp>
<boolProp name="displaySystemProperties">false</boolProp>
</DebugSampler>
<hashTree/>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>

View File

@ -24,4 +24,9 @@ public class PluginChangeServiceInvoker implements PluginChangeService {
public void handlePluginLoad(String pluginId) { public void handlePluginLoad(String pluginId) {
this.pluginChangeServices.forEach(service -> service.handlePluginLoad(pluginId)); this.pluginChangeServices.forEach(service -> service.handlePluginLoad(pluginId));
} }
@Override
public void handlePluginUnLoad(String pluginId) {
this.pluginChangeServices.forEach(service -> service.handlePluginUnLoad(pluginId));
}
} }

View File

@ -6,8 +6,13 @@ package io.metersphere.system.service;
*/ */
public interface PluginChangeService { public interface PluginChangeService {
/** /**
* 插件时调用 * 插件加载时调用
* @param pluginId 插件ID * @param pluginId 插件ID
*/ */
void handlePluginLoad(String pluginId); void handlePluginLoad(String pluginId);
/**
* 插件卸载时调用
* @param pluginId 插件ID
*/
void handlePluginUnLoad(String pluginId);
} }

View File

@ -210,6 +210,7 @@ public class PluginLoadService {
*/ */
public synchronized void unloadPlugin(String pluginId) { public synchronized void unloadPlugin(String pluginId) {
if (hasPlugin(pluginId)) { if (hasPlugin(pluginId)) {
pluginChangeServiceInvoker.handlePluginUnLoad(pluginId);
msPluginManager.deletePlugin(pluginId); msPluginManager.deletePlugin(pluginId);
} }
} }

View File

@ -10,6 +10,8 @@ import org.springframework.stereotype.Service;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException;
import java.util.UUID;
import static io.metersphere.sdk.constants.InternalUserRole.ADMIN; import static io.metersphere.sdk.constants.InternalUserRole.ADMIN;
@ -37,19 +39,23 @@ public class BaseApiPluginTestService {
if (hasJdbcPlugin()) { if (hasJdbcPlugin()) {
return jdbcPlugin; return jdbcPlugin;
} }
jdbcPlugin = addPlugin("file/jdbc-sampler-v3.x.jar");
return jdbcPlugin;
}
public Plugin addPlugin(String filePath) throws IOException {
PluginUpdateRequest request = new PluginUpdateRequest(); PluginUpdateRequest request = new PluginUpdateRequest();
File jarFile = new File( File jarFile = new File(
this.getClass().getClassLoader().getResource("file/jdbc-sampler-v3.x.jar") this.getClass().getClassLoader().getResource(filePath)
.getPath() .getPath()
); );
FileInputStream inputStream = new FileInputStream(jarFile); FileInputStream inputStream = new FileInputStream(jarFile);
MockMultipartFile mockMultipartFile = new MockMultipartFile(jarFile.getName(), jarFile.getName(), "jar", inputStream); MockMultipartFile mockMultipartFile = new MockMultipartFile(jarFile.getName(), jarFile.getName(), "jar", inputStream);
request.setName("测试jdbc插件"); request.setName(UUID.randomUUID().toString());
request.setGlobal(true); request.setGlobal(true);
request.setEnable(true); request.setEnable(true);
request.setCreateUser(ADMIN.name()); request.setCreateUser(ADMIN.name());
jdbcPlugin = pluginService.add(request, mockMultipartFile); return pluginService.add(request, mockMultipartFile);
return jdbcPlugin;
} }

View File

@ -187,7 +187,7 @@ public class PluginControllerTests extends BaseTest {
); );
this.requestMultipartWithOkAndReturn(DEFAULT_ADD, this.requestMultipartWithOkAndReturn(DEFAULT_ADD,
getDefaultMultiPartParam(request, myDriver)); getDefaultMultiPartParam(request, myDriver));
Assertions.assertEquals(jdbcDriverPluginService.getJdbcDriverClass(DEFAULT_ORGANIZATION_ID), Arrays.asList("io.jianxing.MyDriver", "com.mysql.cj.jdbc.Driver")); Assertions.assertEquals(jdbcDriverPluginService.getJdbcDriverClass(DEFAULT_ORGANIZATION_ID), Arrays.asList("io.jianxing.MyDriver", "io.jianxing.MyDriver", "com.mysql.cj.jdbc.Driver"));
// 校验QUOTA动上传成功 // 校验QUOTA动上传成功
request.setName("cloud-quota-plugin"); request.setName("cloud-quota-plugin");