基于 Java 的插件化集成项目实践

简介: 之前已经写了一篇关于《几种Java热插拔技术实现总结》,在该文中我总结了好几种Java实现热插拔的技术,其中各有优缺点,在这篇文章我将介绍Java热插拔技术在我司项目中的实践。
之前已经写了一篇关于《几种Java热插拔技术实现总结》,在该文中我总结了好几种Java实现热插拔的技术,其中各有优缺点,在这篇文章我将介绍Java热插拔技术在我司项目中的实践。

前言

在开始之前,先看下插件系统的整体框架
image.png

  • 插件开发模拟环境

“插件开发模拟环境”主要用于插件的开发和测试,一个独立项目,提供给插件开发人员使用。开发模拟环境依赖插件核心包插件依赖的主程序包
插件核心包-负责插件的加载,安装、注册、卸载
插件依赖的主程序包-提供插件开发测试的主程序依赖

  • 主程序

插件的正式安装使用环境,线上环境。插件在本地开发测试完成后,通过插件管理页面安装到线上环境进行插件验证。可以分多个环境,线上dev环境提供插件的线上验证,待验证完成后,再发布到prod环境。

代码实现

插件加载流程

image.png

在监听到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(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;

@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.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);
    }
}

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");
    }

}

插件Mock包

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

插件开发环境

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

总结

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

相关文章
|
5天前
|
Java Maven
2022最新版超详细的Maven下载配置教程、IDEA中集成maven(包含图解过程)、以及导入项目时jar包下载不成功的问题解决
这篇文章是一份关于Maven的安装和配置指南,包括下载、环境变量设置、配置文件修改、IDEA集成Maven以及解决jar包下载问题的方法。
2022最新版超详细的Maven下载配置教程、IDEA中集成maven(包含图解过程)、以及导入项目时jar包下载不成功的问题解决
|
5天前
|
前端开发 Java 编译器
【前端学java】java基础练习缺少项目?看这篇文章就够了!(完结)
【8月更文挑战第11天】java基础练习缺少项目?看这篇文章就够了!(完结)
14 0
|
4天前
|
前端开发 Java Maven
【前端学java】全网最详细的maven安装与IDEA集成教程!
【8月更文挑战第12天】全网最详细的maven安装与IDEA集成教程!
18 2
【前端学java】全网最详细的maven安装与IDEA集成教程!
|
3天前
|
Java API 数据中心
百炼平台Java 集成API上传文档到数据中心并添加索引
本文主要演示阿里云百炼产品,如何通过API实现数据中心文档的上传和索引的添加。
|
3天前
|
存储 设计模式 Java
Java中的if-else语句:深入解析与应用实践
Java中的if-else语句:深入解析与应用实践
|
5天前
|
前端开发 Java 编译器
【前端学java】java基础练习缺少项目?看这篇文章就够了!(17)
【8月更文挑战第11天】java基础练习缺少项目?看这篇文章就够了!
11 0
【前端学java】java基础练习缺少项目?看这篇文章就够了!(17)
|
3天前
|
算法 安全 Java
深入解析Java多线程:源码级别的分析与实践
深入解析Java多线程:源码级别的分析与实践
|
5天前
|
Java Spring
Java SpringBoot Bean InitializingBean 项目初始化
Java SpringBoot Bean InitializingBean 项目初始化
12 0
|
5天前
|
存储 缓存 Java
Java本地高性能缓存实践问题之定义Caffeine的缓存的问题如何解决
Java本地高性能缓存实践问题之定义Caffeine的缓存的问题如何解决
|
5天前
|
存储 缓存 Java
Java本地高性能缓存实践问题之使用@CachePut注解来更新缓存中的数据的问题如何解决
Java本地高性能缓存实践问题之使用@CachePut注解来更新缓存中的数据的问题如何解决