JDK1.1的新功能--序列化接口(Serializableinterface),简化了对象持久化(Persistence)的实现。以下介绍如何通过SMTPE-mail将对象传送给另一个用户。
摘要:一些应用程序需要以一种非实时的方式(例如旅行指南、错误报告(bugreport)、 时间表(timesheet)等)和其余用户共享对象。Java语言开发工具包(JDK)1.1版提供了一 个重要的功能:java.io.Serializable接口。该技术能让你知道如何序列化一个对象,然 后用e-mail传给其它用户。
对象持久化和用户间对象共享是许多商业解决方案的基础。例如,一个公司可以用从本公司网址启动的Applet来完成一个时间表的制作。同样该公司也可以提供象具有开支报告、旅行指 南、错误报告(bugreport)等功能的Applet。在这些情况下,从Applet的使用者获得的数据需要和负责薪水、付款、旅行房间预订的人们共享。执行这些职能的人们可能分布在不同的 城市和国家,可能工作在不同的时区,不能希望每个工作人员都能象猫头鹰一样在晚上工作以填写这样的表格。相同的信息也不应该重新输入。因此,能够存储并且把这些相关对象传 送到商业应用中是这些applet有别于其它applet的优势。
目前已经有许多方法能实现对象的持久化,例如使用对象数据库和磁盘文件。同样的也有许多办法可以共享对象,例如将数据写入一个套接字,或者实现一个符合CORBA,SOM的模 型。以上这几种方案均有自己的优点,当你设计你的商业解决方案时,需要认真地考虑这些方案。但是,还有一种开销不大但可靠的方法,它使用Internet和Intranet用户能够获取的 技术服务在世界范围传送对象的拷贝。它就是简单邮件传输协议,SMTP。
用E-mail发送Java对象
存储和保存对象的一个简单方法是将对象序列化而后用E-mail将它发送给别的用户。这种 方法有以下优点:
发送的计算机或NC(网络计算机)无需硬盘空间
使用现有的系统传送、排队、发送对象
允许用户使用最喜欢的邮件客户程序来接受邮件
提供简单的机制将同一对象的拷贝分发给许多人
这种方法也有不足之处:
邮件的传送可能因为E-mail主机的关机而被较长时间地延迟。所有的主机都可能出现这 种情况,E-mail服务器的错误恢复优先级通常比数据库服务器低。
邮件的传送不能得到保证--在你的E-mail服务器通知你邮件没有发出时,你不得不重新 发送邮件。
E-mail服务器和POP客户程序的功能不足以处理大量交易信息。
这些不足和你使用的应用程序有关。对于很多商业解决方案,这些不足并不重要。作为一个设计人员,你工作的一部分就是在全面考虑价格、性能和需求的情况下确定系统的最佳整体结构。
使用Java传送对象的四个步骤:
Applet必须依次以下面所列出的四个步骤传送Java对象:
序列化有关对象
发送时选择Base64编码方式对序列化对象编码(RFC1521)
与一个SMTP服务器连接
将该对象传送到这个SMTP服务器
下面将介绍如何用E-mail发送一个假设的"臭虫"报告到公司的质量保证部门。
将对象序列化
JDK1.1提供的一个奇妙的机制,java.io.Serializable接口,能够序列化并且重建对象。 这个接口能使用存储对象(writeObject())和恢复对象(readObject())方法函数。在很多 情况下,使用这个接口很方便,只需实现并且调用这两个方法函数。
以下的代码定义了一个简单的BugReport对象,它实现了最简单的序列化接口。
1 import java.Io.*;
2 public class BugReport implements Serializable {
3 private Float m_SoftwareVersion; // version number from Help.About, e.g. "1.0"
4 private String m_ErrorDescription; // Description of error
5 private int m_Severity; // 1=System unusable - 5=Minor Aesthetic defect
6 public BugReport (Float SoftwareVersion, String ErrorDescription, int Severity) {
7 m_SoftwareVersion = SoftwareVersion;
8 m_ErrorDesctiption = ErrorDescription;
9 m_Severity = Severity;
10 }
11 public BugReport () {} // for reconstituting serialized objects
12 public void save (OutputStream os)
13 throws IOException {
14 try {
15 ObjectOutputStream o = new ObjectOutputStream(os);
16 o.writeObject(this);
17 o.flush();
18 }
19 catch (IOException e) {throw e;}
20 }
21 public BugReport restore (InputStream is)
22 throws IOException, ClassNotFoundException {
23 BugReport RestoredBugReport = null;
24 try {
25 ObjectInputStream o = new ObjectInputStream(is);
26 RestoredBugReport = (BugReport)o.readObject();
27 }
28 catch (IOException e) {throw e;}
29 catch (ClassNotFoundException e) {throw e;}
30 return RestoredBugReport;
31 }
32 }
1使用import语句引入I/O包,包括序列化接口。
2-5定义类中的成员变量,并指出该类实现了序列化接口。
6-10提供一个简单的构造函数
11一个空的构造函数。这个构造函数在重建序列化对象时使用。见以下的例子。
12-20定义一个方法函数,它把对象写入一个已经打开了的ObjectOutputStream。这个方 法函数首先创建一个ObjectOutputStream对象,然后调用writeObject方法函数,最后在 函数返回前显式清空输出缓冲区。
21-30定义一个方法函数,它从一个打开了的InputStream中读入一个BugReport对象。注 意,如果输入流中下一个对象和正在读入对象的类型不一致时,readObject()将会抛出一 个异常。
使用BugReport对象相当简单。譬如我们想要创建一个新的BugReport对象并且把它存入 一个文件,我们会用到以下代码:
1 import java.io.*;
.
.
2 BugReport bug = new BugReport(1.0, "Crashes when spell checker invoked", 2);
3 FileOUtputStream os = new FileOutputStream("MyBug.test");
4 bug.save(os);
很简单,对吗?当然,一旦对象已经被序列化,没有人能阻止你继续操纵对象的状态。上一 个例子中包涵了一个在被写入磁盘时已经存在对象的拷贝。因此你必须要十分谨慎,以防 在对对象做出所有的修改之后没有序列化对象,从而丢失了对象的状态修改信息。
以下是怎样恢复一个对象的拷贝:
1 import java.io.*
.
.
2 FileInputStream fis = new FileInputStream("MyBug.test");
3 BugReport bug = new BugReport().restore(fis);
这更简单!是不是Java的功能越来越强大了?
现在我们修改第二个例子的第3行,使对象被写入一个字节数组而不是一个文件:
1 import java.io.*
.
.
2 BugReport bug = new BugReport(1.0, "Crashes when spell checker invoked", 2);
3 字 节ArrayOutputStream os = new 字 节ArrayOutputStream();
4 bug.save(os);
好了,我们已经构造了一个对象,并且学会把它序列化后放入一个字节OutputStream。然 后,我们将把这个字节OutputStream转化为一个Base64编码的字符串。
.Base64编码
目前的Internet E-mail标准--简单邮件传递协议(SMTP)在RFC821中宣布。对于我们来说, RFC821对邮件的内容规定了两条重要但不难实现的限制。
1.邮件的内容必须全部为7-比特的美国ASCII码。
2.每一行的长度不能超过1000的字符。
因此为了通过SMTP用E-mail进行传送,内存的序列化对象必须转化为和以上相容的格式。
RFC1521提供了一个可行的方案。它定义了邮件的内容部分,使之能包涵多种形式的数 据。这种标准就是目前众所周知的MIME。
按照RFC1521编码过程为:输入是24个比特,输出是4个字节。24个比特输入组从左至右 由3个8比特的输入组形成。这24个比特被看成4个连续的6比特组,而每个6比特输入组被翻 译为Base64码表中的一个数字。
这意味着如果我们有下面的3个字节的输入--xC,xF3,xFF--它将会被转化为如下 的Base64的编码:x3,xF,xF,x3F。
图Base64编码实例
Base64编码似乎有点神秘,但实现它的代码却非常简单,在下面的程序中我们可以看到 这一点。在这个例子中,我们创建了一个新类,Codecs。现在,Codecs有两个方法函数:一 个用来对字符数组编码,一个用来对String类编码。对String类编码的方法函数简单地调 用String类的getBytes()函数,然后对返回的结果字符数组进行编码。我们将增加从Base6解 码至原先格式的方法函数。
1 public class Codecs {
2 private Codecs() {} // do not instantiate this class
3 public final static String base64Encode(String strInput) {
4 if (strInput == null) return null;
5 byte byteData[] = new byte[strInput.length()];
6 strInput.getBytes(0, strInput.length(), byteData, 0);
7 return new String(base64Encode(byteData), 0);
8 }
9 public final static byte[] base64Encode(byte[] byteData) {
10 if (byteData == null) return null;
11 int iSrcIdx; // index into source (byteData)
12 int iDestIdx; // index into destination (byteDest)
13 byte byteDest[] = new byte[((byteData.length+2)/3)*4];
14 for (iSrcIdx=0, iDestIdx=0; iSrcIdx 2) %26amp; 077);
16 byteDest[iDestIdx++] = (byte) ((byteData[iSrcIdx+1] 4) %26amp; 017 |
(byteData[iSrcIdx] 6) %26amp; 003 |
(byteData[iSrcIdx+1]
1-2定义public的Codecs类和一个不能被用户调用的构造函数。通常,这个类
不应被例示。
3-8定义一个encodeBase64()方法函数。它的参数类型是String类型,返回Base64编码的String。 它通过调用String。getBytes()并将结果数组传送至encodeBase64(byte[])来完成函数的 功能。
9-39定义一个encodeBase64()方法函数。参数为字符数组,返回Base64编码的数组数组。
10如果参数值为null,退出方法函数。
11-13定义工作变量,其中字节Dest数组包含了返回调用者的编码。注意,转换后的数组 比输入数组大约大三分之一。这是因为每个三字节s组被转换成四个字节。
14-19循环遍历整个输入数组,每次24比特,把这三个8比特组转换成四个两两之间相距二 比特的6比特组。这段代码比最初出现时简单。仔细学习,看看和前面的例子有什么不同。
20-28如果输入数组的字节s数目不是3的倍数,则转换余下的1或2个字节。
29-35把所得的编码数据作为Base64码表的下标。(Base64码表在RFC1521中说明)
36-37把目标串中的没有使用的字符置为'='。
38返回给调用者基于Based64的编码。
我们已经取得了很重要的进展。到目前为止,我们已能序列化对象并将它放入内存,把它 转化为基于Base64编码,目的是使用E-mail工具将它发给目标用户。作为我们目前进展的 总结,下面有一个代码片断,它生成一个BugReport对象实例,把它序列化并放入内存,然后 转化为Base64编码。
1 import java.io.*;
2 import Codecs.base64Encode;
:
3 BugReport bug = new BugReport(1.0, "Crashes when spell checker invoked", 2);
4 字 节ArrayOutputStream os = new 字 节ArrayOutputStream();
5 bug.save(os);
6 String strSerializedBug = os.toString();
7 strSerializedBug = Codecs.base64Encode(strSerializedBug);
.和SMTP服务器连接
如果你想发一封邮件,必须通过以下五个步骤:
1.为SMTP服务器申请一个域名
2.建立一个TCP/IP的会话(session)
3.在服务器上登录
4.填写收信人的地址
5.撰写邮件的内容
下面分别就这些步骤进行讨论。
为SMTP服务器申请一个域名
正如你发送普通信件一样,你首先得找一个邮局。对居民区和商业单位来说这是一件非 常简单的事,因为邮筒就在门口。Internet上的电子邮件也同样简单。
你或许已经有一个Internet的电子邮件帐号,如果这样,你的邮件阅读工具,可能是和WWW 浏览器捆绑在一起的软件,或者是单独的软件如Eudora,要求你输入一个有关SMTP服务器 的信息。它就是你所使用的DNS服务器的域名。这里有一些Internet服务提供商(ISP)供商的SMTP 服务器域名:
你的Internet服务提供商将为它的SMTP服务器使用和上例相似的域名。
..与服务器建立一个TCP/IP会话
一般SMTP服务器在25号端口(port25)监听连接请求。因此,和一个SMTP主机建立一个TCP/IP 连接就是建立一个和25号端口连接的套接字(socket)。下面这段Java程序试图建立一个和 域名为"smtp.tjd.com"的主机的连接:
1 import java.net.*;
2 import java.io.*;
3 :
4 Socket socketSmtpServer = null;
5 DataOutputStream dos = null;
6 DataInputStream dis = null;
7 try {
8 socketSmtpServer = new Socket("smtp.tjd.com", 25);
9 dos = new DataOutputStream(socketSmtpServer.getOutputStream());
10 dis = new DataInputStream (socketSmtpServer.getInputStream());
11 }
12 catch (UnknownHostException e) {throw (e);}
13 catch (IOException e) {throw (e);}
这段代码在建立TCP/IP连接的同时创建一个DataOutputStream对象和一个DataInputStream 对象,我们以后将会使用它们从SMTP服务器发送和接收数据。
..在服务器上登录
和在UNIX系统或者数据库系统上登录不同,你无须在一个SMTP服务器上真正地登录,因为 这里没有确认/授权的过程,根本不需要真正登录。你只是简单地让服务器能识别你,这样才 有资格成发送邮件。这一步其实不真正需要,但是不能忽略它。
当你第一次和服务器连接时,服务器给你发送确认它自己和它的SMTP版本号两行数据。 我们不关心这些数据,我们只是读入它们并且将其忽略。在我们读入这些数据后,服务器将 把我们推到"司机"的座位上然后等待和回答命令。下面是用Java语言实现的登 录过程:
1 String strBuf;
2 String strMyName = "tomdaley";
3 strBuf = dis.readLine();
4 strBuf = dis.readLine();
5 dos.writeBytes("HELO " + strMyName + "\n");
6 strBuf = dis.readLine();
7 dos.writeBytes("RSET\n");
8 strBuf = dis.readLine();
HELO命令让服务器识别你的身份,RSET命令重置SMTP服务器的状态。如果一切顺利,RSET 命令不是必须的。但是因为事情并非总是进行顺利,而RSET是一个发送和执行起来很" 便宜"的命令,因此首先执行这条命令是一个很好的方法。
注意在每条write字节s()语句后的readLine()语句,SMTP服务器为你发送的每条命令 返回一个状态信息。状态信息以一个3字节的数字开始,它被用来判断命令执行成功与否。 RFC821对此有详尽的解释。
..写明收信人的地址
下一步我们准备填写收信人的地址。和所有礼貌的信件相似,我们应该提供给邮件传送 代理和接收者地址同样清楚的回信地址。下面Java的代码实现了这个功能:
1 dos.writeBytes("MAIL FROM:\n");
2 dis.readLine();
3 dos.writeBytes("RCPT TO:\n");
4 dos.readLine();
..撰写邮件的内容
现在我们准备创建邮件最有趣的部分--数据区域。数据区域包括两个子区域:
1.邮件客户程序阅读的头部信息
2.MIME-编码的正文和数据
数据区域的头部信息并不是必须的,但在你使用收信客户程序看信时,它能使你的邮件 看起来更美观。头部信息是邮件内容的一个概括,它使邮件更容易管理。
数据区域头部信息和办公室间的备忘录的开头相似,可以这样发送:
1 dos.writeBytes ("DATA\N");
2 strBuf = dis.readLine();
3 dos.writeBytes ("To: Tom Daley \n");
4 dos.writeBytes ("From: Tom Daley \n");
5 dos.writeBytes ("Subject: Bug Report\n");
注意,我们在发送"DATA"命令后读入且只读入一行。当你发送"DATA" 命令的时候,服务器可能回送下面形式的消息作为应答:"354Entermail,endwith "."onalinebyitself"。这意味着如果SMTP服务器没有看到信件的 结尾,不会通过套接字发送任何数据。
为了能传送序列化编码对象,我们把它封闭到邮件内容的MIME部分。MIME部分最开始是 纯文本,用MIME的语法来描述,就是"Content-Type:text/plain。"。在文本部 分我们将发送若干指令,用来指明一起发送的对象和关于它的简单说明。下面的代码实现 了这种功能:
1 String strBoundary = "SimpleBoundary";
2 String strInstructions = "Save the attached file and read it with BugNews.class.";
2 dos.writeBytes("Mime-Version 1.0\n");
3 dos.writeBytes("Content-Type: multipart/mixed; boundary=\"" + strBoundary + "\"\n");
4 dos.writeBytes("--" + strBoundary + "\n");
5 dos.writeBytes("Content-Type: text/plain; charset=\"us-ascii\"\n\n");
6 dos.writeBytes(strInstructions + "\n");
现在我们该做费了这么多的口舌一直想做的事情,将序列化的对象附在SMTP邮件内容上。 请记住,邮件内容每行不能超出1000个字节。对于有多个部分的MIME则有更多的限制,即每 行不能超出74个字节的二进制编码。这意味着我们必须声明一个string对象,它包含序列 化的,基于BASE64编码对象,然后我们以每次74个字节的方式将这个string对象写入SMTP 套接字。
1 dos.writeBytes("--" + strBoundary + "\n");
2 dos.writeBytes("Content-Type: application/octet-stream; name=\"BugReport.bug\"\n");
3 dos.writeBytes("Content-Transfer-Encoding: base64\n");
4 dos.writeBytes("Content-Disposition: attachment; filename=\"BugReport.bug\"\n");
5 dos.writeBytes("Content-Description: Bug Report from a customer\n\n");
6 int iLines = strObject.length() / 74;
7 for (i = 0; i
我们完成了!现在我们的错误报告(bugreport)正延着全球SMTP邮件分发系统迂回前进,它将很快出现在一些人的办公桌上。不过我敢打赌,他们收到错误报告时的心情不会和你解决如何发送它时的心情一样愉快 ,