springboot 프로젝트를 개발하다 보면, 코드를 약간만 변경하고 다시 시작해야 하는 경우를 다들 경험해 보셨을 텐데요, 그게 굉장히 귀찮습니다
인터넷에는 일반적으로 두 가지 방법이 소개되어 있습니다. spring-boot-devtools 또는 <code>JRebel
플러그인을 통해 "핫 배포"를 구현합니다 spring-boot-devtools
,或者通过JRebel
插件 来实现"热部署"
热部署就是当应用正在运行时,修改应用不需要重启应用。
其中 spring-boot-devtools
其实是自动重启,主要是节省了我们手动点击重启的时间,不算真正意义上的热部署。JRebel插件啥都好,就是需要收费
但如果平时我们在调试debug的情况下,只是在方法块内代码修改了一下,我们还得重启项目,就很浪费时间。这个时候我们其实可以直接build ,不重启项目,即可 实现热部署。
我们先来写一个例子演示一下:
@RestController public class TestController { @RequestMapping(value = "/test",method = {RequestMethod.GET, RequestMethod.POST}) public void testclass() { String name = "zj"; int weight = 100; System.out.println("name:"+ name); System.out.println("weight: "+weight); } }
结果:
name:zj weight: 100
修改代码,然后直接build项目,不重启项目,我们再请求这个测试接口:
String name = "ming"; int weight = 300;
神奇的一幕出现了,结果为:
name:ming weight: 300
当我们修改.java文件,只需重新生成对应的.class文件,就能影响到程序运行结果, 无需重启,Why? 背后JVM的操作原理且看本文娓娓道来。
首先我们得先了解一下 什么是.class文件
举个简单的例子,创建一个Person类:
public class Person { /** * 状态 or 属性 */ String name;//姓名 String sex;//性别 int height;//身高 int weight;//体重 /** * 行为 */ public void sleep(){ System.out.println(this.name+"--"+ "睡觉"); } public void eat(){ System.out.println("吃饭"); } public void Dance(){ System.out.println("跳舞"); } }
我们执行javac命令,生成Person.class文件
然后我们通过vim 16进制
그중 spring-boot-devtools
는 실제로 자동 재시작으로, 주로 수동으로 클릭하여 재시작하는 시간을 절약해주며 진정한 핫 배포는 아닙니다. JRebel 플러그인은 모두 좋지만 충전이 필요합니다. 하지만 디버깅 중에 메소드 블록의 코드만 수정하면 프로젝트를 다시 시작해야 하는데 이는 시간 낭비입니다. 이때 실제로 프로젝트를 다시 시작하지 않고 직접 빌드하여 핫 배포를 달성할 수 있습니다.
먼저 시연할 예제를 작성해 보겠습니다.
#打开file文件 vim Person.class #在命令模式下输入.. 以16进制显示 :%!xxd #在命令模式下输入.. 切换回默认显示 :%!xxd -r
Result:
코드를 수정한 다음 프로젝트를 다시 시작하지 않고 직접 프로젝트를 빌드하면 요청하겠습니다. 이 테스트를 다시 인터페이스:name:zj Weight: 100
Person zhang = new Person();
name:ming Weight: 300.java 파일을 수정할 때 해당 .java 파일만 재생성하면 됩니다. class 파일입니다. 프로그램 실행 결과에 영향을 미치며 다시 시작할 필요가 없습니다. 이 기사를 읽고 그 뒤에 있는 작동 원리를 설명하겠습니다.
static int value = 3;//类变量 初始化,设为默认值 0,不是 3哦 !!! int num = 4;//类成员变量,在这个阶段不初始化;在 new类,调用对应类的构造函数才进行初始化 final static valFin = 5;//这个比较特殊,在这个阶段也不会分配内存!!!javac 명령을 실행하여 Person.class 파일을 생성합니다
vim 16진수
를 전달합니다. Open itpublic class TestClassLoader { public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader = TestClassLoader.class.getClassLoader(); System.out.println(classLoader); System.out.println(classLoader.getParent());//获取其父类加载器 System.out.println(classLoader.getParent().getParent());//获取父类的父类加载器 } }
운영체제와 CPU마다 명령어 세트가 다릅니다. JAVA는 플랫폼 독립적일 수 있으며 Java 가상 머신에 의존합니다. .java 소스 코드는 사람이 읽기 위한 것이고, .class 바이트코드는 JVM 가상 머신이 읽기 위한 것입니다. 컴퓨터는 0과 1로 구성된 바이너리 파일만 인식할 수 있으므로 가상 머신은 우리가 작성하는 코드 사이의 다리 역할을 합니다. 그리고 컴퓨터.
이전 글에서 우리는 Java가 객체를 생성하는 방법과 클래스, super 및 static 키워드에 대해 이야기했습니다. 작성은 간단했지만 JVM 내부 구현 과정은 복잡합니다.
하드 디스크의 지정된 위치에 있는 Person.class 파일을 메모리에 로드합니다.
메인 메소드를 실행할 때, 스택 메모리 메인 메소드의 공간을 오픈(push-push)한 후, 메인 메소드의 스택 영역에 zhang 변수를 할당한다.
new를 실행하고 힙 메모리에 엔터티 클래스를 위한 공간을 열고 메모리 첫 번째 주소 값을 할당합니다.초기화를 위해 엔터티 클래스에 해당하는 생성자를 호출합니다. (생성자가 없으면 Java는 이를 기본 생성자로 구성합니다).
엔터티 클래스의 첫 번째 주소를 zhang에 할당하고 변수 zhang은 해당 엔터티를 참조합니다. (객체를 가리키며)
클래스 로딩 과정
위 그림의 1단계 클래스 로더(클래스 로더) 클래스 파일을 메모리에 로딩하는 과정은 로딩, 연결, 초기화 3단계로 나뉜다
라이프사이클 클래스는 일반적으로 아래와 같이 7단계로 이루어지며, 1~5단계는 클래스 로딩 과정이며, 검증, 준비, 파싱을 총칭하여 클래스의 라이프사이클을 1. 로딩 단계(Loading Phase)라고 합니다. 클래스에 해당하는 .class 파일의 바이너리 바이트 스트림을 메모리로 변환하고, 이 바이트 스트림을 메소드 영역에서 런타임 데이터 구조로 변환한 다음, 이에 대한 액세스 지점인 힙 영역 .Class 객체에 java.lang을 생성합니다. 메소드 영역의 데이터
🎜 클래스 로딩의 다른 단계와 비교할 때 로딩 단계(정확히 말하면 로딩 단계에서 클래스의 바이너리 바이트 스트림을 얻는 동작)는 제어 단계에서 할 수 있는 가장 효과적인 방법입니다. 개발자는 시스템에서 제공하는 클래스 로더를 사용하여 로딩을 완료하거나 클래스 로더를 사용자 정의하여 로딩을 완료할 수 있기 때문입니다. 이에 대해서는 나중에 기사🎜🎜🎜2에서 자세히 설명하겠습니다. 검증🎜🎜🎜검증 단계: 바이트코드 파일의 정확성을 검증합니다. 이 단계의 목적은 클래스 파일의 바이트 스트림에 포함된 정보가 현재 가상 머신의 요구 사항을 충족하고 가상 머신 자체의 보안을 위협하지 않는지 확인하는 것입니다. 🎜🎜이 부분은 개발자가 개입할 수 없습니다. 다음 내용을 이해하시면 됩니다. 🎜🎜검증 단계에서는 대략 4단계의 검사 작업이 완료됩니다. 🎜文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE
开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
3.准备
准备阶段:为类变量(static 修饰的变量)分配内存,并将其初始化为默认值
注意此阶段仅仅是为类变量 即静态变量分配内存,并将其初始化为默认值
举个例子,在这个准备阶段:
static int value = 3;//类变量 初始化,设为默认值 0,不是 3哦 !!! int num = 4;//类成员变量,在这个阶段不初始化;在 new类,调用对应类的构造函数才进行初始化 final static valFin = 5;//这个比较特殊,在这个阶段也不会分配内存!!!
注意: valFin
是被final static修饰的常量
在 **编译 **的时候已分配好了,所以在准备阶段 此时的值为5,所以在这个阶段也不会初始化!
4.解析
解析阶段:是虚拟机将常量池内的符号引用
替换为直接引用
的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
这个阶段了解一下即可
5.初始化
直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。
初始化阶段 是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段
用户应用程序可以通过自定义类加载器
的方式局部参与外,其余动作都完全由Java虚拟机来主导控 制。
Java程序对类的使用方式可分为两种:主动使用
与被动使用
。一般来说只有当对类的首次主动使用的时候才会导致类的初始化,所以主动使用又叫做类加载过程中“初始化”开始的时机。
类实例初始化方式,主要是以下几种:
1、创建类的实例,也就是new的方式
2、访问某个类或接口的静态变量,或者对该静态变量赋值
3、调用类的静态方法
4、反射(如Class.forName("com.test.Person")
)
5、初始化某个类的子类,则其父类也会被初始化
6、Java虚拟机启动时被标明为启动类的类(JavaTest),还有就是Main方法的类会 首先被初始化
这边就不展开说了,大家记住即可
6.使用
当JVM完成初始化阶段之后,JVM便开始从入口方法开始执行用户的程序代码
7.卸载
当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象,最后负责运行的JVM也退出内存
在如下几种情况下,Java虚拟机将结束生命周期
执行了System.exit()方法
程序正常执行结束
程序在执行过程中遇到了异常或错误而异常终止
由于操作系统出现错误而导致Java虚拟机进程终止
上文类加载过程中,是需要类加载器的参与,类加载器在Java中非常重要,它使得 Java 类可以被动态加载到 Java 虚拟机中并执行
那什么是类加载器?通过一个类的全限定名来获取描述此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例
Java虚拟机支持类加载器的种类:主要包括3中:引导类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用类加载器(系统类加载器,AppClassLoader),另外我们还可以自定义加载器-用户自定义类加载器
引导类加载器(Bootstrap ClassLoader):BootStrapClassLoader
是由c++实现的。引导类加载器加载java运行过程中的核心类库JRE\lib\rt.jar,sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar
以及存放 在JRE\classes
里的类,也就是JDK提供的类等常见的比如:Object、Stirng、List
等
扩展类加载器(Extension ClassLoader):它用来加载/jre/lib/ext
目录以及java.ext.dirs
系统变量指定的类路径下的类。
应用类加载器(AppClassLoader):它主要加载应用程序ClassPath下的类(包含jar包中的类)。它是java应用程序默认的类加载器。其实就是加载我们一般开发使用的类
用户自定义类加载器:用户根据自定义需求,自由的定制加载的逻辑,只需继承应用类加载器AppClassLoader,负责加载用户自定义路径下的class字节码文件
线程上下文类加载器:除了以上列举的三种类加载器,其实还有一种比较特殊的类型就是线程上下文类加载器
。ThreadContextClassLoader可以是上述类加载器的任意一种,这个我们下文再细说
我们来看一个例子:
public class TestClassLoader { public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader = TestClassLoader.class.getClassLoader(); System.out.println(classLoader); System.out.println(classLoader.getParent());//获取其父类加载器 System.out.println(classLoader.getParent().getParent());//获取父类的父类加载器 } }
结果:
sun.misc.Launcher
ExtClassLoader@5caf905d null
结果显示分别打印应用类加载器、扩展类加载器和引导类加载器
由于 引导类加载器 是由c++实现的,所以并不存在一个Java的类,因此会打印出null
我们还可以看到结果里面打印了 sun.misc.Launcher
,这个是什么东东?
其实Launcher是JRE中用于启动程序入口main()的类,我们看下Launcher的源码:
public class Launcher { private static Launcher launcher = new Launcher(); private static String bootClassPath = System.getProperty("sun.boot.class.path"); public static Launcher getLauncher() { return launcher; } private ClassLoader loader; public Launcher() { // Create the extension class loader ClassLoader extcl; try { extcl = ExtClassLoader.getExtClassLoader(); //加载扩展类类加载器 } catch (IOException e) { throw new InternalError( "Could not create extension class loader", e); } // Now create the class loader to use to launch the application try { loader = AppClassLoader.getAppClassLoader(extcl);//加载应用程序类加载器,并设置parent为extClassLoader } catch (IOException e) { throw new InternalError( "Could not create application class loader", e); } Thread.currentThread().setContextClassLoader(loader); //设置AppClassLoader为线程上下文类加载器 } /* * Returns the class loader used to launch the main application. */ public ClassLoader getClassLoader() { return loader; } /* * The class loader used for loading installed extensions. */ static class ExtClassLoader extends URLClassLoader {} /** * The class loader used for loading from java.class.path. * runs in a restricted security context. */ static class AppClassLoader extends URLClassLoader {}
其中loader = AppClassLoader.getAppClassLoader(extcl);
的核心方法源码如下:
private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent;//设置parent if (ParallelLoaders.isRegistered(this.getClass())) { parallelLockMap = new ConcurrentHashMap<>(); package2certs = new ConcurrentHashMap<>(); assertionLock = new Object(); } else { // no finer-grained lock; lock on the classloader instance parallelLockMap = null; package2certs = new Hashtable<>(); assertionLock = this; } }
通过以上源码我们可以知晓:
Launcher的ClassLoader
是BootstrapClassLoader
,在Launcher创建的同时,还会同时创建ExtClassLoader,AppClassLoader(并设置其parent为extClassLoader)。其中代码中 "sun.boot.class.path"是BootstrapClassLoader
加载的jar包路径。
这几种类加载器 都遵循 双亲委派机制
双亲委派机制说的其实就是,当一个类加载器收到一个类加载请求时,会去判断有没有加载过,如果加载过直接返回,否则该类加载器会把请求先委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。
双亲委派模式优势:
避免类的重复加载, 当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次, 这样保证了每个类只被加载一次。
保护程序安全,防止核心API被随意篡改,比如 java核心api中定义类型不会被随意替换
我们这里看一个例子:
我们新建一个自己的类“String”放在src/java/lang目录下
public class String { static { System.out.println("自定义 String类"); } }
新建StringTest类:
public class StringTest { public static void main(String[] args) { String str=new java.lang.String(); System.out.println("start test-------"); } }
结果:
start test-------
可以看出,程序并没有运行我们自定义的“String”类,而是直接返回了String.class。像String,Integer等类 是JAVA中的核心类,是不允许随意篡改的!
ClassLoader
是一个抽象类,负责加载类,像 ExtClassLoader,AppClassLoader
都是由该类派生出来,实现不同的类装载机制。这块的源码太多了,就不贴了
我们来看下 它的核心方法loadClass()
,传入需要加载的类名,它会帮你加载:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 一开始先 检查是否已经加载该类 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // 如果未加载过类,则遵循 双亲委派机制,来加载类 if (parent != null) { c = parent.loadClass(name, false); } else { //如果父类是null就是BootstrapClassLoader,使用 启动类类加载器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { long t1 = System.nanoTime(); // 如果还是没有加载成功,调用findClass(),让当前类加载器加载 c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } // 继承的子类得重写该方法 protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
loadClass()源码 展示了,一般加载.class文件大致流程:
先去缓存中 检查是否已经加载该类,有就直接返回,避免重复加载;没有就下一步
遵循 双亲委派机制,来加载.class文件
上面两步都失败了,调用findClass()方法,让当前类加载器加载
注意:由于ClassLoader
类是抽象类,而抽象类是无法通过new创建对象的,所以它最核心的findClass()
方法,没有具体实现,只抛了一个异常,而且是protected的,这是应用了模板方法模式
,具体的findClass()方法丢给子类实现, 所以继承的子类得重写该方法。
那我们仿照 ExtClassLoader,AppClassLoader
来实现一个自定义的类加载器,我们同样是继承ClassLoader
类
编写一个测试类TestPerson
public class TestPerson { String name = "xiao ming"; public void print(){ System.out.println("hello my name is: "+ name); } }
接着 编写一个自定义类加载器MyTestClassLoader:
public class MyTestClassLoader extends ClassLoader { final String classNameSpecify = "TestPerson"; public MyTestClassLoader() { } public MyTestClassLoader(ClassLoader parent) { super(parent); } protected Class<?> findClass(String name) throws ClassNotFoundException { File file = getClassFile(name); try { byte[] bytes = getClassBytes(file); Class<?> c = this.defineClass(name, bytes, 0, bytes.length); return c; } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); } private File getClassFile(String name) { File file = new File("D:\\ideaProjects\\src\\main\\java\\com\\zj\\ideaprojects\\test2\\"+ classNameSpecify+ ".class"); return file; } private byte[] getClassBytes(File file) throws Exception { // 这里要读入.class的字节,因此要使用字节流 FileInputStream fis = new FileInputStream(file); FileChannel fc = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel wbc = Channels.newChannel(baos); ByteBuffer by = ByteBuffer.allocate(1024); while (true) { int i = fc.read(by); if (i == 0 || i == -1) break; by.flip(); wbc.write(by); by.clear(); } fis.close(); return baos.toByteArray(); } //我们这边要打破双亲委派模型,重写整个loadClass方法 @Override public Class<?> loadClass(String name) throws ClassNotFoundException { Class<?> c = findLoadedClass(name); if (c == null && name.contains(classNameSpecify)){//指定的类,不走双亲委派机制,自定义加载 c = findClass(name); if (c != null){ return c; } } return super.loadClass(name); } }
最后在编写一个测试controller:
@RestController public class TestClassController { @RequestMapping(value = "testClass",method = {RequestMethod.GET, RequestMethod.POST}) public void testClassLoader() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { MyTestClassLoader myTestClassLoader = new MyTestClassLoader(); Class<?> c1 = Class.forName("com.zj.ideaprojects.test2.TestPerson", true, myTestClassLoader); Object obj = c1.newInstance(); System.out.println("当前类加载器:"+obj.getClass().getClassLoader()); obj.getClass().getMethod("print").invoke(obj); } }
先找到TestPerson所在的目录, 执行命令:javac TestPerson
,生成TestPerson.class
这里没有使用idea的build,是因为我们代码的class读取路径 是写死了的,不走默认CLASSPATH
D:\ideaProjects\src\main\java\com\zj\ideaprojects\test2\TestPerson.class
我们然后用postman调用testClassLoader()测试接口
结果:
当前类加载器:com.zj.ideaprojects.test2.MyTestClassLoader@1d75e392
hello my name is: xiao ming
然后修改TestPerson,将name 改为 “xiao niu”
public class TestPerson { String name = "xiao niu"; public void print(){ System.out.println("hello my name is: "+ name); } }
然后在当前目录 重新编译, 执行命令:javac TestPerson
,会在当前目录重新生成TestPerson.class 不重启项目,直接用postman 直接调这个测试接口 结果:
当前类加载器:com.zj.ideaprojects.test2.MyTestClassLoader@7091bd27
hello my name is: xiao niu
这样就实现了“热部署”!!!
如果不打破的话,结果 当前类加载器会显示"sun.misc.Launcher$AppClassLoader",原因是由于idea启动项目的时候会自动帮我们编译,将class放到 CLASSPATH路径下。其实可以把默认路径下的.class删除也行。这里也是为了展示如何打破双亲委派机制,才如此实现的。
官方推荐我们自定义类加载器时,遵循双亲委派机制。但是凡事得看实际需求嘛
通过上面的例子我们可以看出:
1、如果不想打破双亲委派机制,我们自定义类加载器,那么只需要重写findClass方法即可
2、如果想打破双亲委派机制,我们自定义类加载器,那么还得重写整个loadClass方法
如果你阅读到这里,你会发现双亲委派机制的各种好处,但万物都不是绝对正确的,我们需要一分为二地看待问题。
在某些场景下双亲委派制过于局限,所以有时候必须打破双亲委派机制来达到目的。比如 :SPI机制、线程上下文类加载器
1.SPI(Service Provider Interface)服务提供接口。它是jdk内置的一种服务发现机制,将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 让服务定义与实现分离、解耦。
SPI机制图
2.线程上下文类加载器(context class loader)是可以破坏Java类加载委托机制,使程序可以逆向使用类加载器,使得java类加载体系显得更灵活。
Java 应用运行的初始线程的上下文类加载器是应用类加载器,在线程中运行的代码可以通过此类加载器来加载类和资源。Java.lang.Thread中的方法getContextClassLoader()和 setContextClassLoader(ClassLoader cl)
用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)
方法进行设置的话,线程将继承其父线程的上下文类加载器。
SPI机制在框架的设计上应用广泛,下面举几个常用的例子:
平时获取jdbc,我们可以这样:Connection connection =DriverManager.getConnection("jdbc://localhost:3306");
我们读DriverManager
的源码发现:其实就是查询classPath下,所有META-INF下给定Class名的文件,并将其内容返回,使用迭代器遍历,这里遍历的内部使用Class.forName
加载了类。
其中有一处非常重要 ServiceLoadera82cdc89bbf5ed1c4b3c294f3327aac0 loadedDrivers = ServiceLoader.load(Driver.class);
我们看下它的实现:
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader();//important ! return ServiceLoader.load(service, cl); }
我们可以看出JDBC,DriverManager
类和ServiceLoader
类都是属于核心库 rt.jar
的,它们的类加载器是Bootstrap ClassLoader类加载器。而具体的数据库驱动相关功能却是第三方提供的,第三方的类不能被引导类加载器(Bootstrap ClassLoader)加载。
所以java.util.ServiceLoader类进行动态装载时,使用了线程的上下文类加载器(ThreadContextClassLoader)让父级类加载器能通过调用子级类加载器来加载类,这打破了双亲委派机制。
Tomcat是web容器,我们把war包放到 tomcat 的webapp目录下,这意味着一个tomcat可以部署多个应用程序。
不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。防止出现一个应用中加载的类库会影响另一个应用的情况。如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。
Tomcat类加载器种类
如果Tomcat本身的依赖和Web应用还需要共享,Common类加载器(CommonClassLoader)来装载实现共享
Catalina类加载器(CatalinaClassLoader) 用来 隔绝Web应用程序与Tomcat本身的类
Shared类加载器(SharedClassLoader):如果WebAppClassLoader自身没有加载到某个类,那就委托SharedClassLoader去加载
WebAppClassLoader:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器WebAppClassLoader(多个应用程序,就有多个WebAppClassLoader),负责优先加载
本身的目录下的class文件,加载不到时再交给CommonClassLoader
以及上层的ClassLoader
进行加载,这破坏了双亲委派机制。
Jsp类加载器(JasperLoader):实现热部署的功能,修改文件不用重启就自动重新装载类库。JasperLoader
的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader
的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap
功能。
我们来模拟一下tomcat 多个版本代码共存:
这边的例子换了个电脑,所以目录结构、路径与上面的例子有点变化
我们先编写 App类
public class App { String name = "webapp 1"; public void print() { System.out.println("this is "+ name); } }
javac App生成的App.class 放入 tomcatTest\war1\com\zj\demotest\tomcatTest
目录下
将name改为webapp 2
,重新生成的App.class
放入 tomcatTest\war2\com\zj\demotest\tomcatTest
目录下
然后我们编写类加载器:
public class MyTomcatClassloader extends ClassLoader { private String classPath; public MyTomcatClassloader(String classPath) { this.classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { File file = getClassFile(name); try { byte[] bytes = getClassBytes(file); Class<?> c = this.defineClass(name, bytes, 0, bytes.length); return c; } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); } private File getClassFile(String name) { name = name.replaceAll("\\.", "/"); File file = new File(classPath+ "/"+ name + ".class");//拼接路径,找到class文件 return file; } private byte[] getClassBytes(File file) throws Exception { // 这里要读入.class的字节,因此要使用字节流 FileInputStream fis = new FileInputStream(file); FileChannel fc = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel wbc = Channels.newChannel(baos); ByteBuffer by = ByteBuffer.allocate(1024); while (true) { int i = fc.read(by); if (i == 0 || i == -1) { break; } by.flip(); wbc.write(by); by.clear(); } fis.close(); return baos.toByteArray(); } //我们这边要打破双亲委派模型,重写整个loadClass方法 @Override public Class<?> loadClass(String name) throws ClassNotFoundException { Class<?> c = findLoadedClass(name); if (c == null && name.contains("tomcatTest")){//指定的目录下的类,不走双亲委派机制,自定义加载 c = findClass(name); if (c != null){ return c; } } return super.loadClass(name); } }
最后编写测试controller:
@RestController public class TestController { @RequestMapping(value = "/testTomcat",method = {RequestMethod.GET, RequestMethod.POST}) public void testclass() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { MyTomcatClassloader myTomcatClassloader = new MyTomcatClassloader("D:\\GiteeProjects\\study-java\\demo-test\\src\\main\\java\\com\\zj\\demotest\\tomcatTest\\war1"); Class cl = myTomcatClassloader.loadClass("com.zj.demotest.tomcatTest.App"); Object obj = cl.newInstance(); System.out.println("当前类加载器:"+obj.getClass().getClassLoader()); obj.getClass().getMethod("print").invoke(obj); MyTomcatClassloader myTomcatClassloader22 = new MyTomcatClassloader("D:\\GiteeProjects\\study-java\\demo-test\\src\\main\\java\\com\\zj\\demotest\\tomcatTest\\war2"); Class cl22 = myTomcatClassloader22.loadClass("com.zj.demotest.tomcatTest.App"); Object obj22 = cl22.newInstance(); System.out.println("当前类加载器:"+obj22.getClass().getClassLoader()); obj22.getClass().getMethod("print").invoke(obj22); } }
然后postman 调一下这个接口, 结果:
当前类加载器:com.zj.demotest.tomcatTest.MyTomcatClassloader@18fbb876
this is webapp 1
当前类加载器:com.zj.demotest.tomcatTest.MyTomcatClassloader@5f7ed4a9
this is webapp 2
我们发现2个同样的类能共存在同一个JVM中,互不影响。
注意:同一个JVM内,2个相同的包名和类名的对象是可以共存的,前提是他们的类加载器不一样。所以我们要判断多个类对象是否是同一个,除了要看包名和类名相同,还得注意他们的类加载器是否一致
springboot自动配置的原因是因为使用了@EnableAutoConfiguration
注解。
当程序包含了EnableAutoConfiguration
注解,那么就会执行下面的方法,然后会加载所有spring.factories
文件,将其内容封装成一个map,spring.factories
其实就是一个名字特殊的properties文件。
在spring-boot应用启动时,会调用loadFactoryNames方法,其中传递的一个参数就是:org.springframework.boot.autoconfigure.EnableAutoConfiguration
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct."); return configurations; }
META-INF/spring.factories会被读取到。
它还使用了this.getBeanClassLoader() 获取类加载器。所以我们立刻明白了文章一开始的例子,SpringBoot项目直接build项目,不重启项目,就能实现热部署效果。
위 내용은 Java 클래스 로더 및 상위 위임 메커니즘을 적용하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!