【Java高级】类加载与反射暴破

发布于:2022-12-07 ⋅ 阅读:(473) ⋅ 点赞:(0)

💁 个人主页:黄小黄的博客主页
❤️ 支持我:👍 点赞 🌷 收藏 🤘关注
🎏 格言:All miracles start from sometime somewhere, make it right now.
本文来自专栏:JavaSE从入门到精通
在这里插入图片描述



1 类加载

1.1 动态加载与静态加载

通过上一节内容,我们了解到,反射机制是Java实现动态语言的关键,也就是通过反射实现类动态加载。

现在,我们需要了解两个概念:

  • 静态加载: 编译时加载相关的类,如果没有则报错,依赖性强;
  • 动态加载: 运行时加载需要的类,如果运行时没有使用到该类,即使不存在该类,也不报错,降低了依赖性。

是不是还是有些懵?不用慌,我们来看下面的例子(假设没有写好Cat类的代码):静态加载的方式

import java.util.Scanner;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * 演示静态加载
 */
public class ClassLoadTest {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入1 or 2: ");
        String key = scanner.next();
        switch (key){
            case "1":
                Cat cat = new Cat();
                cat.hi();
                break;
            case "2":
                System.out.println("程序运行结束!");
                break;
            default:
                System.out.println("输入有误!");
                break;
        }
    }
}

分析上述代码可知:如果用户键入的值是2或者是其他,则压根使用不到Cat类,但是,该方式是静态加载的,所以,程序运行直接报错,无论是否使用到该类。

在这里插入图片描述
接着,我们尝试使用 动态加载的方式 改进程序:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Scanner;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * 演示动态加载
 */
public class ClassLoadTest {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入1 or 2: ");
        String key = scanner.next();
        switch (key){
            case "1":
                //动态加载
                Class<?> cls = Class.forName("reflection.Cat");
                Object cat = cls.newInstance();
                Method hi = cls.getMethod("hi");
                hi.invoke(cat);
                break;
            case "2":
                System.out.println("程序运行结束!");
                break;
            default:
                System.out.println("输入有误!");
                break;
        }
    }
}

当用户输入2 或者其他的时候,程序可以正常运行,动态加载降低了依赖性!
在这里插入图片描述

1.2 类加载时机与类加载流程

类何时被加载?

  1. new时,即创建对象时。(静态加载)
  2. 当子类被加载时,父类也被加载。(静态加载)
  3. 调用类中的静态成员时。(静态加载)
  4. 通过反射。(动态加载)

类加载的流程
回到上一节我们看过的图:
类加载是通过类加载器实现的,不仅会生成Class类对象,还会在方法区生成对应类的字节码二进制数据/元数据。
在这里插入图片描述
这次,我们展开讲讲类加载究竟经历了哪些过程,看下图:类加载过程图

在这里插入图片描述

  1. 在Loading加载阶段,将类的class文件读入内存,并为之创建一个java.lang.Class对象,此过程由类加载器完成;
  2. 在Linking链接阶段,将类的二进制数据合并到了JRE中,实际上是进入了一个可运行的状态;
  3. 而在初始化阶段,JVM负责对类进行初始化,这里主要指静态成员。

类加载后,内存布局情况如下:
在这里插入图片描述

1.3 类加载的五个阶段

在这里插入图片描述

1.3.1 加载阶段

JVM在该阶段主要目的是将字节码从不同的数据源(可能是class文件、也可能是jar包,甚至网络)转化为二进制字节流加载到内存中, 并生成一个代表该类的java.lang.Class对象。

1.3.2 链接阶段(1)验证阶段

验证阶段的目的是为了 确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

包括:文件格式验证、元数据验证、字节码验证、符号引用验证。

可以考虑使用 -Xverify:one 参数来关闭大部分类验证措施,缩短虚拟机类加载的时间。

1.3.3 链接阶段(2)准备阶段

JVM 会在该阶段 对静态变量分配内存并进行默认初始化(对应数据类型默认初始值,如0、0L、null、false等)。这些变量所使用的内存都将在方法区中进行分配。

示例代码:

class T{
    public int n1 = 10;
    public static int n2 = 20;
    public static final int n3 = 30;
}
  1. n1是实例属性,不是静态变量,因此在准备阶段,不会分配内存;
  2. n2是静态变量,分配内存,n2初始化为0,而不是20。值分配成20是在初始化阶段做的事情;
  3. n3是static final修饰的,是常量,与静态变量不同,会一次性分配。(一旦赋值,就不变化),所以n3 = 30。

1.3.4 链接阶段(3)解析阶段

即虚拟机将常量池内的符号引用替换为直接引用的过程。

可以理解成程序的逻辑地址和内存的物理地址

1.3.5 初始化阶段

到了初始化阶段,才真正开始执行类中定义的 Java程序代码,此阶段是执行 < clinit >() 方法的过程。

何为 < clinit >() 方法?
< clinit >() 方法是由编译器 按语句在源文件中出现的顺序,依次自动收集类中所有 静态变量的赋值动作和静态代码块中的语句,并进行合并。

演示类加载的初始化阶段:

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * 演示类加载的初始化阶段
 */
public class ClassLoadTest {
    public static void main(String[] args){
        System.out.println(A.num);
    }
}

class A{
    static {
        System.out.println("A的静态代码块被执行");
        num = 500;
    }
    static int num = 100;
    public A(){
        System.out.println("A的构造器被执行");
    }
}

在这里插入图片描述
过程分析:

  1. 加载A类,并生成A的Class对象;
  2. 链接阶段:num = 0;
  3. 初始化阶段:依次自动收集静态变量的赋值动作和静态代码块中的语句,并合并, 等价于如下代码:
clinit(){
	System.out.println("A的静态代码块被执行");
    num = 500;
    num = 100;
}

补充知识: 虚拟机 会保证一个类的 < clinit >() 方法在多线程的环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 < clinit >() 方法,其他线程都需要阻塞等待,直到活动线程执行 < clinit >() 方法完毕。

保证了某个类在内存中只有一份 Class对象。


2 反射暴破

2.1 通过反射创建对象

有如下两种方式:

  1. 调用类中的public修饰的无参构造器;
  2. 调用类中的指定构造器。

Class类包含了相关的方法:

方法名 作用
newInstance 调用类中的无参构造器,获取相应类的对象
getConstructor(Class…clazz) 根据参数列表,获取相应的public构造器对象
getDecalaredConstructor(Class…clazz) 根据参数列表,获取对应的所有构造器对象

Constructor类相关方法:

方法名 作用
setAccessible 暴破
newInstance(Object…obj) 调用构造器

在上一节中,我们已经尝试过使用第一种方式:调用类中的public修饰的无参构造器 创建过对象。本文不再赘述。

调用类中指定构造器实例:

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * 演示使用特定构造器创建对象
 */
public class CreateInstance {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        //加载Class对象,并获取指定的构造器对象
        Class<?> cls = Class.forName("reflection.Cat");
        Constructor<?> strConstructor = cls.getConstructor(String.class);
        //创建Cat对象实例
        Object o = strConstructor.newInstance("猫咪");
        System.out.println(o);
    }
}

class Cat {
    public String name = "name属性";

    public Cat(){}

    public Cat(String name){
        this.name = name;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                '}';
    }
}

但是,当我们通过这样的方式获取私有构造器、属性或者方法对象时,再操作,则会报异常。
下面我们通过反射暴破的方式,来避开private限制

2.2 反射暴破访问私有构造器

        //加载Class对象,并获取指定的构造器对象
        Class<?> cls = Class.forName("reflection.Cat");
        //得到private构造器对象
        Constructor<?> strConstructor = cls.getDeclaredConstructor(String.class);
        //创建Cat对象实例
        //暴力破解
        strConstructor.setAccessible(true);
        Object o = strConstructor.newInstance("猫咪");

2.3 反射暴破操作属性

反射暴破操作属性的步骤如下:

  1. 根据属性名获取Field对象
Field f = clazz对象.getDeclareField(属性名);
  1. 暴破
f.setAccessible(true);
  1. 访问
f.set(o,);
f.get(o);
  1. 如果是静态属性,则set和get中的参数o,可以写成null

实例如下: 具体见代码注释

import java.lang.reflect.Field;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * 演示反射操作属性
 */
public class AccessProperty {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        //1.得到Student类对应的对象
        Class<?> stuClass = Class.forName("reflection.Student");
        //2.创建对象
        Object o = stuClass.newInstance();
        //3.使用反射得到age属性对象
        Field age = stuClass.getField("age");
        age.set(o, 20);//通过反射设置属性
        System.out.println(o);
        //4.使用反射暴破操作私有静态属性name
        Field name = stuClass.getDeclaredField("name");
        name.setAccessible(true);
        name.set(o, "黄小黄");
        System.out.println(o);
    }
}

class Student{
    public int age;
    private static String name;

    public Student(){
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age + ", name=" + name +
                '}';
    }
}

在这里插入图片描述

2.4 反射暴破操作方法

反射暴破操作方法的步骤如下:

  1. 根据方法名和参数列表获取Method方法对象:
Method m = clazz.getDeclaredMethod(方法名,XX.class);
  1. 获取对象
Object o = clazz.newInstance();
  1. 暴破
m.setAccessible(true);
  1. 访问
Object returnValue = m.invoke(o,实参列表);
  1. 注意,如果是静态方法,则invoke参数o,可以写成null。

实例如下: 具体见代码注释

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * 演示通过反射调用方法
 */
public class AccessibleMethod {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException,
            IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        //1.得到Teacher类对应的Class对象
        Class<?> teacherClass = Class.forName("reflection.Teacher");
        //2.创建对象
        Object o = teacherClass.newInstance();
        //3.调用方法 public
        Method hi = teacherClass.getMethod("hi", String.class);
        hi.invoke(o, "黄小黄");
        //4.调用方法 private
        Method say = teacherClass.getDeclaredMethod("say", int.class, String.class, char.class);
        say.setAccessible(true);//暴破
        Object returnString = say.invoke(null, 1, "黄小黄", 'Y');
        System.out.println(returnString);
    }
}

class Teacher{
    public int age;
    private static String name;

    public Teacher(){
    }

    private static String say(int n, String s, char c){
        return n + " " + s + " " + c;
    }

    public void hi(String s){
        System.out.println("hi " + s);
    }
}

在这里插入图片描述

在反射中,如果方法有返回值,统一返回为Object,编译类型为Object,运行类型还是方法的return类型


写在最后

🌟以上便是本文的全部内容啦,本专栏到此也就告一段落啦,如果文章对你有所帮助,麻烦动动小手点个赞 + 关注,非常感谢 ❤️ ❤️ ❤️ !
如果有问题,欢迎私信或者评论区!
在这里插入图片描述

共勉:“你间歇性的努力和蒙混过日子,都是对之前努力的清零。”
在这里插入图片描述

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到