Java通过URLClassLoader让程序支持插件扩展

  • Post author:
  • Post category:java


插件(Plugin)是什么不用多说。常用的软件,例如Eclipse、Photoshop、VisualStudio,都支持插件扩展。插件可以动态给软件添加一些功能,也可以随时删除,这样的好处是任何人都可以给这个软件进行功能上的扩展,而不用去改软件本身的代码。


为什么使用插件模式


使用插件模式实现某些功能的好处在于能够在不重新编译主程序的情况下,动态为主程序添加一些其他的功能。


插件的存在形式


在Java中插件的物理存在形式为功能模块打成的JAR包,C++中的存在形式为动态链接库。


Java主要实现过程

  1. 抽离出插件功能接口,定义插件的抽象类;
  2. 实现具体功能的插件实现插件抽象类,然后将该具体的插件打成JAR包,并将JAR包放到指定的插件目录下;
  3. 创建插件配置文件,可以使用JSON格式或者XML格式的文件,每当新增一个插件后,在配置文件中添加一条该插件的信息,该信息主要包括“插件名称(jar包名称)”和“插件主类的完整路径(包名类名)”。
  4. 主程序中首先解析插件配置文件,得到插件名称和插件主类的路径,然后获取插件的jar所在的URL,通过URLClassLoader得到插件的类加载器,最后通过类加载器即可得到对应的插件类的实例。

适用场景

比如需要开发一个系统,用来将一些有数据推送给客户,至于是什么数据不是重点。有三个客户:A客户需要把数据组织成一个xml格式的文件,通过FTP上传到客户服务器上;B客户需要把数据组织成一个json,通过HTTP请求提交;C客户希望生成一个Excel文件再通过E-mail发送…以后可能还会有更多的客户,也还会有更多操蛋的需求。

对于这样一个系统的开发,如果使用普通的方式开发,那么每增加一个客户就要修改一次系统代码,在代码中增加一个针对某个客户的功能,很不灵活。如果再减少一个客户,那么其对应的代码也就没有用了,是不是要删除掉又成了问题。

以上只是一个例子,在实际开发中经常会有类似的情形,此时使用插件化的方式会更灵活。

遇到这种情况,可以把数据的获取和整理这块和客户无关的逻辑放在主程序中,而主程序提供一个客户推送的接口,接口定义一个未实现的抽象方法“推送数据”,这个方法由各个客户对应的插件来实现。这样新增一个客户需求,不需要修改主程序的代码,只需要实现这个接口就行,插件写好打成jar包放在指定目录下,再配置一下,主程序就可以使用这个插件了。当不需要这个插件,也可以通过配置来去掉它。

主程序配置插件

上面说到主程序可以通过配置来动态添加和删除插件,配置的方式一般有两种:XML或数据库,二者选其一即可。

方法一:XML

主程序可以通过一个xml配置文件,动态配置插件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<plugins>
	<plugin>
		<name>A客户插件</name>
		<jar>D:/plugin/a-plugin.jar</jar>
		<class>com.xxg.aplugin.APlugin</class>
	</plugin>
	<plugin>
		<name>B客户插件</name>
		<jar>D:/plugin/b-plugin.jar</jar>
		<class>com.xxg.bplugin.BPlugin</class>
	</plugin>
	<plugin>
		<name>C客户插件</name>
		<jar>D:/plugin/c-plugin.jar</jar>
		<class>com.xxg.cplugin.CPlugin</class>
	</plugin>
</plugins>

主程序通过解析这个XML来调用插件,<plugin>元素即一个插件,可以通过添加和删除<plugin>元素来动态的添加和删除插件。<name>是插件名称,<jar>是插件jar文件所在的路径,<class>是插件实现主程序接口的类。

方法二:数据库

如果使用数据库来配置插件,需要一个插件表(plugin_info):

id name jar class
1 A客户插件 D:/plugin/a-plugin.jar com.xxg.aplugin.APlugin
2 B客户插件 D:/plugin/b-plugin.jar com.xxg.bplugin.BPlugin
3 C客户插件 D:/plugin/c-plugin.jar com.xxg.cplugin.CPlugin

两种方法的区别

两种方式从功能上来说是一样的。使用数据库方式的好处是可以很方遍的再开发一个管理界面来管理,不好的地方就是依赖数据库。

主程序

下面是以XML作为插件配置方式的调用插件的主程序。

主程序需要提供一个接口来提供给插件开发者来实现:

1
2
3
4
5
package com.xxg.main;

public interface PluginService {
	public void service();
}

上面是一个接口,包含一个未实现的方法service(),这个方法即和客户相关的逻辑,由插件来实现。

插件封装类:

1
2
3
4
5
6
7
8
9
10
11
package com.xxg.main;

public class Plugin {
	private String name;
	
	private String jar;
	
	private String className;
	
	// setter、getter省略…
}

解析XML获取所有插件信息(这里用到dom4j):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.xxg.main;

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;

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.setJar(pluginEle.elementText("jar"));
			plugin.setClassName(pluginEle.elementText("class"));
			list.add(plugin);
		}
		return list;
	}
}

使用URLClassLoader动态加载jar文件,实例化插件中的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.xxg.main;

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

public class PluginManager {
	private URLClassLoader urlClassLoader;

	public PluginManager(List<Plugin> plugins) throws MalformedURLException {
		init(plugins);
	}
	
	private void init(List<Plugin> plugins) throws MalformedURLException {
		int size = plugins.size();
		URL[] urls = new URL[size];
		
		for(int i = 0; i < size; i++) {
			Plugin plugin = plugins.get(i);
			String filePath = plugin.getJar();

			urls[i] = new URL("file:" + filePath);
		}
		
		// 将jar文件组成数组,来创建一个URLClassLoader
		urlClassLoader = new URLClassLoader(urls);
	}
	
	public PluginService getInstance(String className) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
		// 插件实例化对象,插件都是实现PluginService接口
		Class<?> clazz = urlClassLoader.loadClass(className);
		Object instance = clazz.newInstance();

		return (PluginService)instance;
	}
}

main函数依次调用插件逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.xxg.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();
		}
	}
}

插件开发

插件开发很简单,只需要把主程序的jar包引入到项目中,再实现主程序提供的接口就行:

1
2
3
4
5
6
7
8
9
10
package com.xxg.aplugin;

import com.xxg.main.PluginService;

public class APlugin implements PluginService {
	@Override
	public void service() {
		System.out.println("A客户插件正在执行~");
	}
}

service()方法应该实现客户相关的逻辑,即实现插件的功能。这里就用一句System.out.println来代替。

插件实现完成后,打个jar包,注意不要把主程序的部分也打到jar里。

再实现其他插件,插件实现完成后,配置主程序的plugin.xml。

运行程序

配置好plugin.xml,插件jar放到配置的路径下。

运行主程序main方法:


开始执行[A客户插件]插件…



A客户插件正在执行~



[A客户插件]插件执行完成



开始执行[B客户插件]插件…



B客户插件正在执行~



[B客户插件]插件执行完成



开始执行[C客户插件]插件…



C客户插件正在执行~



[C客户插件]插件执行完成

以上,就完成了主程序和插件的开发。

扩展:service()参数、返回值

如果逻辑需要的话,service()可以添加参数和返回值。例如主程序需要传入数据给插件,可以加入参数,插件需要返回结果给主程序,可以加入返回值。

例如传给插件一些插件需要的配置项。在上面的场景中,各个客户的需求不同。A需要FTP上传,那么需要FTP服务器的地址、端口号、用户名、密码配置项;B需要HTTP请求,那么需要请求地址配置项;C需要发送邮件,那么需要e-mail地址配置项。

这些配置项可以统一配置在XML或数据库中。

XML

每个插件元素中加入<properties>元素来配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8"?>
<plugins>
	<plugin>
		<name>A客户插件</name>
		<jar>D:/plugin/a-plugin.jar</jar>
		<class>com.xxg.aplugin.APlugin</class>
		<properties>
			<property name="FTP_IP">192.168.7.1</property>
			<property name="FTP_PORT">21</property>
			<property name="FTP_USERNAME">XXG</property>
			<property name="FTP_PASSWORD">123456</property>
		</properties>
	</plugin>
	<plugin>
		<name>B客户插件</name>
		<jar>D:/plugin/b-plugin.jar</jar>
		<class>com.xxg.bplugin.BPlugin</class>
		<properties>
			<property name="URL">http://www.xxg.com/api</property>
		</properties>
	</plugin>
	<plugin>
		<name>C客户插件</name>
		<jar>D:/plugin/c-plugin.jar</jar>
		<class>com.xxg.cplugin.CPlugin</class>
		<properties>
			<property name="EMAIL">xxg@xxg.com</property>
		</properties>
	</plugin>
</plugins>

数据库

如果使用数据库配置的话,数据库要再加一个插件配置表(plugin_config_info):

id plugin_id name value
1 1 FTP_IP 192.168.7.1
2 1 FTP_PORT 21
3 1 FTP_USERNAME XXG
4 1 FTP_PASSWORD 123456
5 2 URL
http://www.xxg.com/api
6 3 EMAIL xxg@xxg.com

主程序定义接口,加入一个Map<String,String>参数来传入这些配置:

1
2
3
4
5
6
7
package com.xxg.main;

import java.util.Map;

public interface PluginService {
	public void service(Map<String,String> configs);
}

在插件中,可以获取这些配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.xxg.aplugin;

import java.util.Map;

import com.xxg.main.PluginService;

public class APlugin implements PluginService {
	@Override
	public void service(Map<String, String> configs) {
		String ftpIp = configs.get("FTP_IP");
		String ftpPort = configs.get("FTP_PORT");
		String ftpUsername = configs.get("FTP_USERNAME");
		String ftpPassword = configs.get("FTP_PASSWORD");
		
		// ...
	}
}