之前已经写了一篇关于《几种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
插件状态

@AllArgsConstructorpublic enum PluginState {/**     * 被禁用状态     */    DISABLED("DISABLED"),    /**     * 启动状态     */    STARTED("STARTED"),    /**     * 停止状态     */    STOPPED("STOPPED");private final String status;}

RuntimeMode
插件运行环境

@Getter@AllArgsConstructorpublic 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@Builderpublic class PluginInfo {/** * 插件id */private String id;/** * 版本 */private String version;/** * 描述 */private String description;/** * 插件路径 */private String path;/** * 插件启动状态 */private PluginState pluginState;@Overridepublic 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);}@Overridepublic 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等

@Slf4jpublic 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")@Datapublic 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
配置插件管理操作类,主程序可以注入该类,操作插件的安装、卸载、获取插件上下文

@Configurationpublic 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;//主程序启动后加载插件@Overridepublic void onApplicationEvent(ApplicationStartedEvent event) {super.initialize(applicationContext);}@Overridepublic 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.slf4j.Slf4j;@Slf4jpublic 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;    }}

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;@Slf4jpublic 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);}@Overridepublic 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;}@Overridepublic 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);}}@Overridepublic 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);}@Overridepublic PluginInfo start(String pluginId) {PluginInfo pluginInfo = pluginInfoMap.get(pluginId);start(pluginInfo);return pluginInfo;}@Overridepublic 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);}@Overridepublic ApplicationContext getApplicationContext(String pluginId) {return pluginBeans.get(pluginId);}@Overridepublic 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);}}

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();//取消注册controllerSet<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());//一个插件创建一个applicationContextGenericWebApplicationContext pluginApplicationContext = new GenericWebApplicationContext();pluginApplicationContext.setResourceLoader(new DefaultResourceLoader(classLoader));//注册beanList<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) {//注入beanObject 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 @Autowiredif (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");}}

插件Mock包

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

插件开发环境

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

总结

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