同Dubbo的扩展SPI一样,Pf4j这个插件框架也师出同门,都是由JDK自带的SPI(参见Java的SPI简单实例)衍化而来。但Pf4j毕竟是一个插件框架,对插件的支持相对专业一些。官网上的介绍说:PF4J是一个开源(Apache许可证)轻量级(约100kb)的java插件框架,具有最小的依赖性(只有slf4japi和javasemver),并且具有很强的可扩展性。接下来我们还是用一个简单的例子说明:
1、接口类,继承ExtensionPoint:
package com.wlf.service;
import org.pf4j.ExtensionPoint;
public interface IPf4jGreeting extends ExtensionPoint {
void sayHello();
}
2、接口实现类,需要@Extension注解证明它是扩展点的实现。一般都放在jar包或zip包中,但这里我们为了方便,直接放在同一个项目中:
package com.wlf.service.impl;
import com.wlf.service.IPf4jGreeting;
import org.pf4j.Extension;
@Extension
public class IPf4jGreetingImpl1 implements IPf4jGreeting {
@Override
public void sayHello() {
System.out.println("hello, world.");
}
}
package com.wlf.service.impl;
import com.wlf.service.IPf4jGreeting;
import org.pf4j.Extension;
@Extension
public class IPf4jGreetingImpl2 implements IPf4jGreeting {
@Override
public void sayHello() {
System.out.println("hi, mia.");
}
}
3、应用类,执行插件加载和实例化:
package com.wlf.service;
import org.pf4j.DefaultPluginManager;
import org.pf4j.PluginManager;
import java.util.List;
public class TestPf4jServiceLoader {
public static void main(String[] args) {
PluginManager pluginManager = new DefaultPluginManager();
List<IPf4jGreeting> greetings = pluginManager.getExtensions(IPf4jGreeting.class);
greetings.forEach(greeting -> {
greeting.sayHello();
});
pluginManager.stopPlugins();
}
}
完事了。跑一下试试:
23:03:44.619 [main] INFO org.pf4j.DefaultPluginStatusProvider - Enabled plugins: [] 23:03:44.627 [main] INFO org.pf4j.DefaultPluginStatusProvider - Disabled plugins: [] 23:03:44.637 [main] INFO org.pf4j.DefaultPluginManager - PF4J version 3.0.1 in 'deployment' mode 23:03:44.637 [main] DEBUG org.pf4j.AbstractPluginManager - Lookup plugins in 'plugins' 23:03:44.641 [main] WARN org.pf4j.AbstractPluginManager - No 'plugins' root 23:03:44.650 [main] DEBUG org.pf4j.AbstractExtensionFinder - Finding extensions of extension point 'com.wlf.service.IPf4jGreeting' 23:03:44.650 [main] DEBUG org.pf4j.LegacyExtensionFinder - Reading extensions storages from classpath 23:03:44.650 [main] DEBUG org.pf4j.LegacyExtensionFinder - Read '/E:/workspace/subtitle-synthesis/target/classes/META-INF/extensions.idx' 23:03:44.664 [main] DEBUG org.pf4j.AbstractExtensionFinder - Found possible 2 extensions: 23:03:44.665 [main] DEBUG org.pf4j.AbstractExtensionFinder - com.wlf.service.impl.IPf4jGreetingImpl2 23:03:44.665 [main] DEBUG org.pf4j.AbstractExtensionFinder - com.wlf.service.impl.IPf4jGreetingImpl1 23:03:44.665 [main] DEBUG org.pf4j.LegacyExtensionFinder - Reading extensions storages from plugins 23:03:44.665 [main] DEBUG org.pf4j.AbstractExtensionFinder - Finding extensions of extension point 'com.wlf.service.IPf4jGreeting' for plugin 'null' 23:03:44.665 [main] DEBUG org.pf4j.AbstractExtensionFinder - Loading class 'com.wlf.service.impl.IPf4jGreetingImpl2' using class loader 'sun.misc.Launcher$AppClassLoader@18b4aac2' 23:03:44.673 [main] DEBUG org.pf4j.AbstractExtensionFinder - Checking extension type 'com.wlf.service.impl.IPf4jGreetingImpl2' 23:03:44.697 [main] DEBUG org.pf4j.AbstractExtensionFinder - Added extension 'com.wlf.service.impl.IPf4jGreetingImpl2' with ordinal 0 23:03:44.697 [main] DEBUG org.pf4j.AbstractExtensionFinder - Loading class 'com.wlf.service.impl.IPf4jGreetingImpl1' using class loader 'sun.misc.Launcher$AppClassLoader@18b4aac2' 23:03:44.708 [main] DEBUG org.pf4j.AbstractExtensionFinder - Checking extension type 'com.wlf.service.impl.IPf4jGreetingImpl1' 23:03:44.709 [main] DEBUG org.pf4j.AbstractExtensionFinder - Added extension 'com.wlf.service.impl.IPf4jGreetingImpl1' with ordinal 0 23:03:44.710 [main] DEBUG org.pf4j.AbstractExtensionFinder - Found 2 extensions for extension point 'com.wlf.service.IPf4jGreeting' 23:03:44.710 [main] DEBUG org.pf4j.AbstractExtensionFinder - Found 2 extensions for extension point 'com.wlf.service.IPf4jGreeting' 23:03:44.710 [main] DEBUG org.pf4j.DefaultExtensionFactory - Create instance for extension 'com.wlf.service.impl.IPf4jGreetingImpl2' 23:03:44.711 [main] DEBUG org.pf4j.DefaultExtensionFactory - Create instance for extension 'com.wlf.service.impl.IPf4jGreetingImpl1' hi, mia. hello, world.
按通常用法,需要把实现类1和实现类2分别打成两个jar包或zip包,然后放到一个指定目录(默认是一个叫plugins的目录)中。这里为啥放在同一个项目中,Pf4j还能找到我们的扩展点实现类呢?其实日志已经出卖了一切,它最开始就是去plugins目录中寻找的,没找着,接着去classpath目录下需要,在target目录中找到了编译后extension.idx文件:

文件的内容刚好就是@Extension注解标识出来的扩展点实现类。Pf4j通过getExtensions方法找到我们的实现类后,做了一层包装,把实现类包装进ExtensionWrapper对象,再由该对象的getExtension方法,调用扩展工厂ExtensionFactory生成实例:
/**
* A wrapper over extension instance.
*
* @author Decebal Suiu
*/
public class ExtensionWrapper<T> implements Comparable<ExtensionWrapper<T>> {
private final ExtensionDescriptor descriptor;
private final ExtensionFactory extensionFactory;
private T extension; // cache
public ExtensionWrapper(ExtensionDescriptor descriptor, ExtensionFactory extensionFactory) {
this.descriptor = descriptor;
this.extensionFactory = extensionFactory;
}
@SuppressWarnings("unchecked")
public T getExtension() {
if (extension == null) {
extension = (T) extensionFactory.create(descriptor.extensionClass);
}
return extension;
}
public ExtensionDescriptor getDescriptor() {
return descriptor;
}
public int getOrdinal() {
return descriptor.ordinal;
}
@Override
public int compareTo(ExtensionWrapper<T> o) {
return (getOrdinal() - o.getOrdinal());
}
}
**
* The default implementation for {@link ExtensionFactory}.
* It uses {@link Class#newInstance} method.
*
* @author Decebal Suiu
*/
public class DefaultExtensionFactory implements ExtensionFactory {
private static final Logger log = LoggerFactory.getLogger(DefaultExtensionFactory.class);
/**
* Creates an extension instance.
*/
@Override
public <T> T create(Class<T> extensionClass) {
log.debug("Create instance for extension '{}'", extensionClass.getName());
try {
return extensionClass.newInstance();
} catch (Exception e) {
throw new PluginRuntimeException(e);
}
}
}
包装类ExtensionWrapper做了排序,所以我们可以指定插件加载的顺序,ordinal默认是0,所以我们只需指定插件2的排序为1,即可让插件1先执行:

既然Pf4j是基于JDK的SPI机制而构建的,自然我们也可以直接使用ServiceLoader指定的META-INF/services目录来让Pf4j发现我们的扩展点实现类,此时就没有必要使用注解@Extension来标识了。当然,没有了注解,我们也没法指定扩展点实现类的加载顺序,原生SPI没有那么强的功能。
我们去掉两个实现类的注解:

在META-INF/services目录新增文件com.wlf.service.IPf4jGreeting:

最后在实例化PluginManager时指定使用JDK的SPI方式加载,重写了createExtensionFinder方法,运行结果如下:

它会读取所有META-INF/services目录下的文件,包括依赖的jar包。所以为了排除一些类加载失败,我把其他jar依赖去掉了,日志也就打不出来了。
