add plugin class loader
This commit is contained in:
parent
2432bdc86b
commit
ba8326aa38
|
@ -26,15 +26,6 @@ public interface Platform {
|
|||
*/
|
||||
MsIssueDTO addIssue(PlatformIssuesUpdateRequest issuesRequest);
|
||||
|
||||
/**
|
||||
* 项目设置中选项,从平台获取下拉框选项
|
||||
* frontend.json 中选项值配置了 optionMethod ,项目设置时调用
|
||||
* @return 返回下拉列表
|
||||
* 该接口后续版本将废弃,替换为 getFormOptions
|
||||
*/
|
||||
@Deprecated
|
||||
List<SelectOption> getProjectOptions(GetOptionRequest request);
|
||||
|
||||
/**
|
||||
* 项目设置和缺陷表单中,调用接口获取下拉框选项
|
||||
* 配置文件的表单中选项值配置了 optionMethod ,则调用获取表单的选项值
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 删除文件
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue