Java 自定义类加载器实现插件式开发

最近接触Solr比较多,感觉Solr提供的插件式开发方式很酷,Solr对开发者提供了一个核心api jar包,开发者如果想扩展Solr某一项功能 比如 中文分词,只需要继承Solr提供的分词接口添加自己的实现,然后把自己的分词jar包拷贝到Solr指定目录,并在solr配置文件中配置,重启即可生效。


本文会涉及到自定义类加载,所以先介绍一下java类加载器的原理和工作机制,熟悉的同学可以直接跳过。


java类加载器

类加载器是一个用来加载类文件的类。Java源代码通过javac编译器编译成类文件。然后JVM来执行类文件中的字节码来执行程序。类加载器负责加载文件系统、网络或其他来源的类文件。有三种默认使用的类加载器:Bootstrap类加载器、Extension类加载器和System类加载器(或者叫作Application类加载器)。每种类加载器所负责加载的类也各不相同。
JVM三种预定义类型类加载器

  1. Bootstrap ClassLoader : 将存放于\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用
  2. Extension ClassLoader : 将\lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库加载。开发者可以直接使用扩展类加载器。
  3. Application ClassLoader : 负责加载用户类路径(ClassPath)上所指定的类库,开发者可直接使用。

类加载器的代理模式

类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。在介绍代理模式背后的动机之前,首先需要说明一下 Java 虚拟机是如何判定两个 Java 类是相同的。Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器 ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。


一句话总结就是:查找某个类的时候从下至上,加载某个类的时候从上至下。


自定义类加载器

因为我们需要加载指定路径下的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; j < files.length; j++) {
				try {
					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;
    }

    public  T 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 {

    List getUsers();

    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 List getUsers() {
        List list = 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;
    }
}



example-host中的启动类如下:

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);
        List list = userService.getUsers();
        System.out.println("list = [" + list + "]");

        userService.update(new User("test", "test"));
    }
}


注意:运行之前需要将example-plugin工程打成jar包,然后拷贝到任意路径下即可,本文是D:\osgi目录下。


代码下载

所有代码均已上传至Github:https://github.com/TiFG/cherry


你可能感兴趣的:(Java)