# Java-反射

参考文章:https://susu20061116.github.io/2026/06/29/反射/

还有xiexie师傅的Java-反射.md文件 CuO~

Java反射是指程序在运行时,动态获取类的信息,并操作类、对象、方法、字段和构造方法的能力

正常写Java代码时,我们通常提前知道要使用哪个类,调用哪个方法:

User user = new User();
user.login();

而通过反射,即使在编译时不知道具体类,也可以通过类名字符串动态加载并调用:

Class<?> clazz = class.forName("com.example.User");
Object user = clazz.getDeclaredConstructor().newInstance();

Method method = clazz.getDeclaredMethod("login");
method.invoke(user);

普通调用是通过类去创建对象进而实现方法调用,而反射则是通过类名字符串去获取类的信息,进而创建对象之后查找方法再动态调用

也就是说,程序可以在运行时“观察”自己的类结构

它可以知道一个类叫什么名字,继承了哪个类,实现了哪些接口,有哪些成员变量,有哪些方法,有哪些构造方法,使用了哪些注解,方法参数和返回值是什么

# 动态特性

反射式大多数语言里都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有的方法(包括私有),拿到的方法可以调用,通过反射,我们可以把Java这种静态语言附加上动态特性

那么什么是动态特性呢?在Java安全漫谈中,对于这个概念的含义是:一段代码,改变其中的变量,将会导致这段代码产生功能性的变化,称之为动态特性。换句话说,就是程序在运行之前,无法完全确定自己会调用哪个类,哪个方法或执行什么功能,真正执行的行为要等到运行时根据变量、输入参数决定

普通变量变化与动态特性:举一个例子就很容易知道他们的区别了

int a = 1;
int b = 2;
System.out.println(a + b);

如果把a从1改到10,那么只是计算结果从3变到了12,但程序本身的功能仍然是加法

动态特性强调变量变化之后程序执行的功能发生变化:

public void execute(String className,String methodName) throws Exception {
    Class<?> clazz = Class.forName(className);
    Object obj = clazz.getDeclaredConstructor().newInstance();

    clazz.getMethod(methodName).invoke(obj);
}

顺便提一下这个例子中在反射里极为重要的方法:

# 获取类的方法
forName
# 实例化类对象的方法
newInstance
# 获取函数的方法
getMethod
# 执行函数的方法
invoke

基本上这几个方法包揽了Java安全里各种和反射有关的payload

这里的className和methodName不是普通数据,而是在决定加载哪个类,创建那个对象,调用哪个方法,最终执行哪个功能

传入execute("com.example.Printer","print");程序可能执行打印功能

传入execute("com.example.EmailSender","send");同一段代码程序就可能变成了发送邮件

这种代码本身没有修改,只修改变量值但程序执行的功能发生了变化就是动态特性,而这种动态特性是由反射所带来的:Java通常被称为静态类型语言,正常调用方法时,类名和方法名通常直接写在代码中

Printer printer = new Printer();
Printer.print();

编译代码时就能看出调用的是Printer类,调用的是print()方法

而使用反射时:

Class<?> clazz = Class.forName(className);
Method method = clazz.getMethod(methodName);
method.invoke(object);

类名和方法名可以在运行时才确定,其中关键操作是:

# 根据字符串加载类
Class.forName(className);
# 根据字符串寻找方法
clazz.getMethod(methodName);
# 执行找到的方法
method.invoke(object);

反射将固定调用某个类的某个方法变成运行时根据变量决定调用哪个类、哪个方法这样一个转变就是反射带来的动态性,但Java具有动态特性不代表Java就是动态类型语言

所以分析反射代码的时候,需要特别关注关键操作里面的值是否可控

# Class对象

Java中的每个被加载的类,在JVM中都有一个对应的Class对象,例如:

Class<String> c1 = String.class;
Class<Integer> c2 = Integer.class;
Class<Runtime> c3 = Runtime.class;

这里的String.class并不是String对象,而是描述这个String这个类的Class对象,我们可以通过它获取类的信息:

Class<String> clazz = String.class;

System.out.println(clazz.getName());
System.out.println(clazz.getSimpleName());

假设有一个类:

public class User{
    private Srting name;

    public User(){
    }
    
    public void sayHello(){
        System.out.println("Hello");
    }
}

获取他的Class对象有三种常见方式

# 通过类名获取

Class<User> clazz = User.class;

此方法适合编译时已经知道类的情况(这个方法其实不属于反射)

# 通过对象获取

User user = new User();
Class<?> clazz = user.getClass();

# 通过完整类名动态加载

Class<?>  clazz = Class.forName("com.example.User");

这是反射中特别重要的方式,因为类名可以来自配置文件、网络输入或数据库,Class.forName()使用的是完整限定类名,包括包名

Class<?>是Java泛型中的一种写法,表示一个Class类型的对象,但暂时不知道他具体代表哪个Java类型,其中

Class:表示类的运行时信息
<?>:泛型通配符,表示具体类型未知

Class类本身的定义可以简化理解为:

public final class Class<T> {
}

其中T表示这个Class对象所代表的真实类型,例如Class<String>表示这个Class对象代表String类

因此Class<String> clazz = String.class可以理解为Clazz保存的是String类的运行时类型信息(Runtime Type Information,RTTI)

RTTI里面装了些什么内容呢?

当我们写下String.class时,我们获取到的Class对象内部包含了关于String类的所有元数据(Metadata),里面记录了:

  • 基本身份:类的全限定名(java.lang.String)、它的修饰符(比如它是public的,且是final的,不能被继承)
  • 继承关系:它的父类有哪些(java.lang.Object),它实现了哪些接口(如Serializable,Comparable,CharSequence等)
  • 构造:它有哪些构造方法(比如支持用字节数组创建,支持用另一个字符串创建等)
  • 方法信息:它有哪些方法(length(),substring(),indexOF()等),这些方法的返回值是什么,需要什么参数
  • 内部资产:它有哪些成员变量(比如JDK9之后,String内部用来存储字符的byge[] value数组)

写在.java文件中的代码是编译期(静态)的文本,开始运行程序时,进入运行期(动态),JVM把代码加载到内存中。在运行期,JVM真实地分配内存并执行逻辑,在这个截断,JVM需要一个实在的“数据结构”来代表String这个类本身,以便在程序运行过程中随时查阅这个类的相关信息

Class<String> clazz = String.class就是在程序运行的过程中,拿到代表String类的对象信息

那么在这里就学习一下普通对象与类对象的区别吧:

  • 普通对象:也称实例对象,为平时在业务逻辑时,通过new关键字创建出来的实体,包含了类的具体状态(成员变量的值)
  • 类对象:Class对象,为java.lang.Class类的实例,当JVM首次加载一个类时,会自动在内存中创建一个对应的Class对象,这个对象包含了关于该类的元数据(类的名字,有哪些字段,有哪些方法,继承了什么父类等),并且类对象全局唯一,每个类在JVM中只会有一个对应的Class对象

?为泛型中的通配符,表示这里有一个具体类型,但是当前不知道它是什么,例如:

Class<?> clazz;

这个变量可以接收多种不同类型对应的Class对象:

clazz = String.class;
clazz = Integer.class;
clazz = Runtime.class;
clazz = ArrayList.class;

因此Class<?>可以读作任意类型对应的Class对象

# 实例

接下来我们通过一个实例来介绍Java反射payload中的相关方法:

public class ZLARYY{
    //私有属性
    private String friends = "Arache,Chesmond,Su@sU,xiexie";

    //私有无参构造器
    private ZLARYY(){
        System.out.println("Object has been created");
    }

    //私有方法
    private void Secret(String secretKey){
        if("nothing".equals(secretKey)){
            System.out.println("ZLARYY's secret:"+this.friends);
        }else{
            System.out.println("I said nothing");
        }
    }
}

假设现在有一个ZLARYY.class文件,如果我们想要在Main里面实现修改其中的私有属性,创建实例和调用私有方法,用反射该怎么实现呢?

payload示例:

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Main{
    public static void main(String[] args){
        try{
            Class<?> clazz = Class.forName("ZLARYY");
            
            Constructor<?> ZLARYY_Constructor = clazz.getDeclaredConstructor();
            ZLARYY_Constructor.setAccessible(true);
            Object ZLARYY_Instance = ZLARYY_Constructor.newInstance();

            Field ZLARYY_Field = clazz.getDeclaredField("friends");
            ZLARYY_Field.setAccessible(true);
            ZLARYY_Field.set(ZLARYY_Instance,"2L@7yY");

            Method ZLARYY_Method = clazz.getDeclaredMethod("Secret",String.class);
            ZLARYY_Method.setAccessible(true);
            ZLARYY_Method.invoke(ZLARYY_Instance,"nothing");
            
        }catch (Exception e){
            System.err.println("Error");
            e.printStackTrace();
        }
    }
}

执行java Main.java可以得到如下结果:

接下来我们深入地分析一下这一长串payload,首先是获取Class对象,这个没什么好说的

# 导入依赖

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

在java之中,反射相关的核心类都存放在java.lang.reflect包里面

  • Constructor:表示某个类的构造方法,可以用来动态创建对象
  • Field:表示类中的属性/成员变量,可以动态读取和修改字段值
  • Method:表示类中的方法,可以动态调用对象的方法

# 获取构造器对象

Constructor<?> ZLARYY_Constructor = clazz.getDeclaredConstructor();
ZLARYY_Constructor.setAccessible(true);
Object ZLARYY_Instance = ZLARYY_Constructor.newInstance();

在这一部分我们首先用到了ZLARYY的类对象中的getDeclaredConstructor()方法,实际上我们在这一步是获取构造器对象然后调用其中的newInstance()方法实现实例化ZLARYY类

getDeclaredConstructor():该方法返回Constructor<?>,可以获取任意访问级别的构造器,包括protected和private,获取单个构造器对象

getConstructor():该方法返回Constructor<?>,只可以获取public访问修饰,获取单个构造器对象

getDeclaredConstructors():该方法返回Constructor<?>[],可以获取任意访问级别的构造器,获取所有的构造器对象

getConstructors():该方法返回Constructor<?>[],可获取所有的public访问修饰的构造器对象

值得注意的是,我们的实例代码里面只有一个无参构造器,如果他是一个需要String类型参数的构造器的话写法需要改成:

Constructor<?> ZLARYY_Constructor = clazz.getDeclaredConstructor(String.class);

如果参数类型不匹配的话会抛出NoSuchMethodException,但如果使用getConstructor()就会返回空数组

我们获取到了构造器对象但并不等于我们能直接使用,因为Java的访问控制机制还在起作用,这时候去实例化java还是会拦截抛出IllegalAccessException(非法访问异常)

所以我们需要设置可访问性为真,或者说关闭访问检查:

ZLARYY_constructor.setAccessible(true);

接着我们就可以调用newInstance()方法实例化ZLARYY类了,所以其实我们获取构造器的这三行代码等价于:

ZLARYY ZLARYY_Instance = new ZLARYY();

值得一提的是,对于实例化代码:

Object ZLARYY_Instance = ZLARYY_Constructor.newInstance();

我们不能直接写作:

ZLARYY ZLARYY_Instance = ZLARYY_Constructor.newInstance();

不过知道其具体类的话可以强转:

ZLARYY ZLARYY_Instance = (ZLARYY) ZLARYY_Constructor.newInstance();

因为ZLARYY_Constructor是Constructor<?>类型,或者说clazz的类型是Class<?>,Class<?>表示某个未知的类型,所以最终调用的newInstance()返回的结果是未知的捕获类型Object

除此之外构造器对象还有其他一些方法:

constructor.getParameterTypes();             // 获取参数类型
constructor.getParameterCount();             // 参数个数
constructor.getExceptionTypes();             // 声明抛出的异常
constructor.getModifiers();                  // 修饰符
constructor.getName();                       // 构造器名,等于类名
constructor.getDeclaringClass();             // 声明该构造器的类
constructor.isVarArgs();                     // 是否可变参数
constructor.getGenericParameterTypes();      // 泛型参数类型
constructor.getAnnotations();                // 注解

现在我把ZLARYY类里的代码修改为:

public class ZLARYY{
    private ZLARYY(){
        System.out.println("Object has been created");
    }
    public ZLARYY(int example){
        System.out.println("Just a Method");
    }
    protected ZLARYY(String example){
        System.out.println("Just a Method");
    }
    public ZLARYY(Integer example){
        System.out.println("Just a Method");
    }
}

Main代码改为:

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Arrays;

public class Main{
    public static void main(String[] args){
        try{
            Class<?> clazz = Class.forName("ZLARYY");
            
            Constructor<?>[] constructors = clazz.getDeclaredConstructors();
            for(Constructor<?> c : constructors){
                System.out.println("Parameter types:"+Arrays.toString(c.getParameterTypes()));
                System.out.println("Parameter count:"+c.getParameterCount());
                System.out.println("Modifier:"+Modifier.toString(c.getModifiers()));
                System.out.println("Class:"+c.getName());
                System.out.println("---------------------------");
            }
        }catch (Exception e){
            System.err.println("Error");
            e.printStackTrace();
        }
    }
}

最后得到输出:

Parameter types:[class java.lang.Integer]
Parameter count:1
Modifier:public
Class:ZLARYY
---------------------------
Parameter types:[class java.lang.String]
Parameter count:1
Modifier:protected
Class:ZLARYY
---------------------------
Parameter types:[int]
Parameter count:1
Modifier:public
Class:ZLARYY
---------------------------
Parameter types:[]
Parameter count:0
Modifier:private
Class:ZLARYY
---------------------------

# 捕获属性

Field ZLARYY_Field = clazz.getDeclaredField("friends");
ZLARYY_Field.setAccessible(true);
ZLARYY_Field.set(ZLARYY_Instance,"2L@7yY");

通过这一步我们可以获取ZLARYY类的私有属性friends并实现修改它的值

getField(String name):获取指定名称的字段(只能获取public),包含父类的public字段,返回类型为Field

getFields():可获取public访问权限的所有字段,包含父类的public字段,返回类型为Field[]

getDeclaredField(String name):可获取任意访问权限下包括private的指定名称字段,不包含继承字段,返回类型为Field

getDeclaredFields():可获取任意访问权限下的所有字段,不包含继承字段,返回类型为Field[]

这里我们使用getDeclaredField("friends")获取了ZLARYY类里面的私有friends属性

ZLARYY_Field.setAccessible(true);

与获取构造器对象一节一样,我们需要使用setAccessible()关闭这个字段的Java语言访问检查,接下来我们就可以通过set方法修改刚才创建的ZLARYY_Instance实例里面的friends的值:

ZLARYY_Field.set(ZLARYY_Instance,"2L@7yY");

这个方法里面存在两个参数,第一个参数是指定要修改的具体对象的属性,第二个参数是这个属性被修改后的值

除此之外,还有一些其他的方法:

field.getName();        // 字段名
field.getType();        // 字段类型
field.getModifiers();   // 修饰符,如 private、static、final
field.get(obj);         // 获取 obj 对象上的字段值
field.set(obj, value);  // 设置 obj 对象上的字段值

# 调用方法

Method ZLARYY_Method = clazz.getDeclaredMethod("Secret",String.class);
ZLARYY_Method.setAccessible(true);
ZLARYY_Method.invoke(ZLARYY_Instance,"nothing");

Method ZLARYY_Method可以用于声明一个存放方法的变量,而clazz.getDeclaredMethod(...)用于获取这个方法,其中第一个参数是方法名称,第二个参数是该方法接收的参数类型

getMethods():获取所有public方法,包括继承的方法

getMethod(String name, Class<?> ... paramTypes):获取指定的public方法,包括继承的

getDeclaredMethods():获取本类中声明的所有访问级别的所有方法,不包括继承的

getDeclaredMethohds(String name, Class<?> ... paramTypes):获取本类中声明的任意访问级别的方法,不包括继承的

与前面的知识同样的,需要关闭Java的访问控制:

ZLARYY_Method.setAccessible(true);

然后我们就可以通过invoke()调用方法,该方法的第一个参数为目标对象(如果是静态方法传null),后面的参数为所调用类方法的实际参数

顺便一提把其他访问修饰的方法调用一起放了:

# 无参public方法
Method method = User.class.getMethod("say");
method.invoke(user);

# 带参数public方法
Method method = User.class.getMethod("say",String.class);
method.invoke(user,"admin");

# 有返回值的方法
Method method = User.class.getDeclaredMethod("getName");
Object result = method.invoke(user);
String name = (String) result;

# 静态方法
Method method = User.class.getDeclaredMethod("hello");
method.invoke(null);

# 通过反射调用子类的独有方法

public class Animal{
    public void say(){
        System.out.println("animal");
    }
}
public class Dog extends Animal{
    @Override
    public void say(){
        System.out.println("wof");
    }
    public void bite(){
        System.out.println("I'm D0g3");
    }
}

假设有这么一个父类和子类,正常情况下我们通过如下语句创建是无法直接调用子类的bite()方法的:

Animal animal = new Dog();

但通过反射可以实现:

Animal animal = new Dog();
Class<?> clazz = animal.getClass();
Method method = clazz.getDeclaredMethod("bite");
method.invoke(animal);

那么这条语句最终实际上输出的结果是I'm D0g3

通过如下语句我们可以实现调用重写方法:

Method method = Animal.class.getMethod("say");
method.invoke(animal);

虽然方法来自Animal.class,但实际执行的仍然是Dog的重写版本,反射依旧遵循动态分派,这条语句的输出结果是dog

# 类的初始化

假设我们有以下这样一个类:

class Test{
    String ZLARYY="2L@7yY";
    static {
        System.out.println("Static initial");
    }
    {
        System.out.println("No static initial?");
    }
    public Test(){
        System.out.println("Constructor");
    }
    public void Method(){
        System.out.println("A simple method");
    }
}

public class Main{
    public static void main(String args[]){
        Test test1 = new Test();
        Test test2 = new Test();
        Test test = new Test();
    }
}

执行结果如下:

Static initial
No static initial?
Constructor
No static initial?
Constructor
No static initial?
Constructor

可以看到,对于三次创建对象,我们的static {}只执行了一次,而{}和Test(){}里面的代码执行了三次

类初始化就是JVM第一次主动使用一个类时,对这个类进行准备并执行初始化代码的过程,主要目的是给类变量赋值,并执行static初始化块

  • 类加载:把.class文件加载到JVM
  • 类初始化:执行类里面的静态初始化逻辑
  • 对象初始化:执行new对象时的实例初始化逻辑

类初始化只会执行一次,并且主要执行static变量初始化和static代码块,也就是说在执行new ZLARYY()之前执行了:

String ZLARYY="2L@7yY"
static{
	System.out.println("Static initial")
}
  • static {}是静态初始化块,它属于类而不是属于某一个具体对象,第一次执行new Test()时JVM发现Tset还没有初始化,于是先执行了static {}之后再创建对象
  • {}是实例初始化块,它和static {}最大的区别就在于它和对象有关,创建几次对象它就会执行多少次
  • Test()是构造函数,其作用是在对象被创建时对这个对象进行初始化

# 实例化初始块和构造函数的关系

构造函数并不是对象初始化过程中最先执行的东西,在执行构造函数自己的代码之前,还会有一些初始化过程,大概是最开始会调用父类构造器super(),然后进行实例变量初始化,然后实例初始化块{},最后才会执行构造函数自己的代码,比如:

class Test{
    String ZLARYY="2L@7yY";
    static {
        System.out.println("Static initial");
    }
    {
        System.out.println("No static initial?");
    }
    public Test(){
        System.out.println("Constructor");
    }
    public void Method(){
        System.out.println("A simple method");
    }
}

实际顺序可以理解为:

class Test{
    public Test(){
        super();
        String ZLARYY="2L@7yY";
        System.out.println("No static initial");
        System.out.println("Constructor");
    }
}

并且实例字段初始化和实例初始化块按照他们在源代码中的书写顺序执行,比如:

public class Demo {

    String a = print("a");

    {
        print("代码块1");
    }

    String b = print("b");

    {
        print("代码块2");
    }
}

顺序就是:

a
代码块1
b
代码块2

而不是先所有字段再所有代码块

# 反射加载内部类

# 正常加载内部类

我们以一个普通内部类为例:

class Outer{
    public static class Inner{
        static {
            System.out.println("Inner initialized");
        }
        static String ZLARYY = "2L@7yY";
    }
}

这里的private static class Inner就是静态内部类,属于Outer类内部,但本质还是一个独立的class,编译以后会发现内部类会生成一个独立的.class文件:

Outer.class
Outer$Inner.class

我们通过以下代码测试普通情况下的加载机制:

class Outer{
    public static class Inner{
        static {
            System.out.println("Inner initialized");
        }
        static String ZLARYY = "2L@7yY";
    }
}
public class Main{
    public static void main(String args[]){
        System.out.println("Before inner class?");
        System.out.println(Outer.Inner.ZLARYY);
    }
}

执行代码结果如下:

Before inner class?
Inner initialized
2L@7yY

可以看到,在初始化内部类之前代码先执行了我们的System.out.println("Before inner class?");,这是由于JVM看到Outer.Inner.ZLARYY需要使用Inner类,于是加载Outer$Inner.class文件执行Inner的static{}

# 通过反射加载

Class<?> clazz = Class.forName("Outer$Inner");

这里的Outer$Inner就是内部类的真实名字

同样的,参考之前对于反射的介绍,我们这里也可以通过反射获取private内部类以及调用方法等

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
class Outer{
    private static class Inner{
        static {
            System.out.println("Inner initialized");
        }
        private Inner(){
            System.out.println("Inner class constructed");
        }
        static String ZLARYY = "2L@7yY";
        private void say(){
            System.out.println(this.ZLARYY);
        }
    }
}
public class Main{
    public static void main(String args[]){
        try{
            Class<?> clazz = Class.forName("Outer$Inner");

            Constructor<?> inner_constructor = clazz.getDeclaredConstructor();
            inner_constructor.setAccessible(true);
            Object inner_instance = inner_constructor.newInstance();

            Field inner_field = clazz.getDeclaredField("ZLARYY");
            inner_field.setAccessible(true);
            inner_field.set(inner_instance,"Hacker!");

            Method inner_method = clazz.getDeclaredMethod("say");
            inner_method.setAccessible(true);
            inner_method.invoke(inner_instance);
        }catch (Exception e){
            System.out.println("Error");
            e.printStackTrace();
        }
    }
}

最后输出的结果为:

Inner initialized
Inner class constructed
Hacker!

# 内部类单例模式

内部类单例模式是Java中一种利用静态内部类的类加载机制实现的单例写法,核心思想是利用JVM对类初始化的保证,让内部类只在第一次使用时加载,并且只创建一个对象

# 单例模式

单例模式要求一个类在整个程序运行期间只能创建一个对象

public class User{
}

普通类的User,可以创建多个对象:

User a = new User();
User b = new User();

并且a != b,但是对于单例模式:

Singleton a = Singleton.getInstance();
Singleton b = Singleton.getInstance();

得到的结果是a == b,

最简单的单例写法如下:

public class Singleton{
    private static Singleton instance = new Singleton();
    private Singleton(){
        System.out.println("Singleton constructed");
    }
    public static Singleton getInstance(){
        return instance;
    }
}

我们可以通过如下main代码来创建Singleton对象:

public class Main{
    public static void main(String args[]){
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

可以看到Singleton()方法只执行了一次,并且s1 == s2,因为private static Singleton instance只有一份

但是这个对象会在类加载时被创建,即便我们没有使用Singleton,对象也会被创建,这种被称为饿汉式加载

# 内部类单例模式

class Singleton{
    private Singlton(){
        System.out.println("Singleton outer");
    }
    private static class Holder{
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance(){
        return Holder.INSTANCE;
    }
}

这个类由外部类Singleton和静态内部类Holder组成

与之前不同的是,这里如果我们不使用Singleton,对象就不会被创建:

class Singleton{
    private Singleton(){
        System.out.println("Singleton outer");
    }
    private static class Holder{
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance(){
        return Holder.INSTANCE;
    }
}
public class Main{
    public static void main(String args[]){
        System.out.println("Hello");
    }
}

class Singleton{
    private Singleton(){
        System.out.println("Singleton outer");
    }
    private static class Holder{
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance(){
        return Holder.INSTANCE;
    }
}
public class Main{
    public static void main(String args[]){
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

在这些代码中,Singleton类本身并不会创建对象,Holder类初始化的时候才会创建对象。第一步JVM加载了Singleton.class,这里并不会执行new Singleton(),因为Holder还没有被使用,直到代码执行到getInstance(),这里访问了Holder.INSTANCE,(在第一次访问静态内部类的静态成员时,会触发内部类初始化),于是JVM开始加载Singleton$Holder.class,加载private static final Singleton INSTANCE = new Singleton();从而调用Singleton()方法

以上都是发生在Singleton s1 = Singleton.getInstance()执行时,当进入下一行创建s2的时候,Holder已经初始化,所以不会再次调用Singleton(),直接返回之前的对象

这种对象创建发生在第一次调用getInstance()的时候而不是Singleton类加载的时候就称为懒加载

# 对于单例模式的突破

单例模式,实际上特征为:

  • 只允许全局拥有一个实例
  • 通过静态方法(不需要创建对象就能使用)获取唯一实例
  • 构造方法私有,防止外部new

静态内部类单例依靠JVM的类初始化机制保证唯一,但是反射可以绕过private构造方法,重新创建对象从而破坏单例

import java.lang.reflect.Constructor;

class Singleton{
    private Singleton(){
        System.out.println("Singleton outer");
    }
    private static class Holder{
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance(){
        return Holder.INSTANCE;
    }
}
public class Main{
    public static void main(String args[]){
        try{
            Class<?> clazz = Class.forName("Singleton");

            Constructor<?> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            Object instance = constructor.newInstance();
            Object instance2 = constructor.newInstance();
        }catch (Exception e){
            System.out.println("Error");
            e.printStackTrace();
        }
    }
}

这串代码的输出结果为:

Singleton outer
Singleton outer

# java.lang.Runtime

java.lang.Runtime是Java中一个非常重要的系统类,它代表Java程序当前运行环境(JVM)的运行时对象

关于其源码分析可以看Java 源码 - java.lang.Runtime前言Runtime类是一个与JVM运行时环境有关的Singleto - 掘金

通过Runtime,Java程序可以获取JVM运行信息、内存情况、CPU核心数,也可以启动外部进程、添加JVM关闭钩子等,因为属于java.lang包,所以不需要手动import

在创建Runtime对象时不能直接new,因为他的构造方法为私

有,正确创建方法是:

Runtime runtime = Runtime.getRuntime();

Runtime.getRuntime()会返回当前JVM对应的Runtime对象

例如:

public class Main{
    public static void main(String[] args){
        Runtime runtime = Runtime.getRuntime();

        System.out.println(runtime);
    }
}

其输出结果为:

java.lang.Runtime@2a139a55

它由两部分组成,@前为类名,后面为对象的哈希值的十六进制表示,用来作为这个对象的一个默认表示,但它不是内存地址

它属于典型的饿汉式单例模式:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    private Runtime() {}
    public static Runtime getRuntime() {
        return currentRuntime;
    }
}

之前我们有提到单例模式可以通过反射突破创建多个对象:

import java.lang.reflect.Constructor;
public class Main{
    public static void main(String[] args){
        Class<?> clazz = Class.forName("java.lang.Runtime");

        Constructor<?> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        Object instance = constructor.newInstance();
    }
}

但是对于这串代码我们看到的结果是:

Exception in thread "main" java.lang.Error: Unresolved compilation problems: 
        Unhandled exception type ClassNotFoundException
        Unhandled exception type NoSuchMethodException
        Unhandled exception type InstantiationException
        Unhandled exception type IllegalAccessException
        Unhandled exception type InvocationTargetException

        at Main.main(Main.java:4)

这是由于对于现代Java的java.base模块中的JDK内部访问,强封装还可能直接阻止这种行为,抛出InacessibleObjectException,那么实际上我们的思路就是利用getRuntime()方法

import java.lang.reflect.Method;
public class Main{
    public static void main(String[] args){
        try{
            Class<?> clazz = Class.forName("java.lang.Runtime");

            Method getRuntimeMethod = clazz.getMethod("getRuntime");
            Object runtimeObj = getRuntimeMethod.invoke(null);

            Method execMethod = clazz.getMethod("exec",String.class);
            execMethod.invoke(runtimeObj,"calc.exe");
        }catch (Exception e){
            System.out.println("ERROR");
            e.printStackTrace();
        }
    }
}

这里我们介绍一下Runtime.exec()启动操作系统程序,exec()方法最终返回Process,例如Process p = Runtime.getRuntime().exec("java -version");Process对象代表这个新启动的原生进程,可以读取stdout、stderr、等待退出、获取退出码或者终止进程,它实际上是在JVM请求底层操作系统启动一个新的native process

执行上面的代码可以实现弹出计算器

但Runtime.exec()默认不是shell,事实上Runtime.getRuntime().exec(command)并不等同于sh -c "command",所以command1 ; command2,Runtime.getRuntime().exec("someProgram aaa;bbb");,在没有shell的情况下,;bbb很可能只是被当做普通参数的一部分或者独立参数

# 有参构造且无静态方法获取

与无参构造不同的是,处理有参构造需要精准匹配:

  • clazz.getConstruvtor(Class<?>...parameterType):根据传入的参数类型列表定位唯一构造器
  • constructor.newInstance(Object...initargs):创建实例时,传入与构造器参数类型匹配的实际数据

值得注意的是,这里我们使用的getConstructor()只能获取public修饰的构造器

所以我们可以利用ProcessBuilder进行测试:

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
public class ReflectProcessBuilder{
    public static void main(String args[]) throws Exception{
        Class<?> clazz = Class.forName("java.lang.ProcessBuilder");

        Constructor<?> constructor = clazz.getConstructor(List.class);
        Object pbInstance = constructor.newInstance(Arrays.asList("calc.exe"));
        Method startMethod = clazz.getMethod("start");
        startMethod.invoke(pbInstance);

        System.out.println("Process start");
    }
}

这段代码是通过加载ProcessBuilder,找到List构造器,然后创建对象找到start()方法并调用从而启动进程

ProcessBuilder有两个公开的构造器

# 构造器1:接收一个List<String>参数
public ProcessBuilder(List<String> command)
# 构造器2:接收可变长字符串参数(本质是String[]参数)
public ProcessBuilder(String... )

这里我们用的是第一个,意思是告诉JVM寻找参数类型为List的构造方法,但这里我们并没有写List<String>.class,因为Java泛型存在类型擦除,运行时反射看到的基本就是List而不是List.class

所以我们在newInstance()时里面的参数类型应该是List<String>,就需要Arrays.asList("calc.exe"),该方法返回的结果类型是List<String>,内容大致是["calc.exe"],但是该对象创建之后还并没有执行calc.exe,相当于我们保存了命令配置还没有创建操作系统进程,ProcessBuilder可以理解为进程启动配置对象,里面保存了:

执行哪个程序
参数是什么
工作目录是什么
环境变量是什么
标准输入输出怎么处理

真正启动进程需要start()方法,上面的代码执行结果如下:

更新于