什么是java反射机制?

news/2024/5/19 6:03:38 标签: 反射

类的正常加载

image

反射概述

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

Class对象的由来是将class文件读入内存,并为之创建一个Class对象。


反射

如果不知道某个对象的确切类型,RTTI可以告诉你。但是有一个前提条件:这个类型在编译时必须已知,这样才能使用RTTI识别它。换句话说,在编译时,编译器必须知道所有要通过RTTI来处理的类。

人们想要在运行时获取类的信息的另一个动机,便是希望提供在跨网络的远程平台上创建和运行对象的能力。这被称为远程方法调用(RMI),它允许一个java程序将对象分布到多台机器上

Class类与java.lang.reflect类库一起对反射的概念进行了支持,该类库包含了Field、Method以及Constructor类(每个类都实现了Member接口)

这些类型的对象是由JVM在运行时创建的,用以表示未知类里对应的成员。

  • 使用Constructor创建新的对象
  • get()和set()方法读取和修改与Field对象关联的字段
  • 用invoke()方法调用与Method对象关联的方法
  • getField()、getMethod()、getConstructors()返回字段、方法、以及构造器的对象的数组

这样,匿名对象的类信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。

反射机制总结:通过反射与一个未知类型的对象打交道是,JVM只是简单地检查这个对象,看它属于哪个特定的类。在用它做其他事情之前必须先加载那个类的Class对象。因此,那个类的.class文件对于JVM来说是必须获取的:要么在本地机器上,要么可以通过网络取得。

RTTI与反射之间真正的区别:

  • 对RTTI来说,编译器在编译时打开和检查.class文件。(用普通方式调用对象的所有方法)
  • 对于反射机制来说,.class文件在编译时是不可获取的,所以在运行时打开和检查.class文件。

动态代理

代理是基本的设计模式之一。它是为了提供额外的或不同的操作,而插入的用来代替“实际”对象的对象。这些操作通常涉及与“实际”对象的通信。因此代理充当着中间人的角色。

public interface Interface {
	void doSomething();
	void somethingElse(String arg);
}
//实际对象
public class RealObject implements Interface{

	@Override
	public void doSomething() {
		// TODO Auto-generated method stub
		System.out.println("doSomething");
	}

	@Override
	public void somethingElse(String arg) {
		// TODO Auto-generated method stub
		System.out.println("somethingElse "+arg);
	}

}
//代理
public class SimpleProxy implements Interface{
	private Interface proxied;
	public SimpleProxy(Interface proxied){
		this.proxied = proxied;
	}
	@Override
	public void doSomething() {
		// TODO Auto-generated method stub
		System.out.println("SimpleProxy doSomething");
		proxied.doSomething();
	}

	@Override
	public void somethingElse(String arg) {
		// TODO Auto-generated method stub
		System.out.println("SimpleProxy somethingElse "+arg);
		proxied.somethingElse(arg);
	}
}

使用代理场景:在任何时刻,只要你想要将额外的操作从“实际”对象中分离到不同的地方,并且很容易做出修改。

java的动态代理比代理的思想更向前迈进了一步,因为它可以动态地创建代理并动态地处理对所代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定响应的对策。

public class DynamicProxyHandler implements InvocationHandler{
	private Object proxied;
	public DynamicProxyHandler(Object proxied){
		this.proxied = proxied;
	}
	@Override
	public Object invoke(Object proxy, Method method,
			Object[] args) throws Throwable {
		System.out.println("*** proxy: "+proxy.getClass()+", method: "+method+", args: "+args);
		if(args != null){
			for(Object arg:args){
				System.out.println(" "+arg);
			}
		}
		return method.invoke(proxied, args);
	}
}
public class SimpleDynamicProxy{
	public static void consumer(Interface iface){
		iface.doSomething();
		iface.somethingElse("test");
	}
	public static void main(String[] args) {
		RealObject real = new RealObject();
		consumer(real);
		Interface proxy = (Interface)Proxy.newProxyInstance(Interface.class.getClassLoader(), new Class[]{Interface.class},
				new DynamicProxyHandler(real));
		consumer(proxy);
	}
}
output:
doSomething
somethingElse test
*** proxy: class $Proxy0, method: public abstract void com.test.Interface.doSomething(), args: null
doSomething
*** proxy: class $Proxy0, method: public abstract void com.test.Interface.somethingElse(java.lang.String), args: [Ljava.lang.Object;@2ce83912
 test
somethingElse test

调用Proxy.newProxyInstance()可以创建动态代理,这个方法需要得到一个类加载器(通常可以从已经被加载的对象中获取其类加载器,然后传递给它),一个你希望该代理实现的接口列表(不是类或抽象类),以及InvocationHandler接口的一个实现。动态代理可以将所有调用重定向到调用处理器,因此通常会向调用处理器的构造器传递给一个“实际”对象的引用,从而使得调用处理器在执行中介任务时,可以将请求转发。


反射

要想理解动态代理的实现原理,我们还得先来理解反射。什么是反射呢?一般我们操作一个对象时,都会先new一个,比如现在有一个Person类:

public class Person {
    public String name;
    private int age;
    public Person(String name){
        System.out.print("有参数实例化\n");
        this.name = name;
    }
    public Person(){
        System.out.print("无参数实例化\n");
    }
    
    private Person(String name,int age){
        this.name = name;
        this.age = age;
    }
    public void run(){
        System.out.print("跑步");
    }
    public String getName() {
        return name;
    }
}

如上,我们定义了一个Person类,里面定义了几个构造方法,一个有参数一个没有,还有一个私有的带参数的构造方法,另外还有两个方法,一个方法为获取Person的名字,一个是让这个Person跑步,如果我们希望让一个Person跑起来,该怎么做呢,很简单,我们只需要new 一个Person对象,然后调用其run方法就可以了,如下:

Person perosn = new Person("张三");
person.run();

这样就让一个Person跑起来了,但是问题来了,如果在程序运行的时候,我需要操作的不一定是Person,而是一个会变得类,这个类可能是Person也可能是其他类比如叫做Bird或者Dog等等,而我们要操作的方法也不一定是run,也有可能是fly,sleep等等其他方法。那这时怎么办,很显然,我们是无法事先new 一个对象,因为我们根本就不知道程序运行的时候需要操作什么对象,执行什么方法,这时我们就可以用上反射了,反射可以使你在运行时读取类或对象的信息,也可以在运行时创建对象,操作对象的字段属性与方法。

好了理解了什么是反射,接下来,我们来看下如何使用反射。要想使用反射,你需要用到一个类,即Class类。什么是Class类呢?我们知道Java和C语言最大的不同就是,C是面向过程的,而Java是面向对象的。在Java的世界里,一切皆对象,一切都是可描述的,只要你能想到的,不管什么,我们都可以用类来描述,那么问题来了,我们知道类也是客观存在的东西,那么用什么来描述类的,答案就是Class类,即一个描述类的类,这个类的名字为Class。这是一个实际存在的类,路径为JDK的java.lang包中,感兴趣的朋友可以去看下。所谓存在即合理,这个Class类肯定不是Java工程师闲的蛋疼写出来的,而是有用的,拿上面的Person类举个例子,一开始我们通过编码生成了一个Person.java文件,然后通过编译又生成了一个Person.class字节码文件,这个文件就保存了一个Class对象,对象中包含了Person类的所有信息,当我们调用new Person()时,虚拟机就会检查内存中是否加载了Person.class这个字节码文件,如果没有就会加载,此时这个Class对象也会被加载到内存中,当虚拟机实例化Person对象时,虚拟机需要知道一些信息,比如要实例化的类中有什么字段什么方法等等,这些信息都被保存在Class对象中。所以说Class类对于虚拟机来说是个很重要的类。同样在反射中,我们也需要知道Person类的信息,这自然就离不开Class对象。

那么我们如何获取到Class类的对象呢,一共有三种方式,拿上面Person类举例子:
第一种,Class类有一个forName(String className)方法,需要传入一个类的完整类名,返回为该类型的Class对象,如下:

Class clazz = Class.forName("包名.Person");

注意,调用forName时,你需要捕获一个叫做ClassNotFoundException无法找到该类的异常。

第二种,你需要先创建一个Person对象,然后调用Person对象的getClass方法,就可以获取到Class对象:

Person person = new Person("张三");
Class clazz = person.getClass();

这种方式不足的地方在于获取Class对象前你还要创建一个Person对象。

第三种,最简单也是最安全的一种方式为直接调用Person.class,如下:

Class clazz = Person.class;

以上简单说明了下获取Class对象的几种方式,我们之前说过Class对象保存了对应类的类信息,包括这个类的构造方法、字段属性,类中的方法等。接下来我们来看下,如何使用Class对象来获取以及操作这些类信息。

构造方法Constructor

首先我们来看下构造方法,在反射中要想实例化一个类其实还是调用该类构造方法,只不过不是简单new下就可以了。我们先来看一种简单的实例化方式:

Class<?> clazz = Class.forName("包名.Person");
Person person = (Person) clazz.newInstance();
person.run();

上面的代码很简单,我们先调用Class的forName方法来获取Person类的Class对象,然后直接调用该对象的newInstance方法,这样就获取到了一个Person实例,然后调用他的run方法:

输出结果
====================================
无参数实例化
跑步
====================================

可以看到Person的构造方法成功被调用了,不过需要注意的是clazz.newInstance只适用于无参数构造方法,有参的我们需要通过Class获取Constructor对象来操作,Constructor对象是对构造方法的封装,Class获取Constructor对象的主要方法有以下几种:

返回值方法名称说明
ConstructorgetConstructor(Class<?>… parameterTypes)返回指定参数类型、具有public访问权限的构造函数对象
Constructor<?>[]getConstructors()返回所有具有public访问权限的构造函数的Constructor对象数组
ConstructorgetDeclaredConstructor(Class<?>… parameterTypes)返回指定参数类型、所有声明的(包括private)构造函数对象
Constructor<?>[]getDeclaredConstructor()返回所有声明的(包括private)构造函数对象
1. getConstructor(Class<?>… parameterTypes)

这个方法返回权限为public的构造方法对象,需要传入参数类型的Class对象,比如我们想要调用Person类中的Person(String name)构造方法可以通过以下方式来实现:

Class<?> clazz = Class.forName("com.modoutech.designpattern.Person");
Constructor<Person> constructor = (Constructor<Person>) clazz.getConstructor(String.class);
Person person1 = constructor.newInstance("张三");
person1.run();

可以看到我们先获取了Person的Class对象,然后调用Class的getConstructor方法,同时传入String.class参数,因为我们要调用的构造方法参数为String类型的,这样就得到了一个Constructor对象,接着我们就可以调用Constructor对象的newInstance方法并传入name参数,来实例化一个Person对象,获取到Perosn对象,就可以调用里面的方法啦。

2. getConstructors()

这个方法会返回所有权限为public的构造方法的Constructor对象数组。

Constructor<Person>[] constructors = (Constructor<Person>[]) clazz.getConstructors();
for (int i = 0; i < constructors.length; i++) {
    System.out.println("构造函数["+i+"]:"+constructors[i].toString() );
}
--------------------------------------------------
输出结果:
构造函数[0]:public com.modoutech.designpattern.Person()
构造函数[1]:public com.modoutech.designpattern.Person(java.lang.String)
--------------------------------------------------

可以看到我们获取了两个Constructor对象,这两个也正是对应了Person中权限为public的构造方法,至于接下来对这两个构造方法调用和上面的方法同理这里就不在赘述了。

3. getDeclaredConstructor(Class<?>… parameterTypes)

这个方法范围要大点,会返回类中申明的任何构造方法对象,包括权限为private的。比如我们想调用Person中私有的Person(String name,int age)构造方法,可以通过如下方式:

Constructor<Person> constructor = (Constructor<Person>) clazz.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
Person person1 = constructor.newInstance("张三",23);
person1.run();

这里基本和上述步骤相同,不过需要注意的是访问私有的构造方法时,我们需要调用Constructor的setAccessible方法,并设置为true,表示可以访问该私有构造方法。

4. getDeclaredConstructor()

这个方法获取类中已申明的所有构造方法对象数组,包括private的,具体操作方法与上述相同,这里不再赘述。

好了这样我们简单的介绍了用Constructor在反射中实例化对象的几种方式,说完构造方法,接下来我们来看下如何获取以及操作类中的字段属性

字段Field

如果我们要想操作类或对象中的字段,那么就需要用到Field对象。同样这个Field对象也是通过Class获取的,以下是Class获取Field对象的几种方法:

返回值方法名称说明
FieldgetDeclaredField(String name)获取指定name名称的(包含private修饰的)字段,不包括继承的字段
Field[]getDeclaredField()获取Class对象所表示的类或接口的所有(包含private修饰的)字段,不包括继承的字段
FieldgetField(String name)获取指定name名称、具有public修饰的字段,包含继承字段
Field[]getField()获取修饰符为public的字段,包含继承字段

为了演示方便我们再创建一个Student类,继承自上述的Person类:

public class Student extends Person {
    private String course;
    public int score;

    public String getCourse() {
        return course;
    }

    public void setCourse(String course) {
        this.course = course;
    }
}
1.getDeclaredField(String name)

获取指定名称的(包含private修饰的)字段,不包括从父类中继承的字段。

用这个方法,我们可以在Student类中,获取course和score字段,但是无法获取它父类Person中的字段。拿course举个例子:

Class<?> clazz = Class.forName("com.modoutech.designpattern.Student");
Student student = (Student) clazz.newInstance();
Field courseField = clazz.getDeclaredField("course");
courseField.setAccessible(true);
courseField.set(student,"语文");
System.out.print("course is "+student.getCourse());
-------------------------
输出结果:
无参数实例化
course is 语文
-------------------------

上述,我们先获取了Student的Class对象,并实例化一个Student对象,然后通过Class的getDeclaredField方法,并传入course这个字段名,然后获取一个Field对象,由于course这个字段为private,所以还需要调用他的setAccessible并传入true,来打开它的访问权限,之后就可以调用Field的set方法来设置这个字段的值,这个set方法需两个参数,第一个为需要设置的实例对象,第二个为需要设置的字段值。这样我们就完成了对course这个字段的赋值操作。

2. getDeclaredField()

这个方法用于获取Class对象所表示的类或接口的所有(包含private修饰的)字段,不包括继承的字段:

Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
    System.out.print(fields[i].getName()+"\n");
}
---------------------------------------------
输出结果:
course
score
---------------------------------------------

以上我们获取了字段数组,并打印了每个字段名,可以看到我们只获取到了Student类中的course和score字段,并没有他父类的字段。

3. getField(String name)

这个方法获取指定名称的public字段,包括父类中的也可以获取到。接下来我们试着获取下Student父类中的name字段:

Class<?> clazz = Class.forName("com.modoutech.designpattern.Student");
Student student = (Student) clazz.newInstance();
Field field = clazz.getField("name");
field.set(student,"张三");
System.out.print("学生名字:"+student.getName());

---------------------------------------------------------------------------
输出结果:
无参数实例化
学生名字:张三
--------------------------------------------------------------------------

可以看到我们成功获取了Student父类中的字段,并做了赋值操作。

4. getField()

这个方法时获取所有public字段,包括父类中的:

Field[] fields = clazz.getFields();
for (int i = 0; i < fields.length; i++) {
    System.out.print("字段名:"+fields[i].getName()+"\n");
}

---------------------------------------------------------------
输出结果:
字段名:score
字段名:name
---------------------------------------------------------------

可以看到,我们获取到了Student中声明的public字段score,同时还获取到了其父类中public字段name,至于对获取到的字段进行进一步的操作,和上面的一样,就不在赘述了。

在上面,可以看到,我们调用了Field 的set方法来设置字段的值,当然除了set方法外,Field还有其他的一些方法,部分方法如下:

方法返回值方法名称方法说明
Objectget(Object obj)返回指定对象上此 Field 表示的字段的值
Class<?>getType()返回一个 Class 对象,它标识了此Field 对象所表示字段的声明类型。
booleanisEnumConstant()如果此字段表示枚举类型的元素则返回 true;否则返回 false
StringtoGenericString()返回一个描述此 Field(包括其一般类型)的字符串
StringgetName()返回此 Field 对象表示的字段的名称
Class<?>getDeclaringClass()返回表示类或接口的 Class 对象,该类或接口声明由此 Field 对象表示的字段

Method(方法)

如果我们想要操作类中的某个方法,我们可以借助Method这个类,这个类是对类或接口中某个方法的描述,我们可以通过调用Class对象的以下方法来获取Method对象:

返回值方法名称说明
MethodgetDeclaredMethod(String name, Class<?>… parameterTypes)返回一个指定参数的Method对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。
Method[]getDeclaredMethod()返回 Method 对象的一个数组,这些对象反映此 Class 对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
MethodgetMethod(String name, Class<?>… parameterTypes)返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法。
Method[]getMethods()返回一个包含某些 Method 对象的数组,这些对象反映此 Class 对象所表示的类或接口(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的公共 member 方法。

以上简单的列举了获取Method对象的一些方法,具体获取方式就不演示了,和上面的操作步骤差不多。

接下来,我们来看下如何利用Method对象来调用类中方法。拿上面Person类举个例子,来看下如果通过Method来调用里面的run方法:

Class<?> clazz = Class.forName("com.modoutech.designpattern.Student");
Person person = (Person) clazz.newInstance();
Method method = clazz.getDeclaredMethod("run");
method.invoke(person);

---------------------------------------------
输出结果:
无参数实例化
跑步
---------------------------------------------

可以看到我们成功通过反射来调用了Person中的run方法,当然如果这个方法是private的,那么在调用该方法前需要调用Method的setAccessible方法,并传入true来打开该方法的访问权限。

当然除了上述介绍的一些方法外,Method还提供了其他的一些方法:

方法返回值方法名称方法说明
Objectinvoke(Object obj, Object… args)对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。
Class<?>getReturnType()返回一个 Class 对象,该对象描述了此 Method 对象所表示的方法的正式返回类型,即方法的返回类型
TypegetGenericReturnType()返回表示由此 Method 对象所表示方法的正式返回类型的 Type 对象,也是方法的返回类型。
Class<?>[]getParameterTypes()按照声明顺序返回 Class 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型。即返回方法的参数类型组成的数组
Type[]getGenericParameterTypes()按照声明顺序返回 Type 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型的,也是返回方法的参数类型
StringgetParameterTypes()按照声明顺序返回 Class 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型。即返回方法的参数类型组成的数组
Class<?>[]getName()以 String 形式返回此 Method 对象表示的方法名称,即返回方法的名称
booleanisVarArgs()判断方法是否带可变参数,如果将此方法声明为带有可变数量的参数,则返回 true;否则,返回 false。
StringtoGenericString()返回描述此 Method 的字符串,包括类型参数。

以上就是一些常用的方法,这里就不一一展开来讲了,如果想更详细的了解可以查阅官方API。


反射应用

1.反射main方法

/**
 * 该类用于测试main方法
 */
public class Student {

    public static void main(String[] args) {
        System.out.println("main方法执行了。。。");
    }
}
/**
 * 获取Student类的main方法、不要与当前的main方法搞混了
 */
public class Main {

    public static void main(String[] args) {
        try {
            //1、获取Student对象的字节码
            Class clazz = Class.forName("fanshe.main.Student");

            //2、获取main方法
             Method methodMain = clazz.getMethod("main", String[].class);//第一个参数:方法名称,第二个参数:方法形参的类型,
            //3、调用main方法
            // methodMain.invoke(null, new String[]{"a","b","c"});
             //第一个参数,对象类型,因为方法是static静态的,所以为null可以,第二个参数是String数组,这里要注意在jdk1.4时是数组,jdk1.5之后是可变参数
             //这里拆的时候将  new String[]{"a","b","c"} 拆成3个对象。。。所以需要将它强转。
             methodMain.invoke(null, (Object)new String[]{"a","b","c"});//方式一
            // methodMain.invoke(null, new Object[]{new String[]{"a","b","c"}});//方式二

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.反射运行配置文件内容

3.通过反射越过泛型检查

/*
 * 通过反射越过泛型检查
 * 
 * 例如:有一个String泛型的集合,怎样能向这个集合中添加一个Integer类型的值?
 */
public class Demo {
    public static void main(String[] args) throws Exception{
        ArrayList<String> strList = new ArrayList<>();
        strList.add("aaa");
        strList.add("bbb");

    //  strList.add(100);
        //获取ArrayList的Class对象,反向的调用add()方法,添加数据
        Class listClass = strList.getClass(); //得到 strList 对象的字节码 对象
        //获取add()方法
        Method m = listClass.getMethod("add", Object.class);
        //调用add()方法
        m.invoke(strList, 100);

        //遍历集合
        for(Object obj : strList){
            System.out.println(obj);
        }
    }
}
output:
aaa
bbb
100

http://www.niftyadmin.cn/n/5193583.html

相关文章

docker容器自启动

场景 当服务器关机重启后&#xff0c;docker容器每次都要去docker start 容器id 怎么可以下次让它自启动呢&#xff1f; 解决 先 # docker ps -a 查到之前启动过的容器id # docker update --restartalways 容器id重启后&#xff0c;reboot&#xff0c;就不用再单独去启动容…

Linux环境下C++ 接入OpenSSL

接上一篇&#xff1a;Windows环境下C 安装OpenSSL库 源码编译及使用&#xff08;VS2019&#xff09;_vs2019安装openssl_肥宝Fable的博客-CSDN博客 解决完本地windows环境&#xff0c;想赶紧在外网环境看看是否也正常。毕竟现在只是HelloWorld级别的&#xff0c;等东西多了&am…

Tomcat 基线安全加固操作

目录 账号管理、认证授权 日志配置 通信协议 设备其他安全要求 账号管理、认证授权 ELK-tomcat-01-01-01 编号 ELK-Tomcat-01-01-01 名称 为不同的管理员分配不同的账号 实施目的 应按照用户分配账号&#xff0c;避免不同用户间共享账号,提高安全性。 问题影响 …

Java读写excel文件

最近在学习某马的外卖项目&#xff0c;需要使用Java读写excel文件。 导入依赖 <dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>3.16</version> </dependency> <dependency><…

CMSIS-RTOS在stm32使用

目录&#xff1a; 一、安装和配置CMSIS_RTOS.1.打开KEIL工程&#xff0c;点击MANAGE RUN-TIME Environment图标。2.勾选CMSIS CORE和RTX.3.配置RTOS 时钟频率、任务栈大小和数量&#xff0c; 软件定时器. 二、CMSIS_RTOS内核启动和创建线程。1.包含头文件。2.内核初始化和启动。…

【done】剑指offer68:二叉树最近公共祖先

LCA&#xff08;lowest common ancestor&#xff09;问题 力扣&#xff0c;【二叉搜索树】https://leetcode.cn/problems/er-cha-sou-suo-shu-de-zui-jin-gong-gong-zu-xian-lcof/description/ 【普通二叉树】https://leetcode.cn/problems/er-cha-shu-de-zui-jin-gong-gong-zu…

【每日一题】—— C. Yarik and Array(Codeforces Round 909 (Div. 3))(贪心)

&#x1f30f;博客主页&#xff1a;PH_modest的博客主页 &#x1f6a9;当前专栏&#xff1a;每日一题 &#x1f48c;其他专栏&#xff1a; &#x1f534; 每日反刍 &#x1f7e1; C跬步积累 &#x1f7e2; C语言跬步积累 &#x1f308;座右铭&#xff1a;广积粮&#xff0c;缓称…

【C++】plog

GitHub地址&#xff1a;Plog - portable, simple and extensible C logging library 介绍 Plog is一个C日志库&#xff0c;旨在保持尽可能的简单、小巧和灵活。它被创建作为现有大型库的替代品&#xff0c;并提供了一些独特的功能&#xff0c;如CSV日志格式和宽字符串支持。 …