为什么要实现热加载?
希望依赖的jar在需要用的时候依赖,不用的时候就不依赖,项目打包部署就不会过于庞大
为什么需要自定义ClassLoader?
每次加载类都会消耗一些内存,加载越来越多,如果不释放可能会引发OOM
由于每个对象都有相应的Class对象,所以当该类仍有实例的时候,是无法卸载的,因为此时Class对象仍可达;
对于ClassLoader对象,留意双亲委托机制中,每个ClassLoader都会记录自身已加载的类信息,所以如果ClassLoader可达,那么Class对象仍是可达的,这就解释了为什么我们为什么需要自定义ClassLoader
因为系统的ClassLoader永远是可达的,他们加载的类在运行时永远不会被卸载
自定义类加载器
通过jar的路径实现jar的热加载,继承URLClassLoader
public class MyURLClassLoader extends URLClassLoader {private JarURLConnection cachedJarFile;/*** 定位基于当前上下文的父类加载器** @return 返回可用的父类加载器.*/private static ClassLoader findParentClassLoader() {ClassLoader parent = MyURLClassLoader.class.getClassLoader();if (parent == null) {parent = Thread.currentThread().getContextClassLoader();}return parent;}public MyURLClassLoader() {super(new URL[]{}, findParentClassLoader());}/*** 将指定的文件url添加到类加载器的classpath中去,并缓存jar connection,方便以后卸载jar** @param file 一个可想类加载器的classpath中添加的文件url*/public void addURLFile(URL file) {try {// 打开并缓存文件url连接URLConnection uc = file.openConnection();if (uc instanceof JarURLConnection) {uc.setUseCaches(true);((JarURLConnection) uc).getManifest();cachedJarFile = (JarURLConnection) uc;}} catch (Exception e) {System.err.println("Failed to cache plugin JAR file: " + file.toExternalForm());}//加载jar包文件 父类提供addURL(file);}/*** jar包卸载*/public void removeJarFile() {if (cachedJarFile == null) {return;}try {System.err.println("Unloading plugin JAR file " + cachedJarFile.getJarFile().getName());cachedJarFile.getJarFile().close();} catch (Exception e) {System.err.println("Failed to unload JAR file\n" + e);}}}
类加载器管理工具类
向外提供加载jar,加载类,获取目标对象的方法
public class MyClassLoaderManager {/*** 加载jar包的缓存 一个jar包对应一个类加载器对象 key为jar包名*/private static final ConcurrentHashMap<String, MyURLClassLoader> LOADER_CACHE = new ConcurrentHashMap<>();/*** jar包所在父目录*/private String jarPath;public MyClassLoaderManager(String jarPath) {this.jarPath = jarPath;}/*** 加载jar包** @param jarName jar包名称 包括后缀*/public void loadJar(String jarName) {MyURLClassLoader urlClassLoader = LOADER_CACHE.get(jarName);if (urlClassLoader != null) {return;}try {MyURLClassLoader classLoader = new MyURLClassLoader();URL jarUrl = new URL("jar:file:/" + jarPath + "/" + jarName + "!/");classLoader.addURLFile(jarUrl);LOADER_CACHE.put(jarName, classLoader);} catch (MalformedURLException e) {e.printStackTrace();}}/*** 根据jar包名和类路径获取实例化对象** @param jarName jar包名称 包括后缀* @param classPackage 包括包名的类完整路径*/public <T> T getInstance(String jarName, String classPackage) {Class<?> loadClass = loadClass(jarName, classPackage);try {return (T) loadClass.newInstance();} catch (IllegalAccessException e) {throw new IllegalArgumentException(e.getMessage());} catch (InstantiationException e) {throw new IllegalArgumentException("实例化失败");}}/*** 根据jar包名和类路径获取实例化对象** @param jarName jar包名称 包括后缀* @param classPackage 包括包名的类完整路径*/public Class<?> loadClass(String jarName, String classPackage) {MyURLClassLoader urlClassLoader = LOADER_CACHE.get(jarName);if (urlClassLoader == null) {return null;}try {return urlClassLoader.loadClass(classPackage);} catch (ClassNotFoundException e) {throw new IllegalArgumentException("类没有找到");}}/*** 移除jar包** @param jarName jar包名称 包括后缀*/public void removeJarFile(String jarName) {MyURLClassLoader urlClassLoader = LOADER_CACHE.get(jarName);if (urlClassLoader == null) {return;}urlClassLoader.removeJarFile();try {urlClassLoader.close();LOADER_CACHE.remove(jarName);} catch (IOException e) {e.printStackTrace();}}}
用来加载的类
希望达到的目的是classloader项目实现热加载business的jar包,上述写的自定义类加载器都在classloader项目中
在classloader项目中创建一个接口,非常简单的一个接口
public interface Plugin {
void doSome();
}
在business项目中引入classloader的依赖,注意scope为provided,依赖不参与打包,只是为了写Plugin插件接口的实现类时编译不报错
<dependency>
<groupId>com.halayang</groupId>
<artifactId>classloader</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
在business项目中写Plugin接口实现类
public class MyPlugin implements Plugin {
@Override
public void doSome() {
System.out.println("hahahahaha");
}
}
测试
public static void main(String[] args) {
//指定jar包所在目录
MyClassLoaderManager classLoader = new MyClassLoaderManager("E:/JavaStudy/ZZZZZZ StudyingCode/classloaderhotdeploy");
String jarName = "business.jar";
//先加载jar包再加载类再获取实例
classLoader.loadJar(jarName);
Plugin plugin = classLoader.getInstance(jarName, "com.halayang.service.MyPlugin");
plugin.doSome();
//卸载jar包
classLoader.removeJarFile(jarName);
}
监控
为了查看类卸载有没有成功,在测试方法中加死循环
MyClassLoaderManager classLoader = new MyClassLoaderManager("E:/JavaStudy/ZZZZZZ StudyingCode/classloaderhotdeploy");
while (true) {
String jarName = "business.jar";
//先加载jar包再加载类
classLoader.loadJar(jarName);
Plugin plugin1 = classLoader.getInstance(jarName, "com.halayang.service.MyPlugin");
plugin1.doSome();
Plugin plugin2 = classLoader.getInstance(jarName, "com.halayang.service.HaHaPlugin");
plugin2.doSome();
Thread.sleep(2000);
classLoader.removeJarFile(jarName);
//手动gc
//System.gc();
}
运行jvm参数把堆调小一点,调成10m
jdk提供了一个监控工具jvisualvm,可以实时观察线程 ,类加载、卸载, 堆内存的使用情况等
手动gc时 观察到的类卸载会比较明显
