基于Java的插件化集成项目实践
Posted 阿提说说
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于Java的插件化集成项目实践相关的知识,希望对你有一定的参考价值。
之前已经写了一篇关于《几种Java热插拔技术实现总结》,在该文中我总结了好几种Java实现热插拔的技术,其中各有优缺点,在这篇文章我将介绍Java热插拔技术在我司项目中的实践。
前言
在开始之前,先看下插件系统的整体框架
- 插件开发模拟环境
“插件开发模拟环境”主要用于插件的开发和测试,一个独立项目,提供给插件开发人员使用。开发模拟环境依赖插件核心包、插件依赖的主程序包。
插件核心包-负责插件的加载,安装、注册、卸载
插件依赖的主程序包-提供插件开发测试的主程序依赖 - 主程序
插件的正式安装使用环境,线上环境。插件在本地开发测试完成后,通过插件管理页面安装到线上环境进行插件验证。可以分多个环境,线上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 PLUGINVERSION = "pluginVersion";
public static final String PLUGINDESCRIPTION = "pluginDescription";
PluginState
插件状态
@AllArgsConstructor
public enum PluginState
/**
* 被禁用状态
*/
DISABLED("DISABLED"),
/**
* 启动状态
*/
STARTED("STARTED"),
/**
* 停止状态
*/
STOPPED("STOPPED");
private final String status;
RuntimeMode
插件运行环境
@Getter
@AllArgsConstructor
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进行去重
@Data
@Builder
public class PluginInfo
/**
* 插件id
*/
private String id;
/**
* 版本
*/
private String version;
/**
* 描述
*/
private String description;
/**
* 插件路径
*/
private String path;
/**
* 插件启动状态
*/
private PluginState pluginState;
@Override
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);
@Override
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;
@Override
public void startSuccess(PluginInfo pluginInfo)
for (PluginListener listener : listeners)
try
listener.startSuccess(pluginInfo);
catch (Exception e)
@Override
public void startFailure(PluginInfo pluginInfo, Throwable throwable)
for (PluginListener listener : listeners)
try
listener.startFailure(pluginInfo, throwable);
catch (Exception e)
@Override
public void stopSuccess(PluginInfo pluginInfo)
for (PluginListener listener : listeners)
try
listener.stopSuccess(pluginInfo);
catch (Exception e)
@Override
public void stopFailure(PluginInfo pluginInfo, Throwable throwable)
for (PluginListener listener : listeners)
try
listener.stopFailure(pluginInfo, throwable);
catch (Exception e)
工具类
DeployUtils
部署工具类,读取jar包中的文件,判断class是否为Spring bean等
@Slf4j
public class DeployUtils
/**
* 读取jar包中所有类文件
*/
public static Set<String> readJarFile(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.getAnnotation(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 isHaveRequestMapping(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(0, 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
插件自动化配置信息
@ConfigurationProperties(prefix = "plugin")
@Data
public class PluginAutoConfiguration
/**
* 是否启用插件功能
*/
@Value("$enable:true")
private Boolean enable;
/**
* 运行模式
* 开发环境: development、dev
* 生产/部署 环境: deployment、prod
*/
@Value("$runMode:dev")
private String runMode;
/**
* 插件的路径
*/
private List<String> pluginPath;
/**
* 在卸载插件后, 备份插件的目录
*/
@Value("$backupPath:backupPlugin")
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
配置插件管理操作类,主程序可以注入该类,操作插件的安装、卸载、获取插件上下文
@Configuration
public class PluginConfiguration
@Bean
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;
@Import(PluginConfiguration.class)
public class DefaultPluginApplication extends AbstractPluginApplication implements ApplicationContextAware, ApplicationListener<ApplicationStartedEvent>
private ApplicationContext applicationContext;
//主程序启动后加载插件
@Override
public void onApplicationEvent(ApplicationStartedEvent event)
super.initialize(applicationContext);
@Override
public void setApplicationContext(企业微信零耦合集成腾讯会议和腾讯文档插件化架构实践
Jenkins持续集成项目搭建与实践——基于Python Selenium自动化测试(自由风格)
IDEA系列:插件:Upsource团队代码审核的具体介绍与使用