问题原贴:
http://cloverprince.iteye.com/admin/blogs/481307
引用
3. 现有一个主程序用Java语言写成。现在要允许第三方开发人员编写扩展的类,约定第三方开发的类必须包含一个实现了某个已知接口(如interface IFooPlugin)的类,名称不限。如果要求第三方的类必须与主程序的bytecode分开发布,把.class放在classpath相应位置,或把jar丢在某个文件夹内即可被动态装载使用,应如何实现?
回答:
使用jar打包每个插件。里面包含一个实现已知接口的类,在jar的MANIFEST.MF中定义该类的全路径(像com.example.blah.MyPluginClass这样)。使用java.util.jar中的JarFile和Manifest类解析jar包和Manifest文件,用URLClassLoader装载该jar包。获得插件类后,用Class.newInstance()方法创造实例。
适用范围:
在Java-1.6中测试通过。
实现:
先指定一个接口,比如叫com.javaeye.cloverprince.plugins.PluginInterface。插件必须包含一个类,实现这个接口。
package com.javaeye.cloverprince.interfaces;
public interface PluginInterface {
void setName(String name); // 设定名字
void greet(); // 打招呼
}
创建一个插件。插件包含一个类,实现这个接口。
这个类叫com.javaeye.cloverprince.HelloWorldPlugin。
package com.javaeye.cloverprince;
import com.javaeye.cloverprince.interfaces.*;
public class HelloWorldPlugin implements PluginInterface{
private String name;
@Override
public void greet() {
System.out.format("Hello, %s\n",name);
}
@Override
public void setName(String name) {
this.name = name;
}
}
这个包将被打入一个jar包。我们还需要一个MANIFEST.MF文件
Manifest-Version: 1.0
Plugin-Class: com.javaeye.cloverprince.HelloWorldPlugin
注意第二行,这个Plugin-Class属性是我自己编的。
问题出现:
为什么要在Manifest里面放置这个类的路径呢?
回答:因为Java的所有的包/类的组织结构,是一个公共的大树。不同的类一定拥有不同的路径。因此,不同的插件中的类,路径、类名一定不同。根据插件的定义,主程序不应该知道插件的实现细节。也就是说,主程序在编译之前,不可能知道插件的类的路径和名称。但是,要让插件工作,主程序又知道所有的插件的共同特征,想一想,如果主程序不知道插件的“任何”细节,又怎么知道jar里面哪个类才是实现了那个已知接口的类呢?
对于这个实现来说:如果将插件组织到不同的jar包中,那么一个良好的存储这一信息的地方,就是它的Manifest。读取一个jar包时,先看看它的Manifest中的这个Plugin-Class属性,就知道应该装载哪个类了。
这时,这个jar包里只有两个文件:
引用
META-INF/MANIFEST.MF
com/javaeye/cloverprince/HelloWorldPlugin.class
另一个插件也类似的制作。
一个类 com.somecompanyelse.GoodbyeWorld:
package com.somecompanyelse;
import com.javaeye.cloverprince.interfaces.PluginInterface;
public class GoodbyeWorld implements PluginInterface {
private String name;
@Override
public void greet() {
System.out.println("Goodbye, "+name);
}
@Override
public void setName(String name) {
this.name = name;
}
}
一个Manifest文件MANIFEST.MF:
Manifest-Version: 1.0
Plugin-Class: com.somecompanyelse.GoodbyeWorld
打成另一个jar包,包含两个文件:
引用
META-INF/MANIFEST.MF
com/somecompanyelse/GoodbyeWorld.class
现在,两个插件已经有了,只差一个主程序来读取这两个插件了。
主程序:
import java.io.*;
import java.util.jar.*;
import java.util.*;
import java.net.*;
import com.javaeye.cloverprince.interfaces.PluginInterface;
public class Main {
public static final String PLUGINS_PATH = "plugins";
// 用一个ArrayList储存每个插件中的类的实例。
private static ArrayList<PluginInterface> plugins =
new ArrayList<PluginInterface>();
public static void main(String[] args) {
File pluginsDir = new File(PLUGINS_PATH);
if(!pluginsDir.isDirectory()) {
System.err.format("%s isn't directory!\n",pluginsDir.getName());
return;
}
// Load plugins
for(File pluginFile : pluginsDir.listFiles()) {
if(pluginFile.getName().endsWith(".jar")) {
System.out.format("Loading File: %s ...\n", pluginFile.getAbsolutePath());
loadFile(pluginFile);
}
}
// Test plugins
for(PluginInterface plugin : plugins) {
plugin.setName("cloverprince");
plugin.greet();
}
}
private static void loadFile(File pluginFile) {
JarFile jf;
Manifest mf;
// 打开jar包
try {
jf = new JarFile(pluginFile);
mf = jf.getManifest();
} catch (IOException e) {
System.err.format("Error reading jar file.\n", pluginFile.getName());
e.printStackTrace();
return;
}
// 从jar的Manifest中读取Plugin-Class属性
String pluginClassPath = mf.getMainAttributes().getValue("Plugin-Class");
if(pluginClassPath==null) {
System.err.format("Cannot find attribute Plugin-Class in manifest file.\n", pluginFile.getName());
return;
}
// 创造ClassLoader
URLClassLoader cl;
try {
cl = new URLClassLoader(new URL[]{
pluginFile.toURI().toURL()
});
} catch (MalformedURLException e) {
System.err.format("This should not throw.\n");
e.printStackTrace();
return;
}
// 装载这个插件jar中的类
Class pluginClass;
try {
pluginClass = cl.loadClass(pluginClassPath);
} catch (ClassNotFoundException e) {
System.err.println("Cannot load class");
e.printStackTrace();
return;
}
// 实例化这个类
PluginInterface pluginInstance;
try {
pluginInstance = (PluginInterface) pluginClass.newInstance();
} catch (InstantiationException e) {
System.err.println("Cannot instantiate class.");
e.printStackTrace();
return;
} catch (IllegalAccessException e) {
System.err.println("Illegal Access.");
e.printStackTrace();
return;
}
// 把这个类放入数组中,等待以后使用
plugins.add(pluginInstance);
System.out.format("Class %s loaded.\n",pluginInstance.getClass().getCanonicalName());
}
}
精简版(无异常处理):
import java.io.*;
import java.util.jar.*;
import java.util.*;
import java.net.*;
import com.javaeye.cloverprince.interfaces.PluginInterface;
public class Main {
public static final String PLUGINS_PATH = "plugins";
// 用一个ArrayList储存每个插件中的类的实例。
private static ArrayList<PluginInterface> plugins =
new ArrayList<PluginInterface>();
public static void main(String[] args) throws Exception {
File pluginsDir = new File(PLUGINS_PATH);
// Load plugins
for(File pluginFile : pluginsDir.listFiles()) {
if(pluginFile.getName().endsWith(".jar")) {
loadFile(pluginFile);
}
}
// Test plugins
for(PluginInterface plugin : plugins) {
plugin.setName("cloverprince");
plugin.greet();
}
}
private static void loadFile(File pluginFile) throws Exception {
JarFile jf = new JarFile(pluginFile);
Manifest mf = jf.getManifest();
String pluginClassPath = mf.getMainAttributes().getValue("Plugin-Class");
URLClassLoader cl= new URLClassLoader(new URL[]{
pluginFile.toURI().toURL()
});
Class pluginClass = cl.loadClass(pluginClassPath);
PluginInterface pluginInstance = (PluginInterface) pluginClass.newInstance();
plugins.add(pluginInstance);
}
}
以上,所有需要的文件齐备。
编译:
如上所述,每个插件打一个jar包,主程序随意。注意路径。
执行:
执行该程序需要的最小文件集(4个文件):
引用
.
│ Main.class
│
├─com
│ └─javaeye
│ └─cloverprince
│ └─interfaces
│ PluginInterface.class
│
└─plugins
goodbye-3.14159265.jar
helloworld-1.0.jar
执行: java Main
引用
Loading File: D:\wks\workspace\PluginTest\plugins\goodbye-3.14159265.jar ...
Class com.somecompanyelse.GoodbyeWorld loaded.
Loading File: D:\wks\workspace\PluginTest\plugins\helloworld-1.0.jar ...
Class com.javaeye.cloverprince.HelloWorldPlugin loaded.
Goodbye, cloverprince
Hello, cloverprince
总结:
1. 主程序并不了解plugins目录中有多少插件。在运行时列举目录。
2. 主程序对每个plugins文件(比如叫helloworld-1.0.jar)的了解只有:
- helloworld-1.0的META-INF/MANIFEST.MF中有一个Plugin-Class属性,指定了该插件类的路径。
- 这个插件类拥有一个不带参数的构造方法。
- 这个插件类实现了com.javaeye.cloverprince.PluginInterface接口。
后记:
复制第一个插件hello world,将greeting中的字符串修改,其余文件均不变,打成另一个jar包,放在插件目录中,可以和第一个插件共存,分别装载并工作。
这就是说,不同的jar包中,所有类的路径和名称不能相同的说法是不正确的。