侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 285 篇文章
  • 累计创建 125 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

java中如何实现可热插拔插件的开发过程

孔子说JAVA
2022-06-28 / 0 评论 / 0 点赞 / 93 阅读 / 10,543 字 / 正在检测是否收录...

在产品或项目开发过程中,我们经常使用模块化或组件化的开发形式,在开发过程中按业务场景或逻辑单元将程序划分为模块或组件,再通过这些模块或组件的依赖调用或组合来达到我们的业务需求。这类方式一般适用于通用化的业务场景,而对于只有部分用户使用的特殊的业务场景,如客户的定制化开发等,使用上述方式就不太适合,插件化开发方式才是我们的首选。

1、组件、模块、插件的区别

组件化、模块化、插件的中心思想都是分而治之。目的都是将一个庞大的系统拆分成多个组件、模块或插件。

1.1 组件化开发

组件化开发就是基于可重用的目的,将一个大的软件系统按照分离关注点的形式,拆分成多个独立的组件,主要目的就是减少耦合。

  • 一个独立的组件可以是一个软件包、web服务、web资源或者是封装了一些函数的模块。这样,独立出来的组件可以单独维护和升级而不会影响到其他的组件。
  • 组件化的目的是为了解耦,把系统拆分成多个组件,分离组件边界和责任,便于独立升级和维护。
  • 组件化是从功能角度进行划分的,方便功能组件的重用。

1.2 模块化开发

模块化开发的目的在于将一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,模块之间通过接口调用。将一个大的系统模块化之后,每个模块都可以被高度复用。

  • 模块化的目的是为了重用,模块化后可以方便重复使用和插拨到不同的平台,不同的业务逻辑过程中。
  • 模块化是从代码逻辑的角度进行划分的。方便代码分层开发,保证每个功能模块的职能单一。

1.3 插件化开发

插件化开发的目的是通过热插拔的方式实现程序功能的动态扩展。一个插件就像它的名字所说的那样是一个可以插入应用程序以提供新功能的软件。插件通常实现定义良好的API并且是被动的(即它们提供主应用程序可以使用的服务)。比如在IE中,安装相关插件后,WEB浏览器可以直接调用插件来处理特定类型的文件。

  • 系统要具备插件化扩展功能,首先需要由开发人员编写系统框架,并预先定义好系统的扩展接口。
  • 新插件的开发是由开发人员根据系统预定义的接口编写扩展功能,也就是系统的扩展功能模块。
  • 每个插件都是以一个独立的文件出现的,在java中就是jar包。
  • java中的插件主要使用了反射机制来实现。

2、插件化开发基本思路

java 程序一般上都以jar包的形式存在,因此所谓的插件就是一个个jar包,开发思路如下:

  1. 主程序定义好通用插件接口;
  2. 插件项目中引入主程序的jar包,并实现插件接口;
  3. 主程序通过类加载器动态加载插件jar中(java的反射机制)实现了插件接口的类。

3、主程序开发详细步骤

3.1 插件接口定义

插件接口就是系统预留的扩展接口,是提供给插件开发者实现的。在主程序中需要定义一个插件主入口的接口,这里定义为 PluginService,包含一个主入口方法void service(),当然你也可以定义参数和返回值。

  • 插件参数,一般是由主程序传递给插件开发者的系统参数,用于插件逻辑的处理判断。
  • 插件返回值:如果主程序需要知道插件的执行情况,可以定义一个返回值,由插件进行回传。
  • 你也可以在主程序中开放一些可以提供给插件程序调用的接口,比如:提供插件与主程序界面交互的方法。
public interface PluginService {
    /**
     * 插件运行方法
     */
    void service();
}

3.2 插件实体定义

主程序定义插件类Plugin,用于封装从插件配置文件 plugin.xmlplugin.json 中读取的插件信息,其中字段 className 就是用来保存插件中实现了 PluginService 接口的类的实例:

/**
 * 插件封装实体
 */
public class Plugin {
    /**
     * 插件名称
     */
    private String name;
    /**
     * 插件版本
     */
    private String version;
    /**
     * 插件路径
     */
    private String path;
    /**
     * 插件类全路径
     */
    private String className;
    
    // getter和setter方法略
}

3.3 定义插件异常

定义插件异常主要目的是为了正确识别和处理插件产生的异常。这里的 ErrorCodeEnum 是自定义的一个异常枚举值。

public class PluginException extends RuntimeException {
    private int errorCode;
    private String errorMsg;

    public PluginException(String errorMsg) {
        this(ErrorCodeEnum.COMMON_FAILURE.getCode(), errorMsg);
    }

    public PluginException(int errorCode, String errorMsg) {
        super(errorMsg);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public PluginException(ErrorCodeEnum status) {
        this(status.getCode(), status.getMsg());
    }

    public PluginException(String message, Throwable cause) {
        super(message, cause);
    }

    public PluginException(Throwable cause) {
        super(cause);
    }

    public int getErrorCode() {
        return errorCode;
    }

    public String getErrorMsg() {
        return errorMsg;
    }
}

异常状态码枚举类

public enum ErrorCodeEnum {
    // 通用
    COMMON_SUCCESS(0, "成功"),
    COMMON_FAILURE(500, "调用接口异常或结果异常"),
    COMMON_TIMEOUT(400, "超时"),
    COMMON_NOT_IMPLEMENTED(404, "未实现该接口"),
    COMMON_CACHE_VALID(405, "缓存无效"),
    ;

    private int code;
    private String msg;

    private ErrorCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

3.4 插件管理类

创建插件管理类,初始化插件。

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;

/**
 * 使用URLClassLoader动态加载jar文件,实例化插件中的对象
 *
 */
public class PluginManager {
    
    private URLClassLoader urlClassLoader;
 
    public PluginManager(List<Plugin> plugins) throws PluginException {
        init(plugins);
    }
    
    /**
     * 插件初始化方法
     */
    private void init(List<Plugin> plugins) throws MalformedURLException {
        try{
            int size = plugins.size();
            URL[] urls = new URL[size];
            for(int i = 0; i < size; i++) {
                Plugin plugin = plugins.get(i);
                String filePath = plugin.getPath();
                urls[i] = new URL("file:" + filePath);
            }
        
            // 将jar文件组成数组,来创建一个URLClassLoader
            urlClassLoader = new URLClassLoader(urls);
        }catch (MalformedURLException e) {
            throw new PluginException("plugin " + plugin.getPluginName() + " init error," + e.getMessage());
        }
    }
    
    /**
     * 获得插件
     * @param className 插件类全路径
     * @return
     * @throws PluginException
     */
    public PluginService getInstance(String className) throws PluginException {
       
        // 插件实例化对象,插件都是实现PluginService接口
        Class<?> clazz = urlClassLoader.loadClass(className);
        Object instance = null;
        try {
            instance = clazz.newInstance();
        } catch (Exception e) {
            throw new PluginException("plugin " + className + " instantiate error," + e.getMessage());
        }
        return (PluginService)instance;
    }
}

插件管理类的另一种写法:与上述方式的区别是将clazz预先加载并放入Map中,获取插件实例的时候直接从Map中获取,而不需要每次都使用 URLClassLoader 动态加载。

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;

/**
 * 使用URLClassLoader动态加载jar文件,实例化插件中的对象
 *
 */
public class PluginManager {
    
    private Map<String, Class> clazzMap = new HashMap<>();
 
    public PluginManager(List<Plugin> plugins) throws PluginException {
        init(plugins);
    }
    
    /**
     * 插件初始化方法
     */
    private void init(List<Plugin> plugins) throws MalformedURLException {
        try{
            int size = plugins.size();
            for(int i = 0; i < size; i++) {
                Plugin plugin = plugins.get(i);
                String filePath = plugin.getPath();
                // URL url = new URL("file:" + filePath);
                URL url = new File(plugin.getPath()).toURI().toURL();
                URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
                Class<?> clazz = urlClassLoader.loadClass(plugin.getClassName());
                clazzMap.put(plugin.getClassName(), clazz);
            }
        }catch (Exception e) {
            throw new PluginException("plugin " + plugin.getPluginName() + " init error," + e.getMessage());
        }
    }
    
    /**
     * 获得插件
     * @param className 插件类全路径
     * @return
     * @throws PluginException
     */
    public PluginService getInstance(String className) throws PluginException {
       
        // 插件实例化对象,插件都是实现PluginService接口
        Class clazz = clazzMap.get(className);
        Object instance = null;
        try {
            instance = clazz.newInstance();
        } catch (Exception e) {
            throw new PluginException("plugin " + className + " instantiate error," + e.getMessage());
        }
        return (PluginService)instance;
    }
}

3.5 读取插件配置工具类(使用dom4j实现)

从插件配置文件 plugin.xml 中加载所有插件信息,然后从对应目录中加载/读取所有插件,该加载方式的缺点是每次添加新的插件时都需要修改主程序下的插件配置文件,拓展不方便。

import java.io.File;
import java.util.ArrayList;
import java.util.List;
 
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
 
/**
 * 解析XML获取所有插件信息(这里用到dom4j)
 *
 */
public class XMLParser {
    
    public static List<Plugin> getPluginList() throws DocumentException {
        
        List<Plugin> list = new ArrayList<Plugin>();
        
        SAXReader saxReader =new SAXReader();
        Document document = saxReader.read(new File("plugin.xml"));
        
        Element root = document.getRootElement();
        List<?> plugins = root.elements("plugin");
        for(Object pluginObj : plugins) {
            Element pluginEle = (Element)pluginObj;
            Plugin plugin = new Plugin();
            plugin.setName(pluginEle.elementText("name"));
            plugin.setVersion(pluginEle.elementText("version"));
            plugin.setJar(pluginEle.elementText("path"));
            plugin.setClassName(pluginEle.elementText("className"));
            list.add(plugin);
        }
        return list;
    }
}

dom4j 依赖

<!-- 采用xml方式存储配置,dom4j解析 -->
<dependency>
    <groupId>org.dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>2.1.1</version>
</dependency>

3.6 main类客户端测试类

import java.util.List;
 
public class Main {
    
    public static void main(String[] args) {
        try {
            // 从配置文件加载插件
            List<Plugin> pluginList = XMLParser.getPluginList();
            // 初始化插件管理类
            PluginManager pluginManager = new PluginManager(pluginList);
            // 循环调用所有插件
            for(Plugin plugin : pluginList) {
                PluginService pluginService = pluginManager.getInstance(plugin.getClassName());
                System.out.println("开始执行[" + plugin.getName() + "]插件...");
                // 调用插件
                pluginService.service();
                System.out.println("[" + plugin.getName() + "]插件执行完成");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.7 插件配置文件plugins.xml的格式

主程序的配置文件,存放插件的配置信息。

<?xml version="1.0" encoding="UTF-8"?>
<plugins>
    <plugin>
        <name>A插件</name>
        <version>1.0</version>
        <jar>D:/plugins/PluginA.jar</jar>
        <className>com.demo.plugins.PluginA</className>
    </plugin>
    <plugin>
        <name>B插件</name>
        <version>1.0</version>
        <jar>D:/plugins/PluginB.jar</jar>
        <className>com.demo.plugins.PluginB</className>
    </plugin>
</plugins>

做好以上步骤后,将主程序打包成 jar 包,用来给插件项目作为依赖引入。

4、插件程序开发详细步骤

  1. 新建 Java 项目用来开发插件,引入主程序 jar 包作为依赖。
  2. 插件项目中提供一个实现了 PluginService 接口的类,作为插件的主入口。
  3. 将插件打成 jar 包(不需要包含3中引入的主程序jar),置于主程序的plugins目录(也可以自行指定目录)下。
  4. 修改主程序的插件配置文件 plugin.xml,添加相应的插件配置。

插件A的代码示例

public class PluginA implements PluginService {
    @Override
    public void service() {
        System.out.println("Plugin A.");
    }
}

插件B的代码示例

public class PluginB implements PluginService {
    @Override
    public void service() {
        System.out.println("Plugin B.");
    }
}

插件C的代码示例

public class PluginC implements PluginService {
    @Override
    public void service() {
        System.out.println("Plugin C.");
    }
}

5、插件的改进

使用主程序配置插件的方式 plugin.xml,在每次有了新插件的时候都需要手动修改插件的配置信息,我们可以把插件的相关配置放在插件项目的 META-INF 目录中,主程序动态加载,降低耦合度。我们对上述程序作出部分修改。

5.1 主程序修改

5.1.1 读取插件配置工具类

/**
 * 加载所有插件的信息
 */
public class PluginLoader {

    /**
     * 加载所有插件,返回一个插件数组
     */
    public static List<Plugin> load() throws Exception {
        List<Plugin> plugins = new ArrayList<>();
        
        // 所有插件都放于plugins目录下,你也可以指定别的目录
        File parentDir = new File("plugins");
        File[] files = parentDir.listFiles();
        if (null == files) {
            return Collections.emptyList();
        }

        // 此处从 plugins 文件夹下加载所有插件
        Plugin plugin;
        String path;
        for (File dir : files) {
            if (!dir.isDirectory()) {
                break;
            }
            // 插件jar包需与插件根目录同名
            path = dir + System.getProperty("file.separator") + dir.getName() + ".jar";
            plugin = readPlugin(path);
            plugins.add(plugin);
        }
        return plugins;
    }
    
    /**
     * 解析json获取一个插件的信息
     */
    public static Plugin getPlugin() throws IOException {
        JarFile jarFile = new JarFile(path);
        ZipEntry zipEntry = jarFile.getEntry("META-INF/plugin.json");
        InputStream is = jarFile.getInputStream(zipEntry);
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String temp;
        StringBuilder sb = new StringBuilder();
        while ((temp = br.readLine()) != null) {
            sb.append(temp);
        }
        Plugin plugin = null;
        try {
            plugin = JSON.parseObject(sb.toString(), Plugin.class);
            plugin.setPath(path);

            URL[] url = new URL[]{new URL("file:" + path)};
            URLClassLoader loader = new URLClassLoader(url);
            Class<?> clazz = loader.loadClass(plugin.getMainClass());

            plugin.setService((PluginService) clazz.newInstance());
        } catch (Exception e) {
            new ExceptionAlter(e).show();
        }
        return plugin;
    }
}

5.1.2 main类客户端测试类

import java.util.List;
 
public class Main {
    
    public static void main(String[] args) {
        try {
            // 从配置文件加载插件
            List<Plugin> pluginList = PluginLoader.load();
            // 初始化插件管理类
            PluginManager pluginManager = new PluginManager(pluginList);
            // 循环调用所有插件
            for(Plugin plugin : pluginList) {
                PluginService pluginService = pluginManager.getInstance(plugin.getClassName());
                System.out.println("开始执行[" + plugin.getName() + "]插件...");
                // 调用插件
                pluginService.service();
                System.out.println("[" + plugin.getName() + "]插件执行完成");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5.1.3 删除原有的插件配置文件

删除原有的插件配置文件plugins.xml。

5.2 插件程序修改

插件项目类路径下添加 META-INF 目录,该目录下添加该插件的配置文件 plugin.json,内容如下:

{
  "name": "测试插件",
  "version": "1.0",
  "mainClass": "com.demo.plugins.PluginA"
}

其中:

  1. name:插件名称;
  2. version:插件版本;
  3. mainClass:插件主类名(com.demo.plugins.PluginService接口的类。
0

评论区