最近接触Solr比较多,感觉Solr提供的插件式开发方式很酷,Solr对开发者提供了一个核心api jar包,开发者如果想扩展Solr某一项功能 比如 中文分词,只需要继承Solr提供的分词接口添加自己的实现,然后把自己的分词jar包拷贝到Solr指定目录,并在solr配置文件中配置,重启即可生效。
本文会涉及到自定义类加载,所以先介绍一下java类加载器的原理和工作机制,熟悉的同学可以直接跳过。
类加载器是一个用来加载类文件的类。Java源代码通过javac编译器编译成类文件。然后JVM来执行类文件中的字节码来执行程序。类加载器负责加载文件系统、网络或其他来源的类文件。有三种默认使用的类加载器:Bootstrap类加载器、Extension类加载器和System类加载器(或者叫作Application类加载器)。每种类加载器所负责加载的类也各不相同。
JVM三种预定义类型类加载器
一句话总结就是:查找某个类的时候从下至上,加载某个类的时候从上至下。
因为我们需要加载指定路径下的jar文件,所以我们需要自定义类加载器来扫描指定路径下的jar包,代码如下:
package com.bytebeats.switcher.core;
import com.bytebeats.switcher.util.IoUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileFilter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
/**
*
* @author Ricky Fung
* @create 2016-11-12 14:27
*/
public class PluginClassLoader {
private final Logger logger = LoggerFactory.getLogger(PluginClassLoader.class);
private URLClassLoader classLoader;
public PluginClassLoader(String jarfileDir){
this(new File(jarfileDir), null);
}
public PluginClassLoader(File jarfileDir){
this(jarfileDir, null);
}
public PluginClassLoader(File jarfileDir, ClassLoader parent) {
this.classLoader = createClassLoader(jarfileDir, parent);
}
public void addToClassLoader(final String baseDir, final FileFilter filter,
boolean quiet) {
File base = new File(baseDir);
if (base != null && base.exists() && base.isDirectory()) {
File[] files = base.listFiles(filter);
if (files == null || files.length == 0) {
if (!quiet) {
logger.error("No files added to classloader from lib: "
+ baseDir + " (resolved as: "
+ base.getAbsolutePath() + ").");
}
} else {
this.classLoader = replaceClassLoader(classLoader, base, filter);
}
} else {
if (!quiet) {
logger.error("Can't find (or read) directory to add to classloader: "
+ baseDir
+ " (resolved as: "
+ base.getAbsolutePath()
+ ").");
}
}
}
private URLClassLoader replaceClassLoader(final URLClassLoader oldLoader,
final File base, final FileFilter filter) {
if (null != base && base.canRead() && base.isDirectory()) {
File[] files = base.listFiles(filter);
if (null == files || 0 == files.length){
logger.error("replaceClassLoader base dir:{} is empty", base.getAbsolutePath());
return oldLoader;
}
logger.error("replaceClassLoader base dir: {} ,size: {}", base.getAbsolutePath(), files.length);
URL[] oldElements = oldLoader.getURLs();
URL[] elements = new URL[oldElements.length + files.length];
System.arraycopy(oldElements, 0, elements, 0, oldElements.length);
for (int j = 0; jtry {
URL element = files[j].toURI().normalize().toURL();
elements[oldElements.length + j] = element;
logger.info("Adding '{}' to classloader", element.toString());
} catch (MalformedURLException e) {
logger.error("load jar file error", e);
}
}
ClassLoader oldParent = oldLoader.getParent();
IoUtils.closeQuietly(oldLoader); // best effort
return URLClassLoader.newInstance(elements, oldParent);
}
return oldLoader;
}
private URLClassLoader createClassLoader(final File libDir,
ClassLoader parent) {
if (null == parent) {
parent = Thread.currentThread().getContextClassLoader();
}
return replaceClassLoader(
URLClassLoader.newInstance(new URL[0], parent), libDir, null);
}
public Class> loadClass(String className) throws ClassNotFoundException{
return classLoader.loadClass(className);
}
}
为了方便使用, 提供了PluginManager
package com.bytebeats.switcher.core;
import com.bytebeats.switcher.util.StringUtils;
import java.io.File;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2016-11-12 14:35
*/
public class PluginManager {
private volatile static PluginManager mgr;
private PluginClassLoader pluginClassLoader;
private volatile boolean init;
private PluginManager(){
}
public static PluginManager getMgr(){
if(mgr==null){
synchronized (PluginManager.class){
if(mgr==null){
mgr = new PluginManager();
}
}
}
return mgr;
}
publicT getPlugin(String className, Class required){
Class> cls = null;
try {
cls = pluginClassLoader.loadClass(className);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("can not find class:"+className, e);
}
if(required.isAssignableFrom(cls)){
try {
return (T) cls.newInstance();
} catch (Exception e) {
throw new IllegalArgumentException("can not newInstance class:"+className, e);
}
}
throw new IllegalArgumentException("class:"+className+" not sub class of "+required);
}
public void addExternalJar(String basePath){
if (StringUtils.isEmpty(basePath)) {
throw new IllegalArgumentException("basePath can not be empty!");
}
File dir = new File(basePath);
if(!dir.exists()){
throw new IllegalArgumentException("basePath not exists:"+basePath);
}
if(!dir.isDirectory()){
throw new IllegalArgumentException("basePath must be a directory:"+basePath);
}
if(!init){
init= true;
pluginClassLoader = doInit(basePath);
}else{
pluginClassLoader.addToClassLoader(basePath, null, true);
}
}
private synchronized PluginClassLoader doInit(String basePath){
PluginClassLoader pluginClassLoader = new PluginClassLoader(basePath);
return pluginClassLoader;
}
}
下面通过一个简单例子来演示如何实现插件式开发
example-api存放核心api接口,example-plugin是插件工程 提供example-api api接口实现,example-host是主程序 它在启动的时候会去加载plugin。
example-api中的api接口如下:
package com.bytebeats.switcher.example.api;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2016-11-12 15:30
*/
public interface IHelloService {
String hello(String msg);
}
package com.bytebeats.switcher.example.api;
import com.bytebeats.switcher.example.domain.User;
import java.util.List;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2016-11-12 16:09
*/
public interface IUserService {
ListgetUsers();
int update(User user);
}
example-plugin中的api接口实现如下:
package com.bytebeats.example.plugin;
import com.bytebeats.switcher.example.api.IHelloService;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2016-11-12 16:13
*/
public class HelloServiceImpl implements IHelloService {
@Override
public String hello(String msg) {
System.out.println("hello [" + msg + "]");
return "hello [" + msg + "]";
}
}
package com.bytebeats.example.plugin;
import com.bytebeats.switcher.example.api.IUserService;
import com.bytebeats.switcher.example.domain.User;
import java.util.ArrayList;
import java.util.List;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2016-11-12 16:14
*/
public class UserServiceImpl implements IUserService {
@Override
public ListgetUsers() {
Listlist = new ArrayList<>();
list.add(new User("ricky", "12345"));
list.add(new User("kobe", "aaa"));
list.add(new User("jordan", "root"));
return list;
}
@Override
public int update(User user) {
System.out.println("update user = [" + user + "]");
user.setPassword("hello");
return 1;
}
}
package com.bytebeats.example.host;
import com.bytebeats.switcher.core.PluginManager;
import com.bytebeats.switcher.example.api.IHelloService;
import com.bytebeats.switcher.example.api.IUserService;
import com.bytebeats.switcher.example.domain.User;
import java.util.List;
/**
* Hello world!
*
*/
public class App {
public static void main( String[] args ) {
PluginManager pluginManager = PluginManager.getMgr();
pluginManager.addExternalJar("D:\\osgi");
IHelloService helloService = pluginManager.getPlugin("com.bytebeats.example.plugin.HelloServiceImpl", IHelloService.class);
helloService.hello("ricky");
IUserService userService = pluginManager.getPlugin("com.bytebeats.example.plugin.UserServiceImpl", IUserService.class);
Listlist = userService.getUsers();
System.out.println("list = [" + list + "]");
userService.update(new User("test", "test"));
}
}