序列化字段增加問題


  周五在做一個推送需求的時候出現了一個問題。需求大致是講一個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定義規范問題。畢竟這些可序列化車輪也是根據規范寫出的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM