在开始之前,先看下插件系统的整体框架
- 插件开发模拟环境
- “插件开发模拟环境”主要用于插件的开发和测试,一个独立项目,提供给插件开发人员使用。开发模拟环境依赖 插件核心包 、 插件依赖的主程序包 。
- 插件核心包-负责插件的加载,安装、注册、卸载
- 插件依赖的主程序包-提供插件开发测试的主程序依赖
- 主程序
- 插件的正式安装使用环境,线上环境。插件在本地开发测试完成后,通过插件管理页面安装到线上环境进行插件验证。可以分多个环境,线上 DEV 环境提供插件的线上验证,待验证完成后,再发布到prod环境。
代码实现
插件加载流程
在监听到Spring Boot启动后,插件开始加载,从配置文件中获取插件配置、创建插件监听器(用于主程序监听插件启动、停止事件,根据事件自定逻辑)、根据获取的插件配置从指定目录加载插件配置信息(插件id、插件版本、插件描述、插件所在路径、插件启动状态( 后期更新 ))、配置信息加载完成后将插件class类注册到Spring返回插件上下文、最后启动完成。
插件核心包
基础常量和类
PluginConstants
插件常量
public class PluginConstants { | |
public static final String TARGET = "target"; | |
public static final String POM = "pom.xml"; | |
public static final String JAR_SUFFIX = ". Jar "; | |
public static final String REPACKAGE = "repackage"; | |
public static final String CLASSES = "classes"; | |
public static final String CLASS_SUFFIX = ".class"; | |
public static final String MANIFEST = "MANIFEST.MF"; | |
public static final String PLUGINID = "pluginId"; | |
public static final String Plugin VERSION = "pluginVersion"; | |
public static final String PLUGINDESCRIPTION = "pluginDescription"; | |
} |
Plugin State
插件状态
@AllArgs Constructor | |
public enum PluginState { | |
/** | |
* 被禁用状态 | |
*/ | |
DISABLED("DISABLED"), | |
/** | |
* 启动状态 | |
*/ | |
STARTED("STARTED"), | |
/** | |
* 停止状态 | |
*/ | |
STOPPED("STOPPED"); | |
private final String status; | |
} |
RuntimeMode
插件运行环境
public enum RuntimeMode { | |
/** | |
* 开发环境 | |
*/ | |
DEV("dev"), | |
/** | |
* 生产环境 | |
*/ | |
PROD("prod"); | |
private final String mode; | |
public static RuntimeMode byName(String model){ | |
if(DEV.name().equalsIgnoreCase(model)){ | |
return RuntimeMode.DEV; | |
} else { | |
return RuntimeMode.PROD; | |
} | |
} | |
} |
PluginInfo
插件基本信息,重写了 hashCode 和equals,根据插件id进行去重
public class PluginInfo { | |
/** | |
* 插件id | |
*/ | |
private String id; | |
/** | |
* 版本 | |
*/ | |
private String version; | |
/** | |
* 描述 | |
*/ | |
private String description; | |
/** | |
* 插件路径 | |
*/ | |
private String path; | |
/** | |
* 插件启动状态 | |
*/ | |
private PluginState pluginState; | |
public Boolean equals(Object obj) { | |
if (this == obj) | |
return true; | |
if (obj == null) | |
return false; | |
if (getClass() != obj.getClass()) | |
return false; | |
PluginInfo other = (PluginInfo) obj; | |
return Objects.equals(id, other.id); | |
} | |
public int hashCode() { | |
return Objects.hash(id); | |
} | |
public void setPluginState(PluginState started) { | |
this.pluginState = started; | |
} | |
} |
插件监听器
PluginListener
插件监听器接口
public interface PluginListener { | |
/** | |
* 注册插件成功 | |
* @param pluginInfo 插件信息 | |
*/ | |
default void startSuccess(PluginInfo pluginInfo){ | |
} | |
/** | |
* 启动失败 | |
* @param pluginInfo 插件信息 | |
* @param throwable 异常信息 | |
*/ | |
default void startFailure(PluginInfo pluginInfo, Throwable throwable){ | |
} | |
/** | |
* 卸载插件成功 | |
* @param pluginInfo 插件信息 | |
*/ | |
default void stopSuccess(PluginInfo pluginInfo){ | |
} | |
/** | |
* 停止失败 | |
* @param pluginInfo 插件信息 | |
* @param throwable 异常信息 | |
*/ | |
default void stopFailure(PluginInfo pluginInfo, Throwable throwable){ | |
} | |
} |
DefaultPluginListenerFactory
插件监听工厂,对自定义插件监听器发送事件
public class DefaultPluginListenerFactory implements PluginListener { | |
private final List<PluginListener> listeners; | |
public DefaultPluginListenerFactory(ApplicationContext applicationContext){ | |
listeners = new ArrayList<>(); | |
addExtendPluginListener(applicationContext); | |
} | |
public DefaultPluginListenerFactory(){ | |
listeners = new ArrayList<>(); | |
} | |
private void addExtendPluginListener(ApplicationContext applicationContext){ | |
Map<String, PluginListener> beansOfTypeMap = applicationContext.getBeansOfType(PluginListener.class); | |
if (!beansOfTypeMap.isEmpty()) { | |
listeners.addAll(beansOfTypeMap.values()); | |
} | |
} | |
public synchronized void addPluginListener(PluginListener pluginListener) { | |
if(pluginListener != null){ | |
listeners.add(pluginListener); | |
} | |
} | |
public List<PluginListener> getListeners() { | |
return listeners; | |
} | |
public void startSuccess(PluginInfo pluginInfo) { | |
for (PluginListener listener : listeners) { | |
try { | |
listener.startSuccess(pluginInfo); | |
} catch ( Exception e) { | |
} | |
} | |
} | |
public void startFailure(PluginInfo pluginInfo, Throwable throwable) { | |
for (PluginListener listener : listeners) { | |
try { | |
listener.startFailure(pluginInfo, throwable); | |
} catch (Exception e) { | |
} | |
} | |
} | |
public void stopSuccess(PluginInfo pluginInfo) { | |
for (PluginListener listener : listeners) { | |
try { | |
listener.stopSuccess(pluginInfo); | |
} catch (Exception e) { | |
} | |
} | |
} | |
public void stopFailure(PluginInfo pluginInfo, Throwable throwable) { | |
for (PluginListener listener : listeners) { | |
try { | |
listener.stopFailure(pluginInfo, throwable); | |
} catch (Exception e) { | |
} | |
} | |
} | |
} |
DeployUtils
部署工具类,读取 jar 包中的文件,判断class是否为 Spring bean 等
public class DeployUtils { | |
/** | |
* 读取jar包中所有类文件 | |
*/ | |
public static Set<String> readJar File (String jarAddress) { | |
Set<String> classNameSet = new HashSet<>(); | |
try(JarFile jarFile = new JarFile(jarAddress)) { | |
Enumeration<JarEntry> entries = jarFile.entries();//遍历整个jar文件 | |
while (entries.hasMoreElements()) { | |
JarEntry jarEntry = entries.nextElement(); | |
String name = jarEntry.getName(); | |
if (name.endsWith(PluginConstants.CLASS_SUFFIX)) { | |
String className = name.replace(PluginConstants.CLASS_SUFFIX, "").replaceAll("/", "."); | |
classNameSet.add(className); | |
} | |
} | |
} catch (Exception e) { | |
log.warn("加载jar包失败", e); | |
} | |
return classNameSet; | |
} | |
public static InputStream readManifestJarFile(File jarAddress) { | |
try { | |
JarFile jarFile = new JarFile(jarAddress); | |
//遍历整个jar文件 | |
Enumeration<JarEntry> entries = jarFile.entries(); | |
while (entries.hasMoreElements()) { | |
JarEntry jarEntry = entries.nextElement(); | |
String name = jarEntry.getName(); | |
if (name.contains(PluginConstants.MANIFEST)) { | |
return jarFile.getInputStream(jarEntry); | |
} | |
} | |
} catch (Exception e) { | |
log.warn("加载jar包失败", e); | |
} | |
return null; | |
} | |
/** | |
* 方法描述 判断class对象是否带有spring的注解 | |
*/ | |
public static boolean isSpringBeanClass(Class<?> cls) { | |
if (cls == null) { | |
return false; | |
} | |
//是否是接口 | |
if (cls.isInterface()) { | |
return false; | |
} | |
//是否是抽象类 | |
if (Modifier.isAbstract(cls.getModifiers())) { | |
return false; | |
} | |
if (cls.get Annotation (Component.class) != null) { | |
return true; | |
} | |
if (cls.getAnnotation(Mapper.class) != null) { | |
return true; | |
} | |
if (cls.getAnnotation(Service.class) != null) { | |
return true; | |
} | |
if (cls.getAnnotation(RestController.class) != null) { | |
return true; | |
} | |
return false; | |
} | |
public static boolean isController(Class<?> cls) { | |
if (cls.getAnnotation(Controller.class) != null) { | |
return true; | |
} | |
if (cls.getAnnotation(RestController.class) != null) { | |
return true; | |
} | |
return false; | |
} | |
public static boolean isHave Request Mapping(Method method) { | |
return AnnotationUtils.findAnnotation(method, RequestMapping.class) != null; | |
} | |
/** | |
* 类名首字母小写 作为spring容器beanMap的key | |
*/ | |
public static String transformName(String className) { | |
String tmpstr = className.substring(className.lastIndexOf(".") + 1); | |
return tmpstr.substring(, 1).toLowerCase() + tmpstr.substring(1); | |
} | |
/** | |
* 读取 class文件 | |
* @param path | |
* @return | |
*/ | |
public static Set<String> readClassFile(String path) { | |
if (path.endsWith(PluginConstants.JAR_SUFFIX)) { | |
return readJarFile(path); | |
} else { | |
List<File> pomFiles = FileUtil.loopFiles(path, file -> file.getName().endsWith(PluginConstants.CLASS_SUFFIX)); | |
Set<String> classNameSet = new HashSet<>(); | |
for (File file : pomFiles) { | |
String className = CharSequenceUtil.subBetween(file.getPath(), PluginConstants.CLASSES + File.separator, PluginConstants.CLASS_SUFFIX).replace(File.separator, "."); | |
classNameSet.add(className); | |
} | |
return classNameSet; | |
} | |
} | |
} |
插件自动化配置
PluginAutoConfiguration
插件自动化配置信息
public class PluginAutoConfiguration { | |
/** | |
* 是否启用插件功能 | |
*/ | |
private Boolean enable; | |
/** | |
* 运行模式 | |
* 开发环境: development、dev | |
* 生产/部署 环境: deployment、prod | |
*/ | |
private String runMode; | |
/** | |
* 插件的路径 | |
*/ | |
private List<String> pluginPath; | |
/** | |
* 在卸载插件后, 备份插件的目录 | |
*/ | |
private String backupPath; | |
public RuntimeMode environment() { | |
return RuntimeMode.byName(runMode); | |
} | |
} |
PluginStarter
插件自动化配置,配置在spring.factories中
@Configuration(proxyBeanMethods = true) | |
@EnableConfigurationProperties(PluginAutoConfiguration.class) | |
@Import(DefaultPluginApplication.class) | |
public class PluginStarter { | |
} |
PluginConfiguration
配置插件管理操作类,主程序可以注入该类,操作插件的安装、卸载、获取插件上下文
public class PluginConfiguration { | |
public PluginManager createPluginManager(PluginAutoConfiguration configuration, ApplicationContext applicationContext) { | |
return new DefaultPluginManager(configuration, applicationContext); | |
} | |
} |
插件加载注册
DefaultPluginApplication
监听Spring Boot启动完成,加载插件,调用父类的加载方法,获取主程序上下文
import org.springframework.beans.BeansException; | |
import org.springframework.boot.context.event.ApplicationStartedEvent; | |
import org.springframework.context.ApplicationContext; | |
import org.springframework.context.ApplicationContextAware; | |
import org.springframework.context.ApplicationListener; | |
import org.springframework.context.annotation.Import; | |
public class DefaultPluginApplication extends AbstractPluginApplication implements ApplicationContextAware, ApplicationListener<ApplicationStartedEvent> { | |
private ApplicationContext applicationContext; | |
//主程序启动后加载插件 | |
public void onApplicationEvent(ApplicationStartedEvent event) { | |
super.initialize(applicationContext); | |
} | |
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { | |
this.applicationContext = applicationContext; | |
} | |
} |
AbstractPluginApplication
提供插件的加载,从主程序中获取插件配置,获取插件管理操作类
import java.util.Objects; | |
import java.util.concurrent.atomic.AtomicBoolean; | |
import org.springframework.beans.factory.BeanCreationException; | |
import org.springframework.context.ApplicationContext; | |
import lombok.extern.slfj.Slf4j; | |
public abstract class AbstractPluginApplication { | |
private final AtomicBoolean beInitialized = new AtomicBoolean(false); | |
public synchronized void initialize(ApplicationContext applicationContext) { | |
Objects.requireNonNull(applicationContext, "ApplicationContext can't be null"); | |
if(beInitialized.get()) { | |
throw new RuntimeException("Plugin has been initialized"); | |
} | |
//获取配置 | |
PluginAutoConfiguration configuration = getConfiguration(applicationContext); | |
if (Boolean.FALSE.equals(configuration.getEnable())) { | |
log.info("插件已禁用"); | |
return; | |
} | |
try { | |
log.info("插件加载环境: {},插件目录: {}", configuration.getRunMode(), String.join(",", configuration.getPluginPath())); | |
DefaultPluginManager pluginManager = getPluginManager(applicationContext); | |
pluginManager.createPluginListenerFactory(); | |
pluginManager.loadPlugins(); | |
beInitialized.set(true); | |
log.info("插件启动完成"); | |
} catch (Exception e) { | |
log.error("初始化插件异常", e); | |
} | |
} | |
protected PluginAutoConfiguration getConfiguration(ApplicationContext applicationContext) { | |
PluginAutoConfiguration configuration = null; | |
try { | |
configuration = applicationContext.getBean(PluginAutoConfiguration.class); | |
} catch (Exception e){ | |
// no show exception | |
} | |
if(configuration == null){ | |
throw new BeanCreationException("没有发现 <PluginAutoConfiguration> Bean"); | |
} | |
return configuration; | |
} | |
protected DefaultPluginManager getPluginManager(ApplicationContext applicationContext) { | |
DefaultPluginManager pluginManager = null; | |
try { | |
pluginManager = applicationContext.getBean(DefaultPluginManager.class); | |
} catch (Exception e){ | |
// no show exception | |
} | |
if(pluginManager == null){ | |
throw new BeanCreationException("没有发现 <DefaultPluginManager> Bean"); | |
} | |
return pluginManager; | |
} | |
} |
DefaultPluginManager
插件操作类,管理插件的加载、安装、卸载,主程序使用该类对插件进行操作
import java.io.File; | |
import java.io.FileInputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.lang.annotation.Annotation; | |
import java.nio.file.Path; | |
import java.nio.file.Paths; | |
import java.util.ArrayList; | |
import java.util.HashSet; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Set; | |
import java.util.concurrent.ConcurrentHashMap; | |
import java.util.concurrent.atomic.AtomicBoolean; | |
import java.util.jar.Attributes; | |
import java.util.jar.Manifest; | |
import org.apache.maven.model.Model; | |
import org.apache.maven.model.io.xpp.MavenXpp3Reader; | |
import org.codehaus.plexus.util.xml.pull.XmlPullParserException; | |
import org.springframework.context.ApplicationContext; | |
import com.greentown.plugin.constants.PluginConstants; | |
import com.greentown.plugin.constants.PluginState; | |
import com.greentown.plugin.constants.RuntimeMode; | |
import com.greentown.plugin.listener.DefaultPluginListenerFactory; | |
import com.greentown.plugin.util.DeployUtils; | |
import cn.hutool.core.collection.CollUtil; | |
import cn.hutool.core.date.DateUtil; | |
import cn.hutool.core.io.FileUtil; | |
import cn.hutool.core.io.file.PathUtil; | |
import cn.hutool.core.text.CharSequenceUtil; | |
import lombok.extern.slfj.Slf4j; | |
public class DefaultPluginManager implements PluginManager { | |
private PluginAutoConfiguration pluginAutoConfiguration; | |
private ApplicationContext applicationContext; | |
private DefaultPluginListenerFactory pluginListenerFactory; | |
private PluginClassRegister pluginClassRegister; | |
private Map<String, ApplicationContext> pluginBeans = new ConcurrentHashMap<>(); | |
private Map<String, PluginInfo> pluginInfoMap = new ConcurrentHashMap<>(); | |
private final AtomicBoolean loaded = new AtomicBoolean(false); | |
public DefaultPluginManager(PluginAutoConfiguration pluginAutoConfiguration, | |
ApplicationContext applicationContext) { | |
this.pluginAutoConfiguration = pluginAutoConfiguration; | |
this.applicationContext = applicationContext; | |
this.pluginClassRegister = new PluginClassRegister(applicationContext, pluginAutoConfiguration, pluginBeans); | |
} | |
public void createPluginListenerFactory() { | |
this.pluginListenerFactory = new DefaultPluginListenerFactory(applicationContext); | |
} | |
public List<PluginInfo> loadPlugins() throws Exception { | |
if(loaded.get()){ | |
throw new PluginException("不能重复调用: loadPlugins"); | |
} | |
//从配置路径获取插件目录 | |
//解析插件jar包中的配置,生成配置对象 | |
List<PluginInfo> pluginInfoList = loadPluginsFromPath(pluginAutoConfiguration.getPluginPath()); | |
if (CollUtil.isEmpty(pluginInfoList)) { | |
log.warn("路径下未发现任何插件"); | |
return pluginInfoList; | |
} | |
//注册插件 | |
for (PluginInfo pluginInfo : pluginInfoList) { | |
start(pluginInfo); | |
} | |
loaded.set(true); | |
return pluginInfoList; | |
} | |
private List<PluginInfo> loadPluginsFromPath(List<String> pluginPath) throws IOException, XmlPullParserException { | |
List<PluginInfo> pluginInfoList = new ArrayList<>(); | |
for (String path : pluginPath) { | |
Path resolvePath = Paths.get(path); | |
Set<PluginInfo> pluginInfos = buildPluginInfo(resolvePath); | |
pluginInfoList.addAll(pluginInfos); | |
} | |
return pluginInfoList; | |
} | |
private Set<PluginInfo> buildPluginInfo(Path path) throws IOException, XmlPullParserException { | |
Set<PluginInfo> pluginInfoList = new HashSet<>(); | |
//开发环境 | |
if (RuntimeMode.DEV == pluginAutoConfiguration.environment()) { | |
List<File> pomFiles = FileUtil.loopFiles(path.toString(), file -> PluginConstants.POM.equals(file.getName())); | |
for (File file : pomFiles) { | |
MavenXppReader reader = new MavenXpp3Reader(); | |
Model model = reader.read(new FileInputStream(file)); | |
PluginInfo pluginInfo = PluginInfo.builder().id(model.getArtifactId()) | |
.version(model.getVersion() == null ? model.getParent().getVersion() : model.getVersion()) | |
.description(model.getDescription()).build(); | |
//开发环境重新定义插件路径,需要指定到classes目录 | |
pluginInfo.setPath(CharSequenceUtil.subBefore(path.toString(), pluginInfo.getId(), false) | |
+ File.separator + pluginInfo.getId() | |
+ File.separator + PluginConstants.TARGET | |
+ File.separator + PluginConstants.CLASSES); | |
pluginInfoList.add(pluginInfo); | |
} | |
} | |
//生产环境从jar包中读取 | |
if (RuntimeMode.PROD == pluginAutoConfiguration.environment()) { | |
//获取jar包列表 | |
List<File> jarFiles = FileUtil.loopFiles(path.toString(), file -> file.getName().endsWith(PluginConstants.REPACKAGE + PluginConstants.JAR_SUFFIX)); | |
for (File jarFile : jarFiles) { | |
//读取配置 | |
try(InputStream jarFileInputStream = DeployUtils.readManifestJarFile(jarFile)) { | |
Manifest manifest = new Manifest(jarFileInputStream); | |
Attributes attr = manifest.getMainAttributes(); | |
PluginInfo pluginInfo = PluginInfo.builder().id(attr.getValue(PluginConstants.PLUGINID)) | |
.version(attr.getValue(PluginConstants.PLUGINVERSION)) | |
.description(attr.getValue(PluginConstants.PLUGINDESCRIPTION)) | |
.path(jarFile.getPath()).build(); | |
pluginInfoList.add(pluginInfo); | |
} catch (Exception e) { | |
log.warn("插件{}配置读取异常", jarFile.getName()); | |
} | |
} | |
} | |
return pluginInfoList; | |
} | |
public PluginInfo install(Path pluginPath) { | |
if (RuntimeMode.PROD != pluginAutoConfiguration.environment()) { | |
throw new PluginException("插件安装只适用于生产环境"); | |
} | |
try { | |
Set<PluginInfo> pluginInfos = buildPluginInfo(pluginPath); | |
if (CollUtil.isEmpty(pluginInfos)) { | |
throw new PluginException("插件不存在"); | |
} | |
PluginInfo pluginInfo = (PluginInfo) pluginInfos.toArray()[]; | |
if (pluginInfoMap.get(pluginInfo.getId()) != null) { | |
log.info("已存在同类插件{},将覆盖安装", pluginInfo.getId()); | |
} | |
uninstall(pluginInfo.getId()); | |
start(pluginInfo); | |
return pluginInfo; | |
} catch (Exception e) { | |
throw new PluginException("插件安装失败", e); | |
} | |
} | |
private void start(PluginInfo pluginInfo) { | |
try { | |
pluginClassRegister.register(pluginInfo); | |
pluginInfo.setPluginState(PluginState.STARTED); | |
pluginInfoMap.put(pluginInfo.getId(), pluginInfo); | |
log.info("插件{}启动成功", pluginInfo.getId()); | |
pluginListenerFactory.startSuccess(pluginInfo); | |
} catch (Exception e) { | |
log.error("插件{}注册异常", pluginInfo.getId(), e); | |
pluginListenerFactory.startFailure(pluginInfo, e); | |
} | |
} | |
public void uninstall(String pluginId) { | |
if (RuntimeMode.PROD != pluginAutoConfiguration.environment()) { | |
throw new PluginException("插件卸载只适用于生产环境"); | |
} | |
PluginInfo pluginInfo = pluginInfoMap.get(pluginId); | |
if (pluginInfo == null) { | |
return; | |
} | |
stop(pluginInfo); | |
backupPlugin(pluginInfo); | |
clear(pluginInfo); | |
} | |
public PluginInfo start(String pluginId) { | |
PluginInfo pluginInfo = pluginInfoMap.get(pluginId); | |
start(pluginInfo); | |
return pluginInfo; | |
} | |
public PluginInfo stop(String pluginId) { | |
PluginInfo pluginInfo = pluginInfoMap.get(pluginId); | |
stop(pluginInfo); | |
return pluginInfo; | |
} | |
private void clear(PluginInfo pluginInfo) { | |
PathUtil.del(Paths.get(pluginInfo.getPath())); | |
pluginInfoMap.remove(pluginInfo.getId()); | |
} | |
private void stop(PluginInfo pluginInfo) { | |
try { | |
pluginClassRegister.unRegister(pluginInfo); | |
pluginInfo.setPluginState(PluginState.STOPPED); | |
pluginListenerFactory.stopSuccess(pluginInfo); | |
log.info("插件{}停止成功", pluginInfo.getId()); | |
} catch (Exception e) { | |
log.error("插件{}停止异常", pluginInfo.getId(), e); | |
} | |
} | |
private void backupPlugin(PluginInfo pluginInfo) { | |
String backupPath = pluginAutoConfiguration.getBackupPath(); | |
if (CharSequenceUtil.isBlank(backupPath)) { | |
return; | |
} | |
String newName = pluginInfo.getId() + DateUtil.now() + PluginConstants.JAR_SUFFIX; | |
String newPath = backupPath + File.separator + newName; | |
FileUtil.copyFile(pluginInfo.getPath(), newPath); | |
} | |
public ApplicationContext getApplicationContext(String pluginId) { | |
return pluginBeans.get(pluginId); | |
} | |
public List<Object> getBeansWithAnnotation(String pluginId, Class<? extends Annotation> annotationType) { | |
ApplicationContext pluginApplicationContext = pluginBeans.get(pluginId); | |
if(pluginApplicationContext != null){ | |
Map<String, Object> beanMap = pluginApplicationContext.getBeansWithAnnotation(annotationType); | |
return new ArrayList<>(beanMap.values()); | |
} | |
return new ArrayList<>(); | |
} | |
} |
PluginClassRegister
插件动态注册、动态卸载,解析插件class,判断是否为Spring Bean或Spring 接口,是注册到Spring 中
public class PluginClassRegister { | |
private ApplicationContext applicationContext; | |
private RequestMappingHandlerMapping requestMappingHandlerMapping; | |
private Method getMappingForMethod; | |
private PluginAutoConfiguration configuration; | |
private Map<String, ApplicationContext> pluginBeans; | |
private Map<String, Set<RequestMappingInfo>> requestMappings = new ConcurrentHashMap<>(); | |
public PluginClassRegister(ApplicationContext applicationContext, PluginAutoConfiguration configuration, Map<String, ApplicationContext> pluginBeans) { | |
this.applicationContext = applicationContext; | |
this.requestMappingHandlerMapping = getRequestMapping(); | |
this.getMappingForMethod = getRequestMethod(); | |
this.configuration = configuration; | |
this.pluginBeans = pluginBeans; | |
} | |
public ApplicationContext register(PluginInfo pluginInfo) { | |
ApplicationContext pluginApplicationContext = registerBean(pluginInfo); | |
pluginBeans.put(pluginInfo.getId(), pluginApplicationContext); | |
return pluginApplicationContext; | |
} | |
public boolean unRegister(PluginInfo pluginInfo) { | |
return unRegisterBean(pluginInfo); | |
} | |
private boolean unRegisterBean(PluginInfo pluginInfo) { | |
GenericWebApplicationContext pluginApplicationContext = (GenericWebApplicationContext) pluginBeans.get(pluginInfo.getId()); | |
pluginApplicationContext.close(); | |
//取消注册controller | |
Set<RequestMappingInfo> requestMappingInfoSet = requestMappings.get(pluginInfo.getId()); | |
if (requestMappingInfoSet != null) { | |
requestMappingInfoSet.forEach(this::unRegisterController); | |
} | |
requestMappings.remove(pluginInfo.getId()); | |
pluginBeans.remove(pluginInfo.getId()); | |
return true; | |
} | |
private void unRegisterController(RequestMappingInfo requestMappingInfo) { | |
requestMappingHandlerMapping.unregisterMapping(requestMappingInfo); | |
} | |
private ApplicationContext registerBean(PluginInfo pluginInfo) { | |
String path = pluginInfo.getPath(); | |
Set<String> classNames = DeployUtils.readClassFile(path); | |
URLClassLoader classLoader = null; | |
try { | |
//class 加载器 | |
URL jarURL = new File(path).toURI().toURL(); | |
classLoader = new URLClassLoader(new URL[] { | |
jarURL }, Thread.currentThread().getContextClassLoader()); | |
//一个插件创建一个applicationContext | |
GenericWebApplicationContext pluginApplicationContext = new GenericWebApplicationContext(); | |
pluginApplicationContext.setResourceLoader(new DefaultResourceLoader(classLoader)); | |
//注册bean | |
List<String> beanNames = new ArrayList<>(); | |
for (String className : classNames) { | |
Class clazz = classLoader.loadClass(className); | |
if (DeployUtils.isSpringBeanClass(clazz)) { | |
String simpleClassName = DeployUtils.transformName(className); | |
BeanDefinitionRegistry beanDefinitonRegistry = (BeanDefinitionRegistry) pluginApplicationContext.getBeanFactory(); | |
BeanDefinitionBuilder usersBeanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz); | |
usersBeanDefinitionBuilder.setScope("singleton"); | |
beanDefinitonRegistry.registerBeanDefinition(simpleClassName, usersBeanDefinitionBuilder.getRawBeanDefinition()); | |
beanNames.add(simpleClassName); | |
} | |
} | |
//刷新上下文 | |
pluginApplicationContext.refresh(); | |
//注入bean和注册接口 | |
Set<RequestMappingInfo> pluginRequestMappings = new HashSet<>(); | |
for (String beanName : beanNames) { | |
//注入bean | |
Object bean = pluginApplicationContext.getBean(beanName); | |
injectService(bean); | |
//注册接口 | |
Set<RequestMappingInfo> requestMappingInfos = registerController(bean); | |
requestMappingInfos.forEach(requestMappingInfo -> { | |
log.info("插件{}注册接口{}", pluginInfo.getId(), requestMappingInfo); | |
}); | |
pluginRequestMappings.addAll(requestMappingInfos); | |
} | |
requestMappings.put(pluginInfo.getId(), pluginRequestMappings); | |
return pluginApplicationContext; | |
} catch (Exception e) { | |
throw new PluginException("注册bean异常", e); | |
} finally { | |
try { | |
if (classLoader != null) { | |
classLoader.close(); | |
} | |
} catch (IOException e) { | |
log.error("classLoader关闭失败", e); | |
} | |
} | |
} | |
private Set<RequestMappingInfo> registerController(Object bean) { | |
Class<?> aClass = bean.getClass(); | |
Set<RequestMappingInfo> requestMappingInfos = new HashSet<>(); | |
if (Boolean.TRUE.equals(DeployUtils.isController(aClass))) { | |
Method[] methods = aClass.getDeclaredMethods(); | |
for (Method method : methods) { | |
if (DeployUtils.isHaveRequestMapping(method)) { | |
try { | |
RequestMappingInfo requestMappingInfo = (RequestMappingInfo) | |
getMappingForMethod.invoke(requestMappingHandlerMapping, method, aClass); | |
requestMappingHandlerMapping.registerMapping(requestMappingInfo, bean, method); | |
requestMappingInfos.add(requestMappingInfo); | |
} catch (Exception e){ | |
log.error("接口注册异常", e); | |
} | |
} | |
} | |
} | |
return requestMappingInfos; | |
} | |
private void injectService(Object instance){ | |
if (instance==null) { | |
return; | |
} | |
Field[] fields = ReflectUtil.getFields(instance.getClass()); //instance.getClass().getDeclaredFields(); | |
for (Field field : fields) { | |
if (Modifier.isStatic(field.getModifiers())) { | |
continue; | |
} | |
Object fieldBean = null; | |
// with bean-id, bean could be found by both @Resource and @Autowired, or bean could only be found by @Autowired | |
if (AnnotationUtils.getAnnotation(field, Resource.class) != null) { | |
try { | |
Resource resource = AnnotationUtils.getAnnotation(field, Resource.class); | |
if (resource.name()!=null && resource.name().length()>){ | |
fieldBean = applicationContext.getBean(resource.name()); | |
} else { | |
fieldBean = applicationContext.getBean(field.getName()); | |
} | |
} catch (Exception e) { | |
} | |
if (fieldBean==null ) { | |
fieldBean = applicationContext.getBean(field.getType()); | |
} | |
} else if (AnnotationUtils.getAnnotation(field, Autowired.class) != null) { | |
Qualifier qualifier = AnnotationUtils.getAnnotation(field, Qualifier.class); | |
if (qualifier!=null && qualifier.value()!=null && qualifier.value().length()>) { | |
fieldBean = applicationContext.getBean(qualifier.value()); | |
} else { | |
fieldBean = applicationContext.getBean(field.getType()); | |
} | |
} | |
if (fieldBean!=null) { | |
field.setAccessible(true); | |
try { | |
field.set(instance, fieldBean); | |
} catch (IllegalArgumentException e) { | |
log.error(e.getMessage(), e); | |
} catch (IllegalAccessException e) { | |
log.error(e.getMessage(), e); | |
} | |
} | |
} | |
} | |
private Method getRequestMethod() { | |
try { | |
Method method = ReflectUtils.findDeclaredMethod(requestMappingHandlerMapping.getClass(), "getMappingForMethod", new Class[] { | |
Method.class, Class.class }); | |
method.setAccessible(true); | |
return method; | |
} catch (Exception ex) { | |
log.error("反射获取detectHandlerMethods异常", ex); | |
} | |
return null; | |
} | |
private RequestMappingHandlerMapping getRequestMapping() { | |
return (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping"); | |
} | |
} |
插件Mock包
plugin-mock
提供插件的开发模拟测试相关的依赖,以Jar包方式提供,根据具体项目提供依赖
插件开发环境
一个独立的项目,依赖上述提供的插件核心包、插件Mock包,提供给插件开发人员使用。
main-application:插件开发测试的主程序
plugins:插件开发目录
在最开始的使用,我们的插件使用Spring Brick来开发,光在集成过程中就发现不少问题,特别是依赖冲突很多,并且对插件的加载比较慢,导致主程序启动慢。
在自研插件后,该插件加载启动使用动态注入Spring的方式,相比较Spring Brick的插件独立Spring Boot方式加载速度更快,占用内存更小,虽然还不支持Freemark、AOP等框架,但对于此类功能后期也可以通过后置处理器扩展。