htttpinvoker管中窥豹

近期,借着修复一个遗留系统的bug的机会,自己对httpinvoker长期存在的一个问题进行了相关的技术调研,在这里记录下来。

 

问题:httpinvoker客户端和服务器端同时部署了一个接口包,接口包中包含了服务器端提供的接口类和返回对象的类,当我改变服务器端返回类的时候,客户端使用老的接口访问httpinvoker服务,会产生什么现象?

 

答案:看情况。

 

解释:

1.private static final long serialVersionUID

 

 写道
serialVersionUID作用:
序列化时为了保持版本的兼容性,即在版本升级时反序列化仍保持对象的唯一性。
有两种生成方式:
一个是默认的1L,比如:private static final long serialVersionUID = 1L;
一个是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:
private static final long serialVersionUID = xxxxL;

当你一个类实现了Serializable接口,如果没有定义serialVersionUID,Eclipse会提供这个
提示功能告诉你去定义 。在Eclipse中点击类中warning的图标一下,Eclipse就会
自动给定两种生成的方式。如果不想定义它,在Eclipse的设置中也
可以把它关掉的,设置如下:
Window ==> Preferences ==> Java ==> Compiler ==> Error/Warnings ==>
Potential programming problems
将Serializable class without serialVersionUID的warning改成ignore即可。

如果你没有考虑到兼容性问题时,就把它关掉,不过有这个功能是好的,只要任何类别实现了Serializable这个接口的话,如果没有加入serialVersionUID,Eclipse都会给你warning提示,这个serialVersionUID为了让该类别Serializable向后兼容。

如果你的类Serialized存到硬盘上面后,可是后来你却更改了类别的field(增加或减少或改名),当你Deserialize时,就会出现Exception的,这样就会造成不兼容性的问题。

但当serialVersionUID相同时,它就会将不一样的field以type的预设值Deserialize,可避开不兼容性问题。
 

 

2.Serializable

 

chenfengcn 写道
我们都知道,当我们new一个对象的时候,Java就会在内存中创建一个相应的对象供我们后续使用。如果我们把这个Java程序关掉的话,那么这个对象也就随着我们程序的关闭而消失,如何才能在这种情况下继续保存这个对象呢?这就是Java序列化要做的事情。简单地说,Java对象列化就是要把内存中的Java对象保存下来(持久化),以便可以在网络上传输或今后继续使用这个对象中的数据。
  那么对象序列化到底做了哪些事呢?其实很简单,一个对象对于我们来说的意义其实就是对象里保存的那些数据,序列化就是把这些数据转换成二进制表示的对象流,你可以把它通过网络传输到远程,当然,也可以把这些信息保存在本地的文件里。下面给出一个最简单的Java的序列化与反序列化的例子,看懂这个例子,相信你应该也能跟我一样,大概理解这是怎么一回事了。
首先是一个Peraon类:
Person.java
Java代码
package com.lanber.serial;

import java.io.Serializable;

public class Person implements Serializable {
private String name;
private int age;
private String sex;
private String nationality;

public Person(String _name, int _age, String _sex, String _nationality) {
this.name = _name;
this.age = _age;
this.sex = _sex;
this.nationality = _nationality;
}

public String toString() {
return "Name:" + name + "\nAge:" + age + "\nSex:" + sex
+ "\nNationality:" + nationality + "\n\n";
}
}

  而后我们要实例化一个Person对象,把我们实例化出来的这个对象序列化,并把序列化后的对象流保存到文件中去:
WritePerson.java
Java代码
package com.lanber.serial;

import java.io.*;

public class WritePerson {

public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("Person.tmp"));
Person obj = new Person("Tony", 25, "Female", "han");
System.out.println("将Person对象写入到文件Person.tmp中...");
oos.writeObject(obj);
oos.close();
System.out.println("完成!");
} catch (Exception e) {
System.out.println(e);
}

}
}

  这样,我们就把序列化后的二进制对象流保存到Person.tmp的文件中去了(特别要注意这里的oos.writeObject()方法,这个方法就是实现把obj对象序列化的方法)。你可以打开生成的这个文件查看,不过我们是看不懂这里面的信息的,因为这是二进制数据啊。
  那我们要怎么才能看懂这些二进制数据呢?这就要用到我们的反序列化了。反序列化就是要把序列化后的二进制数据反向解析成可以正常使用的Java对象。现在让我们来把刚才的那个对象反序列化一下吧:
ReadPerson.java
Java代码
package com.lanber.serial;

import java.io.*;

public class ReadPerson {
public static void main(String[] args) {
Person obj = null;
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
"Person.tmp"));
obj = (Person) ois.readObject();
System.out.println(obj);
ois.close();
} catch (Exception e) {
System.out.println(e);
}
}

}

  要注意一下这个语句:
Java代码
obj = (Person) ois.readObject();

  就是通过这个语句把序列化后的对象流反序列化成正常的Java对象的。

 

 自己做了一个列子:

 

                ClassA a = new ClassA();
		a.setId(1);
		a.setName("hell");
		a.setDate(new Date());

 

 对象序列化以后的结果是

 

 sr #com.sp.invoker.client.serial.ClassA        L datet Ljava/util/Date;L idt Ljava/lang/Integer;L namet Ljava/lang/String;xpsr java.util.Datehj?KYt  xpw  0洐rxsr java.lang.Integer鉅亣8 I valuexr java.lang.Number啲?斷?  xp   t hell
 

 

里边有些乱码,但是从可见的一些信息上我们可以看到,有全类名称,有serialVersionUID,有属性描述,属性值信息,并没有方法信息。

 

3.Jvm中的方法:

 

 

 写道
当JVM使用类装载器装载某个类时,它首先要定位对应的class文件,然后读入这个class文件,最后,JVM提取该文件的内容信息,并将这些信息存储到方法区,最后返回一个class实例。上面是对类的装载过程作了个简单的描述,看了上面一段文字,也许你会问:方法区是什么?里面存了哪些内容?下面我们将对方法区作一个详细的描述。

方法区是什么?有哪些特点?

方法区是系统分配的一个内存逻辑区域,是用来存储类型信息的(类型信息可理解为类的描述信息)。方法区主要有以下几个特点:
一.方法区是线程安全的。由于所有的线程都共享方法区,所以,方法区里的数据访问必须被设计成线程安全的。例如,假如同时有两个线程都企图访问方法区中的同一个类,而这个类还没有被装入JVM,那么只允许一个线程去装载它,而其它线程必须等待
二.方法区的大小不必是固定的,JVM可根据应用需要动态调整。同时,方法区也不一定是连续的,方法区可以在一个堆(甚至是JVM自己的堆)中自由分配。
三.方法区也可被垃圾收集,当某个类不在被使用(不可触及)时,JVM将卸载这个类,进行垃圾收集

方法区里存放的是哪些内容?

方法区里存的都是类型信息,也就是类的信息,而类的信息又包括以下内容:
类的全限定名(类的全路径名)
类的直接超类的全限定名(如果这个类是Object,则它没有超类)
这个类是类型(类)还是接口
类的访问修饰符,如public、abstract、final等
所有的直接接口全限定名的有序列表(假如它实现了多个接口)
常量池
字段、方法信息、类变量信息(静态变量) 装载该类的装载器的引用(classLoader)、类型引用(class)

其实,我们没必要全部记住,只要根据上面内容有个大概的了解,然后对类型这个概念有个大概的认识即可。下面我们将主要对常量池和类变量信息作一下分析。
先说类变量吧,类变量内容少些,描述起来比较容易。类变量,顾名思义,就是属于类的变量,所有类的实例都共享的变量,也就是常说的静态变量。关于类变量,我们只要知道方法区里有个静态区,静态区是专门用来存放静态变量以及静态块的。所有类的实例都共享方法区中的内容。访问类变量的方式可通过实例(对象)来访问,也可通过类型来直接访问,java规范推荐使用类型来直接访问。

 

 

类的方法信息是记录在Jvm方法取里的,java序列化对象的时候,一是将堆中的对象进行了记录,二是记录了类的基本信息(全类名、UID)

 

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

 

 

综上所述当服务器端类改变以后,对象会根据相应信息序列化,到客户端以后首先会验证对象的类型信息(全类名、UID)验证失败则会抛出异常,如果验证通过,则会在客户端jvm中还原这个对象,客户端所加载的类依然是本地的方法信息。

 

 

 

以下是几个的结果,了解原理后,结果就顺理成章了:

 

1.UID使用eclipse默认的1L:

a.改变服务器端类方法:客户端调用是本地类的方法

        b.为服务器端类增加属性并赋值:客户端无法看到新属性,调用正常

c.服务器端类减少属性:客户端调用正常,减少的属性值为null

2.服务器端UID使用eclipse generate出来的随机UID,客户端使用老得:

直接抛出版本不匹配异常

3.ClassA,ClassB具有完全相同的属性方法,UID,序列化ClassA的对象以后,反序列化强制转换为ClassB

抛类型转换异常

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值