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字段。这可能会引起混淆,甚至导致字段值设置错误。
场景三:布


1503

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



