最近在開發中遇到一個Protostuff序列化問題,在這記錄一下問題的根源;分析一下Protostuff序列化和反序列化原理;以及怎麼樣避免改bug。java
有一個push業務用到了mq,mq的生產者和消費者實體序列化咱們用的是Protostuff方式實現的。因爲業務須要,咱們要在一個已有的枚舉類添加一種類型,好比:app
1 public enum LimitTimeUnit { 2 NATURAL_DAY { 3 @Override 4 public long getRemainingMillis() { 5 Date dayEnd = DateUtils.getDayEnd(); 6 return dayEnd.getTime() - System.currentTimeMillis(); 7 } 8 }; 18 /** 19 * 距離當前單位時間結束剩餘毫秒數. 20 * @return 21 */ 22 public abstract long getRemainingMillis(); 23 24 }
中添加一個類型 NATURAL_MINUTE :ide
1 public enum LimitTimeUnit { 2 NATURAL_MINUTE { 3 @Override 4 public long getRemainingMillis() { 5 return 1000 * 60; 6 } 7 }, 8 9 NATURAL_DAY { 10 @Override 11 public long getRemainingMillis() { 12 Date dayEnd = DateUtils.getDayEnd(); 13 return dayEnd.getTime() - System.currentTimeMillis(); 14 } 15 }; 25 /** 26 * 距離當前單位時間結束剩餘毫秒數. 27 * @return 28 */ 29 public abstract long getRemainingMillis(); 30 31 }
消費端項目添加了這個字段升級了版本,可是消費者在有些項目中沒有升級,測試的時候看日誌沒有報錯,因此就很happy上線了回家睡個好覺。次日測試找到我問:爲何昨晚我收到那麼多push...不是限制天天限制只能收到...?我:哦,這是之前的邏輯嗎?...好的,我看看!佛系開發沒辦法!工具
打開app快速(一分鐘內)按測試所說的流程給本身搞幾個push,發現沒有問題啊!而後開始跟測試磨嘴皮,讓他給我重現,哈哈,他也重現不了!就這樣我繼續擼代碼...安靜的過了五分鐘。測試又來了...後面發送的事你們本身YY一下。測試
快速找到對應生產者代碼,封裝的確實是 NATURAL_DAY,那隻能debug消費者這邊接收的代碼。發現消費者接收到是 NATURAL_MINUTE!看到這裏測試是對的,原本限制一天如今變成一分鐘!!!是什麼改變這個值呢?mq只是一個隊列,保存的是字節碼,一個對象須要序列化成字節碼保存到mq,從mq獲取對象須要把字節碼反序列化成對象。那麼問題根源找到了,是序列化和反序列化時出了問題。this
該問題是Protostuff序列化引發的,那麼解決這個問題還得弄懂Protostuff序列化和反序列化原理。弄懂原理最好的辦法就是看源碼:spa
1 public class ProtoStuffSerializer implements Serializer { 2 3 private static final Objenesis objenesis = new ObjenesisStd(true); 4 private static final ConcurrentMap<Class<?>, Schema<?>> schemaCache = new ConcurrentHashMap<>(); 5 private ThreadLocal<LinkedBuffer> bufferThreadLocal = ThreadLocal.withInitial(() -> LinkedBuffer.allocate()); 6 7 @Override 8 public <T> byte[] serialize(T obj) { 9 Schema<T> schema = getSchema((Class<T>) obj.getClass()); 10 11 LinkedBuffer buf = bufferThreadLocal.get(); 12 try { 13 // 實現object->byte[] 14 return ProtostuffIOUtil.toByteArray(obj, schema, buf); 15 } finally { 16 buf.clear(); 17 } 18 } 19 20 @Override 21 public <T> T deserialize(byte[] bytes, Class<T> clazz) { 22 T object = objenesis.newInstance(clazz); // java原生實例化必須調用constructor. 故使用objenesis 23 Schema<T> schema = getSchema(clazz); 24 ProtostuffIOUtil.mergeFrom(bytes, object, schema); // 反序列化源碼跟蹤入口 25 return object; 26 } 27 28 private <T> Schema<T> getSchema(Class<T> clazz) { 29 Schema<T> schema = (Schema<T>) schemaCache.get(clazz); 30 if (schema == null) { 31 // 把可序列化的字段封裝到Schema 32 Schema<T> newSchema = RuntimeSchema.createFrom(clazz); 33 schema = (Schema<T>) schemaCache.putIfAbsent(clazz, newSchema); 34 if (schema == null) { 35 schema = newSchema; 36 } 37 } 38 return schema; 39 }
這是咱們實現Protostuff序列化工具類。接下來看一下 ProtostuffIOUtil.toByteArray(obj, schema, buf) 這個方法裏面重要代碼:debug
1 public static <T> byte[] toByteArray(T message, Schema<T> schema, LinkedBuffer buffer) 2 { 3 if (buffer.start != buffer.offset) 4 throw new IllegalArgumentException("Buffer previously used and had not been reset."); 5 6 final ProtostuffOutput output = new ProtostuffOutput(buffer); 7 try 8 { 9 // 繼續跟進去 10 schema.writeTo(output, message); 11 } 12 catch (IOException e) 13 { 14 throw new RuntimeException("Serializing to a byte array threw an IOException " + 15 "(should never happen).", e); 16 } 17 return output.toByteArray(); 18 }
1 public final void writeTo(Output output, T message) throws IOException 2 { 3 for (Field<T> f : getFields()) 4 // 祕密即將揭曉 5 f.writeTo(output, message); 6 }
RuntimeUnsafeFieldFactory這裏面纔是關鍵:日誌
@Override public void writeTo(Output output, T message) throws IOException { CharSequence value = (CharSequence)us.getObject(message, offset); if (value != null) // 看這裏 output.writeString(number, value, false); }
跟蹤到這裏,咱們把一切謎題都解開了。原來Protostuff序列化時是按可序列化字段順序只把value保存到字節碼中。code
如下是反序列化源碼的跟蹤:ProtostuffIOUtil.mergeFrom(bytes, object, schema) 裏面重要的代碼:
1 public static <T> void mergeFrom(byte[] data, T message, Schema<T> schema) 2 { 3 IOUtil.mergeFrom(data, 0, data.length, message, schema, true); 4 }
1 static <T> void mergeFrom(byte[] data, int offset, int length, T message, 2 Schema<T> schema, boolean decodeNestedMessageAsGroup) 3 { 4 try 5 { 6 final ByteArrayInput input = new ByteArrayInput(data, offset, length, 7 decodeNestedMessageAsGroup); 8 // 繼續跟進 9 schema.mergeFrom(input, message); 10 input.checkLastTagWas(0); 11 } 12 catch (ArrayIndexOutOfBoundsException ae) 13 { 14 throw new RuntimeException("Truncated.", ProtobufException.truncatedMessage(ae)); 15 } 16 catch (IOException e) 17 { 18 throw new RuntimeException("Reading from a byte array threw an IOException (should " + 19 "never happen).", e); 20 } 21 }
1 @Override 2 public final void mergeFrom(Input input, T message) throws IOException 3 { 4 // 按順序獲取字段 5 for (int n = input.readFieldNumber(this); n != 0; n = input.readFieldNumber(this)) 6 { 7 final Field<T> field = getFieldByNumber(n); 8 if (field == null) 9 { 10 input.handleUnknownField(n, this); 11 } 12 else 13 { 14 field.mergeFrom(input, message); 15 } 16 } 17 }
1 public void mergeFrom(Input input, T message) 2 throws IOException 3 { 4 // 負載給字段 5 us.putObject(message, offset, input.readString()); 6 }
經過protostuff的序列化和反序列化源碼知道一個對象序列化時是按照可序列化字段順序把值序列化到字節碼中,反序列化時也是按照當前對象可序列化字段順序賦值。因此會出現 NATURAL_DAY 通過序列化和反序列化後變成 NATURAL_MINUTE。因爲這兩個字段類型是同樣的,反序列化沒有報錯,若是序列化前的對象和反序列化接收對象對應順序字段類型不同時會出現反序列失敗報錯。爲了不以上問題,在使用protostuff序列化時,對已有的實體中添加字段放到最後去就能夠了。