JSON踩BUG实录之parseObject| 8月更文挑战

695 阅读3分钟

问题发现

周二晚上开始上线

  • 测试环境验证通过
  • 预发环境验证通过
  • 开始生产上线 生产上线完成,通知测试进行验证。
    测试开始走测试用例,发现一直失败。
    紧急通知正在与同事打屁的我,怎么可能有问题呢?预发不是验证通过了么?绝对不会有问题,你再试试然并卵,真的有问题。
    oh my god。。。。哪里的问题呢?

问题排查

经过一顿排查(看日志记录),还真是发现了问题: 测试环境与预发环境的消息都是自己发送自己消费,不已经过消息序列化与反序列化的步骤,所以验证通过。生产环境要保证高可用,因此会将消息异步化,通过jmq来保证高可用,利用JSON序列化与反序列化。在这里发现了问题日志,主属性丢失了

问题代码:

基础事件定义:

 
  
/**
 * @Title: EventMsg
 * @Description: 事件消息
  */
public class EventMsg {
 
    private String id;
    private String eventType;
    private String content;
 
    public EventMsg(){}
 
    public EventMsg(String eventType, String content) {
        this.id = content;
        this.eventType = eventType;
        this.content = content;
    }
 
    public EventMsg(String eventType, String content, String id) {
        this.id = id;
        this.eventType = eventType;
        this.content = content;
    }
 
    public String getId() {
        return id;
    }
 
    public void setId(String id) {
        this.id = id;
    }
 
    public String getEventType() {
        return eventType;
    }
 
    public void setEventType(String eventType) {
        this.eventType = eventType;
    }
 
    public String getContent() {
        return content;
    }
 
    public void setContent(String content) {
        this.content = content;
    }
 
    @Override
    public String toString() {
        return "EventMsg{" +
                "eventType=" + eventType +
                ", content='" + content + '\'' +
                '}';
    }
}

继承基础事件定义的产品事件:

主要是拓展产品域事件的额外信息

 
/**
  * @date 2021/1/15
 **/
public class ItemEvent extends EventMsg {
    /**
     * 商品域内部事件类别
     * #{ItemEventTypeEum}
     */
    private String itemEventType;
 
    /**
     * 分发记录id
     */
    private Long distributId;
 
    public String getItemEventType() {
        return itemEventType;
    }
 
    public void setItemEventType(String itemEventType) {
        this.itemEventType = itemEventType;
    }
 
    public Long getDistributId() {
        return distributId;
    }
 
    public ItemEvent setDistributId(Long distributId) {
        this.distributId = distributId;
        return this;
    }
 
 
    public ItemEvent() {
    }
 
    public ItemEvent(String itemEventType, String content) {
        super(EventTypeEum.item.name(), content);
        this.itemEventType = itemEventType;
    }
 
    public ItemEvent(String itemEventType, String skuId, Long distributId) {
        super(EventTypeEum.item.name(), skuId);
        this.itemEventType = itemEventType;
        this.distributId = distributId;
    }
}

这里要注意新增了一个构造函数

    public ItemEvent(String itemEventType, String skuId, Long distributId) {
        super(EventTypeEum.item.name(), skuId);
        this.itemEventType = itemEventType;
        this.distributId = distributId;
    }

通过调用父级的完成基础事件初始化。此处注意content与产品消息的skuId

public EventMsg(String eventType, String content, String id) {
        this.id = id;
        this.eventType = eventType;
        this.content = content;
    }

单元测试:

`@Test`

`public` `void` `test_buildEventMsg() {`

`    ``String message = ``"{"content":"18468377001001000021","eventType":"item","id":"18468377001001000021","itemEventType":"UPDATED"}"``;`

 

`    ``EventMsg eventMsg = EventListener.buildEventMsg(message);`

`    ``Assert.assertEquals(``true``, eventMsg ``instanceof` `ItemEvent);`

`    ``Assert.assertEquals(``"18468377001001000021"``, eventMsg.getContent());`

`}`

 

`@Test`

`public` `void` `test_buildEventMsg_withDistributeId() {`

`    ``String message = ``"{"distributId":111,"content":"18468377001001000021","eventType":"item","id":"18468377001001000021","itemEventType":"UPDATED"}"``;`

 

`    ``EventMsg eventMsg = EventListener.buildEventMsg(message);`

`    ``Assert.assertEquals(``true``, eventMsg ``instanceof` `ItemEvent);`

`    ``Assert.assertEquals(``new` `Long(``111``), ((ItemEvent) eventMsg).getDistributId());`

`}`

结果发现content丢了。

image.png

为啥丢了呢?

1、当存在多个构造函数的情况(不存在空构造函数),会选择参数最多的一个。

2、同时会根据json匹配参数名称映射字段名称取值,如果参数名称与属性(包含父级)名称不一致,会发生属性获取失败的情况

根据如上第一条说明,发现实际调用了第二个构造函数

public ItemEvent(String itemEventType, String content)
public ItemEvent(String itemEventType, String skuId, Long distributId)

再根据第二条检查,发现实际属性应该是content, 传入的是skuId。parseObject会通过参数名称自动反射属性,这时发现没有setSkuId的属性赋值方法,丢弃。这里已经看明白为啥content没值了吧。

解决方案呢:

  • 符合Java Bean规范,存在空构造函数
  • 调整构造函数参数与属性保持一致

这个坑还真没注意过。大家可以看下parseObject的代码跟一下。