refactor(系统设置): 插件加载重构

This commit is contained in:
AgAngle 2023-09-13 18:08:09 +08:00 committed by fit2-zhao
parent bfc70f58f9
commit 69e312e87b
52 changed files with 874 additions and 950 deletions

View File

@ -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;

View File

@ -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

View File

@ -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;
}
}

View File

@ -0,0 +1,6 @@
package io.metersphere.plugin.api.api;
import io.metersphere.plugin.sdk.api.AbstractMsPlugin;
public abstract class AbstractApiProtocolPlugin extends AbstractMsPlugin {
}

View File

@ -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;
}
/**
* 返回插件的描述信息

View File

@ -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 {
/**
* 校验服务集成配置

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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 返回插件的类型
* 目前支持接口插件和平台(APIPLATFORM)
*/
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();
}

View File

@ -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) {

View File

@ -1,5 +1,16 @@
package io.metersphere.sdk.constants;
public enum PluginScenarioType {
API, PLATFORM, JDBC_DRIVER
/**
* 接口协议插件
*/
API_PROTOCOL,
/**
* 项目关联平台插件
*/
PLATFORM,
/**
* jdbc 驱动插件
*/
JDBC_DRIVER
}

View File

@ -1,5 +0,0 @@
package io.metersphere.sdk.constants;
public enum StorageConstants {
MINIO, GIT, FILE_REF
}

View File

@ -0,0 +1,5 @@
package io.metersphere.sdk.constants;
public enum StorageType {
MINIO, GIT, FILE_REF, LOCAL
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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("未找到对应的驱动"));
}
}

View File

@ -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);
}
}

View File

@ -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;
try {
msPlugin = getMsPluginInstance(pluginId);
if (!StringUtils.equals(currentPluginId, pluginId) && StringUtils.equals(msPlugin.getKey(), pluginKey)) {
return true;
}
} catch (MSException e) {
// jdbc 驱动没有实现 MsPlugin 接口
LogUtils.info(String.format("插件%s未实现 MsPlugin 接口", pluginId));
}
/**
* 删除本地插件
* @param fileName
*/
public void deleteLocalPluginFile(String fileName) {
FileRequest fileRequest = getFileRequest(fileName);
try {
FileCenter.getRepository(StorageType.LOCAL).delete(fileRequest);
} catch (Exception e) {
LogUtils.error(e);
}
return false;
}
public InputStream getResourceAsStream(String pluginId, String name) {
return pluginManager.getClassLoaderMap().get(pluginId).getResourceAsStream(name);
return msPluginManager.getPluginClassLoader(pluginId).getResourceAsStream(name);
}
public Map getPluginScriptConfig(String pluginId, String scriptId) {
@ -243,7 +214,66 @@ public class PluginLoadService {
return getPluginScriptConfig(pluginId, scriptId).get("script");
}
public Class getImplClass(String pluginId, Class<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);
}
}
}

View File

@ -1,82 +1,66 @@
package io.metersphere.sdk.service.environment;
import io.metersphere.sdk.constants.PluginScenarioType;
import io.metersphere.sdk.domain.Environment;
import io.metersphere.sdk.domain.EnvironmentBlob;
import io.metersphere.sdk.domain.EnvironmentBlobExample;
import io.metersphere.sdk.domain.EnvironmentExample;
import io.metersphere.sdk.dto.OptionDTO;
import io.metersphere.sdk.dto.environment.EnvironmentConfig;
import io.metersphere.sdk.dto.environment.EnvironmentConfigRequest;
import io.metersphere.sdk.dto.environment.dataSource.DataSource;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.file.FileRequest;
import io.metersphere.sdk.file.MinioRepository;
import io.metersphere.sdk.mapper.EnvironmentBlobMapper;
import io.metersphere.sdk.mapper.EnvironmentMapper;
import io.metersphere.sdk.service.PluginLoadService;
import io.metersphere.sdk.service.JdbcDriverPluginService;
import io.metersphere.sdk.uid.UUID;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.domain.Plugin;
import io.metersphere.system.domain.PluginExample;
import io.metersphere.system.domain.PluginOrganization;
import io.metersphere.system.domain.PluginOrganizationExample;
import io.metersphere.system.mapper.PluginMapper;
import io.metersphere.system.mapper.PluginOrganizationMapper;
import jakarta.annotation.Resource;
import jakarta.transaction.Transactional;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.sql.Driver;
import java.sql.DriverManager;
import java.util.*;
@Service
@Transactional
public class EnvironmentService {
@Resource
private PluginMapper pluginMapper;
@Resource
private PluginLoadService pluginLoadService;
@Resource
private PluginOrganizationMapper pluginOrganizationMapper;
@Resource
private EnvironmentMapper environmentMapper;
@Resource
private EnvironmentBlobMapper environmentBlobMapper;
@Resource
private MinioRepository minioRepository;
@Resource
private JdbcDriverPluginService jdbcDriverPluginService;
public Map<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());
}
});
// 已经内置了 mysql 依赖
pluginDriverClassNames.put(StringUtils.EMPTY, "com.mysql.jdbc.Driver");
return pluginDriverClassNames;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void delete(String id) {

View File

@ -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"));
}
}
}
}
}

View File

@ -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());
}

View File

@ -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);

View File

@ -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");

View File

@ -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;
}

View File

@ -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;

View File

@ -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) {

View File

@ -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) {

View File

@ -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());
}
}

View File

@ -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

View File

@ -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);

View File

@ -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');

View File

@ -93,6 +93,7 @@
<skipAntRunForJenkins>false</skipAntRunForJenkins>
<commons-dbcp2-version>2.9.0</commons-dbcp2-version>
<jacoco.version>0.8.10</jacoco.version>
<pf4j.version>3.10.0</pf4j.version>
</properties>
<modules>