1. 从一次诡异的“set property error”说起
那天下午,我正悠闲地喝着咖啡,突然线上报警响了。点开一看,一个熟悉的异常堆栈映入眼帘:com.alibaba.fastjson.JSONException: set property error, com.example.User#userName。我心里咯噔一下,这个服务已经稳定运行好几个月了,最近也没改过用户模块的代码,怎么会突然报这个错?
我赶紧点开报错的代码,发现是一个简单的用户信息反序列化操作。代码看起来人畜无害,就是从一个JSON字符串还原成User对象。User类是我自己写的,我记得清清楚楚,里面只有firstName和lastName两个字段,外加一个拼接全名的getUserName()方法。这个getUserName()只是个计算属性,为了方便前端显示全名而已,它根本没有对应的userName字段,也没有setUserName()方法。
这就奇怪了,FastJSON怎么会试图去设置一个根本不存在的属性呢?难道是我的JSON数据里混进了奇怪的字段?我检查了传入的JSON字符串,干干净净,只有firstName和lastName。那问题到底出在哪?
我带着满脑子问号开始了调试。当代码执行到JSON.parseObject()时,我一步步跟进FastJSON的内部逻辑。终于,在解析过程中,我看到了关键的一幕:FastJSON的JavaBeanDeserializer正在扫描User类的所有方法。当它看到getUserName()这个方法时,它的“小脑袋”开始运转了——按照JavaBean的命名规范,get开头的方法通常对应着一个属性,去掉get并把首字母小写,不就是userName吗?
于是,FastJSON“理所当然”地认为,User类有一个叫做userName的属性。当它尝试为这个“属性”赋值时,却发现找不到对应的字段,也找不到setUserName()方法。它不死心,又尝试用反射直接设置字段值,当然也失败了。最后,它只能无奈地抛出那个让我头疼的set property error。
原来,罪魁祸首就是这个看似无辜的getUserName()方法。FastJSON在反序列化时,会主动解析类中以get、is、set开头的方法,并根据方法名推断出属性名。这个机制本意是为了更好地支持JavaBean规范,但在某些情况下,却成了引发异常的陷阱。
2. FastJSON的“读心术”:它如何解析你的getter方法
要理解这个陷阱,我们得先看看FastJSON是怎么“读懂”你的类的。当你把一个JSON字符串交给FastJSON,让它反序列化成某个类的对象时,FastJSON会做这么几件事:
首先,它会创建一个JavaBeanDeserializer(JavaBean反序列化器)。这个反序列化器的任务,就是搞清楚目标类到底有哪些属性,以及每个属性该怎么赋值。
接下来,反序列化器会扫描目标类的所有方法和字段。这里有个关键点:FastJSON不仅看字段,更看重方法。它会特别关注那些符合getter/setter命名规范的方法。具体的解析规则是这样的:
- 如果一个方法以
get开头,且方法名长度大于3,那么FastJSON会认为这个方法对应一个属性。它会去掉get前缀,然后把剩余部分的首字母小写,作为属性名。比如getUserName()会被解析出userName属性。 - 如果一个方法以
is开头,且方法名长度大于2,且返回类型是boolean或者Boolean,那么同样会被当作getter方法。比如isActive()会被解析出active属性。 - 如果一个方法以
set开头,且方法名长度大于3,那么会被当作setter方法。比如setUserName(String name)会被关联到userName属性。
这个过程在FastJSON的源码里,主要体现在com.alibaba.fastjson.util.TypeUtils类的computeGetters()和buildBeanInfo()方法中。我翻看过这些代码,里面确实有对方法名的解析逻辑。
那么,FastJSON为什么要这么设计呢?这其实是为了更好地兼容JavaBean规范。在Java的世界里,一个标准的JavaBean通常用私有的字段和公共的getter/setter方法来定义属性。有些类可能没有显式声明某个字段,但通过getter/setter方法暴露了该属性的读写能力。FastJSON的这种解析方式,让它能够处理更多样化的JavaBean。
但是,这种“聪明”的解析也带来了问题。就像我遇到的getUserName(),它只是一个工具方法,用来计算并返回全名,并不是一个真正的属性getter。可FastJSON不管这些,它只认方法名。只要方法名符合规范,它就认为那背后一定有个属性。
更让人头疼的是,这种解析是单向的。FastJSON能通过getUserName()推断出有userName属性,但在反序列化时,它需要的是setUserName()方法来设置值,或者直接有userName字段。如果两者都没有,它就会尝试用反射直接设置字段值,失败后就抛出set property error。
3. 不只是getUserName:那些年我们踩过的坑
getUserName()引发的错误只是个开始。在实际开发中,类似的陷阱还有很多。我整理了几个常见的场景,你可能也在其中某个地方栽过跟头。
场景一:工具方法被误判
这是最典型的场景。比如下面这个Order类:
public class Order {
private List<Item> items;
// 计算订单总金额的方法
public BigDecimal getTotalAmount() {
if (items == null || items.isEmpty()) {
return BigDecimal.ZERO;
}
return items.stream()
.map(Item::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// 判断订单是否有效的方法
public boolean isValid() {
return items != null && !items.isEmpty();
}
}
这里的getTotalAmount()和isValid()都是纯粹的计算方法,它们不依赖任何totalAmount或valid字段。但在FastJSON眼里,这两个方法就是totalAmount和valid属性的getter。当反序列化一个Order对象时,如果JSON里碰巧有totalAmount或valid字段,FastJSON就会尝试设置这些“属性”,然后报错。
场景二:继承带来的混乱
继承关系也会让问题变得更复杂。看下面这个例子:
public class BaseEntity {
private Long id;
// 一个通用的状态检查方法
public boolean isActive() {
return id != null && id > 0;
}
}
public class User extends BaseEntity {
private String name;
private boolean active; // 这里有个同名的字段!
public void setActive(boolean active) {
this.active = active;
}
}
BaseEntity里的isActive()是个计算方法,User类自己又定义了一个boolean类型的active字段和对应的setter。当FastJSON反序列化User对象时,它会发现两个“active”属性:一个来自BaseEntity的isActive()方法,一个来自User类的active字段。这可能会引起混淆,甚至导致字段值设置错误。
场景三:布尔属性的特殊处理
布尔类型的属性在JavaBean里有点特殊。按照规范,布尔属性的getter应该是isXxx()而不是getXxx()。但有些开发者会混用,或者有些方法只是恰好以is开头,并不是真正的getter。
public class Config {
private String name;
// 这个方法只是检查name是否为空
public boolean isEmpty() {
return name == null || name.trim().isEmpty();
}
}
isEmpty()会被FastJSON解析为empty属性。如果JSON里有empty字段,FastJSON就会尝试设置,然后失败。
场景四:内部类的方法泄露
如果你用过Lombok的@Data注解,可能遇到过更隐蔽的问题。@Data会自动生成getter、setter、equals、hashCode和toString方法。其中,hashCode()方法在某些情况下会被FastJSON误判。
我见过一个真实的案例:一个内部类用了@Data,它的hashCode()方法被FastJSON解析,认为有一个叫hashCode的属性。当反序列化时,如果JSON里有hashCode这个键,就会报错。
4. 深入异常堆栈:set property error的背后发生了什么
当FastJSON抛出set property error时,到底发生了什么?我们来看看异常堆栈的深处。
以最开始的例子来说,完整的异常信息是这样的:
com.alibaba.fastjson.JSONException: set property error, com.example.User#userName
at com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(FieldDeserializer.java:110)
at com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer.parseField(DefaultFieldDeserializer.java:86)
... 更多堆栈信息
关键在FieldDeserializer.setValue()这个方法。我研究过FastJSON 1.2.x版本的源码,这个方法的逻辑大致是这样的:
public void setValue(Object object, Object value) throws IOException {
if (method != null) {
// 如果有setter方法,优先用setter
method.invoke(object, value);
} else if (field != null) {
// 如果没有setter但有字段,直接设置字段值
field.set(object, value);
} else {
// 既没有setter也没有字段,只能抛异常了
throw new JSONException("set property error, " + this.fieldInfo.name);
}
}
当FastJSON认为userName是User类的一个属性时,它会创建一个FieldDeserializer实例。这个实例会尝试找到设置该属性的方法:首先找setUserName()方法,如果找不到,就找userName字段。如果两者都没有,那么method和field都是null。当真正要设置值时,就会进入上面的else分支,抛出我们看到的异常。
那么,FastJSON是怎么决定哪些方法要解析成属性的呢?这就要看JavaBeanDeserializer的构建过程了。在创建反序列化器时,FastJSON会调用TypeUtils.buildBeanInfo()来收集类的属性信息。这个过程包括:
- 扫描所有字段,直接作为属性
- 扫描所有公共方法,找出符合getter/setter规范的方法
- 对找到的getter方法,推导出属性名
- 尝试为每个属性找到匹配的setter方法或字段
问题就出在第2步和第3步。FastJSON的判断标准很简单:方法名以get、is、set开头,并且满足一定的长度要求。它不会检查这个方法是不是真正的属性访问器,也不会检查方法内部有没有依赖其他字段。
这种设计在大多数情况下是合理的,毕竟FastJSON不可能读懂每个方法的逻辑。但这也意味着,开发者需要自己注意方法命名,避免让FastJSON产生误解。
5. 如何避免掉入这个陷阱:编码规范与最佳实践
知道了问题的根源,我们就可以有针对性地避免它。根据我的经验,下面这几条实践准则能帮你避开大部分FastJSON反序列化的坑。
第一条:规范方法命名,区分属性访问器和工具方法
这是最根本的解决方案。如果你的方法不是真正的getter/setter,就不要用get、is、set开头。比如前面的例子,getUserName()可以改成buildFullName()或computeUserName(),isValid()可以改成checkValidity()。
// 不推荐:容易被误判为属性getter
public String getUserName() {
return firstName + " " + lastName;
}
// 推荐:明确表明这是计算方法
public String computeFullName() {
return firstName + " " + lastName;
}
// 或者用更清晰的命名
public String buildDisplayName() {
return firstName + " " + lastName;
}
对于布尔类型的方法更要注意。只有真正的属性访问器才用isXxx()命名,检查方法可以用checkXxx()、hasXxx()、canXxx()等前缀。
第二条:使用@JSONField注解明确指导FastJSON
FastJSON提供了@JSONField注解,可以精确控制序列化和反序列化的行为。对于容易被误解的方法,可以用@JSONField(serialize=false, deserialize=false)来告诉FastJSON:这个方法不是属性。
public class User {
private String firstName;
private String lastName;
@JSONField(serialize = false, deserialize = false)
public String getUserName() {
return firstName + " " + lastName;
}
}
这样,FastJSON在扫描方法时,会忽略这个注解的方法,不会把它当作属性getter。
第三条:保持字段和方法的匹配
如果你确实需要一个属性,并且提供了getter方法,那么最好也提供对应的setter方法或字段。这样FastJSON在反序列化时就能正确设置值。
public class User {
private String firstName;
private String lastName;
// 如果要有userName属性,就应该有对应的字段
private String userName;
public String getUserName() {
if (userName == null) {
userName = firstName + " " + lastName;
}
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
当然,这可能会引入数据一致性问题(比如firstName改了,但userName没更新),需要根据实际情况权衡。
第四条:使用自定义序列化/反序列化器
对于特别复杂的类,你可以为它实现自定义的ObjectSerializer和ObjectDeserializer。这样你就完全控制了序列化和反序列化的过程,不会受FastJSON自动解析的影响。
public class UserSerializer implements ObjectSerializer {
@Override
public void write(JSONSerializer serializer, Object object,
Object fieldName, Type fieldType, int features) {
// 自定义序列化逻辑
}
}
public class UserDeserializer implements ObjectDeserializer {
@Override
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
// 自定义反序列化逻辑
}
@Override
public int getFastMatchToken() {
return JSONToken.LBRACE;
}
}
// 注册自定义处理器
ParserConfig.getGlobalInstance().putDeserializer(User.class, new UserDeserializer());
第五条:考虑使用其他JSON库
如果FastJSON的这个特性经常给你带来麻烦,而你的项目又允许更换JSON库,那么可以考虑切换到其他库。比如Jackson和Gson在处理这类问题时更加保守,它们通常只认字段和标准的getter/setter,不会随意解析方法名。
不过,每个库都有自己的特点,切换前要评估好兼容性和性能影响。
6. 当异常已经发生:快速定位与修复方案
尽管我们尽力避免,但有时候异常还是会发生。可能是老代码遗留的问题,可能是第三方库的方法命名不规范,也可能是新同事不了解这个陷阱。当set property error真的出现在线上时,我们需要快速定位和修复。
第一步:读懂异常信息
FastJSON的异常信息其实很有用。set property error, com.example.User#userName这句话告诉了我们两件事:
- 错误发生在设置属性时
- 出问题的属性是
userName,所在的类是com.example.User
有了这些信息,我们就可以直奔User类,查找所有和userName相关的方法和字段。
第二步:定位问题方法
在User类中,搜索包含userName的方法。重点找:
- 以
getUserName开头的方法 - 以
isUserName开头的方法(如果是布尔类型) - 以
setUserName开头的方法
同时,也检查一下有没有userName字段。如果只有getter没有setter和字段,那很可能就是问题所在。
第三步:临时修复方案
如果问题紧急,需要快速恢复服务,可以考虑这几个临时方案:
- 过滤JSON字段:在反序列化前,从JSON中移除有问题的字段
JSONObject jsonObject = JSON.parseObject(jsonString);
jsonObject.remove("userName"); // 移除会导致错误的字段
User user = JSON.parseObject(jsonObject.toJSONString(), User.class);
- 使用@JSONType注解:在类级别忽略未知属性
@JSONType(ignoreUnknown = true)
public class User {
// 类定义
}
这样FastJSON在反序列化时,遇到没有对应setter或字段的属性,会直接忽略而不是报错。
- 自定义反序列化:临时为问题属性添加一个空的setter方法
public class User {
// 临时添加,什么也不做
public void setUserName(String userName) {
// 忽略这个属性
}
}
第四步:彻底修复方案
临时方案只是权宜之计,彻底解决问题还是要从代码层面入手:
- 重命名问题方法:如果方法不是真正的getter,就改个名字
// 修改前
public String getUserName() {
return firstName + " " + lastName;
}
// 修改后
public String computeFullName() {
return firstName + " " + lastName;
}
- 添加@JSONField注解:如果方法不能改名,就用注解明确标记
@JSONField(serialize = false, deserialize = false)
public String getUserName() {
return firstName + " " + lastName;
}
- 补全缺失的setter或字段:如果这确实应该是一个属性,就补全它
public class User {
private String userName; // 添加字段
public String getUserName() {
if (userName == null) {
userName = firstName + " " + lastName;
}
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
第五步:编写测试用例
修复之后,一定要编写测试用例,确保同样的问题不会再次出现。
@Test
public void testUserDeserialization() {
String json = "{\"firstName\":\"张\",\"lastName\":\"三\",\"userName\":\"张三\"}";
// 应该正常反序列化,不报错
User user = JSON.parseObject(json, User.class);
assertNotNull(user);
assertEquals("张", user.getFirstName());
assertEquals("三", user.getLastName());
}
同时,也可以考虑在持续集成中添加静态代码检查,防止类似问题代码被提交。比如可以用Checkstyle或自定义的规则,检查是否有非标准getter/setter方法。
7. 不仅仅是getter:其他可能引发set property error的场景
虽然getXXX方法被误判是最常见的触发原因,但set property error还可能在其他场景下出现。了解这些场景,能帮助我们在遇到问题时更快定位。
场景一:字段类型不匹配
这是另一个常见的错误来源。当JSON中的值类型与Java字段类型不兼容时,FastJSON在尝试设置值时会失败。
public class Product {
private int id;
private String name;
}
// JSON中的id是字符串,但Product.id是int类型
String json = "{\"id\":\"123\", \"name\":\"手机\"}";
Product product = JSON.parseObject(json, Product.class); // 可能报错
FastJSON会尝试将字符串"123"转换为int,通常这能成功。但如果字符串是"abc",转换就会失败,可能抛出类型转换异常,在某些情况下也可能表现为set property error。
场景二:final字段的赋值问题
final字段只能在构造方法中初始化,不能通过反射修改。如果FastJSON尝试给final字段赋值,就会失败。
public class Config {
private final String version = "1.0";
private String name;
}
// FastJSON尝试设置version字段,但它是final的
String json = "{\"version\":\"2.0\", \"name\":\"test\"}";
Config config = JSON.parseObject(json, Config.class); // 可能报错
场景三:访问权限限制
私有字段在没有setter方法的情况下,FastJSON会尝试通过反射直接设置值。但如果安全管理器限制了反射访问,就会失败。
public class SecureData {
private String secretKey;
// 没有setter方法
}
// 如果安全管理器禁止访问私有字段,反序列化会失败
场景四:枚举类型的特殊处理
枚举类型的反序列化也有自己的坑。如果JSON中的值不能映射到枚举常量,就会出错。
public enum Status {
ACTIVE, INACTIVE
}
public class Order {
private Status status;
}
// JSON中的status值不是有效的枚举常量
String json = "{\"status\":\"PENDING\"}";
Order order = JSON.parseObject(json, Order.class); // 报错
在FastJSON的某些版本中,这可能表现为set property error。
场景五:内部类和匿名类的问题
内部类和匿名类在反射访问时可能会有特殊行为,导致字段设置失败。
public class Outer {
public class Inner {
private String value;
}
}
// 反序列化内部类实例时可能需要特殊处理
场景六:FastJSON版本差异
不同版本的FastJSON在处理某些边缘情况时行为可能不同。比如:
- 早期版本可能对方法名的解析更宽松
- 某些版本修复了相关bug,但又引入了新的问题
- 黑名单/白名单机制的变化可能影响反序列化行为
如果你从网络搜索结果中看到,有人提到在修改字段类型后遇到set property error(如<url_content3>中提到的Issue),这可能与FastJSON的缓存机制有关。FastJSON会缓存类的反序列化信息,如果类结构发生变化(如字段类型改变),但缓存没有更新,就可能导致类型不匹配的错误。
8. 从FastJSON的设计角度思考:为什么会有这样的行为?
理解了各种错误场景后,我们不妨退一步,从FastJSON设计者的角度思考:为什么FastJSON要这样设计?这种设计带来了什么好处,又付出了什么代价?
设计的初衷:更好的JavaBean支持
FastJSON的设计目标之一是提供高性能且易用的JSON处理能力。在Java生态中,JavaBean是一种广泛使用的编程约定。很多框架和库都依赖JavaBean规范来操作对象属性。
FastJSON通过解析getter/setter方法来推断属性,是为了:
- 支持只读/只写属性:有些属性只有getter没有setter(只读),或只有setter没有getter(只写)
- 支持计算属性:有些属性的值是通过计算得到的,没有对应的字段
- 更好的兼容性:兼容那些符合JavaBean规范但字段不公开的类
这种设计让FastJSON能够处理更复杂的对象结构,而不必要求每个属性都有对应的字段。
性能考虑:速度与灵活的权衡
FastJSON以高性能著称。为了达到高性能,它采用了一些优化策略:
- 缓存反序列化信息:第一次反序列化某个类时,FastJSON会扫描类结构并缓存起来,下次直接使用
- 快速方法解析:通过简单的方法名前缀匹配来识别getter/setter,而不是复杂的逻辑分析
- 代码生成:某些版本使用ASM动态生成字节码来优化序列化/反序列化
这些优化在大多数情况下很有效,但也带来了副作用:方法名解析过于简单,容易误判。
安全考量:一个被忽视的角度
从安全角度看,FastJSON的这种行为实际上带来了一定的风险。攻击者可以构造特殊的JSON,利用类中的某些方法作为“跳板”,访问或修改本不应暴露的数据。
虽然这不是传统意义上的安全漏洞,但在某些场景下可能被利用。比如,如果一个类有一个getInternalData()方法,攻击者就可以在JSON中尝试设置internalData属性,看看会发生什么。
与其他JSON库的对比
为了更全面理解FastJSON的设计选择,我们可以对比一下其他流行JSON库的行为:
| 特性 | FastJSON | Jackson | Gson |
|---|---|---|---|
| 默认通过getter推断属性 | 是 | 是 | 否 |
| 默认通过setter推断属性 | 是 | 是 | 否 |
| 对非标准getter的处理 | 可能误判 | 可配置 | 忽略 |
| 性能特点 | 极高 | 高 | 中等 |
| 灵活性 | 中等 | 高 | 高 |
从对比可以看出,FastJSON在性能和简单性上做了权衡。它的设计假设开发者会遵循JavaBean规范,当规范被打破时,就可能出现问题。
给开发者的启示
理解了FastJSON的设计思路后,我们可以得到几点启示:
- 遵守约定优于配置:如果你使用FastJSON,最好严格遵守JavaBean规范。不符合规范的方法不要用get/set/is前缀。
- 了解工具的特性:每个工具都有自己的特点和局限,了解这些能帮你更好地使用工具。
- 在灵活性和安全性间权衡:FastJSON的灵活性带来便利,也可能带来风险。在安全要求高的场景,可能需要更严格的控制。
9. 实战演练:一步步解决一个真实案例
现在,让我们通过一个完整的实战案例,把前面讲的知识点串联起来。假设我们接手了一个老项目,里面有一个类经常在反序列化时报错,我们需要找到问题并修复它。
问题描述
项目中的Order类在从JSON反序列化时,偶尔会抛出set property error, com.example.Order#discounted异常。Order类的代码如下:
public class Order {
private Long id;
private BigDecimal amount;
private List<OrderItem> items;
// 计算订单是否享受折扣
public boolean isDiscounted() {
return amount.compareTo(new BigDecimal("1000")) >= 0;
}
// 计算订单总税额
public BigDecimal getTaxAmount() {
if (items == null) {
return BigDecimal.ZERO;
}
return items.stream()
.map(item -> item.getPrice().multiply(item.getQuantity())
.multiply(new BigDecimal("0.13")))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// 标准的getter和setter
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public BigDecimal getAmount() { return amount; }
public void setAmount(BigDecimal amount) { this.amount = amount; }
public List<OrderItem> getItems() { return items; }
public void setItems(List<OrderItem> items) { this.items = items; }
}
报错的JSON数据是这样的:
{
"id": 12345,
"amount": 1500.00,
"items": [...],
"discounted": true
}
第一步:分析问题
从异常信息看,问题出在discounted属性上。查看Order类,我们发现有一个isDiscounted()方法,但没有discounted字段,也没有setDiscounted()方法。
很明显,FastJSON将isDiscounted()方法解析为discounted属性的getter。当JSON中有discounted字段时,FastJSON尝试设置这个属性,但找不到对应的setter或字段,于是报错。
getTaxAmount()方法也有同样的问题,只是当前的JSON中没有taxAmount字段,所以没有触发错误。
第二步:制定解决方案
我们有几种修复方案可选:
- 方案A:重命名
isDiscounted()和getTaxAmount()方法,避免使用get/is前缀 - 方案B:添加
@JSONField(serialize=false, deserialize=false)注解 - 方案C:添加
discounted字段和setDiscounted()方法 - 方案D:在类上添加
@JSONType(ignoreUnknown=true)注解
考虑到这是老代码,可能有其他部分依赖这些方法名,我们选择方案B作为最小影响的修复方式。
第三步:实施修复
修改Order类,为容易被误判的方法添加注解:
public class Order {
private Long id;
private BigDecimal amount;
private List<OrderItem> items;
@JSONField(serialize = false, deserialize = false)
public boolean isDiscounted() {
return amount.compareTo(new BigDecimal("1000")) >= 0;
}
@JSONField(serialize = false, deserialize = false)
public BigDecimal getTaxAmount() {
if (items == null) {
return BigDecimal.ZERO;
}
return items.stream()
.map(item -> item.getPrice().multiply(item.getQuantity())
.multiply(new BigDecimal("0.13")))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// 其他getter/setter保持不变
}
第四步:测试验证
编写测试用例,验证修复是否有效:
public class OrderTest {
@Test
public void testDeserializationWithDiscountedField() {
String json = "{\"id\":12345,\"amount\":1500.00,\"discounted\":true}";
// 修复前这里会抛异常,修复后应该正常
Order order = JSON.parseObject(json, Order.class);
assertNotNull(order);
assertEquals(12345L, order.getId().longValue());
assertEquals(new BigDecimal("1500.00"), order.getAmount());
// discounted字段被忽略,不会影响反序列化
}
@Test
public void testSerialization() {
Order order = new Order();
order.setId(12345L);
order.setAmount(new BigDecimal("1500.00"));
String json = JSON.toJSONString(order);
// 验证discounted和taxAmount字段不会被序列化
assertFalse(json.contains("discounted"));
assertFalse(json.contains("taxAmount"));
assertTrue(json.contains("id"));
assertTrue(json.contains("amount"));
}
}
第五步:预防措施
修复当前问题后,我们还需要防止类似问题再次出现:
- 代码审查:在代码审查时,注意检查非标准getter/setter方法
- 静态分析:配置Checkstyle或SpotBugs规则,检测可能被误判的方法
- 文档记录:在项目文档中记录这个问题的原因和解决方案
- 团队培训:在团队内部分享这个案例,提高大家对FastJSON特性的认识
第六步:考虑长期重构
如果条件允许,可以考虑更彻底的重构:
- 引入DTO模式:专门用于序列化/反序列化的数据传输对象
- 统一方法命名规范:制定团队统一的方法命名规范
- 考虑更换JSON库:如果问题频繁发生,评估切换到其他JSON库的成本和收益
通过这个完整的实战案例,我们可以看到,解决FastJSON的set property error不仅仅是修复一个异常,更是一个理解工具特性、改进代码质量的过程。每个解决方案都有其适用场景,需要根据实际情况权衡选择。

128

被折叠的 条评论
为什么被折叠?



