• 2004-07-16

    叫我如何“面向接口”

    Tag:向上走

    版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
    http://www.blogbus.com/dreamhead-logs/268493.html

    写了个准备远程调用EJB的程序,这是一个存储的程序,有放有取。本着面向接口编程的原则,我把业务方法的参数设计成了接口,下面就是一个:
    void put(Serializable key, Serializable value);

    自己测试的时候,调用EJB时,我先后把String、Integer、HashMap等几个JDK中的类作为参数传递,没有任何问题。等到了把程序集成起来,第一次运行就让我狠狠的摔了一跤:放进去的东西取不出来。一路寻bug而去,最终的发现是我的对象根本没有放进去,不,准确的说,放进去的是个null。既然server端取出的是null,我想当然的认为是client端出了问题,跟踪的结果是,client放进去的的确不是null。client没有放进null,server取出的却是null,那对象跑拿去了?
    经过简化,我们的对象被我变成了这样:
    class MapWrapper implements Serializable {
        private HashMap map = new HashMap();
    }
    我又一次使用String几个类进行尝试,一切正常,问题在哪呢?

    我只有把方向定在了EJB参数传递上。EJB的参数传递涉及到的技术包括序列化、RMI等等,一个个实验。不幸的是,在我的实验中,这个MapWrapper无论是序列化还是RMI都表现良好,看不出有任何问题。

    最终一个同事解决了这个困扰我好些日子的问题,解决办法很简单,他把client用到类加到了server中。一句话点醒梦中人,同事的方法让我的思路一下子清晰起来。
    这是一个序列化的问题,准确的说,是反序列化的问题。

    在Java中,最简单的序列化只要实现java.io.Serializable接口即可,简单是简单,不过简单得让我忽略了太多的东西。Java缺省的序列化实现,会把对象的实际类型也写进去,这样做的目的是为了将来可以准确的反序列化。反序列化利用的是Java中大名鼎鼎的反射机制,根据类名,创建一个类的实例。关于序列化的更多内容,可以参考侯捷先生的《Java的物件永续之道》。

    在我的应用中,因为是远程调用,client和server并没有部署到一台机器上,而server只部署了它用到的类。EJB参数传递实际上就是,client将参数序列化之后发送出去,server收到之后,反序列化出来。序列化的过程在client这边没有问题,到了server,因为虽然可以识别出对象的类型,因为这边没有类的定义,所以会导致反序列化失败。在SUN ONE应用服务器上,虽然反序列化失败,但应用服务器并没有把异常抛出来,而是返回了一个null,这样我在server端取出的就是一个null。

    String几个类之所以畅通无阻,是因为它们是标准的东西,所以应用服务器启动后,这几个类的定义可以轻松的被虚拟机找到。
    为什么前面我自己的实验也没找到问题?因为我的程序都是在一个目录中完成的,而我的classpath中包含有当前目录(.)。在我明白了前面的道理之后,我将序列还和反序列化分开又实验了一次,果然,ClassNotFoundError抛了出来。

    虽然这个道理很容易想通,但另外的道理却让我很难拐过弯来。就代码逻辑而言,server端只要知道参数实现了java.io.Serializable就可以了,而现在server里必须包含client的类,换句话说,server的程序要依赖于client,虽然没有在代码中直接体现,但这足以让人感到恶心了。不管server实现得有多完美,部署上之后,如果有哪个client要访问server,就必须把它的一些类放在server上,而且只要这些类有任何变更,server也要随之改变。那种按照通常协议进行访问的server,大多不会因为client的改变而改变,又是怎样的一种惬意。忽然觉得EJB像一只蒙着盖头的猪,也许3.0会让它的外表变美,但其内在的丑陋让人无法忍受。

    说到底,这还是序列化的问题。序列化简化了Java程序员的工作,不过,上面的例子也证明了它确实在会影响到面向接口的编程方式,有如白纸上的些许污迹,虽不影响大局,却让人不那么舒服。
    曾经,我的同事曾使用序列化作为两个系统之间通信的协议,当我提出使用这种方式会让两个系统必须拥有相同的类,不好维护的时候,他说,一边改动的话,给另一边拷过去一份不就成了。
    多么勤劳的程序员啊!

    分享到:

    历史上的今天:

    忍无可忍 2008-07-16
    引用地址:

    评论

  • 我觉得你得设计有些奇怪。

    void put(Serializable key, Serializable value)有什麽意义呢?可能这样设计有一个前提,就是你认为只要serialzable,就不再需要类的定义了是吗?这样比较吻合你的思路。

    不过vm无法从serialzable的定义得到该对象类的定义,还是要加载类,这样你的代码就要加入服务端的类路径。

    并且这同EJB并没有什麽关系。
  • 序列化的本意只是方便对象的存储和传输,它不关心使用者了不了解对象的细节。而通信协议是规定对象细节。所以序列化用于通信是没有错的,因为序列化本来就是传输使用的工具;问题是给它附加了作为通信协议的职责就有点牛头不对马嘴了。
  • 您所说的问题出自于RMI所采用的protocol所导致的。一般来说,java中有两种的协议支持RMI,即IIOP和JRMP。对于JRMP协议,支持automatically class loading;对于IIOP,则不支持。有一些appserver可以同时支持这两种,不过大多数的appserver在ejb调用中,默认都是采用的IIOP协议,包括sunone。我所知道的JRun可以支持使用JRMP。
  • EJB通信是通过RMI-IIOP来进行的,而RMI规范中当参数反序列化时,如果当远程的参数对象没有相应的Class文件的话,会下载的,怎么会出现反序列化失败的问题?不解
    回复caji说:
    规范归规范,可以想一下,即便可以下载,它到哪去下载呢?所以,归根结底,还是找不到。
    2004-07-21 14:15:48
  • 依我看用Webservice,你也不用过分关心Client的事情,当然这也是变相地将Client的Class放在了Server端。
  • 我觉得序列化传输就是速度慢了些,其它倒没什么不好,它至少让我们可以以对象的方式来传递数据,省去了我们解析和构造对象的过程。即便采用文本格式传输,如果在server端你依然需要面向对象去编程,那么你依然需要client端的class,并且还需要重新将对象重新构造一遍。


    我们在客户端和服务器之间传输的大多应该是业务数据对象,业务数据对象本身就贯穿了整个业务系统,所以同时存在于客户端和服务器端并没有什么值得非议的。
  • 我的意思就是说,传送的应该是接口,而不是对象,呵呵。这里说的是java的interface ,C++的纯虚类




    所以,犯不着序列化啊!
    回复tinyfool说:
    接口怎么传送呢?
    2004-07-19 08:58:41
  • 我覺得這個問題值得大家再加討論。




    就如 dreamhead 兄所說,如果通訊的格式是固定的,那使用物件序列化(也就是通訊內容物件化)就沒有太大優點,只剩下其複雜性了。像這個情況,應該是在 client 端使用 server stub 來處理就行了,把 server 簡化為 function call 並只以簡單的資料來傳遞,會比較簡單。




    而傳遞的資料物件化應該比較適合在資料本身較為複雜,並且資料本身還需要有操作的能力,甚至連傳遞的格式本身都還不確定的情況。也就是不得不以物件的形式來做通訊的內容的情況。當然,如果一開始設計時就避免這種情況,也可以用上面的做法來處理,但不同的問題適合不同的解法,當通訊的內容本身就很複雜 (網狀關係、繼承架構、複雜操作…) 時,讓通訊層來處理這些物件關係也不會是個好主意的。




    而 flyisland 兄所提到的 Map 的方式,其實只是把第一種方式外包一層物件。如果資料本身只需要 map 就可以方便表示並處理,那也就不需要用到物件序列化的情況,直接傳遞簡單的資料即可。




    而 tinyfool 兄所提到說原本 client 包含 server 的介面就很正常,的確是如此沒錯。但在序列化上所碰到的問題並不是如此而已。理想的情況下, client 和 server 間只要定義相同的介面就可以自由溝通,接受者並不需要知道所接受的物件本身的實際型別,只要確定是由介面所衍生即可以介面進行操作,甚至傳送者未來再自行衍生其他的類別也沒問題。但如果傳送和接收之間是經過序列化的話,就不是這麼理想了。因為接收者一定要有傳來的物件的"實際"內容的 class 才行,而不能只有其介面而已,如果傳送者對物件有所修改的話,那也一定要將接收者的 class 更新才行 (以我之前提到的作法可以減輕這個問題,但不能完全避免)。而如果傳送者自行衍生其他的類別來傳送也是不行的。




    這個問題的確是序列化的問題,不能用很優雅的方式來處理這個問題,需要加上一些骯髒的東西才能解決 (在 server 中包含 client 的類別)。所幸除了美感的問題外,通常都能運作並解決問題,一般的情況不會有這麼複雜和嚴格的情況。如果真的有需要, Java 也可以用更複雜的手段來處理他 ( Server 使用特殊的 classloader ,而 client 傳遞資料前先把 class 傳給 server ,而 server 就可以動態載入 class 並用來處理之後收到的物件資料)。


    回复wctang说:
    classloader的做法也许可行,不过实际操作起来,估计会有很大的难度,那还不如重新定义协议。

    多谢你的分析,感谢你的关注!
    2004-07-18 17:05:11
  • 我对java不太懂,弄斧一下,错误了别见怪




    我觉得借口你可以做成没有类的,或者是string这样标准类的,这样没有问题。




    做成有类的时候,那么这个类就是所谓的接口类,就是说这个类必须是server和client都有的,因为他是接口,c/s之间的接口当然是双方都可见的。但是问题在于,这个接口这个协议,是应该有相对不变性的。另外,我觉得你会认为server必须包含client的类,这个想法也许反过来,你就释怀了。广义的说,操作系统也是我们的一个server,函数库也是我们的server,应该说client必须包含server定义的接口类的定义。




    看看com,就是一个例子,实际上com的接口,就像是一个抽象类,但是作为com server的组件和com client的应用程序都必须有这个类定义,这就是因为所谓借口,就是server和client只见的通讯协议。
    回复tinyfool说:
    我可以接受client包含server类的做法,但反过来,让server包含client,至少现在我还觉得很别扭。
    2004-07-17 16:36:06
  • 我觉得你抱怨错了,在这一点上ejb没有错,Serializable也没有错,他们的原理是清楚的。只是你的需求不能用它来直接实现而已。




    当然我有一点疑惑的地方,就是你所说的“client的类”。什么叫做“client的类”,如果一个class只是在客户端使用,把它传送到服务端是为什么,服务端如何处理一个没有定义的类。这是我在你的blog没能弄清楚。




    我们以前遇到类似的问题——服务端与客户端之间传送的数据格式不固定。项目使用c++语言,自行定义了一个类似于Map功能的结构,提供set/getVaule()的方法,只是支持基本类型。Java语言好多了,基本类型的Serializable都直接提供了。实现这样的结构有两个重点:一、Name=Value格式,Name是全局唯一的,一开始要分配好避免后期冲突;二、向下兼容,比如ServiceA(mymap),如果后期service实现变更,mymap增加了新字段,要求原来的客户端依旧可用。
    回复flyisland说:
    文中提到的MapWrapper就是一个client的类,它只在client的代码中显式的出现了,而server的代码中并没有直接的调用它。但是server端必须要有它,否则反序列化就没有办法完成。

    在我遇到的问题中,通信的格式通常是固定的,使用序列化做通信却是一种很糟糕的方法。遗憾的是,很多教材还把它作为一种方法来宣扬,误人子弟。

    至于错与不错,那是个人的感觉,就像有人喜欢EJB,有人喜欢开源一样,我们可以强调自己所爱有多好,却没有办法让所有人都与自己一样。
    2004-07-16 11:19:44
  • 您好,對此問題我也有些經驗供您參考。




    關於 Serializable,最近也我的專案中使用它,也是用在 c/s 間的傳輸。當然也有和您一樣的問題,所以一開始我就有些 c/s 共用的類別,在 release 時兩方都要包含這些類別。當然,也就有您最後的問題了。




    而關於這點,其實不是 Serializable 的問題,不管用什麼方法來做,只要傳的東西有所變動,就必需雙方都有修改。比較優雅的方法是在 Serializable 物件中指定 serialization version UID ,並控制 readObject 和 writeObject,也就是自行控制其 Serialized Form ,就不會遇到您文後的問題了。


    回复wctang说:
    我认同你所提的方法,但我并不喜欢使用Serializable的作为系统之间通信的协议,耦合性太强。而且事实上,许多用Java的人都不是很清楚序列化的细节,更不用说充分利用了。

    我更喜欢那种以普通文本作为通信协议的方式,简单,开发和测试都方便。
    2004-07-16 10:06:14