From 69e312e87be6dd194a04e63eeab892bff2495a5f Mon Sep 17 00:00:00 2001
From: AgAngle <1323481023@qq.com>
Date: Wed, 13 Sep 2023 18:08:09 +0800
Subject: [PATCH] =?UTF-8?q?refactor(=E7=B3=BB=E7=BB=9F=E8=AE=BE=E7=BD=AE):?=
=?UTF-8?q?=20=E6=8F=92=E4=BB=B6=E5=8A=A0=E8=BD=BD=E9=87=8D=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../io/metersphere/system/domain/Plugin.java | 2 +-
.../3.0.0/ddl/V3.0.0_11__system_setting.sql | 4 +-
.../plugin/api/api/AbstractApiPlugin.java | 11 -
.../api/api/AbstractApiProtocolPlugin.java | 6 +
.../platform/api/AbstractPlatformPlugin.java | 5 -
.../plugin/platform/api/Platform.java | 4 +-
.../plugin/metersphere-plugin-sdk/pom.xml | 5 +
.../plugin/sdk/api/AbstractMsPlugin.java | 12 +-
.../metersphere/plugin/sdk/api/MsPlugin.java | 32 +-
.../io/metersphere/sdk/config/RsaConfig.java | 7 +-
.../sdk/constants/PluginScenarioType.java | 13 +-
.../sdk/constants/StorageConstants.java | 5 -
.../sdk/constants/StorageType.java | 5 +
.../environment/EnvironmentController.java | 28 +-
.../sdk/dto/environment/BodyFile.java | 4 +-
.../io/metersphere/sdk/file/FileCenter.java | 21 +-
.../sdk/file/LocalFileRepository.java | 90 +++++
.../plugin/JdbcDriverPluginDescriptor.java | 47 +++
.../JdbcDriverPluginDescriptorFinder.java | 85 +++++
...cDriverServiceProviderExtensionFinder.java | 142 ++++++++
.../sdk/plugin/MsPluginManager.java | 27 ++
.../plugin/loader/PlatformPluginManager.java | 23 --
.../sdk/plugin/loader/PluginClassLoader.java | 230 -------------
.../sdk/plugin/loader/PluginManager.java | 167 ---------
.../sdk/plugin/storage/MsStorageStrategy.java | 64 ----
.../sdk/plugin/storage/StorageStrategy.java | 43 ---
.../sdk/service/BasePluginService.java | 37 ++
.../sdk/service/JdbcDriverPluginService.java | 81 ++++-
.../sdk/service/PlatformPluginService.java | 40 +--
.../sdk/service/PluginLoadService.java | 316 ++++++++++--------
.../environment/EnvironmentService.java | 60 ++--
.../io/metersphere/sdk/util/MsFileUtils.java | 23 ++
.../service/ProjectApplicationService.java | 4 +-
.../ProjectApplicationControllerTests.java | 2 +-
.../file/metersphere-jira-plugin-3.x.jar | Bin 41971 -> 46161 bytes
.../controller/result/SystemResultCode.java | 1 -
.../system/dto/PluginNotifiedDTO.java | 10 +
.../system/listener/PluginListener.java | 16 +-
.../system/service/PluginService.java | 94 +++---
.../service/ServiceIntegrationLogService.java | 16 +-
.../service/ServiceIntegrationService.java | 10 +-
.../controller/PluginControllerTests.java | 23 +-
.../ServiceIntegrationControllerTests.java | 4 +-
.../file/metersphere-jira-plugin-3.x.jar | Bin 41971 -> 46161 bytes
.../file/metersphere-mqtt-plugin-3.x.jar | Bin 6706 -> 7295 bytes
.../metersphere-mqtt-plugin-repeat-key.jar | Bin 6706 -> 0 bytes
...ersphere-plugin-script-id-repeat-error.jar | Bin 6707 -> 7294 bytes
.../metersphere-plugin-script-parse-error.jar | Bin 6710 -> 7298 bytes
.../src/test/resources/file/my-driver-1.0.jar | Bin 2390 -> 2856 bytes
.../resources/file/test-mysql-driver-1.0.jar | Bin 2542 -> 0 bytes
.../pluginManager/components/pluginTable.vue | 4 +-
pom.xml | 1 +
52 files changed, 874 insertions(+), 950 deletions(-)
delete mode 100644 backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiPlugin.java
create mode 100644 backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiProtocolPlugin.java
delete mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/StorageConstants.java
create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/StorageType.java
create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/file/LocalFileRepository.java
create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/JdbcDriverPluginDescriptor.java
create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/JdbcDriverPluginDescriptorFinder.java
create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/JdbcDriverServiceProviderExtensionFinder.java
create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/MsPluginManager.java
delete mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PlatformPluginManager.java
delete mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginClassLoader.java
delete mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java
delete mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MsStorageStrategy.java
delete mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/StorageStrategy.java
create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/util/MsFileUtils.java
create mode 100644 backend/services/system-setting/src/main/java/io/metersphere/system/dto/PluginNotifiedDTO.java
delete mode 100644 backend/services/system-setting/src/test/resources/file/metersphere-mqtt-plugin-repeat-key.jar
delete mode 100644 backend/services/system-setting/src/test/resources/file/test-mysql-driver-1.0.jar
diff --git a/backend/framework/domain/src/main/java/io/metersphere/system/domain/Plugin.java b/backend/framework/domain/src/main/java/io/metersphere/system/domain/Plugin.java
index a6b0068cf0..b1c5752459 100644
--- a/backend/framework/domain/src/main/java/io/metersphere/system/domain/Plugin.java
+++ b/backend/framework/domain/src/main/java/io/metersphere/system/domain/Plugin.java
@@ -51,7 +51,7 @@ public class Plugin implements Serializable {
@Schema(description = "插件描述")
private String description;
- @Schema(description = "插件使用场景PAI/PLATFORM", requiredMode = Schema.RequiredMode.REQUIRED)
+ @Schema(description = "插件使用场景API_PROTOCOL/PLATFORM", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{plugin.scenario.not_blank}", groups = {Created.class})
@Size(min = 1, max = 50, message = "{plugin.scenario.length_range}", groups = {Created.class, Updated.class})
private String scenario;
diff --git a/backend/framework/domain/src/main/resources/migration/3.0.0/ddl/V3.0.0_11__system_setting.sql b/backend/framework/domain/src/main/resources/migration/3.0.0/ddl/V3.0.0_11__system_setting.sql
index 0f09fa9b34..59894f2ca5 100644
--- a/backend/framework/domain/src/main/resources/migration/3.0.0/ddl/V3.0.0_11__system_setting.sql
+++ b/backend/framework/domain/src/main/resources/migration/3.0.0/ddl/V3.0.0_11__system_setting.sql
@@ -72,7 +72,7 @@ CREATE TABLE IF NOT EXISTS novice_statistics
CREATE TABLE IF NOT EXISTS plugin
(
- `id` VARCHAR(50) NOT NULL COMMENT 'ID' ,
+ `id` VARCHAR(100) NOT NULL COMMENT 'ID' ,
`name` VARCHAR(255) NOT NULL COMMENT '插件名称' ,
`plugin_id` VARCHAR(300) NOT NULL COMMENT '插件ID(名称加版本号)' ,
`file_name` VARCHAR(300) NOT NULL COMMENT '文件名' ,
@@ -83,7 +83,7 @@ CREATE TABLE IF NOT EXISTS plugin
`global` BIT NOT NULL DEFAULT 1 COMMENT '是否是全局插件' ,
`xpack` BIT NOT NULL DEFAULT 0 COMMENT '是否是企业版插件' ,
`description` VARCHAR(500) COMMENT '插件描述' ,
- `scenario` VARCHAR(50) NOT NULL COMMENT '插件使用场景PAI/PLATFORM' ,
+ `scenario` VARCHAR(50) NOT NULL COMMENT '插件使用场景API_PROTOCOL/PLATFORM' ,
PRIMARY KEY (id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
diff --git a/backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiPlugin.java b/backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiPlugin.java
deleted file mode 100644
index 9675aa8061..0000000000
--- a/backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiPlugin.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.metersphere.plugin.api.api;
-
-import io.metersphere.plugin.sdk.api.AbstractMsPlugin;
-
-public abstract class AbstractApiPlugin extends AbstractMsPlugin {
- private static final String API_PLUGIN_TYPE = "API";
- @Override
- public String getType() {
- return API_PLUGIN_TYPE;
- }
-}
diff --git a/backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiProtocolPlugin.java b/backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiProtocolPlugin.java
new file mode 100644
index 0000000000..34f998ff03
--- /dev/null
+++ b/backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiProtocolPlugin.java
@@ -0,0 +1,6 @@
+package io.metersphere.plugin.api.api;
+
+import io.metersphere.plugin.sdk.api.AbstractMsPlugin;
+
+public abstract class AbstractApiProtocolPlugin extends AbstractMsPlugin {
+}
diff --git a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java
index 9dfa0c244b..06aa3ac315 100644
--- a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java
+++ b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java
@@ -3,14 +3,9 @@ package io.metersphere.plugin.platform.api;
import io.metersphere.plugin.sdk.api.AbstractMsPlugin;
public abstract class AbstractPlatformPlugin extends AbstractMsPlugin {
- private static final String DEFAULT_PLATFORM_PLUGIN_TYPE = "PLATFORM";
private static final String DEFAULT_INTEGRATION_SCRIPT_ID = "integration";
private static final String DEFAULT_PROJECT_SCRIPT_ID = "project";
private static final String DEFAULT_ACCOUNT_SCRIPT_ID = "account";
- @Override
- public String getType() {
- return DEFAULT_PLATFORM_PLUGIN_TYPE;
- }
/**
* 返回插件的描述信息
diff --git a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/Platform.java b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/Platform.java
index c570791df1..868e1e563b 100644
--- a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/Platform.java
+++ b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/Platform.java
@@ -1,10 +1,12 @@
package io.metersphere.plugin.platform.api;
+import org.pf4j.ExtensionPoint;
+
/**
* 平台对接相关业务接口
* @author jianxing.chen
*/
-public interface Platform {
+public interface Platform extends ExtensionPoint {
/**
* 校验服务集成配置
diff --git a/backend/framework/plugin/metersphere-plugin-sdk/pom.xml b/backend/framework/plugin/metersphere-plugin-sdk/pom.xml
index 7a9f1f6ead..5f19e2a421 100644
--- a/backend/framework/plugin/metersphere-plugin-sdk/pom.xml
+++ b/backend/framework/plugin/metersphere-plugin-sdk/pom.xml
@@ -44,5 +44,10 @@
com.fasterxml.jackson.core
jackson-annotations
+
+ org.pf4j
+ pf4j
+ ${pf4j.version}
+
\ No newline at end of file
diff --git a/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/AbstractMsPlugin.java b/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/AbstractMsPlugin.java
index 73c5b021b5..1905016925 100644
--- a/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/AbstractMsPlugin.java
+++ b/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/AbstractMsPlugin.java
@@ -1,6 +1,6 @@
package io.metersphere.plugin.sdk.api;
-public abstract class AbstractMsPlugin implements MsPlugin {
+public abstract class AbstractMsPlugin extends MsPlugin {
private static final String SCRIPT_DIR = "script";
@@ -12,14 +12,4 @@ public abstract class AbstractMsPlugin implements MsPlugin {
public String getScriptDir() {
return SCRIPT_DIR;
}
-
- @Override
- public String getName() {
- return getKey();
- }
-
- @Override
- public String getPluginId() {
- return getKey().toLowerCase() + "-" + getVersion();
- }
}
diff --git a/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/MsPlugin.java b/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/MsPlugin.java
index 623ab6b834..b5cf42e5bc 100644
--- a/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/MsPlugin.java
+++ b/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/MsPlugin.java
@@ -1,48 +1,28 @@
package io.metersphere.plugin.sdk.api;
+import org.pf4j.Plugin;
+
/**
* 插件的基本信息
*
* @author jianxing.chen
*/
-public interface MsPlugin {
+public abstract class MsPlugin extends Plugin {
/**
* @return 返回该插件是否是开源的,默认是
*/
- boolean isXpack();
-
- /**
- * @return 返回插件的类型
- * 目前支持接口插件和平台(API、PLATFORM)
- */
- String getType();
-
- /**
- * @return 返回插件的关键字,例如 Jira
- */
- String getKey();
+ abstract public boolean isXpack();
/**
* @return 返回插件的名称
* 默认返回 key
*/
- String getName();
-
- /**
- * @return 返回插件的ID
- * 默认是 key + 版本号
- */
- String getPluginId();
-
- /**
- * @return 返回插件的版本
- */
- String getVersion();
+ abstract public String getName();
/**
* @return 返回该加载前端配置文件的目录,默认是 script
* 可以重写定制
*/
- String getScriptDir();
+ abstract public String getScriptDir();
}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/RsaConfig.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/RsaConfig.java
index 9d7b0d0beb..48bf78fd33 100644
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/RsaConfig.java
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/RsaConfig.java
@@ -1,12 +1,12 @@
package io.metersphere.sdk.config;
+import io.metersphere.sdk.file.FileCenter;
import io.metersphere.sdk.file.FileRepository;
import io.metersphere.sdk.file.FileRequest;
import io.metersphere.sdk.log.constants.OperationLogModule;
import io.metersphere.sdk.util.RsaKey;
import io.metersphere.sdk.util.RsaUtil;
-import jakarta.annotation.Resource;
import org.apache.commons.lang3.SerializationUtils;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
@@ -14,8 +14,6 @@ import org.springframework.context.annotation.Configuration;
@Configuration
public class RsaConfig implements ApplicationRunner {
- @Resource
- private FileRepository fileRepository;
@Override
public void run(ApplicationArguments args) throws Exception {
@@ -23,7 +21,8 @@ public class RsaConfig implements ApplicationRunner {
request.setFileName("rsa.key");
request.setProjectId("system");
request.setResourceId(OperationLogModule.SYSTEM_PARAMETER_SETTING);
- //
+ FileRepository fileRepository = FileCenter.getDefaultRepository();
+
try {
byte[] file = fileRepository.getFile(request);
if (file != null) {
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PluginScenarioType.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PluginScenarioType.java
index fb240c13dd..0edfc5936f 100644
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PluginScenarioType.java
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PluginScenarioType.java
@@ -1,5 +1,16 @@
package io.metersphere.sdk.constants;
public enum PluginScenarioType {
- API, PLATFORM, JDBC_DRIVER
+ /**
+ * 接口协议插件
+ */
+ API_PROTOCOL,
+ /**
+ * 项目关联平台插件
+ */
+ PLATFORM,
+ /**
+ * jdbc 驱动插件
+ */
+ JDBC_DRIVER
}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/StorageConstants.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/StorageConstants.java
deleted file mode 100644
index e11ba695e6..0000000000
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/StorageConstants.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package io.metersphere.sdk.constants;
-
-public enum StorageConstants {
- MINIO, GIT, FILE_REF
-}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/StorageType.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/StorageType.java
new file mode 100644
index 0000000000..bdf25013ff
--- /dev/null
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/StorageType.java
@@ -0,0 +1,5 @@
+package io.metersphere.sdk.constants;
+
+public enum StorageType {
+ MINIO, GIT, FILE_REF, LOCAL
+}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/environment/EnvironmentController.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/environment/EnvironmentController.java
index d92fe186a3..be9a5768c2 100644
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/environment/EnvironmentController.java
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/environment/EnvironmentController.java
@@ -2,39 +2,30 @@ package io.metersphere.sdk.controller.environment;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.domain.Environment;
+import io.metersphere.sdk.dto.OptionDTO;
import io.metersphere.sdk.dto.environment.EnvironmentConfigRequest;
import io.metersphere.sdk.dto.environment.dataSource.DataSource;
-import io.metersphere.sdk.service.PluginLoadService;
import io.metersphere.sdk.service.environment.EnvironmentService;
import io.metersphere.sdk.util.SessionUtils;
import io.metersphere.validation.groups.Created;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
-import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
-import java.sql.Driver;
-import java.sql.DriverManager;
import java.util.List;
-import java.util.Map;
-import java.util.Properties;
@RestController
@RequestMapping(value = "/project/environment")
@Tag(name = "项目管理-环境")
public class EnvironmentController {
- @Resource
- private PluginLoadService pluginLoadService;
-
@Resource
private EnvironmentService environmentService;
-
@GetMapping("/list/{projectId}")
@Operation(summary = "项目管理-环境-环境目录-列表")
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ)
@@ -76,26 +67,13 @@ public class EnvironmentController {
@Operation(summary = "项目管理-环境-数据库配置-校验")
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ)
public void validate(@RequestBody DataSource databaseConfig) {
- try {
- if (StringUtils.isNotBlank(databaseConfig.getDriverId())) {
- ClassLoader classLoader = pluginLoadService.getClassLoader(databaseConfig.getDriverId());
- Driver driver = (Driver) classLoader.loadClass(databaseConfig.getDriver()).newInstance();
- Properties properties = new Properties();
- properties.setProperty("user", databaseConfig.getUsername());
- properties.setProperty("password", databaseConfig.getPassword());
- driver.connect(databaseConfig.getDbUrl(), properties);
- } else {
- DriverManager.getConnection(databaseConfig.getDbUrl(), databaseConfig.getUsername(), databaseConfig.getPassword());
- }
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
+ environmentService.validateDataSource(databaseConfig);
}
@GetMapping("/database/driver-options/{organizationId}")
@Operation(summary = "项目管理-环境-数据库配置-数据库驱动选项")
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ)
- public Map driverOptions(@PathVariable String organizationId) {
+ public List driverOptions(@PathVariable String organizationId) {
return environmentService.getDriverOptions(organizationId);
}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/environment/BodyFile.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/environment/BodyFile.java
index 2cefb2719d..7671117ca1 100644
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/environment/BodyFile.java
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/environment/BodyFile.java
@@ -1,7 +1,7 @@
package io.metersphere.sdk.dto.environment;
-import io.metersphere.sdk.constants.StorageConstants;
+import io.metersphere.sdk.constants.StorageType;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
@@ -20,6 +20,6 @@ public class BodyFile {
private String refResourceId;
public boolean isRef() {
- return StringUtils.equals(storage, StorageConstants.FILE_REF.name()) && StringUtils.isNotEmpty(fileId);
+ return StringUtils.equals(storage, StorageType.FILE_REF.name()) && StringUtils.isNotEmpty(fileId);
}
}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileCenter.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileCenter.java
index 68705afb7e..6aceb0d794 100644
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileCenter.java
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileCenter.java
@@ -1,18 +1,21 @@
package io.metersphere.sdk.file;
+import io.metersphere.sdk.constants.StorageType;
import io.metersphere.sdk.util.CommonBeanFactory;
+import java.util.HashMap;
+import java.util.Map;
+
public class FileCenter {
- // 多种实现时打开
- /*public static FileRepository getRepository(String storage) {
- if (StringUtils.equals(StorageConstants.GIT.name(), storage)) {
- LogUtils.info("扩展GIT存储方式");
- return null;
- } else {
- return getDefaultRepository();
- }
- }*/
+ public static FileRepository getRepository(StorageType storageType) {
+ Map repositoryMap = new HashMap<>() {{
+ put(StorageType.MINIO, CommonBeanFactory.getBean(MinioRepository.class));
+ put(StorageType.LOCAL, CommonBeanFactory.getBean(LocalFileRepository.class));
+ }};
+ FileRepository fileRepository = repositoryMap.get(storageType);
+ return fileRepository == null ? getDefaultRepository() : fileRepository;
+ }
public static FileRepository getDefaultRepository() {
return CommonBeanFactory.getBean(MinioRepository.class);
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/LocalFileRepository.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/LocalFileRepository.java
new file mode 100644
index 0000000000..3969a1dcc3
--- /dev/null
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/LocalFileRepository.java
@@ -0,0 +1,90 @@
+package io.metersphere.sdk.file;
+
+import io.metersphere.sdk.util.MsFileUtils;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.aspectj.util.FileUtil;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.util.List;
+
+@Component
+public class LocalFileRepository implements FileRepository {
+
+ @Override
+ public String saveFile(MultipartFile multipartFile, FileRequest request) throws IOException {
+ if (multipartFile == null || request == null || StringUtils.isEmpty(request.getFileName()) || StringUtils.isEmpty(request.getProjectId())) {
+ return null;
+ }
+ MsFileUtils.validateFileName(request.getProjectId(), request.getFileName());
+ createFileDir(request);
+ File file = new File(getFilePath(request));
+ FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), file);
+ return file.getPath();
+ }
+
+ private void createFileDir(FileRequest request) {
+ String dir = getFileDir(request);
+ File fileDir = new File(dir);
+ if (!fileDir.exists()) {
+ fileDir.mkdirs();
+ }
+ }
+
+ @Override
+ public String saveFile(byte[] bytes, FileRequest request) throws IOException {
+ File file = new File(getFilePath(request));
+ try (OutputStream ops = new FileOutputStream(file)) {
+ ops.write(bytes);
+ return file.getPath();
+ } catch (Exception e) {
+ throw e;
+ }
+ }
+
+ @Override
+ public void delete(FileRequest request) throws Exception {
+ String path = StringUtils.join(getFilePath(request));
+ File file = new File(path);
+ FileUtil.deleteContents(file);
+ if (file.exists()) {
+ file.delete();
+ }
+ }
+
+ @Override
+ public void deleteFolder(FileRequest request) throws Exception {
+ this.delete(request);
+ }
+
+ @Override
+ public byte[] getFile(FileRequest request) throws Exception {
+ File file = new File(getFilePath(request));
+ return Files.readAllBytes(file.toPath());
+ }
+
+ @Override
+ public InputStream getFileAsStream(FileRequest request) throws Exception {
+ return new FileInputStream(getFilePath(request));
+ }
+
+ @Override
+ public void downloadFile(FileRequest request, String localPath) {
+ }
+
+ @Override
+ public List getFolderFileNames(FileRequest request) {
+ return null;
+ }
+
+ private String getFilePath(FileRequest request) {
+ return StringUtils.join(getFileDir(request), "/", request.getFileName());
+ }
+
+ private String getFileDir(FileRequest request) {
+ return StringUtils.join(MsFileUtils.DATE_ROOT_DIR, "/", request.getProjectId());
+ }
+}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/JdbcDriverPluginDescriptor.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/JdbcDriverPluginDescriptor.java
new file mode 100644
index 0000000000..976b12b8c1
--- /dev/null
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/JdbcDriverPluginDescriptor.java
@@ -0,0 +1,47 @@
+package io.metersphere.sdk.plugin;
+
+import org.pf4j.DefaultPluginDescriptor;
+import org.pf4j.PluginDescriptor;
+
+/**
+ * @author jainxing
+ * 由于 DefaultPluginDescriptor 中的 set 方法是 protected 的,JdbcDriverPluginDescriptorFinder 无法访问
+ * 这里重写一下,让相同包名下的 JdbcDriverPluginDescriptorFinder 可以访问
+ */
+public class JdbcDriverPluginDescriptor extends DefaultPluginDescriptor {
+
+ @Override
+ protected DefaultPluginDescriptor setPluginId(String pluginId) {
+ return super.setPluginId(pluginId);
+ }
+
+ @Override
+ protected PluginDescriptor setPluginDescription(String pluginDescription) {
+ return super.setPluginDescription(pluginDescription);
+ }
+
+ @Override
+ protected PluginDescriptor setPluginClass(String pluginClassName) {
+ return super.setPluginClass(pluginClassName);
+ }
+
+ @Override
+ protected DefaultPluginDescriptor setPluginVersion(String version) {
+ return super.setPluginVersion(version);
+ }
+
+ @Override
+ protected PluginDescriptor setProvider(String provider) {
+ return super.setProvider(provider);
+ }
+
+ @Override
+ protected PluginDescriptor setDependencies(String dependencies) {
+ return super.setDependencies(dependencies);
+ }
+
+ @Override
+ protected PluginDescriptor setRequires(String requires) {
+ return super.setRequires(requires);
+ }
+}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/JdbcDriverPluginDescriptorFinder.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/JdbcDriverPluginDescriptorFinder.java
new file mode 100644
index 0000000000..1e1ba1502c
--- /dev/null
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/JdbcDriverPluginDescriptorFinder.java
@@ -0,0 +1,85 @@
+package io.metersphere.sdk.plugin;
+
+import org.apache.commons.io.IOUtils;
+import org.pf4j.ManifestPluginDescriptorFinder;
+import org.pf4j.PluginDescriptor;
+import org.pf4j.PluginRuntimeException;
+import org.pf4j.util.FileUtils;
+import org.pf4j.util.StringUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+/**
+ * @author jianxing
+ * 支持解析 jdbc 驱动中的 MANIFEST.MF
+ * 如果使用 ManifestPluginDescriptorFinder 解析,由于 jdbc 驱动没有按照 pf4j 的规范打包
+ * 无法解析出 PluginDescriptor ,这里重写后,按照自定义的规则解析
+ */
+public class JdbcDriverPluginDescriptorFinder extends ManifestPluginDescriptorFinder {
+
+ private String driverClass;
+
+ @Override
+ public boolean isApplicable(Path pluginPath) {
+ return Files.exists(pluginPath, new LinkOption[0]) && (Files.isDirectory(pluginPath, new LinkOption[0]) || FileUtils.isZipOrJarFile(pluginPath))
+ && hasDriverFile(pluginPath); // 这里前判断时候包含 META-INF/services/java.sql.Driver 文件,不包含则走 ManifestPluginDescriptorFinder
+ }
+
+ /**
+ * 判断是否有SPI的驱动实现类
+ * @param jarPath
+ * @return
+ */
+ protected boolean hasDriverFile(Path jarPath) {
+ try (JarFile jar = new JarFile(jarPath.toFile())) {
+ JarEntry jarEntry = jar.getJarEntry("META-INF/services/java.sql.Driver");
+ if (jarEntry == null) {
+ return false;
+ }
+ InputStream inputStream = jar.getInputStream(jarEntry);
+ // 获取SPI中定义的类名
+ driverClass = IOUtils.toString(inputStream);
+ return true;
+ } catch (IOException e) {
+ throw new PluginRuntimeException(e, "Cannot read META-INF/services/java.sql.Driver from {}", jarPath);
+ }
+ }
+
+ @Override
+ protected PluginDescriptor createPluginDescriptor(Manifest manifest) {
+ JdbcDriverPluginDescriptor pluginDescriptor = this.createJdbcDriverPluginDescriptorInstance();
+ Attributes attributes = manifest.getMainAttributes();
+ // 将类名作为ID
+ String id = driverClass;
+ pluginDescriptor.setPluginId(id.split("\n")[0]);
+ if (StringUtils.isNullOrEmpty(id)) {
+ return null;
+ }
+ String description = attributes.getValue("Plugin-Description");
+ if (StringUtils.isNullOrEmpty(description)) {
+ pluginDescriptor.setPluginDescription("");
+ } else {
+ pluginDescriptor.setPluginDescription(description);
+ }
+ String version = attributes.getValue("Implementation-Version");
+ if (StringUtils.isNotNullOrEmpty(version)) {
+ pluginDescriptor.setPluginVersion(version);
+ }
+
+ String provider = attributes.getValue("Implementation-Vendor");
+ pluginDescriptor.setProvider(provider);
+ return pluginDescriptor;
+ }
+
+ protected JdbcDriverPluginDescriptor createJdbcDriverPluginDescriptorInstance() {
+ return new JdbcDriverPluginDescriptor();
+ }
+}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/JdbcDriverServiceProviderExtensionFinder.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/JdbcDriverServiceProviderExtensionFinder.java
new file mode 100644
index 0000000000..37328a2ed8
--- /dev/null
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/JdbcDriverServiceProviderExtensionFinder.java
@@ -0,0 +1,142 @@
+package io.metersphere.sdk.plugin;
+
+
+import io.metersphere.sdk.util.LogUtils;
+import org.pf4j.PluginClassLoader;
+import org.pf4j.PluginManager;
+import org.pf4j.PluginWrapper;
+import org.pf4j.ServiceProviderExtensionFinder;
+import org.pf4j.processor.ExtensionStorage;
+import org.pf4j.processor.ServiceProviderExtensionStorage;
+import org.pf4j.util.FileUtils;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.*;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.*;
+
+/**
+ * @author jianxing
+ * 支持加载 jdbc 驱动
+ * pf4j 中 ServiceProviderExtensionFinder 本身是支持 SPI
+ * 默认会读取 META-INF/services 下的文件
+ * 但是遍历 JarEntry 发现 jdbc 资源中没有 META-INF/services 只有 META-INF/services/java.sql.Driver
+ * 所以使用默认的 ServiceProviderExtensionFinder 会无法加载,这里重写后只修改了 EXTENSIONS_RESOURCE
+ */
+public class JdbcDriverServiceProviderExtensionFinder extends ServiceProviderExtensionFinder {
+
+ // 重写后只修改了这个常量
+ public static final String EXTENSIONS_RESOURCE = ServiceProviderExtensionStorage.EXTENSIONS_RESOURCE + "/java.sql.Driver";
+
+ public JdbcDriverServiceProviderExtensionFinder(PluginManager pluginManager) {
+ super(pluginManager);
+ }
+
+ @Override
+ public Map> readClasspathStorages() {
+ LogUtils.debug("Reading extensions storages from classpath");
+ Map> result = new LinkedHashMap<>();
+
+ final Set bucket = new HashSet<>();
+ try {
+ Enumeration urls = getClass().getClassLoader().getResources(EXTENSIONS_RESOURCE);
+ if (urls.hasMoreElements()) {
+ collectExtensions(urls, bucket);
+ } else {
+ LogUtils.debug("Cannot find '{}'", EXTENSIONS_RESOURCE);
+ }
+
+ debugExtensions(bucket);
+
+ result.put(null, bucket);
+ } catch (IOException | URISyntaxException e) {
+ LogUtils.error(e.getMessage(), e);
+ }
+
+ return result;
+ }
+
+ @Override
+ public Map> readPluginsStorages() {
+ LogUtils.debug("Reading extensions storages from plugins");
+ Map> result = new LinkedHashMap<>();
+
+ List plugins = pluginManager.getPlugins();
+ for (PluginWrapper plugin : plugins) {
+ String pluginId = plugin.getDescriptor().getPluginId();
+ LogUtils.debug("Reading extensions storages for plugin '{}'", pluginId);
+ final Set bucket = new HashSet<>();
+
+ try {
+ Enumeration urls = ((PluginClassLoader) plugin.getPluginClassLoader()).findResources(ServiceProviderExtensionStorage.EXTENSIONS_RESOURCE);
+ if (urls.hasMoreElements()) {
+ // 如果 ServiceProviderExtensionFinder 无法从 "META-INF/services" 加载,才加载
+ return result;
+ }
+
+ urls = ((PluginClassLoader) plugin.getPluginClassLoader()).findResources(EXTENSIONS_RESOURCE);
+ if (urls.hasMoreElements()) {
+ collectExtensions(urls, bucket);
+ } else {
+ LogUtils.debug("Cannot find '{}'", EXTENSIONS_RESOURCE);
+ }
+
+ debugExtensions(bucket);
+
+ result.put(pluginId, bucket);
+ } catch (IOException | URISyntaxException e) {
+ LogUtils.error(e.getMessage(), e);
+ }
+ }
+
+ return result;
+ }
+
+ private void collectExtensions(Enumeration urls, Set bucket) throws URISyntaxException, IOException {
+ while (urls.hasMoreElements()) {
+ URL url = urls.nextElement();
+ LogUtils.debug("Read '{}'", url.getFile());
+ collectExtensions(url, bucket);
+ }
+ }
+
+ private void collectExtensions(URL url, Set bucket) throws URISyntaxException, IOException {
+ Path extensionPath;
+
+ if (url.toURI().getScheme().equals("jar")) {
+ extensionPath = FileUtils.getPath(url.toURI(), EXTENSIONS_RESOURCE);
+ } else {
+ extensionPath = Paths.get(url.toURI());
+ }
+
+ try {
+ bucket.addAll(readExtensions(extensionPath));
+ } finally {
+ FileUtils.closePath(extensionPath);
+ }
+ }
+
+ private Set readExtensions(Path extensionPath) throws IOException {
+ final Set result = new HashSet<>();
+ Files.walkFileTree(extensionPath, Collections.emptySet(), 1, new SimpleFileVisitor() {
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ LogUtils.debug("Read '{}'", file);
+ try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
+ ExtensionStorage.read(reader, result);
+ }
+
+ return FileVisitResult.CONTINUE;
+ }
+
+ });
+
+ return result;
+ }
+
+}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/MsPluginManager.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/MsPluginManager.java
new file mode 100644
index 0000000000..f266068e3c
--- /dev/null
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/MsPluginManager.java
@@ -0,0 +1,27 @@
+package io.metersphere.sdk.plugin;
+
+import org.pf4j.*;
+
+/**
+ * @author jianxing
+ * 为了支持加载使用 SPI 机制加载的 jdbc 驱动
+ * 这里加入自定义的 JdbcDriverServiceProviderExtensionFinder 和 JdbcDriverPluginDescriptorFinder
+ */
+public class MsPluginManager extends DefaultPluginManager {
+ @Override
+ protected ExtensionFinder createExtensionFinder() {
+ DefaultExtensionFinder extensionFinder = (DefaultExtensionFinder) super.createExtensionFinder();
+ extensionFinder.addServiceProviderExtensionFinder();
+ extensionFinder.add(new JdbcDriverServiceProviderExtensionFinder(this));
+ return extensionFinder;
+ }
+
+ @Override
+ protected PluginDescriptorFinder createPluginDescriptorFinder() {
+ // 需要保证 JdbcDriverPluginDescriptorFinder 在 ManifestPluginDescriptorFinder 之前解析
+ return (new CompoundPluginDescriptorFinder())
+ .add(new PropertiesPluginDescriptorFinder())
+ .add(new JdbcDriverPluginDescriptorFinder())
+ .add(new ManifestPluginDescriptorFinder());
+ }
+}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PlatformPluginManager.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PlatformPluginManager.java
deleted file mode 100644
index e425ef9dea..0000000000
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PlatformPluginManager.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.metersphere.sdk.plugin.loader;
-
-import io.metersphere.plugin.platform.api.Platform;
-import io.metersphere.plugin.platform.dto.PlatformRequest;
-
-/**
- * @author jianxing.chen
- */
-public class PlatformPluginManager extends PluginManager {
-
-
-
- /**
- * 获取对应插件的 Platform 对象
- * @param pluginId 插件ID
- * @param request
- * @return
- */
- public Platform getPlatform(String pluginId, PlatformRequest request) {
- return getImplInstance(pluginId, Platform.class, request);
- }
-
-}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginClassLoader.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginClassLoader.java
deleted file mode 100644
index dad09a64f3..0000000000
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginClassLoader.java
+++ /dev/null
@@ -1,230 +0,0 @@
-package io.metersphere.sdk.plugin.loader;
-
-import io.metersphere.sdk.plugin.storage.StorageStrategy;
-import io.metersphere.sdk.util.LogUtils;
-import org.apache.commons.lang3.StringUtils;
-
-import java.io.*;
-import java.util.*;
-import java.util.jar.JarEntry;
-import java.util.jar.JarFile;
-import java.util.jar.JarInputStream;
-
-/**
- * @author jianxing.chen
- */
-public class PluginClassLoader extends ClassLoader {
-
- /**
- * 记录加载的类
- */
- protected final Set clazzSet = new HashSet<>();
- /**
- * 加载重试次数
- */
- protected final static int CLASS_RELOAD_TIME = 20;
- /**
- * 保存加载失败的类,之后重试
- */
- protected Map loadErrorMap = new HashMap<>();
-
- private class ByteArrayWrapper {
- private byte[] values;
-
- public ByteArrayWrapper(byte[] values) {
- this.values = values;
- }
-
- public byte[] getValues() {
- return values;
- }
- }
-
- public Set getClazzSet() {
- return clazzSet;
- }
-
- /**
- * jar包的静态资源的存储策略
- * 可以扩展为对象存储
- */
- protected StorageStrategy storageStrategy;
-
- protected boolean isNeedUploadFile;
-
- public PluginClassLoader() {
- // 将父加载器设置成当前的类加载器,目的是由父加载器加载接口,实现类由该加载器加载
- super(PluginClassLoader.class.getClassLoader());
- }
-
- public PluginClassLoader(StorageStrategy storageStrategy) {
- this(storageStrategy, true);
- }
-
- public PluginClassLoader(StorageStrategy storageStrategy, boolean isNeedUploadFile) {
- this();
- this.storageStrategy = storageStrategy;
- this.isNeedUploadFile = isNeedUploadFile;
- }
-
- public StorageStrategy getStorageStrategy() {
- return storageStrategy;
- }
-
- /**
- * 扫描目录或 jar 包
- * 加载 clazz
- *
- * @param file
- */
- protected void scanJarFile(File file) throws IOException {
- if (file.exists()) {
- if (file.isFile() && file.getName().endsWith(".jar")) {
- try {
- readJar(new JarFile(file));
- } catch (IOException e) {
- throw e;
- }
- } else if (file.isDirectory()) {
- for (File f : file.listFiles()) {
- scanJarFile(f);
- }
- }
- }
- }
-
- /**
- * 加载对应目录下的 jar 包
- *
- * @param jarfileDir
- */
- public void loadJar(String jarfileDir) throws IOException {
- if (StringUtils.isBlank(jarfileDir)) {
- throw new IllegalArgumentException("basePath can not be empty!");
- }
- File dir = new File(jarfileDir);
- if (!dir.exists()) {
- throw new IllegalArgumentException("basePath not exists:" + jarfileDir);
- }
- scanJarFile(new File(jarfileDir));
- }
-
- /**
- * 从输入流中加载 jar 包
- *
- * @param in
- */
- public void loadJar(InputStream in) throws Exception {
- if (in != null) {
- try (JarInputStream jis = new JarInputStream(in)) {
- JarEntry je;
- while ((je = jis.getNextJarEntry()) != null) {
- loadJar(jis, je);
- }
- reloadErrorClazz();
- } catch (IOException e) {
- throw e;
- }
- }
- }
-
- /**
- * 读取 jar 包中的 clazz
- *
- * @param jar
- * @throws IOException
- */
- protected void readJar(JarFile jar) {
- Enumeration en = jar.entries();
- while (en.hasMoreElements()) {
- JarEntry je = en.nextElement();
- try (InputStream in = jar.getInputStream(je)) {
- loadJar(in, je);
- } catch (Exception e) {
- LogUtils.error(e);
- }
- }
- reloadErrorClazz();
- }
-
- /**
- * 加载 jar 包的 class,并存储静态资源
- *
- * @param in
- * @param je
- * @throws IOException
- */
- protected void loadJar(InputStream in, JarEntry je) throws Exception {
- je.getName();
- String name = je.getName();
- if (name.endsWith(".class")) {
- String className = name.replace("\\", ".")
- .replace("/", ".")
- .replace(".class", "");
- BufferedInputStream bis;
- byte[] bytes = null;
- try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
- int line;
- bytes = new byte[1024];
- bis = new BufferedInputStream(in);
- while ((line = bis.read(bytes)) != -1) {
- bos.write(bytes, 0, line);
- }
- bos.flush();
- bytes = bos.toByteArray();
- Class> clazz = defineClass(className, bytes, 0, bytes.length);
- clazzSet.add(clazz);
- } catch (NoClassDefFoundError e) {
- loadErrorMap.put(className, new ByteArrayWrapper(bytes));
- } catch (Throwable e) {
- LogUtils.error(e);
- }
- } else if (!name.endsWith("/")) {
- // 非目录即静态资源
- if (storageStrategy != null && isNeedUploadFile) {
- storageStrategy.store(name, in);
- }
- }
- }
-
- /**
- * 由于 loadJar 中是按照条目加载的
- * 加载顺序不确定,如果父类没有先加载会加载失败
- * 这里针对加载失败的类,再次加载
- */
- private synchronized void reloadErrorClazz() {
- for (int i = 0; i < CLASS_RELOAD_TIME; i++) {
- Iterator iterator = loadErrorMap.keySet().iterator();
- while (iterator.hasNext()) {
- String className = iterator.next();
- try {
- LogUtils.info("reload class: " + className);
- byte[] bytes = loadErrorMap.get(className).getValues();
- Class> clazz = defineClass(className, bytes, 0, bytes.length);
- clazzSet.add(clazz);
- iterator.remove();
- } catch (Throwable e) {
- LogUtils.error(e);
- }
- }
- }
- }
-
- /**
- * 从存储策略中加载静态资源
- * @param name
- * @return
- */
- @Override
- public InputStream getResourceAsStream(String name) {
- if (null != storageStrategy) {
- try {
- return storageStrategy.get(name);
- } catch (Exception e) {
- LogUtils.error(e);
- return null;
- }
- }
- return super.getResourceAsStream(name);
- }
-}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java
deleted file mode 100644
index dabbbc4ca2..0000000000
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java
+++ /dev/null
@@ -1,167 +0,0 @@
-package io.metersphere.sdk.plugin.loader;
-
-import io.metersphere.sdk.controller.handler.result.CommonResultCode;
-import io.metersphere.sdk.exception.MSException;
-import io.metersphere.sdk.plugin.storage.StorageStrategy;
-import io.metersphere.sdk.util.LogUtils;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Type;
-import java.util.*;
-
-/**
- * @author jianxing.chen
- */
-public class PluginManager {
-
- /**
- * 自定义类加载器
- */
- protected Map classLoaderMap = new HashMap<>();
-
- /**
- * 缓存查找过的类
- * 内层 map
- * key 未接口的类
- * value 为实现类
- */
- protected Map> implClassCache = new HashMap<>();
-
- public PluginClassLoader getClassLoader(String pluginId) {
- return classLoaderMap.get(pluginId);
- }
-
- /**
- * 加载对应目录下的 jar 包
- */
- public PluginManager loadJar(String pluginId, String jarfileDir, StorageStrategy storageStrategy) throws IOException {
- PluginClassLoader pluginClassLoader = new PluginClassLoader(storageStrategy);
- classLoaderMap.put(pluginId, pluginClassLoader);
- pluginClassLoader.loadJar(jarfileDir);
- return this;
- }
-
- public PluginManager loadJar(String pluginId, String jarfileDir) throws IOException {
- return this.loadJar(pluginId, jarfileDir, null);
- }
-
- public Map getClassLoaderMap() {
- return classLoaderMap;
- }
-
- public void deletePlugin(String id) {
- classLoaderMap.remove(id);
- implClassCache.remove(id);
- }
-
- /**
- * 从输入流中加载 jar 包
- *
- * @param in
- */
- public PluginManager loadJar(String pluginId, InputStream in, StorageStrategy storageStrategy, boolean isNeedUploadFile) throws Exception {
- PluginClassLoader pluginClassLoader = new PluginClassLoader(storageStrategy, isNeedUploadFile);
- classLoaderMap.put(pluginId, pluginClassLoader);
- pluginClassLoader.loadJar(in);
- return this;
- }
-
- public PluginManager loadJar(String pluginId, InputStream in, boolean isNeedUploadFile) throws Exception {
- return this.loadJar(pluginId, in, null, isNeedUploadFile);
- }
-
- /**
- * 获取接口的单一实现类
- */
- public Class getImplClass(String pluginId, Class superClazz) {
- PluginClassLoader classLoader = getPluginClassLoader(pluginId);
- Map classes = implClassCache.get(pluginId);
- if (classes == null) {
- classes = new HashMap<>();
- implClassCache.put(pluginId, classes);
- }
- if (classes.get(superClazz) != null) {
- return classes.get(superClazz);
- }
- LinkedHashSet> result = new LinkedHashSet<>();
- Set clazzSet = classLoader.getClazzSet();
- for (Class item : clazzSet) {
- if (isImplClazz(superClazz, item) && !result.contains(item)) {
- classes.put(superClazz, item);
- return item;
- }
- }
- return null;
- }
-
- private PluginClassLoader getPluginClassLoader(String pluginId) {
- PluginClassLoader classLoader = classLoaderMap.get(pluginId);
- if (classLoader == null) {
- throw new MSException("插件未加载");
- }
- return classLoader;
- }
-
- /**
- * 获取指定接口最后一次加载的实现类实例
- */
- public T getImplInstance(String pluginId, Class superClazz) {
- return this.getImplInstance(pluginId, superClazz, null);
- }
-
- public T getImplInstance(String pluginId, Class superClazz, Object param) {
- try {
- Class clazz = getImplClass(pluginId, superClazz);
- if (clazz == null) {
- throw new MSException(CommonResultCode.PLUGIN_GET_INSTANCE);
- }
- if (param == null) {
- return clazz.getConstructor().newInstance();
- } else {
- return clazz.getConstructor(param.getClass()).newInstance(param);
- }
- } catch (InvocationTargetException e) {
- LogUtils.error(e.getTargetException());
- throw new MSException(CommonResultCode.PLUGIN_GET_INSTANCE, e.getTargetException().getMessage());
- } catch (Exception e) {
- LogUtils.error(e);
- throw new MSException(CommonResultCode.PLUGIN_GET_INSTANCE, e.getMessage());
- }
- }
-
-
- /**
- * 判断 impClazz 是否是 superClazz 的实现类
- *
- * @param impClazz
- * @return
- */
- private boolean isImplClazz(Class superClazz, Class impClazz) {
- if (impClazz == superClazz) {
- return true;
- }
- try {
- Type[] interfaces = impClazz.getGenericInterfaces();
-
- if (interfaces != null && interfaces.length > 0) {
- for (Type genericInterface : interfaces) {
- if (genericInterface instanceof Class && isImplClazz(superClazz, (Class) genericInterface)) {
- return true;
- }
- }
- }
- Type superclass = impClazz.getGenericSuperclass();
- if (superclass != null
- && superclass instanceof Class
- && isImplClazz(superClazz, (Class) superclass)) {
- return true;
-
- }
- } catch (Throwable e) {
- LogUtils.error(e);
- }
- return false;
- }
-}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MsStorageStrategy.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MsStorageStrategy.java
deleted file mode 100644
index 959b949afb..0000000000
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MsStorageStrategy.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package io.metersphere.sdk.plugin.storage;
-
-import io.metersphere.sdk.file.FileCenter;
-import io.metersphere.sdk.file.FileRepository;
-import io.metersphere.sdk.file.FileRequest;
-import org.apache.commons.lang3.StringUtils;
-
-import java.io.InputStream;
-import java.util.List;
-
-/**
- * jar包静态资源存储策略
- * @author jianxing
- */
-public class MsStorageStrategy implements StorageStrategy {
-
-
- private final FileRepository fileRepository;
- private final String pluginId;
-
- public static final String DIR_PATH = "system/plugin";
-
- public MsStorageStrategy(String pluginId) {
- this.pluginId = pluginId;
- fileRepository = FileCenter.getDefaultRepository();
- }
-
- @Override
- public String store(String name, InputStream in) throws Exception {
- FileRequest request = getFileRequest(name);
- return fileRepository.saveFile(in.readAllBytes(), request);
- }
-
- @Override
- public InputStream get(String name) throws Exception {
- FileRequest request = getFileRequest(name);
- return fileRepository.getFileAsStream(request);
- }
-
- @Override
- public List getFolderFileNames(String dirName) throws Exception {
- FileRequest request = getFileRequest(dirName);
- List fileNames = fileRepository.getFolderFileNames(request);
- return fileNames.stream().map(s -> s.replace(getPluginDir(), StringUtils.EMPTY)).toList();
- }
-
- @Override
- public void delete() throws Exception {
- FileRequest request = new FileRequest();
- request.setProjectId(getPluginDir());
- fileRepository.deleteFolder(request);
- }
-
- private FileRequest getFileRequest(String name) {
- FileRequest request = new FileRequest();
- request.setProjectId(getPluginDir());
- request.setFileName(name);
- return request;
- }
-
- private String getPluginDir() {
- return DIR_PATH + "/" + this.pluginId;
- }
-}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/StorageStrategy.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/StorageStrategy.java
deleted file mode 100644
index 5ce880f51f..0000000000
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/StorageStrategy.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package io.metersphere.sdk.plugin.storage;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.List;
-
-/**
- * jar包、图片、前端配置文件等静态资源存储策略
- * @author jianxing
- */
-public interface StorageStrategy {
-
- /**
- * 存储文件
- * @param name
- * @param in
- * @return
- * @throws IOException
- */
- String store(String name, InputStream in) throws Exception;
-
- /**
- * 获取文件
- * @param path
- * @return
- * @throws IOException
- */
- InputStream get(String path) throws Exception;
-
- /**
- * 获取指定文件夹下的文件名列表
- *
- * @param dirName
- * @throws Exception
- */
- List getFolderFileNames(String dirName) throws Exception;
-
- /**
- * 删除文件
- * @throws IOException
- */
- void delete() throws Exception;
-}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/BasePluginService.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/BasePluginService.java
index b262d05efc..7df63a16c5 100644
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/BasePluginService.java
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/BasePluginService.java
@@ -1,13 +1,21 @@
package io.metersphere.sdk.service;
+import io.metersphere.sdk.constants.PluginScenarioType;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.system.domain.Plugin;
+import io.metersphere.system.domain.PluginExample;
+import io.metersphere.system.domain.PluginOrganization;
import io.metersphere.system.mapper.PluginMapper;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
import static io.metersphere.sdk.controller.handler.result.CommonResultCode.PLUGIN_ENABLE;
import static io.metersphere.sdk.controller.handler.result.CommonResultCode.PLUGIN_PERMISSION;
@@ -31,4 +39,33 @@ public class BasePluginService {
throw new MSException(PLUGIN_PERMISSION);
}
}
+
+ public Plugin get(String pluginId) {
+ return pluginMapper.selectByPrimaryKey(pluginId);
+ }
+
+ public List getOrgEnabledPlugins(String orgId, PluginScenarioType pluginScenarioType) {
+ List plugins = getEnabledPlugins(pluginScenarioType);
+ List unGlobalIds = plugins.stream().filter(i -> !i.getGlobal()).map(Plugin::getId).toList();
+ // 如果没有非全局,直接返回全局插件
+ if (CollectionUtils.isEmpty(unGlobalIds)) {
+ return plugins;
+ }
+ // 查询当前组织下的插件列表
+ List pluginOrganizations = basePluginOrganizationService.getByPluginIds(unGlobalIds);
+ Set orgPluginIdSet = pluginOrganizations.stream()
+ .filter(i -> StringUtils.equals(i.getOrganizationId(), orgId))
+ .map(PluginOrganization::getPluginId)
+ .collect(Collectors.toSet());
+ // 返回全局插件和当前组织下的插件
+ return plugins.stream().filter(i -> i.getGlobal() || orgPluginIdSet.contains(i.getId())).collect(Collectors.toList());
+ }
+
+ public List getEnabledPlugins(PluginScenarioType PluginScenarioType) {
+ PluginExample example = new PluginExample();
+ example.createCriteria()
+ .andEnableEqualTo(true)
+ .andScenarioEqualTo(PluginScenarioType.name());
+ return pluginMapper.selectByExample(example);
+ }
}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/JdbcDriverPluginService.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/JdbcDriverPluginService.java
index aec45e3afe..543193977f 100644
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/JdbcDriverPluginService.java
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/JdbcDriverPluginService.java
@@ -1,14 +1,17 @@
package io.metersphere.sdk.service;
import io.metersphere.sdk.constants.PluginScenarioType;
+import io.metersphere.sdk.dto.OptionDTO;
+import io.metersphere.sdk.exception.MSException;
+import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.domain.Plugin;
-import io.metersphere.system.domain.PluginExample;
-import io.metersphere.system.mapper.PluginMapper;
import jakarta.annotation.Resource;
+import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.sql.Driver;
+import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@@ -19,25 +22,71 @@ public class JdbcDriverPluginService {
@Resource
private PluginLoadService pluginLoadService;
@Resource
- private PluginMapper pluginMapper;
+ private BasePluginService basePluginService;
+ public static final String MYSQL_DRIVER_CLASS_NAME = "com.mysql.cj.jdbc.Driver";
+ public static final String DRIVER_OPTION_SEPARATOR = "&";
+ public static final String SYSTEM_PLUGIN_ID = "system";
- public boolean isJdbcDriver(String pluginId) {
- return pluginLoadService.getImplClass(pluginId, Driver.class) != null;
+
+ public List getJdbcDriverClass(String orgId) {
+ List plugins = basePluginService.getOrgEnabledPlugins(orgId, PluginScenarioType.JDBC_DRIVER);
+ List drivers = new ArrayList<>(10);
+ for (Plugin plugin : plugins) {
+ drivers.addAll(pluginLoadService.getMsPluginManager().getExtensionClasses(Driver.class, plugin.getId()));
+ }
+
+ List pluginDriverClassNames = drivers.stream()
+ .map(Class::getName)
+ .collect(Collectors.toList());
+ if (!pluginDriverClassNames.contains(MYSQL_DRIVER_CLASS_NAME)) {
+ // 如果不包含新版本 mysql 的驱动,则添加内置的 mysql 驱动
+ pluginDriverClassNames.add(MYSQL_DRIVER_CLASS_NAME);
+ }
+ return pluginDriverClassNames;
}
/**
- * 获取所有的 JDBC 驱动的实现类
+ * 获取所有的 JDBC 驱动的实现类选项
* @return
*/
- public List getJdbcDriverClass() {
- PluginExample example = new PluginExample();
- example.createCriteria().andScenarioEqualTo(PluginScenarioType.JDBC_DRIVER.name());
- List plugins = pluginMapper.selectByExample(example);
- List pluginDriverClassNames = plugins.stream().map(plugin ->
- pluginLoadService.getImplClass(plugin.getId(), Driver.class)
- ).map(Class::getName).collect(Collectors.toList());
- // 已经内置了 mysql 依赖
- pluginDriverClassNames.add("com.mysql.jdbc.Driver");
- return pluginDriverClassNames;
+ public List getJdbcDriverOption(String orgId) {
+ List plugins = basePluginService.getOrgEnabledPlugins(orgId, PluginScenarioType.JDBC_DRIVER);
+ List options = new ArrayList<>();
+ for (Plugin plugin : plugins) {
+ List> extensionClasses = pluginLoadService.getMsPluginManager().getExtensionClasses(Driver.class, plugin.getId());
+ extensionClasses.forEach(driver -> {
+ options.add(new OptionDTO(plugin.getId() + DRIVER_OPTION_SEPARATOR + driver.getName(), driver.getName()));
+ });
+ }
+ List pluginDriverClassNames = options.stream().map(OptionDTO::getName).toList();
+ if (!pluginDriverClassNames.contains(MYSQL_DRIVER_CLASS_NAME)) {
+ // 如果不包含新版本 mysql 的驱动,则添加内置的 mysql 驱动
+ options.add(new OptionDTO(SYSTEM_PLUGIN_ID + DRIVER_OPTION_SEPARATOR + MYSQL_DRIVER_CLASS_NAME, MYSQL_DRIVER_CLASS_NAME));
+ }
+ return options;
+ }
+
+ public Plugin wrapperPlugin(Plugin plugin) {
+ plugin.setScenario(PluginScenarioType.JDBC_DRIVER.name());
+ plugin.setXpack(false);
+ return plugin;
+ }
+
+ public Driver getDriverByOptionId(String driverId) {
+ String[] split = driverId.split(DRIVER_OPTION_SEPARATOR);
+ String pluginId = split[0];
+ String className = split[1];
+ if (StringUtils.equals(pluginId, SYSTEM_PLUGIN_ID)) {
+ try {
+ return (Driver) Class.forName(MYSQL_DRIVER_CLASS_NAME).getConstructor().newInstance();
+ } catch (Exception e) {
+ LogUtils.error(e);
+ throw new MSException(e);
+ }
+ }
+ List extensions = pluginLoadService.getMsPluginManager().getExtensions(Driver.class, pluginId);
+ return extensions.stream().filter(driver -> StringUtils.equals(driver.getClass().getName(), className))
+ .findFirst()
+ .orElseThrow(() -> new MSException("未找到对应的驱动"));
}
}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PlatformPluginService.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PlatformPluginService.java
index 2d49c0906a..690b4bb792 100644
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PlatformPluginService.java
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PlatformPluginService.java
@@ -3,18 +3,15 @@ package io.metersphere.sdk.service;
import io.metersphere.plugin.platform.api.Platform;
import io.metersphere.plugin.platform.dto.PlatformRequest;
import io.metersphere.sdk.constants.PluginScenarioType;
-import io.metersphere.system.domain.*;
-import io.metersphere.system.mapper.PluginMapper;
+import io.metersphere.system.domain.Plugin;
+import io.metersphere.system.domain.ServiceIntegration;
+import io.metersphere.system.domain.ServiceIntegrationExample;
import io.metersphere.system.mapper.ServiceIntegrationMapper;
import jakarta.annotation.Resource;
-import org.apache.commons.collections.CollectionUtils;
-import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
@Service
@Transactional(rollbackFor = Exception.class)
@@ -25,10 +22,6 @@ public class PlatformPluginService {
@Resource
private ServiceIntegrationMapper serviceIntegrationMapper;
@Resource
- private PluginMapper pluginMapper;
- @Resource
- private BasePluginOrganizationService basePluginOrganizationService;
- @Resource
private BasePluginService basePluginService;
/**
@@ -44,12 +37,10 @@ public class PlatformPluginService {
PlatformRequest pluginRequest = new PlatformRequest();
pluginRequest.setIntegrationConfig(integrationConfig);
pluginRequest.setOrganizationId(orgId);
- return pluginLoadService.getImplInstance(pluginId, Platform.class, pluginRequest);
+ return pluginLoadService.getImplInstance(Platform.class, pluginId, pluginRequest);
}
public Platform getPlatform(String pluginId, String orgId) {
- // 这里会校验插件是否存在
- pluginLoadService.getMsPluginInstance(pluginId);
ServiceIntegration serviceIntegration = getServiceIntegrationByPluginId(pluginId);
return getPlatform(pluginId, orgId, new String(serviceIntegration.getConfiguration()));
}
@@ -60,28 +51,7 @@ public class PlatformPluginService {
return serviceIntegrationMapper.selectByExampleWithBLOBs(example).get(0);
}
- public List getEnabledPlatformPlugins() {
- PluginExample example = new PluginExample();
- example.createCriteria()
- .andEnableEqualTo(true)
- .andScenarioEqualTo(PluginScenarioType.PLATFORM.name());
- return pluginMapper.selectByExample(example);
- }
-
public List getOrgEnabledPlatformPlugins(String orgId) {
- List plugins = getEnabledPlatformPlugins();
- List unGlobalIds = plugins.stream().filter(i -> !i.getGlobal()).map(Plugin::getId).toList();
- // 如果没有非全局,直接返回全局插件
- if (CollectionUtils.isEmpty(unGlobalIds)) {
- return plugins;
- }
- // 查询当前组织下的插件列表
- List pluginOrganizations = basePluginOrganizationService.getByPluginIds(unGlobalIds);
- Set orgPluginIdSet = pluginOrganizations.stream()
- .filter(i -> StringUtils.equals(i.getOrganizationId(), orgId))
- .map(PluginOrganization::getPluginId)
- .collect(Collectors.toSet());
- // 返回全局插件和当前组织下的插件
- return plugins.stream().filter(i -> i.getGlobal() || orgPluginIdSet.contains(i.getId())).collect(Collectors.toList());
+ return basePluginService.getOrgEnabledPlugins(orgId, PluginScenarioType.PLATFORM);
}
}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java
index 922caa5669..dd69629313 100644
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java
@@ -1,33 +1,39 @@
package io.metersphere.sdk.service;
-import io.metersphere.plugin.platform.api.AbstractPlatformPlugin;
import io.metersphere.plugin.sdk.api.MsPlugin;
+import io.metersphere.sdk.constants.StorageType;
+import io.metersphere.sdk.controller.handler.result.CommonResultCode;
import io.metersphere.sdk.exception.MSException;
-import io.metersphere.sdk.plugin.loader.PluginClassLoader;
-import io.metersphere.sdk.plugin.loader.PluginManager;
-import io.metersphere.sdk.plugin.storage.MsStorageStrategy;
-import io.metersphere.sdk.plugin.storage.StorageStrategy;
+import io.metersphere.sdk.file.FileCenter;
+import io.metersphere.sdk.file.FileRequest;
+import io.metersphere.sdk.plugin.MsPluginManager;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.LogUtils;
+import io.metersphere.sdk.util.MsFileUtils;
import io.metersphere.system.domain.Plugin;
import io.metersphere.system.domain.PluginExample;
import io.metersphere.system.domain.PluginScript;
import io.metersphere.system.mapper.PluginMapper;
import io.metersphere.system.mapper.PluginScriptMapper;
import jakarta.annotation.Resource;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.io.FileUtils;
import org.codehaus.plexus.util.IOUtil;
-import org.codehaus.plexus.util.StringUtils;
+import org.pf4j.PluginWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
-import java.io.IOException;
+import java.io.File;
import java.io.InputStream;
-import java.sql.Driver;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Paths;
import java.util.ArrayList;
+import java.util.Enumeration;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
/**
* @author jianxing
@@ -36,44 +42,102 @@ import java.util.stream.Collectors;
@Transactional(rollbackFor = Exception.class)
public class PluginLoadService {
- private final PluginManager pluginManager = new PluginManager();
-
@Resource
private PluginMapper pluginMapper;
@Resource
private PluginScriptMapper pluginScriptMapper;
+ private MsPluginManager msPluginManager = new MsPluginManager();
/**
- * 上传插件到 minio
+ * 从文件系统中加载jar
+ *
+ * @param fileName
+ * @return
*/
- public void uploadPlugin(String id, MultipartFile file) {
+ public String loadPlugin(String fileName) {
+ return msPluginManager.loadPlugin(Paths.get(MsFileUtils.PLUGIN_DIR + "/" + fileName));
+ }
+
+
+ /**
+ * 从默认的对象存储下载插件到本地,再加载
+ * @param fileName
+ * @return
+ * @throws Exception
+ */
+ public void loadPluginFromRepository(String fileName) {
+ String filePath = MsFileUtils.PLUGIN_DIR + "/" + fileName;
+ File file = new File(filePath);
try {
- getStorageStrategy(id).store(file.getOriginalFilename(), file.getInputStream());
+ if (!file.exists()) {
+ InputStream fileAsStream = FileCenter.getDefaultRepository().getFileAsStream(getFileRequest(fileName));
+ FileUtils.copyInputStreamToFile(fileAsStream, file);
+ }
+ msPluginManager.loadPlugin(Paths.get(filePath));
+ } catch (Exception e) {
+ LogUtils.error("从对象存储加载插件异常", e);
+ }
+ }
+
+ /**
+ * 将插件上传到本地文件系统中
+ *
+ * @param file
+ * @return
+ */
+ public String uploadPlugin2Local(MultipartFile file) {
+ try {
+ return FileCenter.getRepository(StorageType.LOCAL).saveFile(file, getFileRequest(file.getOriginalFilename()));
} catch (Exception e) {
LogUtils.error(e);
throw new MSException("文件上传异常", e);
}
}
+ /**
+ * 将文件上传到默认的对象存储中
+ *
+ * @param file
+ */
+ public void uploadPlugin2Repository(MultipartFile file) {
+ try {
+ FileCenter.getDefaultRepository().saveFile(file, getFileRequest(file.getOriginalFilename()));
+ } catch (Exception e) {
+ LogUtils.error(e);
+ throw new MSException("文件上传异常", e);
+ }
+ }
+
+ private FileRequest getFileRequest(String name) {
+ FileRequest request = new FileRequest();
+ request.setProjectId(MsFileUtils.PLUGIN_DIR_NAME);
+ request.setFileName(name);
+ return request;
+ }
+
/**
* @return 返回前端渲染需要的数据
* 默认会返回 resources下的 script 下的 json 文件
*/
public List getFrontendScripts(String pluginId) {
- MsPlugin msPluginInstance = getMsPluginInstance(pluginId);
+ MsPlugin msPluginInstance = (MsPlugin) msPluginManager.getPlugin(pluginId).getPlugin();
String scriptDir = msPluginInstance.getScriptDir();
- StorageStrategy storageStrategy = pluginManager.getClassLoader(pluginId).getStorageStrategy();
try {
- // 查询脚本文件名
- List folderFileNames = storageStrategy.getFolderFileNames(scriptDir);
- // 获取脚本内容
- List scripts = new ArrayList<>(folderFileNames.size());
- for (String folderFileName : folderFileNames) {
- InputStream in = storageStrategy.get(folderFileName);
- if (in == null) {
- continue;
+ List scripts = new ArrayList<>(10);
+ String jarPath = msPluginManager.getPlugin(pluginId).getPluginPath().toString();
+ JarFile jarFile = new JarFile(jarPath);
+ Enumeration entries = jarFile.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry jarEntry = entries.nextElement();
+ //获取文件路径
+ String innerPath = jarEntry.getName();
+ if (innerPath.startsWith(scriptDir) && !jarEntry.isDirectory()) {
+ //获取到文件流
+ InputStream inputStream = msPluginManager.getPluginClassLoader(pluginId).getResourceAsStream(innerPath);
+ if (inputStream != null) {
+ scripts.add(IOUtil.toString(inputStream));
+ }
}
- scripts.add(IOUtil.toString(storageStrategy.get(folderFileName)));
}
return scripts;
} catch (Exception e) {
@@ -82,77 +146,16 @@ public class PluginLoadService {
}
}
- private static StorageStrategy getStorageStrategy(String id) {
- return new MsStorageStrategy(id);
- }
-
- public void loadPlugin(String id, MultipartFile file) {
- // 加载 jar
- InputStream inputStream;
- try {
- inputStream = file.getInputStream();
- } catch (IOException e) {
- LogUtils.error(e);
- throw new MSException("获取文件输入流异常", e);
- }
- loadPlugin(id, inputStream, true);
- }
-
- public void loadPlugin(String pluginId, String fileName) {
- PluginClassLoader classLoader = pluginManager.getClassLoader(pluginId);
- if (classLoader != null) {
- return;
- }
- // 加载 jar
- InputStream inputStream;
- try {
- inputStream = classLoader.getStorageStrategy().get(fileName);
- } catch (Exception e) {
- LogUtils.error(e);
- throw new MSException("下载文件异常", e);
- }
- loadPlugin(pluginId, inputStream, false);
- }
-
- public void loadPlugin(String id, InputStream inputStream, boolean isNeedUploadFile) {
- if (inputStream == null) {
- return;
- }
- loadPlugin(id, inputStream, new MsStorageStrategy(id), isNeedUploadFile);
- }
-
- /**
- * 加载插件
- *
- * @param id 插件ID
- * @param inputStream 输入流
- * @param storageStrategy 静态文件及jar包存储策略
- */
- public void loadPlugin(String id, InputStream inputStream, StorageStrategy storageStrategy, boolean isNeedUploadFile) {
- if (inputStream == null || pluginManager.getClassLoader(id) != null) {
- return;
- }
- // 加载 jar
- try {
- pluginManager.loadJar(id, inputStream,
- storageStrategy == null ? getStorageStrategy(id) : storageStrategy, isNeedUploadFile);
- } catch (Exception e) {
- LogUtils.error(e);
- throw new MSException("加载插件异常", e);
- }
- }
-
/**
* 项目启动时加载插件
*/
public synchronized void loadPlugins() {
List plugins = pluginMapper.selectByExample(new PluginExample());
plugins.forEach(plugin -> {
- String id = plugin.getId();
- StorageStrategy storageStrategy = getStorageStrategy(id);
+ String fileName = plugin.getFileName();
try {
- InputStream inputStream = storageStrategy.get(plugin.getFileName());
- loadPlugin(id, inputStream, storageStrategy, false);
+ loadPlugin(fileName);
+ msPluginManager.startPlugin(plugin.getId());
} catch (Exception e) {
LogUtils.error("初始化插件异常" + plugin.getFileName(), e);
}
@@ -163,75 +166,43 @@ public class PluginLoadService {
* 卸载插件
*/
public void unloadPlugin(String pluginId) {
- pluginManager.deletePlugin(pluginId);
+ if (msPluginManager.getPlugin(pluginId) != null) {
+ msPluginManager.deletePlugin(pluginId);
+ }
}
-
- public PluginClassLoader getClassLoader(String pluginId) {
- return pluginManager.getClassLoader(pluginId);
+ public boolean hasPlugin(String pluginId) {
+ return msPluginManager.getPlugin(pluginId) != null;
}
/**
* 删除插件
*/
- public void deletePlugin(String pluginId) {
- // 删除文件
- PluginClassLoader classLoader = pluginManager.getClassLoader(pluginId);
+ public void deletePluginFile(String fileName) {
+ FileRequest fileRequest = getFileRequest(fileName);
try {
- if (classLoader != null) {
- classLoader.getStorageStrategy().delete();
- }
+ FileCenter.getRepository(StorageType.LOCAL).delete(fileRequest);
+ FileCenter.getDefaultRepository().delete(fileRequest);
} catch (Exception e) {
LogUtils.error(e);
- throw new MSException("删除插件异常 ", e);
}
- unloadPlugin(pluginId);
}
- public MsPlugin getMsPluginInstance(String id) {
- return pluginManager.getImplInstance(id, MsPlugin.class);
- }
-
- public T getImplInstance(String pluginId, Class superClazz, Object param) {
- return pluginManager.getImplInstance(pluginId, superClazz, param);
- }
-
- public T getImplInstance(String pluginId, Class superClazz) {
- return pluginManager.getImplInstance(pluginId, superClazz);
- }
-
- public List getPlatformPluginInstanceList() {
- return getImplInstanceList(AbstractPlatformPlugin.class);
- }
-
- public AbstractPlatformPlugin getPlatformPluginInstance(String pluginId) {
- return getImplInstance(pluginId, AbstractPlatformPlugin.class);
- }
-
- public List getImplInstanceList(Class clazz, Object... initArgs) {
- return pluginManager.getClassLoaderMap().keySet().stream()
- .map(pluginId -> pluginManager.getImplInstance(pluginId, clazz, initArgs)
- ).collect(Collectors.toList());
- }
-
- public boolean hasPluginKey(String currentPluginId, String pluginKey) {
- for (String pluginId : pluginManager.getClassLoaderMap().keySet()) {
- MsPlugin msPlugin;
- try {
- msPlugin = getMsPluginInstance(pluginId);
- if (!StringUtils.equals(currentPluginId, pluginId) && StringUtils.equals(msPlugin.getKey(), pluginKey)) {
- return true;
- }
- } catch (MSException e) {
- // jdbc 驱动没有实现 MsPlugin 接口
- LogUtils.info(String.format("插件%s未实现 MsPlugin 接口", pluginId));
- }
+ /**
+ * 删除本地插件
+ * @param fileName
+ */
+ public void deleteLocalPluginFile(String fileName) {
+ FileRequest fileRequest = getFileRequest(fileName);
+ try {
+ FileCenter.getRepository(StorageType.LOCAL).delete(fileRequest);
+ } catch (Exception e) {
+ LogUtils.error(e);
}
- return false;
}
public InputStream getResourceAsStream(String pluginId, String name) {
- return pluginManager.getClassLoaderMap().get(pluginId).getResourceAsStream(name);
+ return msPluginManager.getPluginClassLoader(pluginId).getResourceAsStream(name);
}
public Map getPluginScriptConfig(String pluginId, String scriptId) {
@@ -243,7 +214,66 @@ public class PluginLoadService {
return getPluginScriptConfig(pluginId, scriptId).get("script");
}
- public Class getImplClass(String pluginId, Class driverClass) {
- return pluginManager.getImplClass(pluginId, driverClass);
+ public PluginWrapper getPluginWrapper(String id) {
+ return msPluginManager.getPlugin(id);
+ }
+
+ /**
+ * 获取插件中的是实现类列表
+ * @param clazz
+ * @return
+ * @param
+ */
+ public List getExtensions(Class clazz) {
+ return msPluginManager.getExtensions(clazz);
+ }
+
+ /**
+ * 获取插件中的是实现类
+ * @param clazz
+ * @param pluginId
+ * @return
+ * @param
+ */
+ public Class extends T> getExtensionsClass(Class clazz, String pluginId) {
+ List> classes = msPluginManager.getExtensionClasses(clazz, pluginId);
+ return CollectionUtils.isEmpty(classes) ? null : classes.get(0);
+ }
+
+ public MsPluginManager getMsPluginManager() {
+ return msPluginManager;
+ }
+
+ public T getImplInstance(Class extensionClazz, String pluginId, Object param) {
+ try {
+ Class extends T> clazz = getExtensionsClass(extensionClazz, pluginId);
+ if (clazz == null) {
+ throw new MSException(CommonResultCode.PLUGIN_GET_INSTANCE);
+ }
+ if (param == null) {
+ return clazz.getConstructor().newInstance();
+ } else {
+ return clazz.getConstructor(param.getClass()).newInstance(param);
+ }
+ } catch (InvocationTargetException e) {
+ LogUtils.error(e.getTargetException());
+ throw new MSException(CommonResultCode.PLUGIN_GET_INSTANCE, e.getTargetException().getMessage());
+ } catch (Exception e) {
+ LogUtils.error(e);
+ throw new MSException(CommonResultCode.PLUGIN_GET_INSTANCE, e.getMessage());
+ }
+ }
+
+ public void handlePluginAddNotified(String pluginId, String fileName) {
+ if (!hasPlugin(pluginId)) {
+ loadPluginFromRepository(fileName);
+ }
+ }
+
+ public void handlePluginDeleteNotified(String pluginId, String fileName) {
+ if (hasPlugin(pluginId)) {
+ unloadPlugin(pluginId);
+ deleteLocalPluginFile(fileName);
+ }
}
}
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/environment/EnvironmentService.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/environment/EnvironmentService.java
index c84eea5c15..9be22827b0 100644
--- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/environment/EnvironmentService.java
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/environment/EnvironmentService.java
@@ -1,82 +1,66 @@
package io.metersphere.sdk.service.environment;
-import io.metersphere.sdk.constants.PluginScenarioType;
import io.metersphere.sdk.domain.Environment;
import io.metersphere.sdk.domain.EnvironmentBlob;
import io.metersphere.sdk.domain.EnvironmentBlobExample;
import io.metersphere.sdk.domain.EnvironmentExample;
+import io.metersphere.sdk.dto.OptionDTO;
import io.metersphere.sdk.dto.environment.EnvironmentConfig;
import io.metersphere.sdk.dto.environment.EnvironmentConfigRequest;
+import io.metersphere.sdk.dto.environment.dataSource.DataSource;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.file.FileRequest;
import io.metersphere.sdk.file.MinioRepository;
import io.metersphere.sdk.mapper.EnvironmentBlobMapper;
import io.metersphere.sdk.mapper.EnvironmentMapper;
-import io.metersphere.sdk.service.PluginLoadService;
+import io.metersphere.sdk.service.JdbcDriverPluginService;
import io.metersphere.sdk.uid.UUID;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.Translator;
-import io.metersphere.system.domain.Plugin;
-import io.metersphere.system.domain.PluginExample;
-import io.metersphere.system.domain.PluginOrganization;
-import io.metersphere.system.domain.PluginOrganizationExample;
-import io.metersphere.system.mapper.PluginMapper;
-import io.metersphere.system.mapper.PluginOrganizationMapper;
import jakarta.annotation.Resource;
import jakarta.transaction.Transactional;
import org.apache.commons.collections.CollectionUtils;
-import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.sql.Driver;
+import java.sql.DriverManager;
import java.util.*;
@Service
@Transactional
public class EnvironmentService {
-
- @Resource
- private PluginMapper pluginMapper;
- @Resource
- private PluginLoadService pluginLoadService;
- @Resource
- private PluginOrganizationMapper pluginOrganizationMapper;
@Resource
private EnvironmentMapper environmentMapper;
@Resource
private EnvironmentBlobMapper environmentBlobMapper;
@Resource
private MinioRepository minioRepository;
+ @Resource
+ private JdbcDriverPluginService jdbcDriverPluginService;
- public Map getDriverOptions(String organizationId) {
- Map pluginDriverClassNames = new HashMap<>();
- PluginExample example = new PluginExample();
- example.createCriteria().andScenarioEqualTo(PluginScenarioType.JDBC_DRIVER.name()).andEnableEqualTo(true);
- List plugins = pluginMapper.selectByExample(example);
- plugins.forEach(plugin -> {
- if (BooleanUtils.isTrue(plugin.getGlobal())) {
- pluginDriverClassNames.put(plugin.getId(), pluginLoadService.getImplClass(plugin.getId(), Driver.class).getName());
+ public List getDriverOptions(String organizationId) {
+ return jdbcDriverPluginService.getJdbcDriverOption(organizationId);
+ }
+
+ public void validateDataSource(DataSource databaseConfig) {
+ try {
+ if (StringUtils.isNotBlank(databaseConfig.getDriverId())) {
+ Driver driver = jdbcDriverPluginService.getDriverByOptionId(databaseConfig.getDriverId());
+ Properties properties = new Properties();
+ properties.setProperty("user", databaseConfig.getUsername());
+ properties.setProperty("password", databaseConfig.getPassword());
+ driver.connect(databaseConfig.getDbUrl(), properties);
} else {
- //判断组织id
- if (StringUtils.isNotBlank(organizationId)) {
- //判断组织id是否在插件组织id中
- PluginOrganizationExample pluginOrganizationExample = new PluginOrganizationExample();
- pluginOrganizationExample.createCriteria().andPluginIdEqualTo(plugin.getId()).andOrganizationIdEqualTo(organizationId);
- List pluginOrganizations = pluginOrganizationMapper.selectByExample(pluginOrganizationExample);
- if (pluginOrganizations.size() > 0) {
- pluginDriverClassNames.put(plugin.getId(), pluginLoadService.getImplClass(plugin.getId(), Driver.class).getName());
- }
- }
+ DriverManager.getConnection(databaseConfig.getDbUrl(), databaseConfig.getUsername(), databaseConfig.getPassword());
}
- });
- // 已经内置了 mysql 依赖
- pluginDriverClassNames.put(StringUtils.EMPTY, "com.mysql.jdbc.Driver");
- return pluginDriverClassNames;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
}
public void delete(String id) {
diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/MsFileUtils.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/MsFileUtils.java
new file mode 100644
index 0000000000..37399e00ad
--- /dev/null
+++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/MsFileUtils.java
@@ -0,0 +1,23 @@
+package io.metersphere.sdk.util;
+
+import io.metersphere.sdk.exception.MSException;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+
+public class MsFileUtils {
+
+ public static final String DATE_ROOT_DIR = "/opt/metersphere/data/app";
+ public static final String PLUGIN_DIR_NAME = "plugins";
+ public static final String PLUGIN_DIR = DATE_ROOT_DIR + "/" + PLUGIN_DIR_NAME;
+
+ public static void validateFileName(String... fileNames) {
+ if (fileNames != null) {
+ for (String fileName : fileNames) {
+ if (StringUtils.isNotEmpty(fileName) && StringUtils.contains(fileName, "." + File.separator)) {
+ throw new MSException(Translator.get("invalid_parameter"));
+ }
+ }
+ }
+ }
+}
diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/service/ProjectApplicationService.java b/backend/services/project-management/src/main/java/io/metersphere/project/service/ProjectApplicationService.java
index cf2d7fe62d..27a32b5ed7 100644
--- a/backend/services/project-management/src/main/java/io/metersphere/project/service/ProjectApplicationService.java
+++ b/backend/services/project-management/src/main/java/io/metersphere/project/service/ProjectApplicationService.java
@@ -194,11 +194,9 @@ public class ProjectApplicationService {
return options;
}
-
-
public Object getPluginScript(String pluginId) {
this.checkResourceExist(pluginId);
- AbstractPlatformPlugin platformPlugin = pluginLoadService.getImplInstance(pluginId, AbstractPlatformPlugin.class);
+ AbstractPlatformPlugin platformPlugin = (AbstractPlatformPlugin) pluginLoadService.getMsPluginManager().getPlugin(pluginId).getPlugin();
return pluginLoadService.getPluginScriptContent(pluginId, platformPlugin.getProjectScriptId());
}
diff --git a/backend/services/project-management/src/test/java/io/metersphere/project/controller/ProjectApplicationControllerTests.java b/backend/services/project-management/src/test/java/io/metersphere/project/controller/ProjectApplicationControllerTests.java
index fcc770fd25..0262dbc7c8 100644
--- a/backend/services/project-management/src/test/java/io/metersphere/project/controller/ProjectApplicationControllerTests.java
+++ b/backend/services/project-management/src/test/java/io/metersphere/project/controller/ProjectApplicationControllerTests.java
@@ -395,7 +395,7 @@ public class ProjectApplicationControllerTests extends BaseTest {
.getPath()
);
FileInputStream inputStream = new FileInputStream(jarFile);
- MockMultipartFile mockMultipartFile = new MockMultipartFile(jarFile.getName(), inputStream);
+ MockMultipartFile mockMultipartFile = new MockMultipartFile(jarFile.getName(), jarFile.getName(), "jar", inputStream);
request.setName("测试插件1");
request.setGlobal(true);
request.setEnable(true);
diff --git a/backend/services/project-management/src/test/resources/file/metersphere-jira-plugin-3.x.jar b/backend/services/project-management/src/test/resources/file/metersphere-jira-plugin-3.x.jar
index c8a6ea2338f7f162367e6ade82cf0a7eac40ceb6..c19d1732dc2711182270754e6710f76090484aec 100644
GIT binary patch
literal 46161
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