原创

基于微服务的插件化解决方案(一)

基于微服务架构的web应用程序开发已成为目前主流的解决方案,通常我们会将系统按照业务功能拆分成不同的微服务来降低系统的耦合度,但是如果将服务单元拆分的过小又会大大提高系统复杂度。从运维的层面上看,为满足高可用每个服务需要启动多个服务容器,每多拆分出来一个微服务就需要多维护多个服务容器。从开发层面来看,微服务间的调用采用远程调用,底层还是http请求,服务A调用服务B,那么服务A和服务B中必然存在相同的request bean和response bean,服务A中的两个bean如果发生变更同样需要修改服务B中的两个bean,大大增加了开发成本,从系统性能上来看,服务的增多会导致调用链的延长,高并发场景下服务间的调用会导IO操作的增多,占据大量资源,降低系统性能。因此微服务的拆分不宜过细。那么有什么方法能进一步降低耦合度,又不增加维护难度,还不损失性能?那就是接下来要讨论的微服务的插件化方案。

一、什么是插件化解决方案?

在微服务架构中,一组微服务组成了一个系统,在插件化解决方案中,一组插件插件组成了一个微服务。每个微服务是一个大的功能模块,每个微服务中的小的功能模块使用插件的方式安装到微服务中,这样就可以在不同的交付方向根据不同的需求安装功能有差异化的功能插件灵活组装系统。插件化的思路是加载多个插件到同一个JVM中,微服务服务数量不会增多。因此运维的复杂度不会提高。对于开发者来说只是项目结构多划分几个模块,执行的打包命令有所不同,不会增加太多开发成本。插件之间可以相互依赖且调用是在同一个JVM的内存中,不会额外占用IO资源影响系统性能。

二、微服务插件化整体思路

提起插件化我们首先想到的可能是OSGI、SPI规范,但是对于基于SSM框架的web项目来说这两种解决方案显然不太适用,这两种方案均未解决spring的依赖注入问题以及spingMVC的接口注册问题,因此适合普通Java项目的插件化实现。springboot是springcloud微服务架构的基础,因此我们使用springboot来整合SSM框架,本方案的整体思路就是动态加载springboot打包后的jar包,扫描需要注册到IOC容器中的bean,实例化之后注册到IOC容器,对于被@controller和@RestController注解的bean,还需要扫描API接口注册到RequestMappingHandlerMapping中,本次方案仅讨论Restful风格接口,暂时不考虑前后端一体项目中的静态资源加载。其中核心部分为动态加载jar包并注册bean到IOC容器中,此过程需要自定义类加载器打破Java类加载器的双亲委派机制,关于双亲委派机制此处不再赘述,接下来通过代码来实现此过程。

三、效果演示

我们先看下最终效果,下图演示的流程为:

  1. 调用插件中接口返回404
  2. 调用安装插件接口热加载插件包
  3. 再次调用插件中的接口返回插件接口调用成功信息
  4. 调用卸载插件接口卸载插件包
  5. 再次调用插件中的接口返回404

效果如下:
效果图

四、自定义类加载器

首先我们需要有一个类加载器来加载插件jar包,该类加载器要先从自己的classpath加载所需类,如果加载不到,再从父类加载器加载,这样看起来就打破了双亲委派机制,实现了类插件类加载功能,但是如果插件之间有依赖关系呢?类加载器是相互隔离的,如果在当前插件类加载器中加载不到会一直向上委托加载,各个插件类加载器是同级的,因此最终是加载不到其他插件类的。那么我们还需要一个类加载器,作为插件类加载器的父加载器,在这个类加载器中再尝试使用其他插件的类加载器加载。因此需要定义两个类加载器,分别如下:

插件类的根加载器

此加载器为单例,此加载器的加载顺序如下:

  1. 先判断此类是否插件类的加载器
  2. 如果是某个插件类,则尝试使用插件类加载进行加载,加载成功返回class,如果加载失败则委托父类加载
  3. 如果不是某个插件类,则直接委托父类加载器加载
    具体代码如下:
/**
 * 插件类根加载器,单例模式,调度插件加载器
 * 打破双亲委派机制,先从子类加载器加载,加载不成功再从父类加载器加载
 *
 * @author dongxingli
 * createTime 2022/4/7
 * @version V1.0
 */
@Slf4j
public class PluginRootClassLoader extends ClassLoader {

    private static volatile PluginRootClassLoader pluginRootClassLoader;

    private static PluginContext pluginContext;


    public static PluginRootClassLoader getInstance() {

        if (Objects.isNull(pluginRootClassLoader)) {
            synchronized (PluginRootClassLoader.class) {
                if (Objects.isNull(pluginRootClassLoader)) {
                    pluginRootClassLoader = new PluginRootClassLoader();
                    PluginRootClassLoader.pluginContext = SpringUtils.getApplicationContext().getBean(PluginContext.class);
                }
            }
        }
        return pluginRootClassLoader;
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (this.getClassLoadingLock(name)) {
            Class<?> clazz = null;
            //解析该类的包名
            String packageName = this.resolvePackageName(name);
            //通过包名从插件上下文获取插件描述信息
            PluginMetaDescriptor pluginDefinition = pluginContext.getPluginDescriptorByPackage(packageName);
            //如果获取到,从子类加载器加载,未获取到,从父类加载器加载
            if (pluginDefinition != null) {
                try {
                    clazz = pluginDefinition.getPluginClassLoader().loadClass(name, resolve);
                } catch (ClassNotFoundException ignored) {
                    log.info("PluginRoot类加载器调用插件类加载器{}未加载到{}类,尝试委托父类加载器加载。", pluginDefinition.getPluginName(), name);
                    try {
                        clazz = this.getParent().loadClass(name);
                    } catch (ClassNotFoundException ignored1) {
                    }
                }
            } else {
                try {
                    clazz = this.getParent().loadClass(name);
                } catch (ClassNotFoundException ignored1) {
                }
            }
            if (clazz == null) {
                throw new ClassNotFoundException(name);
            }
            return clazz;
        }
    }

    public boolean isPluginClass(String fullClassName) {
        String packageName = resolvePackageName(fullClassName);
        long count = pluginContext.getAllPluginDescriptor().stream().map(PluginMetaDescriptor::getPackageName).filter(packageName::startsWith).count();
        return count != 0;
    }

    public String resolvePackageName(String fullClassName) {
        String[] split = fullClassName.split("\\.");
        return Arrays.stream(split).limit(split.length - 1).collect(Collectors.joining("."));
    }
}

插件资源类加载器

此加载器为每个插件创建一个,属于插件独有,利用类加载器之间的隔离功能,解决多个插件之间的第三方库版本冲突问题,执行类加载顺序如下:

  1. 先尝试调用自己的findClass方法加载类(解决多版本第三方库并存的关键)
  2. 如果加载成功,返回该类
  3. 如果失败,委托给父类加载器加载
    具体实现代码如下:

    /**
    * 插件类加载器,具体负责加载每个插件的class
    * 打破双亲委派机制,所有类先自己加载,加载不成功再从父类加载器加载
    * 此机制可以进行类隔离,解决版本冲突问题,各个插件可以引用不同版本的第三方包
    *
    * @author dongxingli
    * createTime 2022/4/7
    * @version V1.0
    */
    public class PluginResourceClassLoader extends URLClassLoader {
    
     private final PluginRootClassLoader parent;
    
     private final URLClassPath urlClassPath;
    
     private final AccessControlContext access;
    
     public PluginResourceClassLoader(URL[] urls, PluginRootClassLoader parent) {
         super(urls, parent);
         this.parent = parent;
         access = AccessController.getContext();
         urlClassPath = new URLClassPath(urls, access);
     }
    
     @Override
     public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
         Class<?> clazz = this.findLoadedClass(name);
         if (clazz == null) {
             try {
                 //先尝试自己加载
                 clazz = this.findClass(name);
             } catch (ClassNotFoundException | NoClassDefFoundError e) {
                 try {
                     //无法加载委托父加载器加载
                     clazz = this.parent.loadClass(name);
                 } catch (Exception ignored) {
                 }
             }
         }
         if (clazz != null) {
             if (resolve) {
                 resolveClass(clazz);
             }
             return clazz;
         }
         throw new ClassNotFoundException(name);
     }
    
     @Override
     protected Class<?> findClass(String name) throws ClassNotFoundException {
         final Class<?> result;
         try {
             result = AccessController.doPrivileged(
                     (PrivilegedExceptionAction<Class<?>>) () -> {
                         String path = name.replace('.', '/').concat(".class");
                         if (this.parent.isPluginClass(name)) {
                             //根据springboot打包特性,自己开发的类放在BOOT-INF/classes/package路径下,第三方依赖包直接放在package路径下
                             path = "BOOT-INF/classes/" + path;
                         }
                         Resource res = urlClassPath.getResource(path, false);
                         if (res != null) {
                             byte[] bytes = res.getBytes();
                             return defineClass(null, bytes, 0, bytes.length);
                         } else {
                             return null;
                         }
                     }, access);
         } catch (java.security.PrivilegedActionException pae) {
             throw (ClassNotFoundException) pae.getException();
         }
         if (result == null) {
             throw new ClassNotFoundException(name);
         }
         return result;
     }
    }
    

五、插件的安装及卸载

定义完类加载器,接下来就是插件的加载及卸载,定义PluginUtils类,此类负责插件jar的加载和卸载,安装前、安装后、安装出错的情况会以事件的方式进行通知,具体的注册bean和api信息的操作不在此处进行,具体代码如下:

/**
 * 插件操作类
 *
 * @author dongxingli
 * createTime 2022/4/11
 * @version V1.0
 */
@Slf4j
public class PluginUtils {

    public static final String PLUGIN_PATH = "F:\\plugin\\";

    /**
     * 初始化加载,spring容器启动后调用
     */
    public static void initLoad() {
        File[] list = FileUtil.ls(PLUGIN_PATH);
        Arrays.stream(list).forEach(PluginUtils::install);
    }

    /**
     * 安装插件,上传文件类型
     * @param jarFile 插件文件
     * @return 插件描述
     */
    public static PluginMetaDescriptor install(MultipartFile jarFile) {
        String filePath = PLUGIN_PATH + jarFile.getOriginalFilename();
        try {
            File file = FileUtil.writeFromStream(jarFile.getInputStream(), filePath);
            return install(file);
        } catch (IOException e) {
            throw new RuntimeException("保存插件文件失败");
        }
    }

    /**
     * 安装插件,系统文件类型
     * @param jarFile 插件文件
     * @return 插件描述
     */
    public static PluginMetaDescriptor install(File jarFile) {
        Objects.requireNonNull(jarFile, "插件文件不能为空");
        try {
            PluginMetaDescriptor descriptor = PluginUtils.resolvePluginJar(jarFile);
            URL url = jarFile.toURI().toURL();
            PluginContext pluginContext = SpringUtils.getApplicationContext().getBean(PluginContext.class);
            pluginContext.getPluginDescriptorByName(descriptor.getPluginName());

            PluginResourceClassLoader pluginClassLoader = new PluginResourceClassLoader(new URL[]{url}, PluginRootClassLoader.getInstance());
            descriptor.setPluginClassLoader(pluginClassLoader);
            pluginContext.addPluginMetaDescriptor(descriptor);
            pluginContext.afterInstallEvent(descriptor);
            return descriptor;
        } catch (RuntimeException | MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 卸载插件
     * @param pluginName 插件名称
     */
    public static void uninstall(String pluginName) {
        pluginName = Objects.requireNonNull(pluginName);
        PluginContext pluginContext = SpringUtils.getApplicationContext().getBean(PluginContext.class);
        PluginMetaDescriptor descriptor = pluginContext.getPluginDescriptorByName(pluginName);
        if (Objects.isNull(descriptor)) {
            throw new PluginNotDefinedException("已安装插件中未找到" + pluginName + ",无需卸载");
        }
        unregisterBean(descriptor);
        PluginResourceClassLoader pluginClassLoader = descriptor.getPluginClassLoader();
        try {
            pluginClassLoader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        pluginContext.removePluginMetaDescriptor(pluginName);
    }


    /**
     * 注销API接口和被IOC容器托管的bean
     * @param pluginMetaDescriptor 插件信息描述
     */
    public static void unregisterBean(PluginMetaDescriptor pluginMetaDescriptor) {
        //先注销api接口
        RequestMappingHandlerMapping requestMappingHandlerMapping = SpringUtils.getApplicationContext().getBean(RequestMappingHandlerMapping.class);
        for (Set<RequestMappingInfo> requestMappingInfo : pluginMetaDescriptor.getRequestMappingInfoMap().values()) {
            for (RequestMappingInfo mappingInfo : requestMappingInfo) {
                try {
                    requestMappingHandlerMapping.unregisterMapping(mappingInfo);
                } catch (Exception ignored) {
                    log.error("注销api接口{}失败,已忽略。", mappingInfo.getName());
                }
            }
        }
        //再注销IOC容器中的bean
        DefaultListableBeanFactory registry = (DefaultListableBeanFactory) SpringUtils.getApplicationContext().getAutowireCapableBeanFactory();
        pluginMetaDescriptor.getBeanDefinitionWrapSet().forEach(bean -> bean.unRegisterBean(registry));
    }

    /**
     * 解析插件jar包
     * @param file 插件文件
     * @return 插件描述类
     */
    private static PluginMetaDescriptor resolvePluginJar(File file) {
        try {
            JarFile jarFile = new JarFile(file);
            JarEntry jarEntry = jarFile.getJarEntry("BOOT-INF/classes/plugin-config.yml");
            Objects.requireNonNull(jarEntry, "未找到插件配置文件,插件文件路径:" + file.getAbsolutePath());
            InputStream inputStream = jarFile.getInputStream(jarEntry);
            Properties properties = new Properties();
            properties.load(inputStream);
            jarFile.close();
            return PluginMetaDescriptor.builder()
                    .pluginName(properties.get("pluginName").toString())
                    .packageName(properties.get("packageName").toString())
                    .version(properties.get("version").toString())
                    .path(file.getAbsolutePath())
                    .build();
        } catch (IOException e) {
            throw new RuntimeException("解析插件配置文件失败,路径:" + file.getAbsolutePath());
        }
    }
}

六、注册bean到IOC容器和注册API信息

插件被类加载器加载以后,接下来需要继续扫描插件中需要注册的IOC容器中的class并实例化对象进行注册,同时如果是被@Controller和@RestController注解的类,需要获取RequestMappingInfo信息注册到RequestMappingHandlerMapping中,实现代码如下:

插件加载监听类

/**
 * 默认插件安装事件监听
 *
 * @author dongxingli
 * createTime 2022/4/11
 * @version V1.0
 */
@Component
@Slf4j
public class DefaultPluginEventListener implements PluginEventListener {

    @Override
    public void afterInstall(PluginMetaDescriptor pluginMetaDescriptor) {
        try {
            //为插件中每个class生成的注册操作者
            Set<BeanRegisterOperator> beanRegisterOperators = getAllPluginClass(pluginMetaDescriptor);
            BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
            RequestMappingHandlerMapping handlerMapping = SpringUtils.getApplicationContext().getBean(RequestMappingHandlerMapping.class);
            beanRegisterOperators.forEach(x -> {
                //注册bean
                x.registerToApplicationContext(beanNameGenerator, pluginMetaDescriptor);
                //注册api
                x.registerHandlerMapping(handlerMapping, pluginMetaDescriptor);
            });
        } catch (Exception e) {
            e.printStackTrace();
            PluginUtils.unregisterBean(pluginMetaDescriptor);
        }
        log.info("插件{}加载成功", pluginMetaDescriptor.getPluginName());
    }

    private Set<BeanRegisterOperator> getAllPluginClass(PluginMetaDescriptor pluginMetaDescriptor) {
        Set<BeanRegisterOperator> beanRegisterOperators = new HashSet<>();
        try {
            JarFile jarFile = new JarFile(pluginMetaDescriptor.getPath());
            Enumeration<?> enu = jarFile.entries();
            while (enu.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enu.nextElement();
                String name = jarEntry.getName();
                if (name.endsWith(".class")) {
                    String className = name.replace("/",".");
                    String fullClassName = className.substring(0,className.length()-6);
                    if (fullClassName.startsWith("BOOT-INF.classes")){
                        fullClassName = fullClassName.substring(17);
                    }
                    Class<?> beanClass = pluginMetaDescriptor.getPluginClassLoader().loadClass(fullClassName);
                    AnnotatedGenericBeanDefinition beanDefinition = new AnnotatedGenericBeanDefinition(beanClass);
                    BeanRegisterOperator operator = BeanRegisterOperator.builder().beanType(beanClass).definition(beanDefinition).build();
                    beanRegisterOperators.add(operator);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return beanRegisterOperators;
    }
}

bean注册操作类,记录bean定义信息等bean的基本信息

/**
 * Bean注册相关操作类,负责注册bean到IOC容器,注册RequestMappingInfo到RequestMappingHandlerMapping
 *
 * @author dongxingli
 * createTime 2022/4/8
 * @version V1.0
 */
@Data
@Builder
@Slf4j
public class BeanRegisterOperator {

    /**
     * Bean定义
     **/
    BeanDefinition definition;
    /**
     * bean名称
     */
    private String beanName;

    /**
     * bean的class对象
     */
    private Class<?> beanType;


    public boolean isController() {
        Controller controller = beanType.getAnnotation(Controller.class);
        RestController restController = beanType.getAnnotation(RestController.class);
        return Objects.nonNull(controller) || Objects.nonNull(restController);
    }

    public boolean isService() {
        Service service = beanType.getAnnotation(Service.class);
        return Objects.nonNull(service);
    }

    public boolean isComponent() {
        Component component = beanType.getAnnotation(Component.class);
        return Objects.nonNull(component);
    }

    public void registerToApplicationContext(BeanNameGenerator beanNameGenerator, PluginMetaDescriptor pluginMetaDescriptor) {
        if (isComponent() || isController() || isService()) {
            DefaultListableBeanFactory registry = (DefaultListableBeanFactory) SpringUtils.getApplicationContext().getAutowireCapableBeanFactory();
            this.beanName = beanNameGenerator.generateBeanName(this.definition, registry);
            if (registry.containsSingleton(beanName) || registry.containsBeanDefinition(beanName)) {
                String duplicateBean = registry.getBean(beanName).getClass().getName();
                throw new DuplicateBeanDefinitionException(String.format("容器中已存在与%s同名bean:%s,注册失败。", beanName, duplicateBean));
            }
            registry.registerBeanDefinition(beanName, definition);
            pluginMetaDescriptor.getBeanDefinitionWrapSet().add(this);
        }
    }

    public void registerHandlerMapping(RequestMappingHandlerMapping requestMappingHandlerMapping, PluginMetaDescriptor pluginMetaDescriptor) {
        if (isController()) {
            Object bean = SpringUtils.getApplicationContext().getBean(beanName);
            Method getMappingForMethod = ReflectionUtils.findMethod(RequestMappingHandlerMapping.class, "getMappingForMethod", Method.class, Class.class);
            assert getMappingForMethod != null;
            getMappingForMethod.setAccessible(true);
            Set<RequestMappingInfo> requestMappingInfos = new HashSet<>();
            try {
                Method[] methodArr = beanType.getMethods();
                for (Method method : methodArr) {
                    RequestMappingInfo mappingInfo = (RequestMappingInfo) getMappingForMethod.invoke(requestMappingHandlerMapping, method, beanType);
                    if (mappingInfo != null) {
                        requestMappingHandlerMapping.registerMapping(mappingInfo, bean, method);
                        requestMappingInfos.add(mappingInfo);
                        log.info("{}中扫描到接口:{}", beanName, mappingInfo);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            pluginMetaDescriptor.getRequestMappingInfoMap().put(beanName, requestMappingInfos);
        }
    }

    public void unRegisterBean(DefaultListableBeanFactory registry) {
        try {
            registry.removeBeanDefinition(beanName);
        } catch (Exception e) {
            log.error("注销容器中的{}失败,已忽略。", beanName);
        }

    }
}

七、启动加载和热加载

关于插件的加载时机,首先在容器启动的时候需要对已上传的jar进行统一加载,还需要在容器运行的时候进行热加载,对于热加载和卸载,只需要编写接口调用PluginUtils中的install和uninstall方法即可,启动加载有很多种方案,笔者这里选择了监听spring的ApplicationStartedEvent,这样的好处是即使部分插件加载失败也不会影响主工程启动,其他加载成功的插件依然可用,部分代码如下:

@Component
public class PluginContext implements ApplicationListener<ApplicationStartedEvent> {

    private Collection<PluginEventListener> pluginEvents;

    private Map<String, PluginMetaDescriptor> pluginDescriptorPackageNameMap = new ConcurrentHashMap<>();

    private Map<String, PluginMetaDescriptor> pluginDescriptorNameMap = new ConcurrentHashMap<>();


    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        Map<String, PluginEventListener> eventMap = event.getApplicationContext().getBeansOfType(PluginEventListener.class);
        pluginEvents = eventMap.values();
        PluginUtils.initLoad();
    }

    //部分代码省略
}

八、总结

以上便是比较关键的代码,通过以上代码我们实现了将一个普通的springboot工程的jar包作为插件加载到主工程中。在写插件工程时如果插件工程要使用主工程中的类,则需要以compileOnly的方式(compileOnly project(":boot-plugin-parent"))方式依赖了主工程,这样编译可以通过,而插件工程打包后的jar包中并不存在主工程的代码。本文只是讨论了一个整体的思路,在实际实现过程中依然有大量的细节和问题需要完善处理,以下问题本次未做处理,有兴趣的小伙伴可以思考一下,后续我会继续完善这些问题。

  1. 插件中存在AOP的cglib动态代理类如何处理?
  2. 插件中存在mybatis的jdk动态代理类如何处理?
  3. 本文只处理了@Component、@Controller、@RestController和@Service,那么@Configuration、@SpringBootConfiguration、@Bean等注解注入的bean该如何处理?
  4. 如果多个插件需要注入同名bean和多个相同第三库配置该如何处理?
  5. 非前后端分离的架构静态资源该如何获取?
正文到此结束