Pf4j的SPI简单实例
同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依赖去掉了,日志也就打不出来了。