Java反序列化基础篇-类加载器
2022-6-5 16:39:32 Author: www.freebuf.com(查看原文) 阅读量:4 收藏

0x01 前言

这篇文章/笔记的话,打算从类加载器,双亲委派到代码块的加载顺序这样来讲。最后才是加载字节码。

0x02 类加载器及双亲委派

  • 说类加载器有些师傅可能没听过,但是说 Java ClassLoader,相信大家耳熟能详。

1. 类加载器有什么用

  • 加载 Class 文件

以这段简单代码为例

Student student = new Student();

我们知道,Student 本身其实是一个抽象类,是通过 new 这个操作,将其实例化的,类加载器做的便是这个工作。

ClassLoader 的工作如图所示
image

加载器也分多种加载器,每个加载器负责不同的功能。

主要分为这四种加载器

  1. 虚拟机自带的加载器

  2. 启动类(根)加载器

  3. 扩展类加载器

  4. 应用程序加载器

2. 几种加载器

引导类加载器

引导类加载器(BootstrapClassLoader),底层原生代码是 C++ 语言编写,属于 JVM 一部分。

不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载核心 java 库(即 JVM 本身),存储在/jre/lib/rt.jar目录当中。(同时处于安全考虑,BootstrapClassLoader只加载包名为javajavaxsun等开头的类)。

扩展类加载器(ExtensionsClassLoader)

扩展类加载器(ExtensionsClassLoader),由sun.misc.Launcher$ExtClassLoader类实现,用来在/jre/lib/ext或者java.ext.dirs中指明的目录加载 java 的扩展库。Java 虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载 java 类。

App类加载器(AppClassLoader)

App类加载器/系统类加载器(AppClassLoader),由sun.misc.Launcher$AppClassLoader实现,一般通过通过(java.class.path或者Classpath环境变量)来加载 Java 类,也就是我们常说的 classpath 路径。通常我们是使用这个加载类来加载 Java 应用类,可以使用ClassLoader.getSystemClassLoader()来获取它。

3. 双亲委派机制

  • 在 Java 开发当中,双亲委派机制是从安全角度出发的。

我们这里以代码先来感受一下,双亲委派机制确实牛逼。

从报错的角度感受双亲委派机制

  • 尽量别尝试,看看就好了。要不然整个文件夹挺乱的,如果想上手尝试一下的话,我建议是新建一个项目,不要把其他的文件放一起。

新建一个 java.lang的文件夹,在其中新建 String.java的文件。

String.java

package java.lang;  
  
// 双亲委派的错误代码  
public class String {  
  
    public String toString(){  
        return "hello";  
 }  
  
    public static void main(String[] args) {  
        String s = new String();  
 s.toString();  
 }  
}

看着是不是没有问题,没有错误吧?
我们自己定义了一个java.lang的文件夹,并在文件夹中定义了 String.class,还定义了 String 这个类的 toString 方法。我们跑一下程序。(这里如果把 Stirng 类放到其他文件夹会直接报错,原因也是和下面一样的)

  • 结果居然报错了!而且非常离谱

image

我这不是已经定义了 main 方法吗??为什么还会报错,这里就提到双亲委派机制了,双亲委派机制是从安全角度出发的。

首先,我们要知道 Java 的类加载器是分很多层的,如图。

image

我们的类加载器在被调用时,也就是在 new class 的时候,它是以这么一个顺序去找的 BOOT ---> EXC ----> APP

如果 BOOT 当中没有,就去 EXC 里面找,如果 EXC 里面没有,就去 APP 里面找。

  • 所以我们之前报错的程序当中,定义的java.lang.String在 BOOT 当中是有的,所以我们自定义 String 时,会报错,如果要修改的话,是需要去 rt.jar 里面修改的,这里就不展开了。

从正确的角度感受双亲委派机制

前文提到我们新建的java.lang.String报错了,是因为我们定义的 String 和 BOOT 包下面的 String 冲突了,所以才会报错,我们这里定义一个 BOOT 和 EXC 都没有的对象试一试。

在其他的文件夹下,新建 Student.java

Student.java

package src.DynamicClassLoader;  
  
// 双亲委派的正确代码  
public class Student {  
  
    public String toString(){  
        return "Hello";  
 }  
  
    public static void main(String[] args) {  
        Student student = new Student();  
  
 System.out.println(student.getClass().getClassLoader());  
 System.out.println(student.toString());  
 }  
}

并把加载器打印出来

image
我们定义的 Student 类在 APP 加载器中找到了。

0x03 各场景下代码块加载顺序

  • 这里的代码块主要指的是这四种

    • 静态代码块:static{}

    • 构造代码块:{}

    • 无参构造器:ClassName()

    • 有参构造器:ClassName(String name)

场景一、实例化对象

这里有两个文件,分别介绍一下用途:

  • Person.java:一个普普通通的类,里面有静态代码块、构造代码块、无参构造器、有参构造器、静态成员变量、普通成员变量、静态方法。

  • Main.java:启动类

Person.java

package src.DynamicClassLoader;  
  
// 存放代码块  
public class Person {  
    public static int staticVar;  
 public int instanceVar;  
  
 static {  
        System.out.println("静态代码块");  
 }  
  
    {  
        System.out.println("构造代码块");  
 }  
  
    Person(){  
        System.out.println("无参构造器");  
 }  
    Person(int instanceVar){  
        System.out.println("有参构造器");  
 }  
  
    public static void staticAction(){  
        System.out.println("静态方法");  
 }  
}

Main.java

package src.DynamicClassLoader;  
  
// 代码块的启动器  
public class Main {  
    public static void main(String[] args) {  
        Person person = new Person();  
 }  
}

运行结果如图

image

  • 结论:

通过new关键字实例化的对象,先调用静态代码块,然后调用构造代码块,最后根据实例化方式不同,调用不同的构造器。

场景二、调用静态方法

直接调用类的静态方法

Person.java 不变,修改 Main.java 启动器即可。

Main.java

package src.DynamicClassLoader;  
  
// 代码块的启动器  
public class Main {  
    public static void main(String[] args) {  
        Person.staticAction();  
 }  
}

image

  • 结论:

不实例化对象直接调用静态方法,会先调用类中的静态代码块,然后调用静态方法

场景三、对类中的静态成员变量赋值

Main.java

package src.DynamicClassLoader;  
  
// 代码块的启动器  
public class Main {  
    public static void main(String[] args) {  
 		Person.staticVar = 1;  
 	}  
}

image

  • 结论:

在对静态成员变量赋值前,会调用静态代码块

场景四、使用 class 获取类

package src.DynamicClassLoader;  
  
// 代码块的启动器  
public class Main {  
    public static void main(String[] args) {  
 		Class c = Person.class;  
 	}  
}

// 空屁
  • 结论:

利用class关键字获取类,并不会加载类,也就是什么也不会输出。

场景五、使用 forName 获取类

  • 这里要抛出异常一下。

我们写三种forName的方法调用。
修改 Main.java

package src.DynamicClassLoader;  
  
// 代码块的启动器  
public class Main {  
    public static void main(String[] args) throws ClassNotFoundException{  
 		Class.forName("src.DynamicClassLoader.Person");
 	}  
}
// 静态代码块
package src.DynamicClassLoader;  
  
// 代码块的启动器  
public class Main {  
    public static void main(String[] args) throws ClassNotFoundException{   
 	Class.forName("src.DynamicClassLoader.Person", true, ClassLoader.getSystemClassLoader());  
 }  
}
// 静态代码块
package src.DynamicClassLoader;  
  
// 代码块的启动器  
public class Main {  
    public static void main(String[] args) throws ClassNotFoundException{   
 	Class.forName("src.DynamicClassLoader.Person", false, ClassLoader.getSystemClassLoader());
 }  
}
//没有输出
  • 结论:

Class.forName(className)Class.forName(className, true, ClassLoader.getSystemClassLoader())等价,这两个方法都会调用类中的静态代码块,如果将第二个参数设置为false,那么就不会调用静态代码块

场景六、使用 ClassLoader.loadClass() 获取类

Main.java

package com.xiinnn.i.test;

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("com.xiinnn.i.test.Person", false, ClassLoader.getSystemClassLoader());
    }
}
//没有输出
  • 结论:

ClassLoader.loadClass()方法不会进行类的初始化,当然,如果后面再使用newInstance()进行初始化,那么会和场景一、实例化对象一样的顺序加载对应的代码块。


文章来源: https://www.freebuf.com/articles/web/335267.html
如有侵权请联系:admin#unsafe.sh