MENU

JAVA反射从零到一

August 18, 2022 • Read: 618 • Code auditing

反射概述

官方定义:

Reflection is a feature in the Java programming language. It allows an executing Java program to examine or "introspect" upon itself, and manipulate internal properties of the program. For example, it's possible for a Java class to obtain the names of all its members and display them.

反射是指一个运行中的Java程序可以检查、修改自己内部的属性,也可称之为自省。反射是Java有别于其它编程语言的一大特性。从reflection的字面意思看,就是倒影、反射,就好比你照镜子,通过倒影就能知道自己长什么样,理一理头发就能改变发型。

反射原理

JVM会动态加载Class,一个Class实例包含了该类的所有完整信息,如:包名、类名、各个字段、各个方法、父类、实现的接口等。因此,如果获取了某个类或对象的Class实例,就可以通过它获取到对应类的所有信息。

Class类对象的三种实例化模式
H$TKYLS%GB}Z~XUQ_5.png

其中第一种是静态方法会产生实例化对象,而其他的不会产生实例化对象

我们来看一个例子就更好理解了

package Reflection;

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

public class Reflection {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        //加载类,返回Class类型的对象aClass
        String s = "Reflection.Cat";
        Class<?> aClass = Class.forName(s);
        //通过aClass得到加载的类Reflection.Cat的对象实例
        Object o = aClass.newInstance();
        System.out.println("o的运行对象为:"+o.getClass());
        //通过aClass得到加载类的"hi"方法对象
        //即在反射中,可以把方法视为对象(万物皆对象)
        Method method1 = aClass.getMethod("hi");
        method1.invoke(o);
    }
}
class Cat{
    private String name;
    public Cat(){
        System.out.println("构造器被调用");
    }
    public void hi(){
        System.out.println("方法被调用");
    }
}

输出
RNKHZ9Q0SNG7S6LME}NH.png

这个是采取了反射实例化对象的方法来,注意newInstance()方法内部实际上调用了无参数构造方法,必须保证无参构造存在才可以。
然后我们根据这个来画一张流程图就更加能理解反射了
)JZR_WOJ@SK1)%AT@`$7ZFC.png

反射相关类

  1. java.lang.Class 代表一个类,Class对象表示某个类加载后在堆中的对象
  2. Method:代表类的方法,Method 对象
  3. Field:代表类的成员变量
  4. Constructor:代表类的构造方法

这些类在 java.lang.reflection

Class类详解

基本介绍:

  1. Class也是类因此也是继承的Object类
  2. Class类对象不是new出来的,而是系统创建的
  3. 对于某个类的Class类对象,在内存中只有一份,因此类只加载一次
  4. 每个类的实例都会记得自己是由哪个Class实例所产生的
  5. 通过Class对象可以完整地得到一个类的完整结构,通过一系列的API
  6. Class类是放在堆中的
  7. 类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据

第一点在类图上就能很好的体现了
_HP6G$Z@XA8SGE4ZNGCBR.png

关于第二点我们来追一下源码就知道了
截屏2022-08-22 15.57.38.png

发现是一个loadClass的方法创建的,其他实例化模式同理,只是new更加直观
{16D99F00-447E-524A-D788-74DB223A64DE}.png

第三点也举例一下就行
5J)STE{Y72A4XIU(R0Z8.png

输出
{80B77D65-B4E7-3957-D632-EA74674F9DB7}.png

第四点就是涉及到了class的常用方法了

class的常用方法

Field[] getFields()             //返回一个包含Field对象的数组,存放该类或接口的所有可访问公共属性(含继承的公共属性)。
Field[] getDeclaredFields()     //返回一个包含Field对象的数组,存放该类或接口的所有属性(不含继承的属性)。
Field getField(String name)     //返回一个指定公共属性名的Field对象。
Method[] getMethods()           //返回一个包含Method对象的数组,存放该类或接口的所有可访问公共方法(含继承的公共方法)。
Method[] getDeclaredMethods()   //返回一个包含Method对象的数组,存放该类或接口的所有方法(不含继承的方法)。
Constructor[] getConstructors() //返回一个包含Constructor对象的数组,存放该类的所有公共构造方法。
Constructor getConstructor(Class[] args)//返回一个指定参数列表的Constructor对象。
Class[] getInterfaces()         //返回一个包含Class对象的数组,存放该类或接口实现的接口。
T newInstance()                 //使用无参构造方法创建该类的一个新实例。
String getName()                //以String的形式返回该类(类、接口、数组类、基本类型或void)的完整名。

第六点和第七点根据这个图就很好理解了
6QI71HNXD2YO%4R80Z94.jpg

这里再写一个常用方法的例子

package Reflection;

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

public class Reflection {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {

        //1:加载类,返回Class类型的对象aClass
        String s = "Reflection.Cat";
        Class<?> aClass = Class.forName(s);

        //2:输出aClass
        System.out.println(aClass); //显示aClass是哪个类的运行对象
        System.out.println(aClass.getClass());//显示aClass的运行类型

        //3:得到包名
        System.out.println(aClass.getPackage().getName());

        //4:得到类名
        System.out.println(aClass.getName());

        //5:通过aClass得到加载的类Reflection.Cat的对象实例
        Object o = aClass.newInstance();
        System.out.println("o的运行对象为:"+o.getClass());

        //6:通过aClass得到加载类的"hi"方法对象
        //即在反射中,可以把方法视为对象(万物皆对象)
        Method method1 = aClass.getMethod("hi");
        method1.invoke(o);

        //7:通过反射获取属性
        Field b = aClass.getDeclaredField("name");//属性为私有属性
        b.setAccessible(true);//暴力访问
        Object c = b.get(o);//取值
        System.out.println(c);
        
    }
}
class Cat{
    private String name;
    public Cat(){
        System.out.println("构造器被调用");
    }
    public void hi(){
        System.out.println("方法被调用");
    }
}

输出
{DE481D50-9280-82D6-522E-B247D55DB019}.png

获取Class类的方式

1:已知一个类的全类名,且该类在类路径下,可以通过Class类的静态方法forName()获取

eg:Class aClass = Class.forName("Reflectiom.Cat")

2:若已知具体的类,通过类的calss获取,该方式最为安全可靠,性能最高

eg:Class aClass = Cat.class

3:已知某个类的实例,调用该实例的getClass()方法获取Class对象

eg:Class aClass = 对象.getClass

4:其他方式

eg:ClassLoader aClass = 对象.getClass().getClassLoader();
    Class bClass = aClass.loadClass("类的全名称") 

5:基本数据(int,char,booleam,float等)

eg:Class aClass = 基本数据类型.class

6:基本数据类型对应的包装类,可以通过.TYPE得到Class类对象

eg:Class aClass = 包装类.TYPE

例子

package Reflection;

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

public class Reflection {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {

        //1:Class.forName
        Class<?> aClass = Class.forName("Reflection.Cat");
        System.out.println("第一种:" + aClass);

        //2:类名.class
        Class bClass = Cat.class;
        System.out.println("第二种:" + bClass);

        //3:对象.getClass()
        Cat cClass = new Cat();
        Class dClass = cClass.getClass();
        System.out.println("第三种:" + dClass);

        //4:类加载器获取到类到Class对象
        ClassLoader classLoader = cClass.getClass().getClassLoader();//得到类加载器
        Class<?> eClass = classLoader.loadClass("Reflection.Cat");
        System.out.println("第四种:" + eClass);

        //5:基本数据得到Class类
        Class<Integer> integerClass = int.class;
        System.out.println("第五种:" + integerClass);

        //6:基本数据类型对应到包装类得到Class
        Class<Integer> type = Integer.TYPE;
        System.out.println("第六种:" + type);

    }
}
class Cat{
    private String name;
    public Cat(){
        System.out.println("构造器被调用");
    }
    public void hi(){
        System.out.println("方法被调用");
    }
}

输出
{36470F9B-F15D-CE39-2E93-623B9485DB4B}.png

再总结一下哪些类型有Class对象

1:外部类和内部类 2:接口 3:数组 4:枚举 5:注解 6:基本数据类型 7:void

静态加载和动态加载

反射机制是java实现动态语言的关键,就是通过反射实现类动态加载
再来区分一下静态加载和动态加载的区别

1:静态加载:编译时加载相关的类,如果没有则报错,依赖性太强
2:动态加载:运行时加载需要的类,如果运行时不用该类,则不报错,降低了依赖性

理解很简单就不多解释了

类加载流程

类加载的时机

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

类加载流程图
SOVT9M)Z}(%1%KO$FADU.jpg

重点在类加载的三个阶段
GBN6~GQGSS)7J~8%{W0%}C.jpg

加载阶段

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

这里又不得不提一下类加载器与双亲委派了
类加载器:
JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
DE3(}~TNH0_W4D4XW3JQO.png

类加载器介绍:
BootstrapClassLoader(启动类加载器)

负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,加载System.getProperty(“sun.boot.class.path”)所指定的路径或jar。

ExtensionClassLoader(标准扩展类加载器)

负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。载System.getProperty(“java.ext.dirs”)所指定的路径或jar。

AppClassLoader(系统类加载器)

负责记载classpath中指定的jar包及目录中class

CustomClassLoader(自定义加载器)

属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现。

类加载器顺序:
XAE23QE78AMACT6V@2B@0.png

我们再来看一下loadClass源码就更好理解了

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // 首先,检查是否已经被类加载器加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 存在父加载器,递归的交由父加载器
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 直到最上面的Bootstrap类加载器
                    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.
                c = findClass(name);
            }
        }
        return c;
}

就是当一个Cat.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。

更加仔细的可以看看这篇文章:https://www.cnblogs.com/luckforefforts/p/13642685.html

继续回到我们刚才的加载图
连接阶段-验证

  1. 目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
  2. 文件格式验证(是否以魔数oxcafebabe开头),元数据验证,字节码验证和符号引用验证
  3. 可以考虑使用-Xverify:none参数来关闭大部分的类验证措,缩短虚拟机加载的时间

在源码中我们会生成一个SecurityManager的对象用于验证
截屏2022-08-22 20.06.27.png

连接阶段-准备

  1. JVM会在该阶段对静态变量,分配内存并初始化(对于数据类型的默认值),这些变量所使用的内存都将在方法区中分配

例子

class test{
    public  int n1 = 10;//实例变量,不是静态属性,因此在准备阶段是不会分配内存的
    public static int n2 = 20;//默认初始化值为0
    public static final int n3 = 30;//static final是常量,一旦赋值就不能变

}

连接阶段-解析

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

初始化

  1. 到初始化阶段,才是真正开始执行类中定义的java程序代码,此阶段是执行<clint>()方法的过程。
  2. <clint>()方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量赋值动作和静态代码块中的语句,并进行整合
  3. 虚拟机会保证一个类的<clint>()方法在多线程环境中被正确地加锁,同步,如果多个线程区同时初始化一个类,那么只有一个线程区执行这个方法,其他线程都需要阻塞等待,直到这个活动线程执行完毕。

反射的应用

反射获取类的结构信息

}@_V(N%H}A(J}3BDHJLI8Q.png

我自己写了个例子

package ClassLoder;

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

public class ClassLoder {
    

    public static void main(String[] args) throws ClassNotFoundException {
        //得到Class对象
        Class<?> aClass = Class.forName("ClassLoder.test");
        //得到全类名
        System.out.println(aClass.getName());
        //获取简单类名
        System.out.println(aClass.getSimpleName());
        //获取所有pulic属性,包括父类的
        Field[] fields = aClass.getFields();
        for (Field field : fields) {
            System.out.println("本类和父类的public属性:" + field.getName());
        }
        //获取本类所有属性
        Field[] declaredFields = aClass.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            System.out.println("本类所有属性:" + declaredField.getName());
        }
        //获取所有pulic方法,包括父类的
        Method[] methods = aClass.getMethods();
        for (Method method : methods) {
            System.out.println("本类和父类的public方法:" + method.getName());//父类还有Object
        }
        //获取本类所有方法
        Method[] declaredMethods = aClass.getDeclaredMethods();
        for (Field field : declaredFields) {
            System.out.println("本类的所有方法:" + field.getName());
        }
        //获取本类所有的public修饰的构造器
        Constructor<?>[] constructors = aClass.getConstructors();
        for (Constructor<?> constructor : constructors) {
            System.out.println("本类的public构造器" + constructor.getName());
        }
        //本类的所有构造器
        Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println("本类所有构造器" + declaredConstructor.getName());
        }
        //以package的形式返回包信息
        System.out.println(aClass.getPackage());
        //以Class的形式返回父类信息
        Class<?> superclass = aClass.getSuperclass();
        System.out.println("父类信息" + superclass);
        //得到接口返回信息
        Class<?>[] interfaces = aClass.getInterfaces();
        for (Class<?> anInterface : interfaces) {
            System.out.println("接口返回信息:" + anInterface);
        }
        //得到注解信息
        AnnotatedType[] annotatedInterfaces = aClass.getAnnotatedInterfaces();
        for (AnnotatedType annotatedInterface : annotatedInterfaces) {
            System.out.println("注解信息:" + annotatedInterface);
        }


    }
}
class Class001{
    public String hobby;
    public void dispaly(){}
    public Class001(){}
}
class test extends Class001 implements IA,IB{
    @Deprecated
    //属性
    public String name;
    protected int age;
    String job;
    private double sal;

    //方法
    public void m1(){}
    protected void m2(){}
    void m3(){}
    private void m4(){}

    //构造器
    public test(){}
    public test(String name,String age){}
    private test(String name,String age,double sal){}
}
interface IA{}
interface IB{}

就是所见及所得,很简单的
然后我们重点来看这几个
java.lang.reflect.Field类

  1. getModifiers:以int的形式返回修饰符(默认为0,public为1,private为2,protected为4,final为6,static为8)
  2. getType:以Class的形式返回类型
  3. getNamei:返回属性名

例子

package ClassLoder;

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

public class ClassLoder {
    

    public static void main(String[] args) throws ClassNotFoundException {
        //得到Class对象
        Class<?> aClass = Class.forName("ClassLoder.test");

        //获取本类所有属性,如果是组合关系就相加
        Field[] declaredFields = aClass.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            System.out.println("本类所有属性:" + declaredField.getName()
                    +" ,本类属性的修饰符:" + declaredField.getModifiers()
                    +" ,该属性的类型返回Class类为:" + declaredField.getType());
        }

    }
}

class test{
    @Deprecated
    //属性
    public String name;
    protected int age;
    String job;
    private double sal;

    //方法
    public void m1(){}
    protected void m2(){}
    void m3(){}
    private void m4(){}

    //构造器
    public test(){}
    public test(String name,String age){}
    private test(String name,String age,double sal){}
}

输出
{C2A6F3E3-3F29-0562-F11F-BAE1A27A4993}.png

java,lang.reflect.Method类

  1. getModifiers:以int的形式返回修饰符(默认为0,public为1,private为2,protected为4,static为8,final为16)
  2. getReturnType:以Class的形式返回类型
  3. getNamei:返回方法名
  4. getParameterTypes:以Class[]返回参数类型数组

java.lang.reflect.Constructor类

  1. getModifiers:以int的形式返回修饰符
  2. getNamei:返回构造器名(全类名)
  3. getParameterTypes:以Class[]返回参数类型数组

和前面差不多的就不用举例了

反射爆破创建实例

首先我们得要知道怎么通过反射机制创建实例

  • 方式一:调用类中的public修饰无参构造器
  • 方式二:调用类中指定构造器

然后是相关类的操作

Class类的相关方法

  1. newInstance:调用类中的无参构造器,获取对应类的对象
  2. getConstructor(Class...clazz):根据参数列表,获取对应的构造器对象
  3. getDecalaredConstructor(Class...clazz):根据参数列表,获取对应的构造器对象

Constructor类相关方法

  1. setAccessible:爆破
  2. newInstance(Object...obj):调用构造器

实例

package ReflectionInstance;

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

public class ReflectionInstance {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        //1:通过User类得到Class类
        Class<?> aClass = Class.forName("ReflectionInstance.User");
        //2:通过public的无参构造器创建实例
        Object o = aClass.newInstance();
        System.out.println(o);
        //3:通过public的有参构造器创建实例
        Constructor<?> constructor = aClass.getConstructor(String.class);
        Object m1 = constructor.newInstance("小明");
        System.out.println(m1);
        //4:通过非public的有参构造器创建实例
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);//爆破【暴力破解,使用反射可以访问私有的构造器
        Object m2 = declaredConstructor.newInstance("小红", 6);
        System.out.println(m2);


    }
}
class User{
    private int age = 10;
    private String name = "李华";

    //无参构造器
    public User(){}

    //public有参构造器
    public User(String name){
        this.name=name;
    }

    //private有参构造器
    private User(String name,int age){
        this.age=age;
        this.name=name;
    }

    public String toString(){
        return "User [age=" + age +",name=" + name +"]";
    }

}

输出
截屏2022-08-23 13.10.42.png

反射爆破操作属性

涉及到的方法

  1. getDeclaredField:根据属性名来获取Field对象
  2. setAccessible:爆破
  3. set(实例对象.值),syso(对象.get(实例对象)):访问
  4. 如果是静态属性,则set和get中的实例对象可以写成null

实例


package ReflectionProperties;

import java.lang.reflect.Field;

public class ReflectionProperty {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchFieldException {
        //1:得到Student类对应的Class对象
        Class<?> aClass = Class.forName("ReflectionProperties.Student");
        //2:实例化对象
        Object o = aClass.newInstance();//o的运行类型救赎Student
        //2:反射得到操作public的age属性
        Field age = aClass.getField("age");
        age.set(o,10);
        System.out.println(o);
        //3:反射操作name属性[私有并且静态]
        Field name = aClass.getDeclaredField("name");
        name.setAccessible(true);//爆破私有属性
        name.set(null,"李华");
        System.out.println(o);


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

    public Student(){}

    public String toString(){
        return "Student [age=" + age + ",name=" + name + "]";
    }
}

输出
截屏2022-08-23 14.30.00.png

反射爆破操作方法

同样需要注意几点

  1. getDeclaredMethod:根据方法名和参数列表获取Method方法对象
  2. newInstance:获取实例化对象
  3. setAccessible:暴破
  4. invoke(实例化对象,实参列表):访问
  5. 如果是静态方法,则invoke的实例化对象参数可以写成null

实例

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

public class ReflecAccessMethod {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        //1:得到Test类的Class对象
        Class<?> aClass = Class.forName("Test");
        //2:得到实例化对象
        Object o = aClass.newInstance();
        //3:反射调用public的display方法对象
        Method display = aClass.getMethod("display",String.class);
        display.invoke(o,"李华");
        //4:调用private static的say方法
        Method hit = aClass.getDeclaredMethod("hit",String.class);
        hit.setAccessible(true);
        hit.invoke(o,"爱你孤身走暗巷~");

    }
}
class Test{
    public int age;
    private static String name;

    public Test(){}

    private void hit(String c){
        System.out.println(c);
    }
    public void display(String s){
        System.out.println("hi,"+s);
    }
}

输出
截屏2022-08-23 15.52.17.png

反射的更多利用:https://www.w3cschool.cn/java/java-reflex.html

反射总结

反射优点

  1. 增加程序的灵活性,避免将程序写死到代码里。
  2. 代码简洁,提高代码的复用率,外部调用方便。

反射缺点

1.性能问题

使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此Java反射机制主要应用在对灵活性和扩展性要求很高的系统框架上,普通程序不建议使用。

反射包括了一些动态类型,所以JVM无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被 执行的代码或对性能要求很高的程序中使用反射。

2.使用反射会模糊程序内部逻辑

程序人员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂。

3.安全限制

使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如Applet,那么这就是个问题了。

4.内部暴露

由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用--代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

所以,总的来说,Java反射机制实际上是一把双刃剑,我们只有熟练掌握Java反射的优缺点,才能妥善使用Java反射这一利器,为我们的编程扫清障碍而不至于影响到我们的程序本身。

Last Modified: August 24, 2022
Archives Tip
QR Code for this page
Tipping QR Code