序列化字段增加问题
周五在做一个推送需求的时候出现了一个问题。需求大致是讲一个Im通信中的消息通信的实体类存入缓存的时候,但在Redis里面多了几个字段,导致之后序列化出来的时候,属性增多无法转化为实体,代码报错。
先说解决办法由于引用的是jar包,无法修改实体类而且用的是一套相对成熟的sdk所以不推荐改动源码。使用JsonObject先进行一遍序列化,来让JsonObject序列化和反序列化的时候来对无信息的属性进行过滤以及多余的属性进行剔除。这样就可以解决从redis中的实体再转化为服务中的实体报错的问题。
先看下这个有问题的实体类
/** * IoSession包装类,集群时 将此对象存入表中 */ public class CIMSession implements Serializable { private transient static final long serialVersionUID = 1L; public transient static String HOST = "HOST"; public transient static final int STATE_ENABLED = 0; public transient static final int STATE_DISABLED = 1; public transient static final int APNS_ON = 1; public transient static final int APNS_OFF = 0; public transient static String CHANNEL_IOS = "ios"; public transient static String CHANNEL_ANDROID = "android"; public transient static String CHANNEL_WINDOWS = "windows"; private transient Channel session; /** * 数据库主键ID */ private Long id; /** * session绑定的用户账号 */ private String account; /** * session在本台服务器上的ID */ private String nid; /** * 客户端ID (设备号码+应用包名),ios为deviceToken */ private String deviceId; /** * session绑定的服务器IP */ private String host; /** * 终端设备类型 */ private String channel; /** * 终端设备型号 */ private String deviceModel; /** * 终端应用版本 */ private String clientVersion; /** * 终端系统版本 */ private String systemVersion; /** * 登录时间 */ private Long bindTime; /** * 经度 */ private Double longitude; /** * 维度 */ private Double latitude; /** * 位置 */ private String location; /** * APNs推送状态 */ private int apns; /** * 状态 */ private int state; public CIMSession(Channel session) { this.session = session; this.nid = session.id().asShortText(); } public CIMSession() { } public void setSession(Channel session) { this.session = session; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getAccount() { return account; } public void setAccount(String account) { this.account = account; setAttribute(CIMConstant.KEY_ACCOUNT, account); } public Double getLongitude() { return longitude; } public void setLongitude(Double longitude) { this.longitude = longitude; } public Double getLatitude() { return latitude; } public void setLatitude(Double latitude) { this.latitude = latitude; } public String getLocation() { return location; } public void setLocation(String location) { this.location = location; } public String getNid() { return nid; } public void setNid(String nid) { this.nid = nid; } public String getDeviceId() { return deviceId; } public String getChannel() { return channel; } public void setChannel(String channel) { this.channel = channel; } public String getDeviceModel() { return deviceModel; } public void setDeviceModel(String deviceModel) { this.deviceModel = deviceModel; } public void setDeviceId(String deviceId) { this.deviceId = deviceId; } public String getHost() { return host; } public Long getBindTime() { return bindTime; } public void setBindTime(Long bindTime) { this.bindTime = bindTime; } public String getClientVersion() { return clientVersion; } public void setClientVersion(String clientVersion) { this.clientVersion = clientVersion; } public String getSystemVersion() { return systemVersion; } public void setSystemVersion(String systemVersion) { this.systemVersion = systemVersion; } public void setHost(String host) { this.host = host; } public int getApns() { return apns; } public void setApns(int apns) { this.apns = apns; } public int getState() { return state; } public void setState(int state) { this.state = state; } public Channel getSession() { return session; } public void setAttribute(String key, Object value) { if (session != null) { session.attr(AttributeKey.valueOf(key)).set(value); } } public boolean containsAttribute(String key) { if (session != null) { return session.hasAttr(AttributeKey.valueOf(key)); } return false; } public Object getAttribute(String key) { if (session != null) { return session.attr(AttributeKey.valueOf(key)).get(); } return null; } public void removeAttribute(String key) { if (session != null) { session.attr(AttributeKey.valueOf(key)).set(null); } } public SocketAddress getRemoteAddress() { if (session != null) { return session.remoteAddress(); } return null; } public void write(Transportable data) { if (session == null || !session.isActive()) { return; } session.writeAndFlush(data); } public boolean isConnected() { return (session != null && session.isActive()) || state == STATE_ENABLED; } public void closeNow() { if (session != null) { session.close(); } } public void closeOnFlush() { if (session != null) { session.close(); } } public boolean isIOSChannel() { return Objects.equals(channel, CHANNEL_IOS); } public boolean isAndroidChannel() { return Objects.equals(channel, CHANNEL_ANDROID); } public boolean isWindowsChannel() { return Objects.equals(channel, CHANNEL_WINDOWS); } public boolean isApnsEnable() { return Objects.equals(apns, APNS_ON); } @Override public int hashCode() { return getClass().hashCode(); } @Override public boolean equals(Object o) { if (o instanceof CIMSession) { CIMSession target = (CIMSession) o; return Objects.equals(target.deviceId, deviceId) && Objects.equals(target.nid, nid) && Objects.equals(target.host, host); } return false; } public byte[] getProtobufBody() { SessionProto.Model.Builder builder = SessionProto.Model.newBuilder(); if (id != null) { builder.setId(id); } if (account != null) { builder.setAccount(account); } if (nid != null) { builder.setNid(nid); } if (deviceId != null) { builder.setDeviceId(deviceId); } if (host != null) { builder.setHost(host); } if (channel != null) { builder.setChannel(channel); } if (deviceModel != null) { builder.setDeviceModel(deviceModel); } if (clientVersion != null) { builder.setClientVersion(clientVersion); } if (systemVersion != null) { builder.setSystemVersion(systemVersion); } if (bindTime != null) { builder.setBindTime(bindTime); } if (longitude != null) { builder.setLongitude(longitude); } if (latitude != null) { builder.setLatitude(latitude); } if (location != null) { builder.setLocation(location); } builder.setState(state); builder.setApns(apns); return builder.build().toByteArray(); } public static CIMSession decode(byte[] protobufBody) throws InvalidProtocolBufferException { if(protobufBody == null) { return null; } SessionProto.Model proto = SessionProto.Model.parseFrom(protobufBody); CIMSession session = new CIMSession(); session.setId(proto.getId()); session.setApns(proto.getApns()); session.setBindTime(proto.getBindTime()); session.setChannel(proto.getChannel()); session.setClientVersion(proto.getClientVersion()); session.setDeviceId(proto.getDeviceId()); session.setDeviceModel(proto.getDeviceModel()); session.setHost(proto.getHost()); session.setLatitude(proto.getLatitude()); session.setLongitude(proto.getLongitude()); session.setLocation(proto.getLocation()); session.setNid(proto.getNid()); session.setSystemVersion(proto.getSystemVersion()); session.setState(proto.getState()); session.setAccount(proto.getAccount()); return session; } }
明显可以看到在转JsonObeject的时候,多了一些实体类中没有的属性,这也就是之前代码运行报错的原因。
刚刚开始遇到这个问题的时候,思考的是为什么没有属性依然可以多出这些字段。至于是多了“androidChannel”这种玩意,那肯定从代码里面有这些的地方开始着手。
可以看到的是在最上面定义了几个静态变量 好像是有这些有关
然后找到了如下的几个方法。
public boolean isIOSChannel() { return Objects.equals(channel, CHANNEL_IOS); } public boolean isAndroidChannel() { return Objects.equals(channel, CHANNEL_ANDROID); } public boolean isWindowsChannel() { return Objects.equals(channel, CHANNEL_WINDOWS); }
把这几个方法注释掉之后就不会有这个问题了。简单做个测试
public class Test implements Serializable { /** * 测试Id */ private int id; /** * 终端设备类型 */ private String channel; public transient static String CHANNEL_ANDROID = "android"; public transient static String CHANNEL_WINDOWS = "windows"; //get/set/toString public int getId() { return id; } public void setId(int id) { this.id = id; } public String getChannel() { return channel; } public void setChannel(String channel) { this.channel = channel; } @Override public String toString() { return "Test{" + "id=" + id + ", channel='" + channel + '\'' + '}'; } public boolean isAndroidChannel() { System.out.println("进入了这个isAndroidChannel"); return Objects.equals(channel, CHANNEL_ANDROID); } public boolean isWindowsChannel() { System.out.println("进入了这个isWindowsChannel"); return Objects.equals(channel, CHANNEL_WINDOWS); } public static void main(String[] args) { Test test = new Test(); test.setId(1); System.out.println("序列化前:"+test.toString()); String s = JSONObject.toJSONString(test); System.out.println("序列化后:"+s); } }
//运行结果:
序列化前:Test{id=1, channel='null'}
进入了这个isAndroidChannel
进入了这个isWindowsChannel
序列化后:{"androidChannel":false,"id":1,"windowsChannel":false}
channel是否为null都会产生这两个额外的字段。那么问题就来了,为什么在转JsonObject的时候会触发这两个方法呢?那只能打一下Debug了,利用IDEA的JPDA能力了。
一个一个思路打过来看。
首先需要获取writer。而writer是通过config得到的。clazz相当于得到了有关Test的一切那么
也就是说在JSONSerializer的时候就出现了这个问题。但Debug跳不动了(native方法?没找到)。
逻辑上:fastjson和jackson在把对象序列化成json字符串的时候,是通过反射遍历出该类中的所有getter方法,得到getZZH和isSuccess,然后根据JavaBeans规则,他会认为这是两个属性ZZH和success的值。
在这个问题里面就是,它先get到了这个Bean的所有Context。然后看到了一个Isxxx的返回值是boolean的方法,然后就认为Bean有这个xxx的属性,于是在序列化的时候进行了添加。于是就导致了这个出错。(好家伙,写这个SDK的人是真的细,成功利用缺陷写代码。负负得正思维)
然后就被顺带设置进去了里面,Gson则不会有这个问题,Google protobuf也会这个问题。至于原理大概现在能猜出应该是可序列化框架在对于boolean的判断上增加了“is”这个问题,根源应该是Java Beans定义规范问题。毕竟这些可序列化车轮也是根据规范写出的。