Java基于自定义类加载器实现热部署过程解析
热部署:
热部署就是在不重启应用的情况下,当类的定义即字节码文件修改后,能够替换该Class创建的对象。一般情况下,类的加载都是由系统自带的类加载器完成,且对于同一个全限定名的java类,只能被加载一次,而且无法被卸载。可以使用自定义的ClassLoader替换系统的加载器,创建一个新的ClassLoader,再用它加载Class,得到的Class对象就是新的(因为不是同一个类加载器),再用该Class对象创建一个实例,从而实现动态更新。如:修改JSP文件即生效,就是利用自定义的ClassLoader实现的。
还需要创建一个守护线程,不断地检查class文件是否被修改过,通过判断文件的上次修改时间实现。
演示:
原来的程序:
修改后重新编译:
代码:
packageDynamic; importjava.io.*; importjava.nio.file.Files; importjava.nio.file.Path; importjava.util.concurrent.TimeUnit; publicclassClassLoadStudy{ publicstaticvoidmain(String[]args)throwsException{ HotDeployhot=newHotDeploy("Dynamic.Task"); hot.monitor(); while(true){ TimeUnit.SECONDS.sleep(2); hot.getTask().run(); } } } //热部署 classHotDeploy{ privatestaticvolatileRunnableinstance; privatefinalStringFILE_NAME; privatefinalStringCLASS_NAME; publicHotDeploy(Stringname){ CLASS_NAME=name;//类的完全限定名 name=name.replaceAll("\\.","/")+".class"; FILE_NAME=(getClass().getResource("/")+name).substring(6);//判断class文件修改时间使用,substring(6)去掉开头的file:/ } //获取一个任务 publicRunnablegetTask(){ if(instance==null){//双重检查锁,单例,线程安全 synchronized(HotDeploy.class){ if(instance==null){ try{ instance=createTask(); }catch(Exceptione){ e.printStackTrace(); } } } } returninstance; } //创建一个任务,重新加载class文件 privateRunnablecreateTask(){ try{ Classclazz=MyClassLoader.getLoader().loadClass(CLASS_NAME); if(clazz!=null) return(Runnable)clazz.newInstance(); }catch(Exceptione){ e.printStackTrace(); } returnnull; } //监视器,监视class文件是否被修改过,如果是的话,则重新加载 publicvoidmonitor()throwsIOException{ Threadt=newThread(()->{ try{ longlastModified=Files.getLastModifiedTime(Path.of(FILE_NAME)).toMillis(); while(true){ Thread.sleep(500); longnow=Files.getLastModifiedTime(Path.of(FILE_NAME)).toMillis(); if(now!=lastModified){//如果class文件被修改过了 lastModified=now; instance=createTask();//重新加载 } } }catch(InterruptedException|IOExceptione){ e.printStackTrace(); } }); t.setDaemon(true);//守护线程 t.start(); } } //自定义的类加载器 classMyClassLoaderextendsClassLoader{ @Override publicClass>findClass(Stringname)throwsClassNotFoundException{ try{ StringfileName="/"+name.replaceAll("\\.","/")+".class"; InputStreamis=getClass().getResourceAsStream(fileName); byte[]b=is.readAllBytes(); returndefineClass(name,b,0,b.length); }catch(IOExceptione){ thrownewClassNotFoundException(name); } } publicstaticMyClassLoadergetLoader(){ returnnewMyClassLoader(); } }
遇到的坑:
刚开始自定义类加载器时,重写的是loadClass(Stringname)方法,但不断地报错,后来明白了,因为Task类实现了Java.lang.Runnable接口,且重写loadClass方法破坏了双亲委派机制,导致了自定义的类加载器去加载java.lang.Runnable,但被Java安全机制禁止了所以会报错。defineClass调用preDefineClass,preDefineClass会检查包名,如果以java开头,就会抛出异常,因为让用户自定义的类加载器来加载Java自带的类库会引起混乱。
于是又重写findClass方法,但还是不行,findClass方法总是得不到执行,因为编译好的类是在classpath下的,而自定义的ClassLoader的父加载器是AppClassLoader,由于双亲委派机制,类就会被ApplicationClassLoader来加载了。因此自定义的findClass方法就不会被执行。解决方法是,向构造器ClassLoader(ClassLoaderparent)传入null,或传入getSystemClassLoader().getParent()。
还有就是路径问题:
- path不以/开头时,默认是从此类所在的包下取资源;path以/开头时,则是从ClassPath根下获取;
- URLgetClass.getResource(Stringpath)
- InputStreamgetClass().getResourceAsStream(Stringpath)
- getResource("")返回当前类所在的包的路径
- getResource("/")返回当前的classpath根据路径
- path不能以/开始,path是从classpath根开始算的,因为classloader不是用户自定义的类,所以没有相对路径的配置文件可以获取,所以默认都是从哪个classpath路径下读取,自然就没有必要以/开头了。
- URLClass.getClassLoader().getResource(Stringpath)
- InputStreamClass.getClassLoader().getResourceAsStream(Stringpath)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。