refactor(系统设置): 插件加载重构
This commit is contained in:
parent
bfc70f58f9
commit
69e312e87b
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package io.metersphere.plugin.api.api;
|
||||
|
||||
import io.metersphere.plugin.sdk.api.AbstractMsPlugin;
|
||||
|
||||
public abstract class AbstractApiProtocolPlugin extends AbstractMsPlugin {
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回插件的描述信息
|
||||
|
|
|
@ -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 {
|
||||
|
||||
/**
|
||||
* 校验服务集成配置
|
||||
|
|
|
@ -44,5 +44,10 @@
|
|||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.pf4j</groupId>
|
||||
<artifactId>pf4j</artifactId>
|
||||
<version>${pf4j.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
package io.metersphere.sdk.constants;
|
||||
|
||||
public enum PluginScenarioType {
|
||||
API, PLATFORM, JDBC_DRIVER
|
||||
/**
|
||||
* 接口协议插件
|
||||
*/
|
||||
API_PROTOCOL,
|
||||
/**
|
||||
* 项目关联平台插件
|
||||
*/
|
||||
PLATFORM,
|
||||
/**
|
||||
* jdbc 驱动插件
|
||||
*/
|
||||
JDBC_DRIVER
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
package io.metersphere.sdk.constants;
|
||||
|
||||
public enum StorageConstants {
|
||||
MINIO, GIT, FILE_REF
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package io.metersphere.sdk.constants;
|
||||
|
||||
public enum StorageType {
|
||||
MINIO, GIT, FILE_REF, LOCAL
|
||||
}
|
|
@ -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<String, String> driverOptions(@PathVariable String organizationId) {
|
||||
public List<OptionDTO> driverOptions(@PathVariable String organizationId) {
|
||||
return environmentService.getDriverOptions(organizationId);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<StorageType, FileRepository> 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);
|
||||
|
|
|
@ -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<String> 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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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<String, Set<String>> readClasspathStorages() {
|
||||
LogUtils.debug("Reading extensions storages from classpath");
|
||||
Map<String, Set<String>> result = new LinkedHashMap<>();
|
||||
|
||||
final Set<String> bucket = new HashSet<>();
|
||||
try {
|
||||
Enumeration<URL> 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<String, Set<String>> readPluginsStorages() {
|
||||
LogUtils.debug("Reading extensions storages from plugins");
|
||||
Map<String, Set<String>> result = new LinkedHashMap<>();
|
||||
|
||||
List<PluginWrapper> plugins = pluginManager.getPlugins();
|
||||
for (PluginWrapper plugin : plugins) {
|
||||
String pluginId = plugin.getDescriptor().getPluginId();
|
||||
LogUtils.debug("Reading extensions storages for plugin '{}'", pluginId);
|
||||
final Set<String> bucket = new HashSet<>();
|
||||
|
||||
try {
|
||||
Enumeration<URL> 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<URL> urls, Set<String> 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<String> 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<String> readExtensions(Path extensionPath) throws IOException {
|
||||
final Set<String> result = new HashSet<>();
|
||||
Files.walkFileTree(extensionPath, Collections.<FileVisitOption>emptySet(), 1, new SimpleFileVisitor<Path>() {
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<Class> clazzSet = new HashSet<>();
|
||||
/**
|
||||
* 加载重试次数
|
||||
*/
|
||||
protected final static int CLASS_RELOAD_TIME = 20;
|
||||
/**
|
||||
* 保存加载失败的类,之后重试
|
||||
*/
|
||||
protected Map<String, ByteArrayWrapper> loadErrorMap = new HashMap<>();
|
||||
|
||||
private class ByteArrayWrapper {
|
||||
private byte[] values;
|
||||
|
||||
public ByteArrayWrapper(byte[] values) {
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
public byte[] getValues() {
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
public Set<Class> 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<JarEntry> 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<String> 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);
|
||||
}
|
||||
}
|
|
@ -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<String, PluginClassLoader> classLoaderMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 缓存查找过的类
|
||||
* 内层 map
|
||||
* key 未接口的类
|
||||
* value 为实现类
|
||||
*/
|
||||
protected Map<String, Map<Class, Class>> 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<String, PluginClassLoader> 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 <T> Class<T> getImplClass(String pluginId, Class<T> superClazz) {
|
||||
PluginClassLoader classLoader = getPluginClassLoader(pluginId);
|
||||
Map<Class, Class> classes = implClassCache.get(pluginId);
|
||||
if (classes == null) {
|
||||
classes = new HashMap<>();
|
||||
implClassCache.put(pluginId, classes);
|
||||
}
|
||||
if (classes.get(superClazz) != null) {
|
||||
return classes.get(superClazz);
|
||||
}
|
||||
LinkedHashSet<Class<T>> result = new LinkedHashSet<>();
|
||||
Set<Class> 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> T getImplInstance(String pluginId, Class<T> superClazz) {
|
||||
return this.getImplInstance(pluginId, superClazz, null);
|
||||
}
|
||||
|
||||
public <T> T getImplInstance(String pluginId, Class<T> superClazz, Object param) {
|
||||
try {
|
||||
Class<T> 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;
|
||||
}
|
||||
}
|
|
@ -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<String> getFolderFileNames(String dirName) throws Exception {
|
||||
FileRequest request = getFileRequest(dirName);
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
|
@ -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<String> getFolderFileNames(String dirName) throws Exception;
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @throws IOException
|
||||
*/
|
||||
void delete() throws Exception;
|
||||
}
|
|
@ -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<Plugin> getOrgEnabledPlugins(String orgId, PluginScenarioType pluginScenarioType) {
|
||||
List<Plugin> plugins = getEnabledPlugins(pluginScenarioType);
|
||||
List<String> unGlobalIds = plugins.stream().filter(i -> !i.getGlobal()).map(Plugin::getId).toList();
|
||||
// 如果没有非全局,直接返回全局插件
|
||||
if (CollectionUtils.isEmpty(unGlobalIds)) {
|
||||
return plugins;
|
||||
}
|
||||
// 查询当前组织下的插件列表
|
||||
List<PluginOrganization> pluginOrganizations = basePluginOrganizationService.getByPluginIds(unGlobalIds);
|
||||
Set<String> 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<Plugin> getEnabledPlugins(PluginScenarioType PluginScenarioType) {
|
||||
PluginExample example = new PluginExample();
|
||||
example.createCriteria()
|
||||
.andEnableEqualTo(true)
|
||||
.andScenarioEqualTo(PluginScenarioType.name());
|
||||
return pluginMapper.selectByExample(example);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> getJdbcDriverClass(String orgId) {
|
||||
List<Plugin> plugins = basePluginService.getOrgEnabledPlugins(orgId, PluginScenarioType.JDBC_DRIVER);
|
||||
List<Class> drivers = new ArrayList<>(10);
|
||||
for (Plugin plugin : plugins) {
|
||||
drivers.addAll(pluginLoadService.getMsPluginManager().getExtensionClasses(Driver.class, plugin.getId()));
|
||||
}
|
||||
|
||||
List<String> 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<String> getJdbcDriverClass() {
|
||||
PluginExample example = new PluginExample();
|
||||
example.createCriteria().andScenarioEqualTo(PluginScenarioType.JDBC_DRIVER.name());
|
||||
List<Plugin> plugins = pluginMapper.selectByExample(example);
|
||||
List<String> 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<OptionDTO> getJdbcDriverOption(String orgId) {
|
||||
List<Plugin> plugins = basePluginService.getOrgEnabledPlugins(orgId, PluginScenarioType.JDBC_DRIVER);
|
||||
List<OptionDTO> options = new ArrayList<>();
|
||||
for (Plugin plugin : plugins) {
|
||||
List<Class<? extends Driver>> 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<String> 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<Driver> extensions = pluginLoadService.getMsPluginManager().getExtensions(Driver.class, pluginId);
|
||||
return extensions.stream().filter(driver -> StringUtils.equals(driver.getClass().getName(), className))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new MSException("未找到对应的驱动"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Plugin> getEnabledPlatformPlugins() {
|
||||
PluginExample example = new PluginExample();
|
||||
example.createCriteria()
|
||||
.andEnableEqualTo(true)
|
||||
.andScenarioEqualTo(PluginScenarioType.PLATFORM.name());
|
||||
return pluginMapper.selectByExample(example);
|
||||
}
|
||||
|
||||
public List<Plugin> getOrgEnabledPlatformPlugins(String orgId) {
|
||||
List<Plugin> plugins = getEnabledPlatformPlugins();
|
||||
List<String> unGlobalIds = plugins.stream().filter(i -> !i.getGlobal()).map(Plugin::getId).toList();
|
||||
// 如果没有非全局,直接返回全局插件
|
||||
if (CollectionUtils.isEmpty(unGlobalIds)) {
|
||||
return plugins;
|
||||
}
|
||||
// 查询当前组织下的插件列表
|
||||
List<PluginOrganization> pluginOrganizations = basePluginOrganizationService.getByPluginIds(unGlobalIds);
|
||||
Set<String> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> 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<String> folderFileNames = storageStrategy.getFolderFileNames(scriptDir);
|
||||
// 获取脚本内容
|
||||
List<String> scripts = new ArrayList<>(folderFileNames.size());
|
||||
for (String folderFileName : folderFileNames) {
|
||||
InputStream in = storageStrategy.get(folderFileName);
|
||||
if (in == null) {
|
||||
continue;
|
||||
List<String> scripts = new ArrayList<>(10);
|
||||
String jarPath = msPluginManager.getPlugin(pluginId).getPluginPath().toString();
|
||||
JarFile jarFile = new JarFile(jarPath);
|
||||
Enumeration<JarEntry> 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<Plugin> 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> T getImplInstance(String pluginId, Class<T> superClazz, Object param) {
|
||||
return pluginManager.getImplInstance(pluginId, superClazz, param);
|
||||
}
|
||||
|
||||
public <T> T getImplInstance(String pluginId, Class<T> superClazz) {
|
||||
return pluginManager.getImplInstance(pluginId, superClazz);
|
||||
}
|
||||
|
||||
public List<AbstractPlatformPlugin> getPlatformPluginInstanceList() {
|
||||
return getImplInstanceList(AbstractPlatformPlugin.class);
|
||||
}
|
||||
|
||||
public AbstractPlatformPlugin getPlatformPluginInstance(String pluginId) {
|
||||
return getImplInstance(pluginId, AbstractPlatformPlugin.class);
|
||||
}
|
||||
|
||||
public <T> List<T> getImplInstanceList(Class<T> 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;
|
||||
/**
|
||||
* 删除本地插件
|
||||
* @param fileName
|
||||
*/
|
||||
public void deleteLocalPluginFile(String fileName) {
|
||||
FileRequest fileRequest = getFileRequest(fileName);
|
||||
try {
|
||||
msPlugin = getMsPluginInstance(pluginId);
|
||||
if (!StringUtils.equals(currentPluginId, pluginId) && StringUtils.equals(msPlugin.getKey(), pluginKey)) {
|
||||
return true;
|
||||
FileCenter.getRepository(StorageType.LOCAL).delete(fileRequest);
|
||||
} catch (Exception e) {
|
||||
LogUtils.error(e);
|
||||
}
|
||||
} catch (MSException e) {
|
||||
// jdbc 驱动没有实现 MsPlugin 接口
|
||||
LogUtils.info(String.format("插件%s未实现 MsPlugin 接口", pluginId));
|
||||
}
|
||||
}
|
||||
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<Driver> driverClass) {
|
||||
return pluginManager.getImplClass(pluginId, driverClass);
|
||||
public PluginWrapper getPluginWrapper(String id) {
|
||||
return msPluginManager.getPlugin(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件中的是实现类列表
|
||||
* @param clazz
|
||||
* @return
|
||||
* @param <T>
|
||||
*/
|
||||
public <T> List<T> getExtensions(Class<T> clazz) {
|
||||
return msPluginManager.getExtensions(clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件中的是实现类
|
||||
* @param clazz
|
||||
* @param pluginId
|
||||
* @return
|
||||
* @param <T>
|
||||
*/
|
||||
public <T> Class<? extends T> getExtensionsClass(Class<T> clazz, String pluginId) {
|
||||
List<Class<? extends T>> classes = msPluginManager.getExtensionClasses(clazz, pluginId);
|
||||
return CollectionUtils.isEmpty(classes) ? null : classes.get(0);
|
||||
}
|
||||
|
||||
public MsPluginManager getMsPluginManager() {
|
||||
return msPluginManager;
|
||||
}
|
||||
|
||||
public <T> T getImplInstance(Class<T> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,83 +1,67 @@
|
|||
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<String, String> getDriverOptions(String organizationId) {
|
||||
Map<String, String> pluginDriverClassNames = new HashMap<>();
|
||||
PluginExample example = new PluginExample();
|
||||
example.createCriteria().andScenarioEqualTo(PluginScenarioType.JDBC_DRIVER.name()).andEnableEqualTo(true);
|
||||
List<Plugin> 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<OptionDTO> 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<PluginOrganization> 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());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
// 已经内置了 mysql 依赖
|
||||
pluginDriverClassNames.put(StringUtils.EMPTY, "com.mysql.jdbc.Driver");
|
||||
return pluginDriverClassNames;
|
||||
}
|
||||
|
||||
public void delete(String id) {
|
||||
environmentMapper.deleteByPrimaryKey(id);
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Binary file not shown.
|
@ -20,7 +20,6 @@ public enum SystemResultCode implements IResultCode {
|
|||
*/
|
||||
NO_ORG_USER_ROLE_PERMISSION(101007, "organization_user_role_permission_error"),
|
||||
PLUGIN_EXIST(101008, "plugin.exist"),
|
||||
PLUGIN_TYPE_EXIST(101009, "plugin.type.exist"),
|
||||
PLUGIN_SCRIPT_EXIST(101010, "plugin.script.exist"),
|
||||
PLUGIN_SCRIPT_FORMAT(101011, "plugin.script.format"),
|
||||
NO_PROJECT_USER_ROLE_PERMISSION(101012, "project_user_role_permission_error");
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package io.metersphere.system.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PluginNotifiedDTO {
|
||||
private String operate;
|
||||
private String pluginId;
|
||||
private String fileName;
|
||||
}
|
|
@ -4,7 +4,9 @@ package io.metersphere.system.listener;
|
|||
import io.metersphere.sdk.constants.KafkaPluginTopicType;
|
||||
import io.metersphere.sdk.constants.KafkaTopicConstants;
|
||||
import io.metersphere.sdk.service.PluginLoadService;
|
||||
import io.metersphere.sdk.util.JSON;
|
||||
import io.metersphere.sdk.util.LogUtils;
|
||||
import io.metersphere.system.dto.PluginNotifiedDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
||||
|
@ -21,17 +23,17 @@ public class PluginListener {
|
|||
// groupId 必须是每个实例唯一
|
||||
@KafkaListener(id = PLUGIN_CONSUMER, topics = KafkaTopicConstants.PLUGIN, groupId = PLUGIN_CONSUMER + "_" + "${random.uuid}")
|
||||
public void handlePluginChange(ConsumerRecord<?, String> record) {
|
||||
LogUtils.info("Service consume platform_plugin message: " + record);
|
||||
String[] info = record.value().split(":");
|
||||
String operate = info[0];
|
||||
String pluginId = info[1];
|
||||
LogUtils.info("Service consume platform_plugin message: " + record.value());
|
||||
PluginNotifiedDTO pluginNotifiedDTO = JSON.parseObject(record.value(), PluginNotifiedDTO.class);
|
||||
String operate = pluginNotifiedDTO.getOperate();
|
||||
String pluginId = pluginNotifiedDTO.getPluginId();
|
||||
String fileName = pluginNotifiedDTO.getFileName();
|
||||
switch (operate) {
|
||||
case KafkaPluginTopicType.ADD:
|
||||
String pluginName = info[2];
|
||||
pluginLoadService.loadPlugin(pluginId, pluginName);
|
||||
pluginLoadService.handlePluginAddNotified(pluginId, fileName);
|
||||
break;
|
||||
case KafkaPluginTopicType.DELETE:
|
||||
pluginLoadService.unloadPlugin(pluginId);
|
||||
pluginLoadService.handlePluginDeleteNotified(pluginId, fileName);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package io.metersphere.system.service;
|
||||
|
||||
|
||||
import io.metersphere.plugin.api.api.AbstractApiProtocolPlugin;
|
||||
import io.metersphere.plugin.platform.api.AbstractPlatformPlugin;
|
||||
import io.metersphere.plugin.sdk.api.MsPlugin;
|
||||
import io.metersphere.sdk.constants.KafkaPluginTopicType;
|
||||
import io.metersphere.sdk.constants.KafkaTopicConstants;
|
||||
|
@ -11,12 +13,13 @@ import io.metersphere.sdk.exception.MSException;
|
|||
import io.metersphere.sdk.service.BaseUserService;
|
||||
import io.metersphere.sdk.service.JdbcDriverPluginService;
|
||||
import io.metersphere.sdk.service.PluginLoadService;
|
||||
import io.metersphere.sdk.uid.UUID;
|
||||
import io.metersphere.sdk.util.BeanUtils;
|
||||
import io.metersphere.sdk.util.JSON;
|
||||
import io.metersphere.sdk.util.ServiceUtils;
|
||||
import io.metersphere.system.domain.Plugin;
|
||||
import io.metersphere.system.domain.PluginExample;
|
||||
import io.metersphere.system.dto.PluginDTO;
|
||||
import io.metersphere.system.dto.PluginNotifiedDTO;
|
||||
import io.metersphere.system.mapper.ExtPluginMapper;
|
||||
import io.metersphere.system.mapper.PluginMapper;
|
||||
import io.metersphere.system.request.PluginUpdateRequest;
|
||||
|
@ -25,6 +28,8 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.pf4j.PluginDescriptor;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
@ -37,7 +42,6 @@ import java.sql.Driver;
|
|||
import java.util.*;
|
||||
|
||||
import static io.metersphere.system.controller.result.SystemResultCode.PLUGIN_EXIST;
|
||||
import static io.metersphere.system.controller.result.SystemResultCode.PLUGIN_TYPE_EXIST;
|
||||
|
||||
/**
|
||||
* @author jianxing
|
||||
|
@ -58,7 +62,7 @@ public class PluginService {
|
|||
@Resource
|
||||
private PluginLoadService pluginLoadService;
|
||||
@Resource
|
||||
JdbcDriverPluginService jdbcDriverPluginService;
|
||||
private JdbcDriverPluginService jdbcDriverPluginService;
|
||||
@Resource
|
||||
private KafkaTemplate<String, String> kafkaTemplate;
|
||||
@Resource
|
||||
|
@ -90,10 +94,9 @@ public class PluginService {
|
|||
}
|
||||
|
||||
public Plugin add(PluginUpdateRequest request, MultipartFile file) {
|
||||
String id = UUID.randomUUID().toString();
|
||||
String id = null;
|
||||
Plugin plugin = new Plugin();
|
||||
BeanUtils.copyBean(plugin, request);
|
||||
plugin.setId(id);
|
||||
plugin.setFileName(file.getOriginalFilename());
|
||||
plugin.setCreateTime(System.currentTimeMillis());
|
||||
plugin.setUpdateTime(System.currentTimeMillis());
|
||||
|
@ -105,34 +108,25 @@ public class PluginService {
|
|||
checkPluginAddExist(plugin);
|
||||
|
||||
try {
|
||||
// 加载插件
|
||||
pluginLoadService.loadPlugin(id, file);
|
||||
// 上传插件
|
||||
pluginLoadService.uploadPlugin(id, file);
|
||||
// 上传插件到本地文件系统
|
||||
pluginLoadService.uploadPlugin2Local(file);
|
||||
|
||||
if (jdbcDriverPluginService.isJdbcDriver(id)) {
|
||||
Class implClass = pluginLoadService.getImplClass(id, Driver.class);
|
||||
// mysql 已经内置了依赖,不允许上传
|
||||
if (implClass.getName().startsWith("com.mysql")) {
|
||||
throw new MSException(PLUGIN_TYPE_EXIST);
|
||||
}
|
||||
plugin.setScenario(PluginScenarioType.JDBC_DRIVER.name());
|
||||
plugin.setXpack(false);
|
||||
// 从文件系统中加载插件
|
||||
id = pluginLoadService.loadPlugin(file.getOriginalFilename());
|
||||
pluginLoadService.getMsPluginManager().startPlugin(id);
|
||||
plugin.setId(id);
|
||||
|
||||
List<Driver> extensions = pluginLoadService.getMsPluginManager().getExtensions(Driver.class, id);
|
||||
|
||||
if (CollectionUtils.isNotEmpty(extensions)) {
|
||||
plugin = jdbcDriverPluginService.wrapperPlugin(plugin);
|
||||
plugin.setPluginId(file.getOriginalFilename());
|
||||
} else {
|
||||
|
||||
plugin = wrapperPlugin(id, plugin);
|
||||
|
||||
// 非数据库驱动插件,解析脚本和插件信息
|
||||
// 获取插件前端配置脚本
|
||||
List<String> frontendScript = pluginLoadService.getFrontendScripts(id);
|
||||
|
||||
MsPlugin msPlugin = pluginLoadService.getMsPluginInstance(id);
|
||||
plugin.setScenario(msPlugin.getType());
|
||||
plugin.setXpack(msPlugin.isXpack());
|
||||
plugin.setPluginId(msPlugin.getPluginId());
|
||||
|
||||
// 校验插件类型是否重复
|
||||
checkPluginKeyExist(id, msPlugin.getKey());
|
||||
|
||||
// 保存插件脚本
|
||||
pluginScriptService.add(id, frontendScript);
|
||||
}
|
||||
|
||||
|
@ -143,20 +137,32 @@ public class PluginService {
|
|||
|
||||
pluginMapper.insert(plugin);
|
||||
|
||||
// 上传插件到对象存储
|
||||
pluginLoadService.uploadPlugin2Repository(file);
|
||||
|
||||
// 通知其他节点加载插件
|
||||
notifiedPluginAdd(id, plugin.getFileName());
|
||||
} catch (Exception e) {
|
||||
// 删除插件
|
||||
pluginLoadService.deletePlugin(id);
|
||||
pluginLoadService.unloadPlugin(id);
|
||||
pluginLoadService.deletePluginFile(file.getOriginalFilename());
|
||||
throw e;
|
||||
}
|
||||
return plugin;
|
||||
}
|
||||
|
||||
private void checkPluginKeyExist(String pluginId, String pluginKey) {
|
||||
if (pluginLoadService.hasPluginKey(pluginId, pluginKey)) {
|
||||
throw new MSException(PLUGIN_TYPE_EXIST);
|
||||
public Plugin wrapperPlugin(String id, Plugin plugin) {
|
||||
PluginWrapper pluginWrapper = pluginLoadService.getPluginWrapper(id);
|
||||
PluginDescriptor descriptor = pluginWrapper.getDescriptor();
|
||||
MsPlugin msPlugin = (MsPlugin) pluginWrapper.getPlugin();
|
||||
if (msPlugin instanceof AbstractApiProtocolPlugin) {
|
||||
plugin.setScenario(PluginScenarioType.API_PROTOCOL.name());
|
||||
} else if (msPlugin instanceof AbstractPlatformPlugin) {
|
||||
plugin.setScenario(PluginScenarioType.PLATFORM.name());
|
||||
}
|
||||
plugin.setXpack(msPlugin.isXpack());
|
||||
plugin.setPluginId(descriptor.getPluginId() + "-" + descriptor.getVersion());
|
||||
return plugin;
|
||||
}
|
||||
|
||||
public Plugin checkResourceExist(String id) {
|
||||
|
@ -183,8 +189,15 @@ public class PluginService {
|
|||
* @param fileName
|
||||
*/
|
||||
public void notifiedPluginAdd(String pluginId, String fileName) {
|
||||
// 初始化项目默认节点
|
||||
kafkaTemplate.send(KafkaTopicConstants.PLUGIN, String.format("%s:%s:%s", KafkaPluginTopicType.ADD, pluginId, fileName));
|
||||
notifiedPluginOperate(pluginId, fileName, KafkaPluginTopicType.ADD);
|
||||
}
|
||||
|
||||
public void notifiedPluginOperate(String pluginId, String fileName, String operate) {
|
||||
PluginNotifiedDTO pluginNotifiedDTO = new PluginNotifiedDTO();
|
||||
pluginNotifiedDTO.setOperate(operate);
|
||||
pluginNotifiedDTO.setPluginId(pluginId);
|
||||
pluginNotifiedDTO.setFileName(fileName);
|
||||
kafkaTemplate.send(KafkaTopicConstants.PLUGIN, JSON.toJSONString(pluginNotifiedDTO));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -192,9 +205,8 @@ public class PluginService {
|
|||
*
|
||||
* @param pluginId
|
||||
*/
|
||||
public void notifiedPluginDelete(String pluginId) {
|
||||
// 初始化项目默认节点
|
||||
kafkaTemplate.send(KafkaTopicConstants.PLUGIN, String.format("%s:%s", KafkaPluginTopicType.DELETE, pluginId));
|
||||
public void notifiedPluginDelete(String pluginId, String fileName) {
|
||||
notifiedPluginOperate(pluginId, fileName, KafkaPluginTopicType.DELETE);
|
||||
}
|
||||
|
||||
public Plugin update(PluginUpdateRequest request) {
|
||||
|
@ -212,7 +224,7 @@ public class PluginService {
|
|||
request.setOrganizationIds(new ArrayList<>(0));
|
||||
}
|
||||
pluginOrganizationService.update(plugin.getId(), request.getOrganizationIds());
|
||||
return plugin;
|
||||
return pluginMapper.selectByPrimaryKey(request.getId());
|
||||
}
|
||||
|
||||
private void checkPluginUpdateExist(Plugin plugin) {
|
||||
|
@ -230,14 +242,16 @@ public class PluginService {
|
|||
|
||||
public void delete(String id) {
|
||||
checkResourceExist(id);
|
||||
Plugin plugin = pluginMapper.selectByPrimaryKey(id);
|
||||
pluginMapper.deleteByPrimaryKey(id);
|
||||
// 删除插件脚本
|
||||
pluginScriptService.deleteByPluginId(id);
|
||||
// 删除和组织的关联关系
|
||||
pluginOrganizationService.deleteByPluginId(id);
|
||||
// 删除和卸载插件
|
||||
pluginLoadService.deletePlugin(id);
|
||||
notifiedPluginDelete(id);
|
||||
pluginLoadService.unloadPlugin(id);
|
||||
pluginLoadService.deletePluginFile(plugin.getFileName());
|
||||
notifiedPluginDelete(id, plugin.getFileName());
|
||||
}
|
||||
|
||||
public String getScript(String pluginId, String scriptId) {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
package io.metersphere.system.service;
|
||||
|
||||
import io.metersphere.sdk.service.PluginLoadService;
|
||||
import io.metersphere.sdk.constants.OperationLogConstants;
|
||||
import io.metersphere.sdk.dto.LogDTO;
|
||||
import io.metersphere.sdk.log.constants.OperationLogModule;
|
||||
import io.metersphere.sdk.log.constants.OperationLogType;
|
||||
import io.metersphere.sdk.service.BasePluginService;
|
||||
import io.metersphere.sdk.util.JSON;
|
||||
import io.metersphere.system.domain.ServiceIntegration;
|
||||
import io.metersphere.system.request.ServiceIntegrationUpdateRequest;
|
||||
import io.metersphere.sdk.constants.OperationLogConstants;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import io.metersphere.sdk.dto.LogDTO;
|
||||
import io.metersphere.sdk.util.JSON;
|
||||
import io.metersphere.sdk.log.constants.OperationLogType;
|
||||
import io.metersphere.sdk.log.constants.OperationLogModule;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
/**
|
||||
* @author jianxing
|
||||
|
@ -22,7 +22,7 @@ public class ServiceIntegrationLogService {
|
|||
@Resource
|
||||
private ServiceIntegrationService serviceIntegrationService;
|
||||
@Resource
|
||||
private PluginLoadService pluginLoadService;
|
||||
private BasePluginService basePluginService;
|
||||
|
||||
public LogDTO addLog(ServiceIntegrationUpdateRequest request) {
|
||||
LogDTO dto = new LogDTO(
|
||||
|
@ -38,7 +38,7 @@ public class ServiceIntegrationLogService {
|
|||
}
|
||||
|
||||
private String getName(String pluginId) {
|
||||
return pluginLoadService.getPlatformPluginInstance(pluginId).getName();
|
||||
return basePluginService.get(pluginId).getName();
|
||||
}
|
||||
|
||||
public LogDTO updateLog(ServiceIntegrationUpdateRequest request) {
|
||||
|
|
|
@ -55,14 +55,14 @@ public class ServiceIntegrationService {
|
|||
|
||||
List<Plugin> plugins = platformPluginService.getOrgEnabledPlatformPlugins(organizationId);
|
||||
return plugins.stream().map(plugin -> {
|
||||
AbstractPlatformPlugin msPluginInstance = pluginLoadService.getPlatformPluginInstance(plugin.getId());
|
||||
AbstractPlatformPlugin msPlugin = (AbstractPlatformPlugin) pluginLoadService.getPluginWrapper(plugin.getId()).getPlugin();
|
||||
// 获取插件基础信息
|
||||
ServiceIntegrationDTO serviceIntegrationDTO = new ServiceIntegrationDTO();
|
||||
serviceIntegrationDTO.setTitle(msPluginInstance.getName());
|
||||
serviceIntegrationDTO.setTitle(msPlugin.getName());
|
||||
serviceIntegrationDTO.setEnable(false);
|
||||
serviceIntegrationDTO.setConfig(false);
|
||||
serviceIntegrationDTO.setDescription(msPluginInstance.getDescription());
|
||||
serviceIntegrationDTO.setLogo(String.format(PLUGIN_IMAGE_GET_PATH, plugin.getId(), msPluginInstance.getLogo()));
|
||||
serviceIntegrationDTO.setDescription(msPlugin.getDescription());
|
||||
serviceIntegrationDTO.setLogo(String.format(PLUGIN_IMAGE_GET_PATH, plugin.getId(), msPlugin.getLogo()));
|
||||
serviceIntegrationDTO.setPluginId(plugin.getId());
|
||||
ServiceIntegration serviceIntegration = serviceIntegrationMap.get(plugin.getId());
|
||||
if (serviceIntegration != null) {
|
||||
|
@ -149,7 +149,7 @@ public class ServiceIntegrationService {
|
|||
|
||||
public Object getPluginScript(String pluginId) {
|
||||
pluginService.checkResourceExist(pluginId);
|
||||
AbstractPlatformPlugin platformPlugin = pluginLoadService.getImplInstance(pluginId, AbstractPlatformPlugin.class);
|
||||
AbstractPlatformPlugin platformPlugin = (AbstractPlatformPlugin) pluginLoadService.getPluginWrapper(pluginId).getPlugin();
|
||||
return pluginLoadService.getPluginScriptContent(pluginId, platformPlugin.getIntegrationScriptId());
|
||||
}
|
||||
}
|
|
@ -100,7 +100,7 @@ public class PluginControllerTests extends BaseTest {
|
|||
Assertions.assertEquals(plugin.getGlobal(), request.getGlobal());
|
||||
Assertions.assertEquals(plugin.getXpack(), false);
|
||||
Assertions.assertEquals(plugin.getFileName(), jarFile.getName());
|
||||
Assertions.assertEquals(plugin.getScenario(), PluginScenarioType.API.name());
|
||||
Assertions.assertEquals(plugin.getScenario(), PluginScenarioType.API_PROTOCOL.name());
|
||||
Assertions.assertEquals(new ArrayList<>(0), getOrgIdsByPlugId(plugin.getId()));
|
||||
Assertions.assertEquals(Arrays.asList("connect", "disconnect", "pub", "sub"), getScriptIdsByPlugId(plugin.getId()));
|
||||
addPlugin = plugin;
|
||||
|
@ -135,7 +135,7 @@ public class PluginControllerTests extends BaseTest {
|
|||
);
|
||||
this.requestMultipartWithOkAndReturn(DEFAULT_ADD,
|
||||
getDefaultMultiPartParam(request, myDriver));
|
||||
Assertions.assertEquals(jdbcDriverPluginService.getJdbcDriverClass(), Arrays.asList("io.jianxing.MyDriver", "com.mysql.jdbc.Driver"));
|
||||
Assertions.assertEquals(jdbcDriverPluginService.getJdbcDriverClass(DEFAULT_ORGANIZATION_ID), Arrays.asList("io.jianxing.MyDriver", "com.mysql.cj.jdbc.Driver"));
|
||||
|
||||
// @@重名校验异常
|
||||
// 校验插件名称重名
|
||||
|
@ -147,21 +147,6 @@ public class PluginControllerTests extends BaseTest {
|
|||
assertErrorCode(this.requestMultipart(DEFAULT_ADD,
|
||||
getDefaultMultiPartParam(request, jarFile)), PLUGIN_EXIST);
|
||||
|
||||
// 校验插件 key 重复
|
||||
File typeRepeatFile = new File(
|
||||
this.getClass().getClassLoader().getResource("file/metersphere-mqtt-plugin-repeat-key.jar")
|
||||
.getPath()
|
||||
);
|
||||
assertErrorCode(this.requestMultipart(DEFAULT_ADD,
|
||||
getDefaultMultiPartParam(request, typeRepeatFile)), PLUGIN_TYPE_EXIST);
|
||||
|
||||
// @@校验禁止上传mysql驱动
|
||||
File mysqlDriver = new File(
|
||||
this.getClass().getClassLoader().getResource("file/test-mysql-driver-1.0.jar")
|
||||
.getPath()
|
||||
);
|
||||
assertErrorCode(this.requestMultipart(DEFAULT_ADD,
|
||||
getDefaultMultiPartParam(request, mysqlDriver)), PLUGIN_TYPE_EXIST);
|
||||
|
||||
// @@校验插件脚本解析失败
|
||||
File scriptParseFile = new File(
|
||||
|
@ -296,10 +281,10 @@ public class PluginControllerTests extends BaseTest {
|
|||
@Order(5)
|
||||
public void getPluginImg() throws Exception {
|
||||
// @@请求成功
|
||||
mockMvc.perform(getRequestBuilder(PLUGIN_IMAGE, anotherAddPlugin.getId(), "/static/jira.jpg"))
|
||||
mockMvc.perform(getRequestBuilder(PLUGIN_IMAGE, anotherAddPlugin.getId(), "static/jira.jpg"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
assertErrorCode(this.requestGet(PLUGIN_IMAGE, anotherAddPlugin.getId(), "/static/jira.doc"), FILE_NAME_ILLEGAL);
|
||||
assertErrorCode(this.requestGet(PLUGIN_IMAGE, anotherAddPlugin.getId(), "static/jira.doc"), FILE_NAME_ILLEGAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -200,7 +200,7 @@ public class ServiceIntegrationControllerTests extends BaseTest {
|
|||
serviceIntegrationDTO.getConfiguration());
|
||||
Assertions.assertEquals(serviceIntegration.getEnable(), serviceIntegrationDTO.getEnable());
|
||||
Assertions.assertEquals(serviceIntegration.getPluginId(), serviceIntegrationDTO.getPluginId());
|
||||
AbstractPlatformPlugin msPluginInstance = pluginLoadService.getPlatformPluginInstance(plugin.getId());
|
||||
AbstractPlatformPlugin msPluginInstance = (AbstractPlatformPlugin) pluginLoadService.getPluginWrapper(plugin.getId()).getPlugin();
|
||||
Assertions.assertEquals(serviceIntegrationDTO.getDescription(), msPluginInstance.getDescription());
|
||||
Assertions.assertEquals(serviceIntegrationDTO.getOrganizationId(), defaultOrg.getId());
|
||||
Assertions.assertEquals(serviceIntegrationDTO.getTitle(), msPluginInstance.getName());
|
||||
|
@ -333,7 +333,7 @@ public class ServiceIntegrationControllerTests 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("测试插件");
|
||||
request.setGlobal(true);
|
||||
request.setEnable(true);
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -209,7 +209,7 @@
|
|||
},
|
||||
{
|
||||
label: 'system.plugin.interfaceTest',
|
||||
value: 'API',
|
||||
value: 'API_PROTOCOL',
|
||||
},
|
||||
{
|
||||
label: 'system.plugin.projectManagement',
|
||||
|
@ -280,7 +280,7 @@
|
|||
|
||||
function getScenarioType(scenario: string) {
|
||||
switch (scenario) {
|
||||
case 'API':
|
||||
case 'API_PROTOCOL':
|
||||
return t('system.plugin.interfaceTest');
|
||||
case 'JDBC_DRIVER':
|
||||
return t('system.plugin.databaseDriver');
|
||||
|
|
Loading…
Reference in New Issue