diff --git a/api-test/backend/src/main/java/io/metersphere/api/jmeter/NewDriverManager.java b/api-test/backend/src/main/java/io/metersphere/api/jmeter/NewDriverManager.java index ba346bc4cc..5200c35ffe 100644 --- a/api-test/backend/src/main/java/io/metersphere/api/jmeter/NewDriverManager.java +++ b/api-test/backend/src/main/java/io/metersphere/api/jmeter/NewDriverManager.java @@ -11,6 +11,7 @@ import io.metersphere.commons.utils.ApiFileUtil; import io.metersphere.commons.utils.CommonBeanFactory; import io.metersphere.dto.FileInfoDTO; import io.metersphere.dto.ProjectJarConfig; +import io.metersphere.jmeter.ProjectClassLoader; import io.metersphere.metadata.service.FileMetadataService; import io.metersphere.utils.JarConfigUtils; import io.metersphere.vo.BooleanPool; @@ -57,9 +58,12 @@ public class NewDriverManager { Map> map = JarConfigUtils.getJarConfigs(projectIds, jarConfigMap); if (MapUtils.isNotEmpty(map)) { //获取文件内容 + List loaderProjectIds = new ArrayList<>(); + FileMetadataService fileMetadataService = CommonBeanFactory.getBean(FileMetadataService.class); map.forEach((key, value) -> { if (CollectionUtils.isNotEmpty(value)) { + loaderProjectIds.add(key); //历史数据 value.stream().distinct().filter(s -> s.isHasFile()).forEach(s -> { //获取文件内容 @@ -81,6 +85,9 @@ public class NewDriverManager { } } }); + + // 初始化类加载器 + ProjectClassLoader.initClassLoader(loaderProjectIds); } } return jarConfigMap; diff --git a/api-test/backend/src/main/java/io/metersphere/listener/ApiAppStartListener.java b/api-test/backend/src/main/java/io/metersphere/listener/ApiAppStartListener.java index c6dd4fba8b..da7d2f9612 100644 --- a/api-test/backend/src/main/java/io/metersphere/listener/ApiAppStartListener.java +++ b/api-test/backend/src/main/java/io/metersphere/listener/ApiAppStartListener.java @@ -7,6 +7,7 @@ import io.metersphere.commons.constants.ScheduleGroup; import io.metersphere.commons.utils.CommonBeanFactory; import io.metersphere.commons.utils.LogUtil; import io.metersphere.dto.BaseSystemConfigDTO; +import io.metersphere.jmeter.ProjectClassLoader; import io.metersphere.service.*; import io.metersphere.service.definition.ApiModuleService; import io.metersphere.service.scenario.ApiScenarioModuleService; @@ -95,6 +96,8 @@ public class ApiAppStartListener implements ApplicationRunner { scheduleService.startEnableSchedules(ScheduleGroup.API_SCENARIO_TEST); scheduleService.startEnableSchedules(ScheduleGroup.SWAGGER_IMPORT); LogUtil.info("Startup complete"); + + ProjectClassLoader.initClassLoader(); } diff --git a/framework/sdk-parent/jmeter/src/main/java/io/metersphere/jmeter/JoinClassLoader.java b/framework/sdk-parent/jmeter/src/main/java/io/metersphere/jmeter/JoinClassLoader.java new file mode 100644 index 0000000000..0d7cb832ec --- /dev/null +++ b/framework/sdk-parent/jmeter/src/main/java/io/metersphere/jmeter/JoinClassLoader.java @@ -0,0 +1,95 @@ +package io.metersphere.jmeter; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.ByteBuffer; +import java.util.Enumeration; +import java.util.Vector; + +/** + * A class loader that combines multiple class loaders into one.
+ * The classes loaded by this class loader are associated with this class loader, + * i.e. Class.getClassLoader() points to this class loader. + */ +public class JoinClassLoader extends ClassLoader { + + private ClassLoader[] delegateClassLoaders; + + public JoinClassLoader(ClassLoader parent, ClassLoader... delegateClassLoaders) { + super(parent); + this.delegateClassLoaders = delegateClassLoaders; + } + + protected Class findClass(String name) throws ClassNotFoundException { + // It would be easier to call the loadClass() methods of the delegateClassLoaders + // here, but we have to load the class from the byte code ourselves, because we + // need it to be associated with our class loader. + String path = name.replace('.', '/') + ".class"; + URL url = findResource(path); + if (url == null) { + throw new ClassNotFoundException(name); + } + ByteBuffer byteCode; + try { + byteCode = loadResource(url); + } catch (IOException e) { + throw new ClassNotFoundException(name, e); + } + return defineClass(name, byteCode, null); + } + + private ByteBuffer loadResource(URL url) throws IOException { + InputStream stream = null; + try { + stream = url.openStream(); + int initialBufferCapacity = Math.min(0x40000, stream.available() + 1); + if (initialBufferCapacity <= 2) { + initialBufferCapacity = 0x10000; + } else { + initialBufferCapacity = Math.max(initialBufferCapacity, 0x200); + } + ByteBuffer buf = ByteBuffer.allocate(initialBufferCapacity); + while (true) { + if (!buf.hasRemaining()) { + ByteBuffer newBuf = ByteBuffer.allocate(2 * buf.capacity()); + buf.flip(); + newBuf.put(buf); + buf = newBuf; + } + int len = stream.read(buf.array(), buf.position(), buf.remaining()); + if (len <= 0) { + break; + } + buf.position(buf.position() + len); + } + buf.flip(); + return buf; + } finally { + if (stream != null) { + stream.close(); + } + } + } + + protected URL findResource(String name) { + for (ClassLoader delegate : delegateClassLoaders) { + URL resource = delegate.getResource(name); + if (resource != null) { + return resource; + } + } + return null; + } + + protected Enumeration findResources(String name) throws IOException { + Vector vector = new Vector(); + for (ClassLoader delegate : delegateClassLoaders) { + Enumeration enumeration = delegate.getResources(name); + while (enumeration.hasMoreElements()) { + vector.add(enumeration.nextElement()); + } + } + return vector.elements(); + } +} diff --git a/framework/sdk-parent/jmeter/src/main/java/io/metersphere/jmeter/ProjectClassLoader.java b/framework/sdk-parent/jmeter/src/main/java/io/metersphere/jmeter/ProjectClassLoader.java new file mode 100644 index 0000000000..8335186983 --- /dev/null +++ b/framework/sdk-parent/jmeter/src/main/java/io/metersphere/jmeter/ProjectClassLoader.java @@ -0,0 +1,57 @@ +package io.metersphere.jmeter; + +import groovy.lang.GroovyClassLoader; +import io.metersphere.utils.JarConfigUtils; +import io.metersphere.utils.JsonUtils; +import io.metersphere.utils.LocalPathUtil; +import io.metersphere.utils.LoggerUtil; +import org.apache.commons.collections4.CollectionUtils; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ProjectClassLoader { + // 记录以项目为单位的自定义JAR的类加载器 + private final static Map classLoaderMap = new ConcurrentHashMap<>(); + + public static void initClassLoader(List projectIds) { + // 读取所有JAR路径 + for (String projectId : projectIds) { + List jarPaths = JarConfigUtils.walk(LocalPathUtil.prePath + File.separator + projectId); + if (CollectionUtils.isNotEmpty(jarPaths)) { + LoggerUtil.info("加载JAR-PATH:" + JsonUtils.toJSONString(jarPaths), projectId); + // 初始化类加载器 + GroovyClassLoader loader = MsClassLoader.getDynamic(jarPaths); + classLoaderMap.put(projectId, loader); + } + } + } + + public static ClassLoader getClassLoader(List projectIds) { + GroovyClassLoader classLoader = new GroovyClassLoader(); + List classLoaders = new ArrayList<>(); + for (String projectId : projectIds) { + if (classLoaderMap.containsKey(projectId)) { + classLoaders.add(classLoaderMap.get(projectId)); + } + } + // 单项目加载器 + if (classLoaders.size() == 1) { + return classLoaders.get(0); + } + // 跨项目加载器 + return new JoinClassLoader(classLoader, classLoaders.toArray(new ClassLoader[classLoaders.size()])); + } + + public static void initClassLoader() { + // 读取所有JAR路径 + List projectIds = JarConfigUtils.getFileNames(LocalPathUtil.prePath + File.separator); + LoggerUtil.info("初始化所有JAR:" + JsonUtils.toJSONString(projectIds)); + if (CollectionUtils.isNotEmpty(projectIds)) { + initClassLoader(projectIds); + } + } +} diff --git a/framework/sdk-parent/jmeter/src/main/java/io/metersphere/utils/CustomizeFunctionUtil.java b/framework/sdk-parent/jmeter/src/main/java/io/metersphere/utils/CustomizeFunctionUtil.java index 69d18bfab0..18a2afa71b 100644 --- a/framework/sdk-parent/jmeter/src/main/java/io/metersphere/utils/CustomizeFunctionUtil.java +++ b/framework/sdk-parent/jmeter/src/main/java/io/metersphere/utils/CustomizeFunctionUtil.java @@ -1,7 +1,6 @@ package io.metersphere.utils; -import groovy.lang.GroovyClassLoader; -import io.metersphere.jmeter.MsClassLoader; +import io.metersphere.jmeter.ProjectClassLoader; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; @@ -30,11 +29,18 @@ public class CustomizeFunctionUtil { String pathStr = testPlan.getPropertyAsString(JAR_PATH); JMeterContext context = JMeterContextService.getContext(); if (StringUtils.isNotEmpty(pathStr) && context != null) { - List jarPaths = JsonUtils.parseObject(pathStr, List.class); - LoggerUtil.info(testPlan.getName() + "加载JAR:", jarPaths); - if (CollectionUtils.isNotEmpty(jarPaths)) { + List projectIds = JsonUtils.parseObject(pathStr, List.class); + LoggerUtil.info("加载JAR-PROJECT-ID:" + projectIds, testPlan.getName()); + LoggerUtil.info("PRE-PATH:" + LocalPathUtil.prePath, testPlan.getName()); + if (CollectionUtils.isNotEmpty(projectIds)) { + // 读取所有JAR路径 + List jarPaths = JarConfigUtils.findPathByProjectIds(projectIds); + LoggerUtil.info("加载JAR-PATH:" + JsonUtils.toJSONString(jarPaths), testPlan.getName()); + if (CollectionUtils.isEmpty(jarPaths)) { + return; + } // 初始化类加载器 - GroovyClassLoader loader = MsClassLoader.getDynamic(jarPaths); + ClassLoader loader = ProjectClassLoader.getClassLoader(projectIds); if (loader == null) { return; } diff --git a/framework/sdk-parent/jmeter/src/main/java/io/metersphere/utils/JarConfigUtils.java b/framework/sdk-parent/jmeter/src/main/java/io/metersphere/utils/JarConfigUtils.java index 6ccbdc025c..830525a749 100644 --- a/framework/sdk-parent/jmeter/src/main/java/io/metersphere/utils/JarConfigUtils.java +++ b/framework/sdk-parent/jmeter/src/main/java/io/metersphere/utils/JarConfigUtils.java @@ -6,11 +6,16 @@ import org.apache.commons.lang3.StringUtils; import org.aspectj.util.FileUtil; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; public class JarConfigUtils { @@ -99,4 +104,24 @@ public class JarConfigUtils { file.delete(); } } + + public static List findPathByProjectIds(List projectIds) { + List jarPaths = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(projectIds)) { + projectIds.forEach(item -> { + jarPaths.addAll(walk(LocalPathUtil.prePath + File.separator + item)); + }); + } + return jarPaths; + } + + + public static List walk(String dirName) { + try (Stream paths = Files.walk(Paths.get(dirName), 2)) { + return paths.map(path -> path.toString()).filter(f -> f.endsWith(".jar")).collect(Collectors.toList()); + } catch (IOException e) { + LoggerUtil.error("读取文件路径异常:", e); + } + return new ArrayList<>(); + } } diff --git a/framework/sdk-parent/jmeter/src/main/java/org/apache/jmeter/util/JSR223TestElement.java b/framework/sdk-parent/jmeter/src/main/java/org/apache/jmeter/util/JSR223TestElement.java index d281d8f2c8..15e509a6b8 100644 --- a/framework/sdk-parent/jmeter/src/main/java/org/apache/jmeter/util/JSR223TestElement.java +++ b/framework/sdk-parent/jmeter/src/main/java/org/apache/jmeter/util/JSR223TestElement.java @@ -168,10 +168,11 @@ public abstract class JSR223TestElement extends ScriptingTestElement // 设置自定义类加载器 Object dynamicClassLoader = JMeterContextService.getContext().getVariables().getObject(CustomizeFunctionUtil.MS_CLASS_LOADER); if (dynamicClassLoader != null) { - GroovyClassLoader classLoader = (GroovyClassLoader) dynamicClassLoader; + ClassLoader classLoader = (ClassLoader) dynamicClassLoader; if (scriptEngine instanceof GroovyScriptEngineImpl) { + GroovyClassLoader groovyClassLoader = new GroovyClassLoader(classLoader); GroovyScriptEngineImpl groovyScriptEngine = (GroovyScriptEngineImpl) scriptEngine; - groovyScriptEngine.setClassLoader(classLoader); + groovyScriptEngine.setClassLoader(groovyClassLoader); } else { Thread.currentThread().setContextClassLoader(classLoader); }