• 基于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";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    PluginState
    插件状态

    @AllArgsConstructor
    public enum PluginState {
    	/**
         * 被禁用状态
         */
        DISABLED("DISABLED"),
    
        /**
         * 启动状态
         */
        STARTED("STARTED"),
    
    
        /**
         * 停止状态
         */
        STOPPED("STOPPED");
    	
    	private final String status;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    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;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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;
    	}
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    插件监听器

    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){}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    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) {
                	
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    工具类

    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;
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114

    插件自动化配置

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    PluginStarter
    插件自动化配置,配置在spring.factories中

    @Configuration(proxyBeanMethods = true)
    @EnableConfigurationProperties(PluginAutoConfiguration.class)
    @Import(DefaultPluginApplication.class)
    public class PluginStarter {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    PluginConfiguration
    配置插件管理操作类,主程序可以注入该类,操作插件的安装、卸载、获取插件上下文

    @Configuration
    public class PluginConfiguration {
        @Bean
        public PluginManager createPluginManager(PluginAutoConfiguration configuration, ApplicationContext applicationContext) {
            return new DefaultPluginManager(configuration, applicationContext);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    插件加载注册

    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(ApplicationContext applicationContext) throws BeansException {
    		this.applicationContext = applicationContext;
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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.slf4j.Slf4j;
    
    @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("没有发现  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("没有发现  Bean");
            }
            return pluginManager;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    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.xpp3.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.slf4j.Slf4j;
    
    @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);
    	}
    
    	@Override
    	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) {
    				MavenXpp3Reader 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;
    	}
    
    	@Override
    	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()[0];
    			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);
    		}
    	}
    
    	@Override
    	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);
    	}
    	
    	@Override
    	public PluginInfo start(String pluginId) {
    		PluginInfo pluginInfo = pluginInfoMap.get(pluginId);
    		start(pluginInfo);
    		return pluginInfo;
    	}
    
    	@Override
    	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);
    	}
    
    	@Override
    	public ApplicationContext getApplicationContext(String pluginId) {
    		return pluginBeans.get(pluginId);
    	}
    
    	@Override
    	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<>(0);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237

    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()>0){
    						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()>0) {
    					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");
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193

    插件Mock包

    plugin-mock
    提供插件的开发模拟测试相关的依赖,以Jar包方式提供,根据具体项目提供依赖

    插件开发环境

    一个独立的项目,依赖上述提供的插件核心包、插件Mock包,提供给插件开发人员使用。
    main-application:插件开发测试的主程序
    plugins:插件开发目录

    总结

    在最开始的使用,我们的插件使用Spring Brick来开发,光在集成过程中就发现不少问题,特别是依赖冲突很多,并且对插件的加载比较慢,导致主程序启动慢。
    在自研插件后,该插件加载启动使用动态注入Spring的方式,相比较Spring Brick的插件独立Spring Boot方式加载速度更快,占用内存更小,虽然还不支持Freemark、AOP等框架,但对于此类功能后期也可以通过后置处理器扩展。

  • 相关阅读:
    git简易入门教学
    “磐云杯”中职网络安全技能大赛A模块新题
    浅析Hive窗口分析函数
    景联文数据标注平台助力标注效率翻倍,年处理图像数据过亿
    分布式协调服务
    数据结构:队列
    langchain加载.doc、.docx遇到的问题
    Ubuntu apt PPA源的定义及相关操作(搜索线上PPA源、查看本地PPA源、添加、修改、删除)
    MDC、ThreadLocal、InheritableThreadLocal的区别和联系
    在IDEA中如何新建一个web工程
  • 原文地址:https://blog.csdn.net/weixin_40972073/article/details/126135356