Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【ClassLoader】自定义ClassLoader实现一个支持热加载的应用启动器 #8

Open
0xbitboy opened this issue Mar 17, 2019 · 0 comments

Comments

@0xbitboy
Copy link
Owner

JVM 默认是不支持Class的热加载的,也就是说我们的代码有变动,就要重启JVM来达到加载新的Class目的,但是很多容器如Tomcat、Jetty等都可以支持热加载,其底层的原理就是自定义ClassLoader。OSGI更是将类加载器玩到极至。我们来看看怎么实现一个简单的支持热加载的应用启动器。

一、实现的目标

  • 支持热加载
  • 可配置的启动类

二、实现

1. 支持热加载

关于类的加载,必然要说一下ClassLoader,JDK中存在这几个ClassLoader:

  • BootstrapClassLoader 加载基础类
  • ExtClassLoader 加载拓展类,父加载器是BootstrapClassLoader
  • AppClassLoader 加载应用程序类 ,父加载器是ExtClassLoader

双亲委派:
官方建议开发者,实现类加载器时遵循双亲委派规则,就是加载一个类时,先交给父加载器加载,如果父加载器无法加载,再由当前类加载器加载,从代码上来说,AppClassLoader已经写好了这个模版类,我们只需要覆盖findClass的逻辑即可。

实现热加载需要违背双亲委派规则吗?

由于ClassLoader中的defineClass方法会对已加载的类进行校验,所以我们无法对一个类进行重复加载,要实现热加载只能创建一个新的ClassLoader,假如我们采用双亲委派规则,那么我们需要加载的类会先被父加载器(AppClassLoader)给加载缓存起来,之后我们无论怎么创建一个新的加载器也无法达到热加载的目的。

public class HotSwapClassLoader extends ClassLoader {

	/**
	 * 指定目录下的类可以热加载
	 */
	private String basePath;

	public HotSwapClassLoader(String basePath) {
		this.basePath = basePath;
	}

	@Override
	public Class<?> loadClass(String name) throws ClassNotFoundException {
		Class<?> c = findLoadedClass(name);
		// 加载指定目录下的class
		if (c == null) {
			try {
				c = findClass(name);
				if (c != null) {
					return c;
				}
			} catch (ClassNotFoundException e) {
				return super.loadClass(name);
			}

		}
		return super.loadClass(name);
	}

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {

		String classResourcePath = this.basePath + "/" + name.replaceAll("\\.", "/");

		try {
			FileInputStream fileInputStream = new FileInputStream(new File(classResourcePath));
			ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
			int len;
			byte[] buffer = new byte[1024];
			while ((len = fileInputStream.read(buffer)) > 0) {
				byteArrayOutputStream.write(buffer, 0, len);
			}
			byte[] bytes = byteArrayOutputStream.toByteArray();
			return defineClass(name, bytes, 0, bytes.length);
		} catch (IOException e) {
			throw new ClassNotFoundException(name);
		}

	}
}

2. 启动器

上面我们已经实现了一个可以随时替换的ClassLoader,我们还需要一个引导类去维护我们的ClassLoader 还有我们的应用启动入口,管理启动和关闭的时机,就好比Tomcat的Catalina一样,或者说我们的任何类的Main函数。

public class Bootstrap {

	private String basePath;
	private Object application;
	private String applicationClassName;
	private volatile ClassLoader applicationClassLoader;


	public Bootstrap(String basePath, String applicationClassName) {
		this.basePath = basePath;
		this.applicationClassLoader = new HotSwapClassLoader(this.basePath);
		this.applicationClassName = applicationClassName;
		try {
			this.application = getApplication();
		} catch (Exception e) {
			e.printStackTrace();
		}

	}
    
}

有了上面的那些成员,我们就可以利用Java的反射来实现自定义的Application类的启动(这个类可以方在任意位置,就好比我们的war包可以方在任意位置,只要在tomcat的server.xml中配置好baseApps的路径就好了)

	private void startApplication() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
		this.application.getClass().getDeclaredMethod("start", null).invoke(this.application, new Object[0]);
	}

	private void stopApplication() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
		this.application.getClass().getDeclaredMethod("stop", null).invoke(this.application, new Object[0]);
	}

最后,剩下最后一个问题就是,我们怎么知道我们的类需要加载呢?有2种方式就是主动刷新,还有一种就是程序监听文件夹的文件变动。我们可以利用jdk7之后提供的WatchService来监控文件或者目录的变动情况,一发生变动,则先注销之前的Application 然后再创建一个新的HotSwapClassLoader来启动新的Application。

private void registerResourceWatcher() {
		try {
			WatchService watchService = FileSystems.getDefault().newWatchService();
			Path p = Paths.get(basePath);
			p.register(watchService, new WatchEvent.Kind[]{ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE});
			while (true) {
				WatchKey k = watchService.take();
				for (WatchEvent<?> e : k.pollEvents()) {
					reloadApplication();
					break;
				}
				k.reset();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

三、测试

public class Application {

	public Integer version = 46;

	/**
	 * 应用的启动入口
	 */
	public void start() {
		System.out.println("Start... version=" + version);
	}

	/**
	 * 应用的停止入口
	 */
	public void stop() {
		System.out.println("Stop... version=" + version);
	}
}

public class Main {

	public static void main(String[] args) throws IOException, InterruptedException {

		new Bootstrap("/Users/liaojiacan/Workspace/java/personal/code-snippets/java-language/target/classes"
				,"com.github.liaojiacan.classloader.app.Application").boot();
	}
}

启动后,我们修改Application的 version=47,然后rebuild project,这个时候这个文件就会发生改变,输出如下:

Start... version=46
Stop... version=46
Start... version=47
0xbitboy added a commit that referenced this issue Mar 17, 2019
1. 自定义了一个HotSwapClassLoader 破坏了双亲委派规则,先加载自己类,加载不成功再去使用父加载器加载。
2. 利用一个WatchService 监听指定文件夹下的文件变动。
3. Bootstrap类维护一个HotSwapClassLoader和一个Application的对象,Application对象是用户指定的应用入口,需要有start和stop方法。
4. 每次发生class文件变动,先调用application的stop方法停止旧应用,再创建一个新的HotSwapClassLoader,之后重新加载Application对象,再利用放射调用start方法启动应用。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant