SPI思想应用之拔插式插件

1. 插件简介

  • 插件在百度百科中解释为:

​ 插件是一种遵循一定规范的应用程序接口编写出来的程序。其只能运行在程序规定的系统平台下(可能同时支持多个平台),而不能脱离指定的平台单独运行。因为插件需要调用原纯净系统提供的函数库或者数据。很多软件都有插件,插件有无数种。例如在IE中,安装相关的插件后,WEB浏览器能够直接调用插件程序,用于处理特定类型的文件。

1.1 插件应用

插件已经不再是什么新鲜的事物了,在很多地方都可以看到各种各样的插件,特别是那些业界有名的软件系统。

  • 比如我们熟悉的谷歌浏览器就支持各种各样的插件

在这里插入图片描述

  • 这个就是我常用的谷歌插件,连现在的软件开发工具都支持各种插件,比如IntelliJ IDEA

在这里插入图片描述

  • 这些插件都在一定程度上丰富了应用程序的功能,而且都可以按需定制,这种即插即用的高度扩展模式是现在优秀软件必备的一种特征。

1.2 插件思想

​ 软件系统通过定制插件来实现功能的定制与扩展,极大程度上丰富了软件系统本身。试想一个软件想要丰富软件的功能那么一开始就装上全套功能,软件系统必然庞大无比,运行软件的开销也会令机器不堪重负,更何况软件的功能并不是都会用到,很多资源其实都在“尸位素餐”。通过插件来丰富软件系统,当我们需要用到某个功能时寻找到合适的插件接入系统,就像把插头插上插座接通电源,软件系统就拥有了插件赋予的功能。针对不同的用户,可以在软件系统中定制自己喜欢的插件,也就是给软件定制适合自己的功能,软件开销自己把握,这样软件系统符合我的需求也减少了很多不必要的开销。

JAVA中的SPI思想一文的介绍中,提供了一种为服务寻找服务实例的思想。这种通过接口定义一种标准,利用配置寻找这套标准的实现,然后使用实现的手段极大程度提高了扩展性。SPI多运用在框架提高组件的扩展上面,将这种思维发散开来,结合插件的这种模式运用到服务层面实现一个插拔式的插件服务。

在这里插入图片描述

​ 客户端给服务端发送不同的业务请求,服务端根据业务类型在插件注册表寻找不同的插件来解决对应的业务。当客户需要增加一种业务时,开发者只需要开发该类业务的一种特定处理插件就可以轻松接入系统完成系统的升级。当某个业务被时代淘汰无人使用的时候,及时在服务注册表下架该插件,服务管理就是这么轻松。

  • 插件注册表对于开发人员来说就是一种约定的配置文件,可以是xml、数据库表亦或是一个map结构。
  • 插件就是服务外的一个具有特殊意义的jar包。

2. 插件案例

项目工程结构依旧和SPI中的案例一下,如下:

在这里插入图片描述

  • 插件定义工程主要就是定义插件接口的,规定插件生产的规格,就像插座得定义插头是双脚还是三角的,接口必须得有标准。
  • 插件工程就是实现了插件接入规则的一些具有特定功能的jar包。
  • 插件应用工程加载插件,根据客户不同需求使用不同插件进行处理。
  • 插件注册表使用properties文件的KV形式来注册插件的信息。

2.1 插件的定义

  • 在【commons-api】工程中定义插件的规范,即定义接口,接口名称为ComponentService,内容如下:
public interface ComponentService
{
    /**
     * 获取组件名称
     * @return 组件名称
     */
    String getComponentName();
}
  • 这里定义了一套插件的生产规范ComponentService

2.2 插件的实现

  • 在【component-A】工程中引入插件的生产标准,即在pom中添加插件规范
<dependencies>
    <dependency>
        <groupId>com.xxxx</groupId>
        <artifactId>commons-api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>
  • 在【component-A】工程中引入了插件实现规范,接下来实现插件规范生成具体插件,即实现接口,类名为ComponentA,内容如下:
public class ComponentA implements ComponentService
{
    /**
     * 组件名称
     */
    private static final String COMPONENT_NAME = "组件A";

    @Override
    public String getComponentName()
    {
        return COMPONENT_NAME;
    }
}
  • 在【component-B】工程中同样引入插件的生产标准,即在pom中添加插件规范
<dependencies>
    <dependency>
        <groupId>com.xxxx</groupId>
        <artifactId>commons-api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>
  • 在【component-B】工程中实现插件规范的接口,类名为ComponentB,内容如下:
public class ComponentB implements ComponentService
{
    /**
     * 组件名称
     */
    private static final String COMPONENT_NAME = "组件B";

    @Override
    public String getComponentName()
    {
        return COMPONENT_NAME;
    }
}
  • 这里按照插件的生产标准实现两套插件,分别为ComponentA和ComponentB

2.3 插件的使用

在插件的应用工程中需要解决如何加载插件、如何寻找插件的问题,这里使用一个插件管理器来统筹这些事情。

  • 在【component-application】工程中引入插件标准,表示需要应用这类插件,即在pom中添加依赖,如下:
<dependencies>
    <dependency>
        <groupId>com.xxxx</groupId>
        <artifactId>commons-api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>
  • 在【component-application】工程中实现插件管理器,类名为PluginManager,内容如下:
public class PluginManager
{
    /**
     * 插件配置
     */
    private Properties configs;

    /**
     * 插件集合
     */
    private final Map<String, ComponentService> plugins = new ConcurrentHashMap<>();

    public PluginManager(String configPath)
    {
        InputStream is = null;
        try
        {
            // 获取配置文件流
            is = Thread.currentThread().getContextClassLoader().getResourceAsStream(configPath);
            configs = new Properties();
            // 加载插件配置
            configs.load(is);
        }
        catch (FileNotFoundException e)
        {
            // 配置文件不存在
            e.printStackTrace();
        }
        catch (IOException e)
        {
            // 文件读取异常
            e.printStackTrace();
        }
        finally
        {
            if (!Objects.isNull(is))
            {
                try
                {
                    // 关闭输入流
                    is.close();
                }
                catch (IOException e)
                {
                    // 关闭输入流异常
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 加载插件
     * @param pluginPath 插件路径
     * @return 插件实例
     */
    private ComponentService loadComponent(String pluginPath)
    {
        try
        {
            JarFile jarFile = new JarFile(pluginPath);
            URL[] urls = new URL[]{new URL("file:" + pluginPath)};
            URLClassLoader urlClassLoader = new URLClassLoader(urls);
            Enumeration<JarEntry> entries = jarFile.entries();
            // 扫描插件并通过反射创建插件实例
            while (entries.hasMoreElements())
            {
                JarEntry jarEntry = entries.nextElement();
                if (!jarEntry.isDirectory() && jarEntry.getName().endsWith(".class"))
                {
                    String className = jarEntry.getName().split("\\.")[0].replace("/", ".");
                    Class<?> classObject = urlClassLoader.loadClass(className);
                    if (ComponentService.class.isAssignableFrom(classObject))
                    {
                        ComponentService componentService = (ComponentService) classObject.newInstance();
                        return componentService;
                    }
                }
            }
        }
        catch (IOException e)
        {
            // 文件读取异常
            e.printStackTrace();
        }
        catch (IllegalAccessException e)
        {
            e.printStackTrace();
        }
        catch (InstantiationException e)
        {
            e.printStackTrace();
        }
        catch (ClassNotFoundException e)
        {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取插件
     * @param pluginName 插件名称
     * @return 插件实例
     */
    public ComponentService getPlugin(String pluginName)
    {
        if (!Objects.isNull(pluginName) && configs.containsKey(pluginName))
        {
            // 采用懒加载模式加载插件,即用到时采取加载需要的插件
            if (plugins.containsKey(pluginName))
            {
                return plugins.get(pluginName);
            }
            else
            {
                // 插件集合中不存在插件就去加载插件
                ComponentService component = loadComponent(configs.getProperty(pluginName));
                // 将加载的插件放入插件集合
                plugins.put(pluginName, component);
                return component;
            }
        }
        return null;
    }
}

在插件管理器中,需要读取插件注册表来明确服务有哪些插件可以使用、插件的信息是什么样的。加载插件就是将jar里面的class文件通过类加载系统搬运到JVM中进行实例化。插件管理器对外只提供获取插件的方法让服务来使用插件。

  • 将【component-A】工程和【component-B】工程进行打包制作成jar文件,放在磁盘的某个位置,这里我选择放在D:\plugins目录下

在这里插入图片描述

  • 在【component-application】工程中新建插件注册表文件,文件名为plugin.properties,文件内容如下:
# 插件注册表:插件名称 - 插件路径
pluginA=D:/plugins/component-A-1.0-SNAPSHOT.jar
pluginB=D:/plugins/component-B-1.0-SNAPSHOT.jar
  • 在【component-application】工程中新建应用程序启动类应用插件,类名称为ComponentApplication,内容如下:
public class ComponentApplication
{
    public static void main(String[] args)
    {
        // 建造插件管理器
        PluginManager pluginManager = new PluginManager("plugin.properties");

        // 获取插件A实例调用
        ComponentService pluginA = pluginManager.getPlugin("pluginA");
        System.out.println("插件名称:" + pluginA.getComponentName());

        // 获取插件B实例调用
        ComponentService pluginB = pluginManager.getPlugin("pluginB");
        System.out.println("插件名称:" + pluginB.getComponentName());
    }
}
  • 启动【component-application】工程中ComponentApplication类的main方法,结果如下:

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值