初学类加载器
java之ClassLoader
1. 类的加载过程
我们编写的“.java”文件需要经过java编译器编译成“.class”文件,当程序运行时,如果需要用到某个
类,jvm会去加载它,并在内存中创建对应的class对象,这个过程被称为类的加载。
简单来说:将class文件读入内存,并为之创建一个Class对象,
既然是jvm自动去完成的,那么为什么学习类加载器,在什么工作场景中可以用到?
插件功能,比如:
需求:解析Excel,一般用poi就可以搞定,但是目前Excel模板不确定,而且用户不希望每次增加一新
的模板都需要重启应用,这时候应该怎么办?
解决:把解析Excel代码的抽出来,打成一个jar包,上传到应用中,每次用户上传Excel的时候,选择
对应的jar包去解析,因为需要动态的加载jar包,这时候就要用到类加载器把jar包加载到内存中,通
过反射执行指定的方法来获取结果。
总之如果碰到动态加载jar包、.class文件这种场景,就应该使用类加载器。
2. 类加载器
jvm通过类加载器把“.class”文件加载到内存中,java默认提供了3个类加载器,分别是:
-
- Bootstrap ClassLoader 启动类加载器
- ExtClassLoader 扩展类加载器
- AppClassLoader 系统类加载器
三个类加载器各有不同的作用。
1. Bootstrap ClassLoader
主要加载JDK8核心类库,用C++语言实现的,加载时的搜索路径是由 sun.boot.class.path 所指定的,比如:%JRE_HOME%\jre\lib下的rt.jar、resources.jar、charsets.jar等。
其中 rt.jar 里面就存放着我们经常用的java API
JDK8和11略有不同
public class Test07 {
public static void main(String[] args) throws ClassNotFoundException {
// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//AppClassLoader
// 获取系统类加载器的父类加载器 --> 扩展类加载器
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent);//PlatformClassLoader@7c30a502
// 获取扩展类加载器的父类加载器 --> 根加载器(C/C++)
ClassLoader parent1 = parent.getParent();
System.out.println(parent1);//bootstrapClassLoad
// 测试当前类的加载器
ClassLoader classLoader = Class.forName("Test").getClassLoader();
System.out.println(classLoader);//AppClassLoader
// 测试JDK内置的类加载器
classLoader = Class.forName("java.lang.Object").getClassLoader();
System.out.println(classLoader);//bootstrapClassLoad
// 如何获得系统类加载器可以加载的路径
System.out.println(System.getProperty("java.class.path"));
}
}
JDK11
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
jdk.internal.loader.ClassLoaders$PlatformClassLoader@7d907bac
null
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
null
D:\springcloud\es_kb\target\test-classes;D:\springcloud\es_kb\target\classes;D:\download\apache-maven-3.6.0\repo\org\springframework\boot\
JDK8
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null
sun.misc.Launcher$AppClassLoader@18b4aac2
null
D:\Java\jdk1.8.0\jre\lib\charsets.jar;
D:\Java\jdk1.8.0\jre\lib\deploy.jar;
D:\Java\jdk1.8.0\jre\lib\ext\access-bridge-64.jar;
D:\Java\jdk1.8.0\jre\lib\ext\cldrdata.jar;
D:\Java\jdk1.8.0\jre\lib\ext\dnsns.jar;
D:\Java\jdk1.8.0\jre\lib\ext\jaccess.jar;
D:\Java\jdk1.8.0\jre\lib\ext\jfxrt.jar;
D:\Java\jdk1.8.0\jre\lib\ext\localedata.jar;
D:\Java\jdk1.8.0\jre\lib\ext\nashorn.jar;
D:\Java\jdk1.8.0\jre\lib\ext\sunec.jar;
D:\Java\jdk1.8.0\jre\lib\ext\sunjce_provider.jar;
D:\Java\jdk1.8.0\jre\lib\ext\sunmscapi.jar;
D:\Java\jdk1.8.0\jre\lib\ext\sunpkcs11.jar;
D:\Java\jdk1.8.0\jre\lib\ext\zipfs.jar;
D:\Java\jdk1.8.0\jre\lib\javaws.jar;
D:\Java\jdk1.8.0\jre\lib\jce.jar;
D:\Java\jdk1.8.0\jre\lib\jfr.jar;
D:\Java\jdk1.8.0\jre\lib\jfxswt.jar;
D:\Java\jdk1.8.0\jre\lib\jsse.jar;
D:\Java\jdk1.8.0\jre\lib\management-agent.jar;
D:\Java\jdk1.8.0\jre\lib\plugin.jar;
D:\Java\jdk1.8.0\jre\lib\resources.jar;
D:\Java\jdk1.8.0\jre\lib\rt.jar;
D:\IDEAWorkspace\demo\anno;
D:\springcloud\es_kb\target\test-classes;D:\springcloud\es_kb\target\classes;D:\download\apache-maven-3.6.0\repo\org\springframework\boot\
为什么 String.class.getClassLoader() 返回null?
Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,并不是一个JAVA类,也就是
无法在java代码中获取它的引用,对Java不可见,所以返回null
可通过 -Xbootclasspath 配置其他目录。
-Xbootclasspath:路径 | 指定的路径会完全取代jdk核心的搜索路径 | 坚决不要用 |
---|---|---|
-Xbootclasspath/a:路径 | 指定的路径会在jdk核心类后搜索 | 可用 |
-Xbootclasspath/p:路径 | 指定的路径会在jdk核心类前搜索 | 可用,不建议使用 |
以上配置,如果有多个路径,unix下用“:”分割,windows下用“;”分割。
2. ExtClassLoader
扩展类加载器,加载-Djava.ext.dirs
选项指定的目录,比如:%JRE_HOME%\jre\lib**ext**目录下的jar包和class文件。
代码:
public static void main(String[] args) {
//DNSNameService 在dnsns.jar包下
System.out.println(DNSNameService.class.getClassLoader());
//输出 ExtClassLoader 扫描的路径,注意:输出的是文件夹
String paths = System.getProperty("java.ext.dirs");
String[] arr = paths.split(":");//如果有多个路径,unix下用“:”,windows下用“;”.
for(String s : arr){
System.out.println(s);
}
}
3. AppClassLoader
也称为SystemClassLoader(系统类加载器),它的搜索路径由 java.class.path(CLASSPATH) 来指
定,默认是当前文件夹(java命令在哪个目录执行,那么这个目录就是classpath的值),
主要用来加载 自己程序中的类 和 第三方依赖包
代码:
public class test {
public static void main(String[] args) {
System.out.println(test.class.getClassLoader());
System.out.println(test.class.getClassLoader().getParent());
String path = System.getProperty("java.class.path");
String[] split = path.split(";");
for (String s : split) {
System.out.println(s);
}
}
}
上图可以看到,输出中即包含JDK的核心类库,也包含扩展类库,这是因为:
执行java命令时候,指定了classpath参数,把这些jar包都包含了进去。
如何改变默认路径?
运行时指定jvm参数: -classpath /feng/test
如果有多个路径,unix下用“:”分割,windows下用“;”分割。
package com.classloader;
import org.apache.commons.io.ByteOrderMark;
import sun.net.spi.nameservice.dns.DNSNameService;
/**
* 类的加载:
* 我们编写的“.java”文件需要经过java编译器编译成“.class”文件,当程序运行时,
* 如果需要用到某个类,会去加载它,并在内存中创建对应的class对象,这个过程被称为 类的加载。
*
* 为什么学习类加载器,在什么工作场景中可以用到?
* 插件功能,
* 需求:解析Excel,一般用poi就可以,但是目前Excel模板不确定,而且用户不希望每次增加一种模板都重启应用。
* 解决:把解析Excel代码抽出来,打成一个jar包,上传到应用中,当用户每次上传Excel时,让他自己选择使用哪个jar包执行,
* 因为需要动态的加载jar包,这时候就要用到 类加载器,把jar包加载到内存中,通过反射去执行方法,拿到结果。
* 总之,如果碰到需要动态加载jar包、.class文件这种场景,就应该使用 类加载器。
*
* 三大类加载器:
* Bootstrap ClassLoader 启动类加载器
* ExtClassLoader 扩展类类加载
* AppClassLoader 系统类加载器,又叫:应用类加载器
*/
public class Demo {
public static void main(String[] args) {
/**
* 1. Bootstrap ClassLoader 启动类加载器
* 主要用于加载JDK核心类库,C++语言实现的,加载时的搜索路径是由 sun.boot.class.path 所指定的,
* 比如:%JRE_HOME%\jre\lib下的rt.jar、resources.jar、charsets.jar等。
*
* -Xbootclasspath:路径 指定的路径会完全取代jdk核心的搜索路径 坚决不要用
* -Xbootclasspath/a:路径 指定的路径会在jdk核心类后搜索 可用
* -Xbootclasspath/p:路径 指定的路径会在jdk核心类前搜索 可用,不建议使用
*/
// System.out.println(String.class.getClassLoader());//输出:null,就证明 Bootstrap ClassLoader 加载。
// String paths = System.getProperty("sun.boot.class.path");
// String[] arr = paths.split(";");
// for (String s : arr){
// System.out.println(s);
// }
/**
* 2. ExtClassLoader 扩展类类加载
* 扩展类加载器,加载 -Djava.ext.dirs 选项指定的目录,比如:%JRE_HOME%\jre\lib\ext目录下的jar包和class文件。
* 也可以通过配置 -Djava.ext.dirs 参数设置加载路径
*
* 注意:如果配置-Djava.ext.dirs会覆盖Java默认的ext设置。
*/
// System.out.println(DNSNameService.class.getClassLoader());//输出:null,就证明 Bootstrap ClassLoader 加载。
// String paths = System.getProperty("java.ext.dirs");
// String[] arr = paths.split(";");
// for (String s : arr){
// System.out.println(s);
// }
/**
* 3. AppClassLoader 系统类加载器,又叫:应用类加载器
* 它的搜索路径由java.class.path(CLASSPATH)来指定,
* 默认是当前文件夹(java命令在哪个目录执行,那么这个目录就是classpath的值)
* 主要用来加载应用程序中的类文件.
* AppClassLoader 的父亲类加载器是 ExtClassLoader.
*
*/
System.out.println(Demo.class.getClassLoader());
String paths = System.getProperty("java.class.path");
String[] arr = paths.split(";");
for (String s : arr){
System.out.println(s);
}
}
}
4. 类加载器的初始化
从上面可以看出 ExtClassLoader、AppClassLoader都是java对象,而这些对象的生成都是由
Launcher完成的。
Launcher类是java程序的入口,在启动java应用的时候会首先创建Launcher类的对象,创建
Launcher类的时候会创建ExtClassLoader、AppClassLoader,
Launcher的构造方法
public Launcher() {
Launcher.ExtClassLoader var1;
try {
// 创建 ExtClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//创建 AppClassLoader,指定父加载器是 ExtClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//设置当前线程的上下文类加载器是AppClassLoader
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
以上代码可以得出ExtClassLoader是AppClassLoader他爹。
3. 双亲委派模式
为了让上面3个类加载器能够合理的工作,因此有了双亲委派模式。
1. 概念
双亲委派模式:简称:坑爹模式,
即:
- 如果一个类加载器要加载某个类,它并不会自己先去加载,而是让父加载器去加载,
- 如果父加载器还有父亲,则进一步向上委托,依次递归,最终将推给顶层的启动类加载器(Bootstrap ClassLoader)。
- 如果父加载器可以完成加载任务,就成功返回,倘若父加载器无法完成此加载任务,子加载器才会尝试自己去加载。
看下图:
这里要注意:Bootstrap ClassLoader并不是ExtClassLoader的父亲,但可以它看作类加载器届的上帝,所有类的
加载都需要先经过Bootstrap ClassLoader,如果加载失败,才自己去加载
2. 源码
加载一个类,最终会走到ClassLoader类中的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) {
// 父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
// 父加载器为空则调用Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
3. 好处
-
避免类的重复加载
-
保证java核心api不会被随意替换
-
- 假设自定义一个名为java.lang.Integer的类,通过双亲委派模式传递到启动类加载器,
- 启动类加载器在Java 核心类库,就能找到 Integer 类,于是加载,
- 不会重加载自定义的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改
举例说明:
package java.lang;
public class Integer {
public static void test(){
System.out.println("test");
}
public static void main(String[] args) {
test();
}
}
输出:
因为加载得是JDK中的Integer类,所以提示找不到main方法。
4. 另一个规则
类的加载还有一个规则:A中使用了B,如果A被AppClassLoader加载,那么B也被 AppClassLoader 加载
但需要注意的是:在加载的时候遵循 坑爹模式。
4. 动态加载
1. URLClassLoader
URLClassLoader是ClassLoader的子类,也是ExtClassLoader和AppClassLoader的父类
它可以从指定的 jar 文件和目录中加载类和资源。也就是说,可以动态加载jar包中的类。
常用的构造方法:
- URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父加载器创建对象,从指定的urls路径来查询、并加载类。(到实战,解析Excel的时候,什么场景下使用这个构造方法)
- URLClassLoader(URL[] urls):使用默认的父加载器(AppClassLoader)创建一个ClassLoader对象,从指定的urls路径来查询、并加载类。
2. 实战
新建一个普通的maven项目,
建立完成后,应该是:
建立两个model:parse、loader
在 parse 中新建一个类 ParseExcel
public class ParseExcel {
public void parse(){
System.out.println("执行解析Excel方法...........");
}
}
然后打成jar包:parse.jar。
在 loader 中,新建一个 Test 类,利用 URLClassLoader 运行这个jar包:
public static void main(String[] args) throws Exception {
// 创建一个URL数组
File file = new File("/Users/fengxiansheng/Downloads/parse.jar");
URL[] urls = new URL[]{file.toURI().toURL()};
//这时候 myClassLoader 的 parent 是 AppClassLoader
URLClassLoader myClassLoader = new URLClassLoader(urls);
Class<?> aClass = myClassLoader.loadClass("com.test.ParseExcel");
Object obj = aClass.newInstance();//利用反射创建对象
Method method = aClass.getMethod("parse");//获取parse方法
method.invoke(obj,null);
}
输出:
这样做的好处:
- 修改 parse 不会影响到 loader,只需要把 parse 这个模块打成jar,放在固定的地方
- 这样 loader 可以24小时不间断运行
注意:这时候 myClassLoader 的 parent 是 AppClassLoader,这点非常重要
3. 问题
有些时候,parse中可能引用一些第三方库,比如:jackson-core-2.11.0.jar
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.11.0</version>
</dependency>
并在代码中使用:
package com;
import com.fasterxml.jackson.core.JsonFactory;
public class ParseExcel {
public void parse(){
System.out.println("执行解析Excel方法........");
JsonFactory factory = new JsonFactory();
System.out.println("执行jsonFaction的getFormatGeneratorFeatures方法:"+factory.getFormatGeneratorFeatures());
}
}
这时候,在 loader 中运行这个jar:
public static void main(String[] args) throws Exception {
File file = new File("/Users/fengxiansheng/Downloads/parse.jar");
File file2 = new File("/Users/fengxiansheng/Downloads/jackson-core-2.11.0.jar");
URL[] urls = new URL[]{file.toURI().toURL(),file2.toURI().toURL()};
//这时候 myClassLoader 的 parent 是 AppClassLoader
URLClassLoader myClassLoader = new URLClassLoader(urls);
Class<?> aClass = myClassLoader.loadClass("com.test.ParseExcel");
Object obj = aClass.newInstance();
Method method = aClass.getMethod("parse");
method.invoke(obj,null);
}
这样运行完全没有问题。
但问题是,如果 loader 中也引用了jackson,不过版本是:2.5.4,这时候再次运行:
报错,找不到:getFormatGeneratorFeatures() 方法
原因,执行 :myClassLoader.loadClass("com.test.ParseExcel")
- myClassLoader 的 parent 是 AppClassLoader
- 根据坑爹模式,myClassLoader 加载 JsonFactory 之前,会先让 AppClassLoader 去加载
- 而 AppClassLoader 从自己的 classpath 找到了这个类,加载成功,不过版本是2.5.4
- 但是2.5.4的版本中并没有getFormatGeneratorFeatures方法,所以。。。。
解决:
修改myClassLoader的 parent 为ExtClassLoader,这就要用到 URLClassLoader 的另一个构造方法
加载过程:
-
myClassLoader 的父亲是 ExtClassLoader,所以这时候 myClassLoader 跟 appClassLoader 是兄弟关系
-
myClassLoader 在加载 ParseExcel 时,会加载 JsonFactory,同时遵循坑爹模式
-
所以 ExtClassLoader 会尝试加载 JsonFactory 这个类,不会加载成功,转而由 myClassLoader 自己去加载 JsonFactory,加载成功,版本是2.11
-
2.11 版本的 JsonFactory 有 getFormatGeneratorFeatures 方法,所以正常运行
4. 又一个问题
新建一个 loader-api 的model ,并创建一个接口:MyInterface
让 ParseExcel 实现 MyInterface 接口
public class ParseExcel implements MyInterface {
public void parse(){
System.out.println("执行解析Excel方法........");
JsonFactory factory = new JsonFactory();
System.out.println("执行jsonFaction的getFormatGeneratorFeatures方法:"+factory.getFormatGeneratorFeatures());
}
}
修改 main 方法,判断 ParseExcel 是否实现了MyInterface
public static void main(String[] args) throws Exception {
// 创建一个URL数组
File file = new File("/Users/fengxiansheng/Downloads/parse.jar");
File file2 = new File("/Users/fengxiansheng/Downloads/jackson-core-2.11.0.jar");
File file3 = new File("/Users/fengxiansheng/Downloads/loader-api.jar");
URL[] urls = new URL[]{file.toURI().toURL(),file2.toURI().toURL(),file3.toURI().toURL()};
URLClassLoader myClassLoader = new URLClassLoader(urls,Test.class.getClassLoader().getParent());
Class<?> aClass = myClassLoader.loadClass("com.test.ParseExcel");
//判断是否实现了MyInterface
if(!MyInterface.class.isAssignableFrom(aClass)){
System.out.println("该类没有实现 MyInterface 接口,不运行");
return;
}
Object obj = aClass.newInstance();
Method method = aClass.getMethod("parse");
method.invoke(obj,null);
}
输出:
为什么会有这样的输出呢?
java 中,所有的 class 文件都会被加载到 jvm 中的元数据区,而且一个class的唯一标识:类加载器+类名
因为 myClassLoader 跟 AppClassLoader 是兄弟关系
-
执行:myClassLoader.loadClass("com.test.ParseExcel");
-
- myClassLoader 会加载 ParseExcel,而 ParseExcel 引用了 MyInterface
- 所以 myClassLoader 会把 MyInterface,加载到内存中
-
执行:MyInterface.class.isAssignableFrom(aClass)
-
-
AppClassLoader 也会把 MyInterface,加载到内存中
所以此时,内存中有两个 MyInterface 的 class 对象
再看代码, ParseExcel 实现的是 myClassLoader.MyInterface 接口,所以。。。。。。
解决:把 myClassLoader 的 parent ,设置为 AppClassLoader,
但是会产生找不到:getFormatGeneratorFeatures() 方法
5. 自定义ClassLoader
这时候应该自定义类加载器,打破双亲委派模式
在 loader 中新增 CustomClassLoader
package com; import java.net.URL; import java.net.URLClassLoader; /** * 自定义类加载器,打破双亲委派模式 */ public class CustomClassLoader extends URLClassLoader { private URL[] urls;//扫描的jar包路径 public CustomClassLoader(URL[] urls) { //1. 指定搜索路径和父加载器:AppClassLoader super(urls, CustomClassLoader.class.getClassLoader()); this.urls = urls; } /** * 重载loadClass方法 * @param name * @return * @throws ClassNotFoundException */ @Override public Class<?> loadClass(String name) throws ClassNotFoundException { if (urls != null && urls.length>0) { Class<?> c = null; try { //2. 先在自己的扫描路径中找class c = findClass(name);//该方法存在于URLClassLoader中,如果加载不到指定类,会报ClassNotFoundException }catch (ClassNotFoundException e){ //3. 找不到就让 parent 去加载 c = this.getParent().loadClass(name); } return c; } return super.loadClass(name); } }
坑爹模式是:自己加载在class之前,先让 parent 加载
我们自定义的类加载器,先在自己的扫描路径中找,找不到会报:ClassNotFoundException
catch到这个异常,然后让 parent 去找
修改代码:
public static void main(String[] args) throws Exception { // 创建一个URL数组 File file = new File("/Users/fengxiansheng/Downloads/parse.jar"); File file2 = new File("/Users/fengxiansheng/Downloads/jackson-core-2.11.0.jar"); URL[] urls = new URL[]{file.toURI().toURL(),file2.toURI().toURL()}; CustomClassLoader myClassLoader = new CustomClassLoader(urls); Class<?> aClass = myClassLoader.loadClass("com.test.ParseExcel"); //判断是否实现了MyInterface if(!MyInterface.class.isAssignableFrom(aClass)){ System.out.println("该类没有实现 MyInterface 接口,不运行"); return; } Object obj = aClass.newInstance(); Method method = aClass.getMethod("parse"); method.invoke(obj,null); }
输出:
加载过程:
customClassLoader 父亲是 AppClassLoader
-
执行:myClassLoader.loadClass("com.test.ParseExcel");
-
- customClassLoader 在加载 ParseExcel 时,会加载 MyInterface,并拒绝遵循坑爹模式
- 先在自己的搜索路径中查找MyInterface,找不到(因为已经去掉了)
- 所以让它的 parent,也就是 AppClassLoader 去加载,加载成功
-
执行:MyInterface.class.isAssignableFrom(aClass)
-
- AppClassLoader尝试去加载MyInterFace接口,但是因为已经加载成功,所以不需要重复加载
- 所以 MyInterFace.class.isAssignableFrom(aClass) 返回 true
5. 总结
类加载器可以做什么
- 插件功能
- 热加载
- 解决类冲突,一个应用可以使用多个版本的依赖
- 隔离,同级别中不同的类加载器之间隔离,比如Tomcat容器,每个WebApp有自己的ClassLoader,加载每个WebApp的ClassPath路径上的类。
-
-
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律