add plugin class loader

This commit is contained in:
chenjianxing 2023-04-18 15:42:15 +08:00 committed by jianxing
parent 2432bdc86b
commit ba8326aa38
7 changed files with 534 additions and 9 deletions

View File

@ -26,15 +26,6 @@ public interface Platform {
*/
MsIssueDTO addIssue(PlatformIssuesUpdateRequest issuesRequest);
/**
* 项目设置中选项从平台获取下拉框选项
* frontend.json 中选项值配置了 optionMethod 项目设置时调用
* @return 返回下拉列表
* 该接口后续版本将废弃替换为 getFormOptions
*/
@Deprecated
List<SelectOption> getProjectOptions(GetOptionRequest request);
/**
* 项目设置和缺陷表单中调用接口获取下拉框选项
* 配置文件的表单中选项值配置了 optionMethod 则调用获取表单的选项值

View File

@ -0,0 +1,20 @@
package io.metersphere.sdk.exception;
public class MSException extends RuntimeException {
private MSException(String message) {
super(message);
}
private MSException(Throwable t) {
super(t);
}
public static void throwException(String message) {
throw new MSException(message);
}
public static void throwException(Throwable t) {
throw new MSException(t);
}
}

View File

@ -0,0 +1,79 @@
package io.metersphere.sdk.plugin.loader;
import io.metersphere.plugin.platform.api.Platform;
import io.metersphere.plugin.platform.api.PluginMetaInfo;
import io.metersphere.plugin.platform.dto.PlatformRequest;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
/**
* @author jianxing.chen
*/
public class PlatformPluginManager extends PluginManager {
/**
* 获取当前加载的插件元数据列表
* @return
*/
public List<PluginMetaInfo> getPluginMetaInfoList() {
List<PluginMetaInfo> platFormOptions = new ArrayList<>();
for (String pluginId : getClassLoaderMap().keySet()) {
platFormOptions.add(getImplInstance(pluginId, PluginMetaInfo.class));
}
return platFormOptions;
}
/**
* 获取当前插件的元数据
* @param pluginId 插件ID
* @return
*/
public PluginMetaInfo getPluginMetaInfo(String pluginId) {
return getImplInstance(pluginId, PluginMetaInfo.class);
}
/**
* 获取对应插件的 Platform 对象
* @param pluginId 插件ID
* @param request
* @return
*/
public Platform getPlatform(String pluginId, PlatformRequest request) {
return getImplInstance(pluginId, Platform.class, request);
}
/**
* 获取对应插件的 Platform 对象
* @param key 插件 key
* @param request
* @return
*/
public Platform getPlatformByKey(String key, PlatformRequest request) {
for (String pluginId : getClassLoaderMap().keySet()) {
// 查找对应 key 的插件
PluginMetaInfo pluginMetaInfo = getPluginMetaInfo(pluginId);
if (StringUtils.equals(pluginMetaInfo.getKey(), key)) {
return getPlatform(pluginId, request);
}
}
return null;
}
/**
* 获取当前插件的元数据
* @param key 插件key
* @return
*/
public PluginMetaInfo getPluginMetaInfoByKey(String key) {
for (String pluginId : getClassLoaderMap().keySet()) {
// 查找对应 key 的插件
PluginMetaInfo pluginMetaInfo = getPluginMetaInfo(pluginId);
if (StringUtils.equals(pluginMetaInfo.getKey(), key)) {
return pluginMetaInfo;
}
}
return null;
}
}

View File

@ -0,0 +1,222 @@
package io.metersphere.sdk.plugin.loader;
import io.metersphere.sdk.plugin.storage.StorageStrategy;
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;
public PluginClassLoader() {
// 将父加载器设置成当前的类加载器目的是由父加载器加载接口实现类由该加载器加载
super(PluginClassLoader.class.getClassLoader());
}
public PluginClassLoader(StorageStrategy storageStrategy) {
this();
this.storageStrategy = storageStrategy;
}
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 IOException {
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 (IOException e) {
// LogUtils.error(e);
}
}
reloadErrorClazz();
}
/**
* 加载 jar 包的 class并存储静态资源
*
* @param in
* @param je
* @throws IOException
*/
protected void loadJar(InputStream in, JarEntry je) throws IOException {
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) {
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 (IOException e) {
// LogUtils.error(e, logger);
return null;
}
}
return super.getResourceAsStream(name);
}
}

View File

@ -0,0 +1,144 @@
package io.metersphere.sdk.plugin.loader;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.plugin.storage.StorageStrategy;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* @author jianxing.chen
*/
public class PluginManager {
/**
* 自定义类加载器
*/
protected Map<String, PluginClassLoader> classLoaderMap = 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);
}
/**
* 从输入流中加载 jar
*
* @param in
*/
public PluginManager loadJar(String pluginId, InputStream in, StorageStrategy storageStrategy) throws IOException {
PluginClassLoader pluginClassLoader = new PluginClassLoader(storageStrategy);
classLoaderMap.put(pluginId, pluginClassLoader);
pluginClassLoader.loadJar(in);
return this;
}
public PluginManager loadJar(String pluginId, InputStream in) throws IOException {
return this.loadJar(pluginId, in, null);
}
/**
* 获取接口的单一实现类
*/
public <T> Class<T> getImplClass(String pluginId, Class<T> superClazz) {
PluginClassLoader classLoader = classLoaderMap.get(pluginId);
LinkedHashSet<Class<T>> result = new LinkedHashSet<>();
Set<Class> clazzSet = classLoader.getClazzSet();
for (Class item : clazzSet) {
if (isImplClazz(superClazz, item) && !result.contains(item)) {
return item;
}
}
return null;
}
/**
* 获取指定接口最后一次加载的实现类实例
*/
public <T> T getImplInstance(String pluginId, Class<T> superClazz) {
try {
Class<T> clazz = getImplClass(pluginId, superClazz);
return clazz.getConstructor().newInstance();
} catch (InvocationTargetException e) {
// todo log and
// LogUtils.error(e);
MSException.throwException(e.getTargetException());
} catch (Exception e) {
// LogUtil.error(e);
MSException.throwException(e.getMessage());
}
return null;
}
public <T> T getImplInstance(String pluginId, Class<T> superClazz, Object param) {
try {
Class<T> clazz = getImplClass(pluginId, superClazz);
return clazz.getConstructor(param.getClass()).newInstance(param);
} catch (InvocationTargetException e) {
// LogUtil.error(e.getTargetException());
MSException.throwException(e.getTargetException());
} catch (Exception e) {
// LogUtil.error(e);
MSException.throwException(e.getMessage());
}
return null;
}
/**
* 判断 impClazz 是否是 superClazz 的实现类
*
* @param impClazz
* @return
*/
private boolean isImplClazz(Class superClazz, Class impClazz) {
if (impClazz == superClazz) {
return true;
}
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;
}
return false;
}
}

View File

@ -0,0 +1,35 @@
package io.metersphere.sdk.plugin.storage;
import java.io.IOException;
import java.io.InputStream;
/**
* jar包静态资源存储策略存储在 Minio
*/
public class MinioStorageStrategy implements StorageStrategy {
private String pluginId;
public static final String DIR_PATH = "system/plugin";
public MinioStorageStrategy(String pluginId) {
this.pluginId = pluginId;
}
@Override
public String store(String name, InputStream in) throws IOException {
// todo 上传到 minio
return null;
}
@Override
public InputStream get(String name) {
// todo 获取文件
return null;
}
@Override
public void delete() throws IOException {
// todo 删除文件
}
}

View File

@ -0,0 +1,34 @@
package io.metersphere.sdk.plugin.storage;
import java.io.IOException;
import java.io.InputStream;
/**
* jar包图片前端配置文件等静态资源存储策略
*/
public interface StorageStrategy {
/**
* 存储文件
* @param name
* @param in
* @return
* @throws IOException
*/
String store(String name, InputStream in) throws IOException;
/**
* 获取文件
* @param path
* @return
* @throws IOException
*/
InputStream get(String path) throws IOException;
/**
* 删除文件
* @throws IOException
*/
void delete() throws IOException;
}