基于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(企业微信零耦合集成腾讯会议和腾讯文档插件化架构实践

Android热修复与插件化实践之路

Jenkins持续集成项目搭建与实践——基于Python Selenium自动化测试(自由风格)

IDEA系列:插件:Upsource团队代码审核的具体介绍与使用

Confluence集成实践 2 Confluence的RestAPI

Laravel9+Vue+ElementUI框架基本使用实践教程