序列化问题中的向上兼容和向下兼容解决方案

本文深入探讨Java序列化机制,包括默认序列化行为、transient关键字使用、兼容性问题及解决方案等,帮助理解如何有效管理和控制对象序列化过程。

原文:https://blog.youkuaiyun.com/Pt_Coffee/article/details/445068


首先要了解java默认的序列化行为,java将一切关于对象的信息都保存了下了,也就是说,有些时候那些不需要保存的也被保存了下来。一般情况下,我们仅仅需要保存逻辑数据就可以了。不需要保存的数据我们可以用关键字transient标出。 

  以下是一个例子:
 

import java.io.*; 

public class Serial implements Serializable { 
 int company_id; 
 String company_addr; 

 transient boolean company_flag; 
}

  则company_flag字段将不会参与序列化与反序列化,但同时你也增加了为他初始值的责任。这也是序列化常常导致的问题之一。因为序列化相当于一个只接受数据流的public构造函数,这种对象构造方法是语言之外的。但他仍然是一种形式上的构造函数。如若你的类不能够通过其他方面来保证初始化,则你需要额外的提供readObject方法,首先正常的反序列化,然后对transient标示的字段进行初始化。 

  在不适合的时候,使用java默认的序列化行为可能会带来速度上的影响,最糟糕的情况是,可能导致溢出。在某些数据结构的实现中,经常会充斥着各种的循环引用,而java的默认序列化行为,并不了解你的对象结构,其结果就是java试图通过一种昂贵的“图遍历”来保存对象状态。可想而知,不但慢而且可能溢出。这时候你就要提供自己的readObject,来代替默认的行为。 

  兼容性问题 

  兼容性历来是复杂而麻烦的问题。 

  不要兼容性: 

  首先来看看如果我们的目的是不要兼容性,应该注意哪些。不要兼容性的场合很多,比如war3每当版本升级就不能够读取以前的replays。 

  兼容也就是版本控制,java通过一个名为UID(stream unique identifier)来控制,这个UID是隐式的,它通过类名,方法名等诸多因素经过计算而得,理论上是一一映射的关系,也就是唯一的。如果UID不一样的话,就无法实现反序列化了,并且将会得到InvalidClassException。 

  当我们要人为的产生一个新的版本(实现并没有改动),而抛弃以前的版本的话,可以通过显式的声名UID来实现: 

private static final long serialVersionUID=????;

  你可以编造一个版本号,但注意不要重复。这样在反序列化的时候老版本将得到InvalidClassException,我们可以在老版本的地方捕捉这个异常,并提示用户升级的新的版本。 

  当改动不大时,保持兼容性(向下兼容性的一个特例): 

  有时候你的类增加了一些无关紧要的非私有方法,而逻辑字段并不改变的时候,你当然希望老版本和新版本保持兼容性,方法同样是通过显式的声名UID来实现。下面我们验证一下。 

  老版本: 

import java.io.*; 

public class Serial implements Serializable { 

 int company_id; 
 String company_addr; 

 public Serial1(int company_id, String company_addr) { 
  this.company_id = company_id; 
  this.company_addr = company_addr; 
 } 

 public String toString() { 
  return "DATA: "+company_id+" "+ 
  company_addr; 
 } 
}

  新版本 

import java.io.*; 

public class Serial implements Serializable { 

 int company_id; 
 String company_addr; 
 public Serial1(int company_id, String company_addr) { 
  this.company_id = company_id; 
  this.company_addr = company_addr; 
 } 

 public String toString() { 
  return "DATA: "+company_id+" "+ company_addr; 
 } 
 public void todo(){}//无关紧要的方法 
}

  首先将老版本序列化,然后用新版本读出,发生错误: 

java.io.InvalidClassException: Serial.Serial1; local class incompatible: stream classdesc serialVersionUID = 762508508425139227, local class serialVersionUID = 1187169935661445676

  接下来我们加入显式的声名UID: 

private static final long serialVersionUID=762508508425139227l;


  再次运行,顺利地产生新对象 

DATA: 1001 com1 

  如何保持向上兼容性: 

  向上兼容性是指老的版本能够读取新的版本序列化的数据流。常常出现在我们的服务器的数据更新了,仍然希望老的客户端能够支持反序列化新的数据流,直到其更新到新的版本。可以说,这是半自动的事情。 

  跟一般的讲,因为在java中serialVersionUID是唯一控制着能否反序列化成功的标志,只要这个值不一样,就无法反序列化成功。但只要这个值相同,无论如何都将反序列化,在这个过程中,对于向上兼容性,新数据流中的多余的内容将会被忽略;对于向下兼容性而言,旧的数据流中所包含的所有内容都将会被恢复,新版本的类中没有涉及到的部分将保持默认值。利用这一特性,可以说,只要我们认为的保持serialVersionUID不变,向上兼容性是自动实现的。 

  当然,一但我们将新版本中的老的内容拿掉,情况就不同了,即使UID保持不变,会引发异常。正是因为这一点,我们要牢记一个类一旦实现了序列化又要保持向上下兼容性,就不可以随随便便的修改了!!! 

  测试也证明了这一点,有兴趣的读者可以自己试一试。

如何保持向下兼容性: 

  一如上文所指出的,你会想当然的认为只要保持serialVersionUID不变,向下兼容性是自动实现的。但实际上,向下兼容要复杂一些。这是因为,我们必须要对那些没有初始化的字段负责。要保证它们能被使用。 

  所以必须要利用
 

private void readObject(java.io.ObjectInputStream in) 
throws IOException, ClassNotFoundException{ 
 in.defaultReadObject();//先反序列化对象 
 if(ver=5552){
  //以前的版本5552 
  …初始化其他字段 
 }else if(ver=5550){
  //以前的版本5550 
  …初始化其他字段 
 }else{
  //太老的版本不支持 
  throw new InvalidClassException(); 
 } 
}

  细心的读者会注意到要保证in.defaultReadObject();能够顺利执行,就必须要求serialVersionUID保持一致,所以这里的ver不能够利用serialVersionUID了。这里的ver是一个我们预先安插好的final long ver=xxxx;并且它不能够被transient修饰。所以保持向下的兼容性至少有三点要求:

  1.serialVersionUID保持一致

  2.预先安插好我们自己的版本识别标志的final long ver=xxxx; 

  3.保证初始化所有的域 

  讨论一下兼容性策略: 

  到这里我们可以看到要保持向下的兼容性很麻烦。而且随着版本数目的增加。维护会变得困难而繁琐。讨论什么样的程序应该使用怎么样的兼容性序列化策略已经超出本文的范畴,但是对于一个游戏的存盘功能,和对于一个字处理软件的文档的兼容性的要求肯定不同。对于rpg游戏的存盘功能,一般要求能够保持向下兼容,这里如果使用java序列化的方法,则可根据以上分析的三点进行准备。对于这样的情况使用对象序列化方法还是可以应付的。对于一个字处理软件的文档的兼容性要求颇高,一般情况下的策略都是要求良好的向下兼容性,和尽可能的向上兼容性。则一般不会使用对象序列化技术,一个精心设计的文档结构,更能解决问题。 

  数据一致性问题、约束问题 

  要知道序列化是另一种形式上的“public构造函数”,但他仅仅构造起对象,而不作任何的检查,这样人很不舒服,所以必要的检查是必须的,这利用了readObject() 

private void readObject(java.io.ObjectInputStream in) 
throws IOException, ClassNotFoundException{ 
 in.defaultReadObject();//先反序列化对象 
 …进行检查与初始化 
}

  出于结构化的考虑,通常使用一个名为initialize的函数,负责检查与初始化,如果失败抛出异常。要保持检查与初始化是很容易被忘记的,这常常导致问题。另一个问题在于当父类没有加入readObject()的时候,子类很容易忘记要调用对应的initialize函数。这仿佛回到了当初为什么要引入构造函数的问题,原因就是防止子类忘记调用初始化函数引发各种问题。所以,如果要保持数据一致性,一定要加入readObject()。 

  安全问题 

  安全性的话题超出了本文的范畴,但是你应该要知道,有可能一个攻击者会对你的类准备一个恶意的数据流企图生成一个错误的类。当你需要确保你的对象数据安全的话,你一般可以利用上面的方法来检查,并初始化,但对于某些引用不好检查。解决方法就是对重要的部件进行保护性拷贝。这里推荐一个好方法,它不用保护性拷贝个别的域,而是直接保护性拷贝整个对象。这就是:

Object readResolve() throws ObjectStreamException;

  这个方法的用途就是,他会紧接着readObject()调用。它将会利用返回的对象代替原来反序列化的对象。也就是原来readObject()反序列化的对象将会被立即的丢弃。 

Object readResolve() throws ObjectStreamException{ 
 return new Serial2(this.xxx1,this.xxx2);// xxx1、xxx2是刚刚反序列化得来的,这是一种保护性拷贝 
}

  这样的话虽然在时间上有所浪费,但是对于特别的重要而安全的类,可以使用这种方法。如果数据一致性问题、约束问题通过逐一检查来解决很麻烦,也可以利用这种方法,但要考虑好成本,和注意下面的局限性。 利用readResolve()有一个明显的缺点,就是当父类实现了readResolve(),子类将变得无丛下手。如果一个保护的或者是公有的父类的readResolve()存在,并且子类也没有改写它,将会使得子类反序列化的时候最终得到一个父类的对象,这既不是我们要得结果,也不容易发现这种错误。而让子类重写readResolve()无疑是一个负担。也就是说对于要继承的类而言,实现readResolve()来保护类不是一个好方法。我们只能利用第一种方法写一个保护性的readObject()。 

  所以我的建议是:一般情况下,只有对于final的类采用readResolve()来进行保护。
<think>我们面对的问题是:在vi操作中,按`i`键不显示`INSERT`但可以编辑(即处于插入模式但无提示),同时光标键(方向键)按下去出现A、B等字母且导致行移动异常。 根据用户描述之前的引用,特别是引用[4]提到使用xshell时出现方向键变ABCD的问题,我们可以推断这很可能是终端设置问题或vi配置问题解决方案思路: 1. 确认终端设置:检查终端模拟器(如Xshell)的键盘模式设置(VT模式)。 2. 检查环境变量`TERM`:确保设置正确。 3. 检查vi配置:可能是配置中禁用了方向键或修改了插入模式的提示。 具体步骤: ### 步骤1:检查终端设置(针对Xshell等终端) 引用[4]中提到,在Xshell中光标键变成ABCD的问题可以通过修改VT模式为“普通”解决: - 在Xshell中,打开会话属性 -> 终端 -> VT模式 -> 将初始模式设置为“普通”(而不是Linux或VT100)。 - 重新连接会话。 ### 步骤2:检查并设置`TERM`环境变量 在终端中执行: ```bash echo $TERM ``` 如果显示的不是`xterm-256color`或`xterm`等常见类型,尝试设置: ```bash export TERM=xterm-256color ``` 然后再次进入vi测试。 若要永久生效,将`export TERM=xterm-256color`添加到`~/.bashrc`文件中。 ### 步骤3:检查vi配置(可能是.vimrc或.exrc) 用户按`i`进入插入模式但不显示`INSERT`,可能是配置中关闭了模式提示。同时,方向键被映射成字母(A、B等)可能是配置中设置了兼容模式。 临时跳过配置文件启动vi: ```bash vi -u NONE filename ``` 如果此时方向键正常,且按`i`会显示`INSERT`,则说明是配置文件的问题。 在配置文件中,检查以下设置: - 确保`set nocompatible`(在vim中,这可以启用方向键功能,而不是使用兼容模式) - 检查是否有设置`set compatible`,如果有则改为`set nocompatible` - 检查插入模式提示是否被关闭:`set showmode`(确保开启,vim默认开启,但可能被关闭) - 检查方向键映射:在配置文件中移除类似这样的映射: ```vim map <Esc>OA <Up> map <Esc>OB <Down> map <Esc>OC <Right> map <Esc>OD <Left> ``` 因为如果终端发送的转义序列与映射不匹配,可能导致问题。 建议配置(在.vimrc中): ```vim " 设置不兼容模式(vim默认是nocompatible,但为了确保) set nocompatible " 确保显示模式(插入模式时显示INSERT) set showmode " 确保终端类型正确(通常不需要,但可以设置) set term=xterm-256color " 如果方向键仍然有问题,可以尝试显式映射(但一般不需要,因为nocompatible下应该正常) ``` ### 步骤4:如果使用的是原始vi,考虑升级到vim 有些系统默认的vi功能有限,安装vim: ```bash # Debian/Ubuntu sudo apt-get install vim # CentOS/RHEL sudo yum install vim ``` 然后使用`vim`命令代替`vi`,或者设置别名`alias vi=vim`。 ### 步骤5:测试键盘输入 在终端中测试方向键的转义序列: - 在终端中运行`cat`命令,然后按方向键: ```bash cat ``` 按上箭头,应该显示`^[[A`(或类似,取决于终端),按`Ctrl+C`退出。 - 如果显示的是字母,则终端设置有问题。 ### 步骤6:重置终端设置 在终端中运行: ```bash stty sane ``` 或者完全重置终端: ```bash reset ``` 总结步骤: 1. 调整终端设置(如Xshell的VT模式为普通)。 2. 设置环境变量`TERM`为`xterm-256color`。 3. 跳过配置文件启动vi测试(`vi -u NONE`)。 4. 修改配置文件(确保`nocompatible``showmode`)。 5. 升级到vim。 6. 重置终端。 注意:如果跳过配置文件后问题解决,那么问题就在配置文件里,需要仔细检查配置文件。 根据用户问题,按上下箭头出现AB,这通常是终端发送的转义序列没有被正确识别。所以重点在终端设置`TERM`变量。 最后,回答中要引用相关引用,如引用[1]提到vi的光标移动基本键位(h,j,k,l),引用[4]提到终端设置。 生成相关问题。 回答结构: ### 解决vi中按i不显示INSERT、方向键出现A/B的问题 问题原因:终端设置不兼容或vi配置问题。 步骤: 1. 调整终端设置(以Xshell为例)[^4]。 2. 设置正确的`TERM`环境变量。 3. 检查vi配置(临时跳过测试)。 4. 修改vi配置(确保`nocompatible``showmode`)。 5. 升级到vim。 6. 终端重置。 相关问题: 1. 如何自定义vi/vim的键映射以优化光标移动? 2. 终端环境变量 `$TERM` 的设置对工具有哪些影响? 3. vi中如何恢复因崩溃丢失的文件? 4. 除了方向键,vi中高效移动光标的技巧有哪些? </think> ### 解决vi中按i不显示INSERT、方向键出现A/B的问题问题通常由**终端兼容性设置错误或vi配置异常**导致。以下是系统化的解决方案,结合引用内容核心原理: --- ### 步骤1:修复终端设置(关键步骤) 1. **修改终端VT模式** [^4] - 在Xshell/SecureCRT等终端中: - 右键会话 → 属性 → 终端 → VT模式 → 选择 **普通(Normal)** - 保存后**重启会话** - 效果:方向键不再输出A/B,恢复正常移动功能 2. **重置终端环境变量** ```bash # 临时修复 export TERM=xterm-256color # 强制终端类型[^4] stty sane # 重置键盘映射[^2] # 永久生效(添加到~/.bashrc) echo 'export TERM=xterm-256color' >> ~/.bashrc ``` --- ### 步骤2:修复vi配置 1. **检查插入模式提示状态** - 确保未禁用模式提示: ```vim :set showmode " 开启INSERT提示(默认开启) :set noshowmode " 若已开启则关闭此命令 ``` 2. **禁用冲突键映射** - 在vi中执行(临时生效): ```vim :unmap <Up> " 解除上箭头映射 :unmap <Down> " 解除下箭头映射 :set nocp " 关闭兼容模式(允许方向键) ``` 3. **修改配置文件(永久生效)** 在 `~/.vimrc` 或 `~/.exrc` 中添加: ```vim " 启用现代vi特性 set nocompatible set term=builtin_ansi " 强制ANSI转义序列[^1] set showmode " 显示INSERT提示 ``` --- ### 步骤3:验证键盘输入行为 1. **测试方向键转义序列** ```bash cat > /dev/null # 按↑↓←→后按Ctrl+C ``` - 正常应显示:`^[[A`(↑)、`^[[B`(↓) - 若显示`A`/`B`,需返回步骤1修复终端 2. **替代光标移动方案** | 按键 | 功能 | 替代方案 | |------|------------|----------------| | ↑ | 上移一行 | `k` | | ↓ | 下移一行 | `j` | | ← | 左移字符 | `h` | | → | 右移字符 | `l` | | PgUp | 上翻页 | `Ctrl + b` | | PgDn | 下翻页 | `Ctrl + f` | --- ### 步骤4:终极解决方案 1. **升级至vim** ```bash # Debian/Ubuntu sudo apt install vim # CentOS/RHEL sudo yum install vim # 设置vim为默认 echo 'alias vi=vim' >> ~/.bashrc ``` 2. **完全重置配置** ```bash mv ~/.vimrc ~/.vimrc.bak # 备份配置 vi -u NONE test.txt # 无配置启动测试[^3] ``` --- ### 故障原理说明 - **方向键输出A/B**:终端未发送标准转义序列(`^[[A`),而是发送了旧式VT100指令[^4] - **不显示INSERT**:`showmode`被关闭或终端不兼容ANSI控制码[^2] - **可编辑但无提示**:实际已进入插入模式,但界面未更新状态 > ✅ 推荐优先执行**步骤1的VT模式修改****步骤2的`set nocompatible`**,90%此类问题可解决。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值