licheng700

BlogJava 首页 新随笔 联系 聚合 管理
  26 Posts :: 5 Stories :: 5 Comments :: 1 Trackbacks

2005年9月20日 #

 

jungleford如是说

    已经有一个多月没有搭理blog了,原因很多,譬如实验室的项目正在收工,巨忙;譬如找工作及其相关的事情;而且二月份大部分时间是陪老爹老妈,家里拨号的速度可想而知……但主要还是没有找到一个合适的topic,或者说这段时间懒了(临毕业前期综合症),净在看《汉武大帝》和历史方面的书,还有其它乱七八糟的闲书,就是没有认真地玩Java,哈哈!现在工作差不多落实了,好在不算太烂,小资青年jungleford的生活又开始步入正轨了!以上是新年里的一些废话。
    今天稍微聊一点关于“程序状态保存”方面的问题,我们很容易就会想到“序列化”(Serialization,有的书上又翻译为“顺序化”或者“串行化”,但“串行”一词总是让我联想到通信和硬件接口,所以我更习惯于“序列化”的叫法,何况这种叫法是有来头的,后面我会谈到这个名称的由来),当然,序列化是一种方便有效的数据存取方式,但它还有更加广泛的应用。广义上讲,就是讨论一下I/O的一些应用。

文件I/O:文件流→序列化

文件流
    文件操作是最简单最直接也是最容易想到的一种方式,我们说的文件操作不仅仅是通过FileInputStream/FileOutputStream这么“裸”的方式直接把数据写入到本地文件(像我以前写的一个扫雷的小游戏JavaMine就是这样保存一局的状态的),这样就比较“底层”了。

主要类与方法 描述
FileInputStream.read() 从本地文件读取二进制格式的数据
FileReader.read() 从本地文件读取字符(文本)数据
FileOutputStream.write() 保存二进制数据到本地文件
FileWriter.write() 保存字符数据到本地文件

XML
    和上面的单纯的I/O方式相比,XML就显得“高档”得多,以至于成为一种数据交换的标准。以DOM方式为例,它关心的是首先在内存中构造文档树,数据保存在某个结点上(可以是叶子结点,也可以是标签结点的属性),构造好了以后一次性的写入到外部文件,但我们只需要知道文件的位置,并不知道I/O是怎么操作的,XML操作方式可能多数人也实践过,所以这里也只列出相关的方法,供初学者预先了解一下。主要的包是javax.xml.parsersorg.w3c.domjavax.xml.transform

主要类与方法 描述
DocumentBuilderFactory.newDocumentBuilder().parse() 解析一个外部的XML文件,得到一个Document对象的DOM树
DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() 初始化一棵DOM树
Document.getDocumentElement(). appendChild() 为一个标签结点添加一个子结点
Document.createTextNode() 生成一个字符串结点
Node.getChildNodes() 取得某个结点的所有下一层子结点
Node.removeChild() 删除某个结点的子结点
Document. getElementsByTagName() 查找所有指定名称的标签结点
Document.getElementById() 查找指定名称的一个标签结点,如果有多个符合,则返回某一个,通常是第一个
Element.getAttribute() 取得一个标签的某个属性的的值
Element.setAttribute() 设置一个标签的某个属性的的值
Element.removeAttribute() 删除一个标签的某个属性
TransformerFactory.newInstance().newTransformer().transform() 将一棵DOM树写入到外部XML文件

序列化
    使用基本的文件读写方式存取数据,如果我们仅仅保存相同类型的数据,则可以用同一种格式保存,譬如在我的JavaMine中保存一个盘局时,需要保存每一个方格的坐标、是否有地雷,是否被翻开等,这些信息组合成一个“复合类型”;相反,如果有多种不同类型的数据,那我们要么把它分解成若干部分,以相同类型(譬如String)保存,要么我们需要在程序中添加解析不同类型数据格式的逻辑,这就很不方便。于是我们期望用一种比较“高”的层次上处理数据,程序员应该花尽可能少的时间和代码对数据进行解析,事实上,序列化操作为我们提供了这样一条途径。
    序列化(Serialization)大家可能都有所接触,它可以把对象以某种特定的编码格式写入或从外部字节流(即ObjectInputStream/ObjectOutputStream)中读取。序列化一个对象非常之简单,仅仅实现一下Serializable接口即可,甚至都不用为它专门添加任何方法:

public class MySerial implements java.io.Serializable
{
  ...
}

但有一个条件:即你要序列化的类当中,它的每个属性都必须是是“可序列化”的。这句话说起来有点拗口,其实所有基本类型(就是int,char,boolean之类的)都是“可序列化”的,而你可以看看JDK文档,会发现很多类其实已经实现了Serializable(即已经是“可序列化”的了),于是这些类的对象以及基本数据类型都可以直接作为你需要序列化的那个类的内部属性。如果碰到了不是“可序列化”的属性怎么办?对不起,那这个属性的类还需要事先实现Serializable接口,如此递归,直到所有属性都是“可序列化”的

主要类与方法 描述
ObjectOutputStream.writeObject() 将一个对象序列化到外部字节流
ObjectInputStream.readObject() 从外部字节流读取并重新构造对象

    从实际应用上看来,“Serializable”这个接口并没有定义任何方法,仿佛它只是一个标记(或者说像是Java的关键字)而已,一旦虚拟机看到这个“标记”,就会尝试调用自身预定义的序列化机制,除非你在实现Serializable接口的同时还定义了私有的readObject()或writeObject()方法。这一点很奇怪。不过你要是不愿意让系统使用缺省的方式进行序列化,那就必须定义上面提到的两个方法:

public class MySerial implements java.io.Serializable
{
  private void writeObject(java.io.ObjectOutputStream out) throws IOException
  {
    ...
  }
  private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
  {
    ...
  }
  ...
}

    譬如你可以在上面的writeObject()里调用默认的序列化方法ObjectOutputStream.defaultWriteObject();譬如你不愿意将某些敏感的属性和信息序列化,你也可以调用ObjectOutputStream.writeObject()方法明确指定需要序列化那些属性。关于用户可定制的序列化方法,我们将在后面提到。

Bean
    上面的序列化只是一种基本应用,你把一个对象序列化到外部文件以后,用notepad打开那个文件,只能从为数不多的一些可读字符中猜到这是有关这个类的信息文件,这需要你熟悉序列化文件的字节编码方式,那将是比较痛苦的(在《Core Java 2》第一卷里提到了相关编码方式,有兴趣的话可以查看参考资料),某些情况下我们可能需要被序列化的文件具有更好的可读性。另一方面,作为Java组件的核心概念“JavaBeans”,从JDK 1.4开始,其规范里也要求支持文本方式的“长期的持久化”(long-term persistence)。
    打开JDK文档java.beans包里的有一个名为“Encoder”的类,这就是一个可以序列化bean的实用类。和它相关的两个主要类有XMLEcoderXMLDecoder,显然,这是以XML文件的格式保存和读取bean的工具。他们的用法也很简单,和上面ObjectOutputStream/ObjectInputStream比较类似。

主要类与方法 描述
XMLEncoder.writeObject() 将一个对象序列化到外部字节流
XMLDecoder.readObject() 从外部字节流读取并重新构造对象

    如果一个bean是如下格式:

public class MyBean
{
  int i;
  char[] c;
  String s;
  ...(get和set操作省略)...
}

那么通过XMLEcoder序列化出来的XML文件具有这样的形式:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.4.0" class="java.beans.XMLDecoder">
  <object class="MyBean">
    <void property="i">
      <int>1</int>
    </void>
    <void property="c">
      <array class="char" length="3">
        <void index="0">
          <int>a</int>
        </void>
        <void index="1">
          <int>b</int>
        </void>
        <void index="2">
          <int>c</int>
        </void>
      </array>
    </void>
    <void property="s">
      <string>fox jump!</string>
    </void>
  </object>
</java>

    像AWTSwing中很多可视化组件都是bean,当然也是可以用这种方式序列化的,下面就是从JDK文档中摘录的一个JFrame序列化以后的XML文件:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.0" class="java.beans.XMLDecoder">
  <object class="javax.swing.JFrame">
    <void property="name">
      <string>frame1</string>
    </void>
    <void property="bounds">
      <object class="java.awt.Rectangle">
        <int>0</int>
        <int>0</int>
        <int>200</int>
        <int>200</int>
      </object>
    </void>
    <void property="contentPane">
      <void method="add">
        <object class="javax.swing.JButton">
          <void property="label">
            <string>Hello</string>
          </void>
        </object>
      </void>
    </void>
    <void property="visible">
      <boolean>true</boolean>
    </void>
  </object>
</java>

    因此但你想要保存的数据是一些不是太复杂的类型的话,把它做成bean再序列化也不失为一种方便的选择。

Properties
    在以前我总结的一篇关于集合框架的小文章里提到过,Properties是历史集合类的一个典型的例子,这里主要不是介绍它的集合特性。大家可能都经常接触一些配置文件,如Windows的ini文件,Apache的conf文件,还有Java里的properties文件等,这些文件当中的数据以“关键字-值”对的方式保存。“环境变量”这个概念都知道吧,它也是一种“key-value”对,以前也常常看到版上问“如何取得系统某某信息”之类的问题,其实很多都保存在环境变量里,只要用一条

System.getProperties().list(System.out);

就能获得全部环境变量的列表:

-- listing properties --
java.runtime.name=Java(TM) 2 Runtime Environment, Stand...
sun.boot.library.path=C:\Program Files\Java\j2re1.4.2_05\bin
java.vm.version=1.4.2_05-b04
java.vm.vendor=Sun Microsystems Inc.
java.vendor.url=http://java.sun.com/
path.separator=;
java.vm.name=Java HotSpot(TM) Client VM
file.encoding.pkg=sun.io
user.country=CN
sun.os.patch.level=Service Pack 1
java.vm.specification.name=Java Virtual Machine Specification
user.dir=d:\my documents\项目\eclipse\SWTDemo
java.runtime.version=1.4.2_05-b04
java.awt.graphicsenv=sun.awt.Win32GraphicsEnvironment
java.endorsed.dirs=C:\Program Files\Java\j2re1.4.2_05\li...
os.arch=x86
java.io.tmpdir=C:\DOCUME~1\cn2lx0q0\LOCALS~1\Temp\
line.separator=

java.vm.specification.vendor=Sun Microsystems Inc.
user.variant=
os.name=Windows XP
sun.java2d.fontpath=
java.library.path=C:\Program Files\Java\j2re1.4.2_05\bi...
java.specification.name=Java Platform API Specification
java.class.version=48.0
java.util.prefs.PreferencesFactory=java.util.prefs.WindowsPreferencesFac...
os.version=5.1
user.home=D:\Users\cn2lx0q0
user.timezone=
java.awt.printerjob=sun.awt.windows.WPrinterJob
file.encoding=GBK
java.specification.version=1.4
user.name=cn2lx0q0
java.class.path=d:\my documents\项目\eclipse\SWTDemo\bi...
java.vm.specification.version=1.0
sun.arch.data.model=32
java.home=C:\Program Files\Java\j2re1.4.2_05
java.specification.vendor=Sun Microsystems Inc.
user.language=zh
awt.toolkit=sun.awt.windows.WToolkit
java.vm.info=mixed mode
java.version=1.4.2_05
java.ext.dirs=C:\Program Files\Java\j2re1.4.2_05\li...
sun.boot.class.path=C:\Program Files\Java\j2re1.4.2_05\li...
java.vendor=Sun Microsystems Inc.
file.separator=\
java.vendor.url.bug=http://java.sun.com/cgi-bin/bugreport...
sun.cpu.endian=little
sun.io.unicode.encoding=UnicodeLittle
sun.cpu.isalist=pentium i486 i386


主要类与方法 描述
load() 从一个外部流读取属性
store() 将属性保存到外部流(特别是文件)
getProperty() 取得一个指定的属性
setProperty() 设置一个指定的属性
list() 列出这个Properties对象包含的全部“key-value”对
System.getProperties() 取得系统当前的环境变量

    你可以这样保存一个properties文件:

Properties prop = new Properties();
prop.setProperty("key1", "value1");
...
FileOutputStream out = new FileOutputStream("config.properties");
prop.store(out, "--这里是文件头,可以加入注释--");

Preferences
    如果我说Java里面可以不使用JNI的手段操作Windows的注册表你信不信?很多软件的菜单里都有“Setting”或“Preferences”这样的选项用来设定或修改软件的配置,这些配置信息可以保存到一个像上面所述的配置文件当中,如果是Windows平台下,也可能会保存到系统注册表中。从JDK 1.4开始,Java在java.util下加入了一个专门处理用户和系统配置信息的java.util.prefs包,其中一个类Preferences是一种比较“高级”的玩意。从本质上讲,Preferences本身是一个与平台无关的东西,但不同的OS对它的SPI(Service Provider Interface)的实现却是与平台相关的,因此,在不同的系统中你可能看到首选项保存为本地文件、LDAP目录项、数据库条目等,像在Windows平台下,它就保存到了系统注册表中。不仅如此,你还可以把首选项导出为XML文件或从XML文件导入。

主要类与方法 描述
systemNodeForPackage() 根据指定的Class对象得到一个Preferences对象,这个对象的注册表路径是从“HKEY_LOCAL_MACHINE\”开始的
systemRoot() 得到以注册表路径HKEY_LOCAL_MACHINE\SOFTWARE\Javasoft\Prefs 为根结点的Preferences对象
userNodeForPackage() 根据指定的Class对象得到一个Preferences对象,这个对象的注册表路径是从“HKEY_CURRENT_USER\”开始的
userRoot() 得到以注册表路径HKEY_CURRENT_USER\SOFTWARE\Javasoft\Prefs 为根结点的Preferences对象
putXXX() 设置一个属性的值,这里XXX可以为基本数值型类型,如int、long等,但首字母大写,表示参数为相应的类型,也可以不写而直接用put,参数则为字符串
getXXX() 得到一个属性的值
exportNode() 将全部首选项导出为一个XML文件
exportSubtree() 将部分首选项导出为一个XML文件
importPreferences() 从XML文件导入首选项

    你可以按如下步骤保存数据:

Preferences myPrefs1 = Preferences.userNodeForPackage(this);// 这种方法是在“HKEY_CURRENT_USER\”下按当前类的路径建立一个注册表项
Preferences myPrefs2 = Preferences.systemNodeForPackage(this);// 这种方法是在“HKEY_LOCAL_MACHINE\”下按当前类的路径建立一个注册表项
Preferences myPrefs3 = Preferences.userRoot().node("com.jungleford.demo");// 这种方法是在“HKEY_CURRENT_USER\SOFTWARE\Javasoft\Prefs\”下按“com\jungleford\demo”的路径建立一个注册表项
Preferences myPrefs4 = Preferences.systemRoot().node("com.jungleford.demo");// 这种方法是在“HKEY_LOCAL_MACHINE\SOFTWARE\Javasoft\Prefs\”下按“com\jungleford\demo”的路径建立一个注册表项
myPrefs1.putInt("key1", 10);
myPrefs1.putDouble("key2", -7.15);
myPrefs1.put("key3", "value3");
FileOutputStream out = new FileOutputStream("prefs.xml");
myPrefs1.exportNode(out);

网络I/O:Socket→RMI

Socket
    Socket编程可能大家都很熟,所以就不多讨论了,只是说通过socket把数据保存到远端服务器或从网络socket读取数据也不失为一种值得考虑的方式。

RMI
    RMI机制其实就是RPC(远程过程调用)的Java版本,它使用socket作为基本传输手段,同时也是序列化最重要的一个应用。现在网络传输从编程的角度来看基本上都是以流的方式操作,socket就是一个例子,将对象转换成字节流的一个重要目标就是为了方便网络传输。
    想象一下传统的单机环境下的程序设计,对于Java语言的函数(方法)调用(注意与C语言函数调用的区别)的参数传递,会有两种情况:如果是基本数据类型,这种情况下和C语言是一样的,采用值传递方式;如果是对象,则传递的是对象的引用,包括返回值也是引用,而不是一个完整的对象拷贝!试想一下在不同的虚拟机之间进行方法调用,即使是两个完全同名同类型的对象他们也很可能是不同的引用!此外对于方法调用过程,由于被调用过程的压栈,内存“现场”完全被被调用者占有,当被调用方法返回时,才将调用者的地址写回到程序计数器(PC),恢复调用者的状态,如果是两个虚拟机,根本不可能用简单压栈的方式来保存调用者的状态。因为种种原因,我们才需要建立RMI通信实体之间的“代理”对象,譬如“存根”就相当于远程服务器对象在客户机上的代理,stub就是这么来的,当然这是后话了。
    本地对象与远程对象(未必是物理位置上的不同机器,只要不是在同一个虚拟机内皆为“远程”)之间传递参数和返回值,可能有这么几种情形:

  • 值传递:这又包括两种子情形:如果是基本数据类型,那么都是“可序列化”的,统统序列化成可传输的字节流;如果是对象,而且不是“远程对象”(所谓“远程对象”是实现了java.rmi.Remote接口的对象),本来对象传递的应该是引用,但由于上述原因,引用是不足以证明对象身份的,所以传递的仍然是一个序列化的拷贝(当然这个对象也必须满足上述“可序列化”的条件)。
  • 引用传递:可以引用传递的只能是“远程对象”。这里所谓的“引用”不要理解成了真的只是一个符号,它其实是一个留在(客户机)本地stub中的,和远端服务器上那个真实的对象张得一模一样的镜像而已!只是因为它有点“特权”(不需要经过序列化),在本地内存里已经有了一个实例,真正引用的其实是这个“孪生子”。
    由此可见,序列化在RMI当中占有多么重要的地位。

数据库I/O:CMP、Hibernate

什么是“Persistence”
    用过VMWare的朋友大概都知道当一个guest OS正在运行的时候点击“Suspend”将虚拟OS挂起,它会把整个虚拟内存的内容保存到磁盘上,譬如你为虚拟OS分配了128M的运行内存,那挂起以后你会在虚拟OS所在的目录下找到一个同样是128M的文件,这就是虚拟OS内存的完整镜像!这种内存的镜像手段其实就是“Persistence”(持久化)概念的由来。

CMP和Hibernate
    因为我对J2EE的东西不是太熟悉,随便找了点材料看看,所以担心说的不到位,这次就不作具体总结了,人要学习……真是一件痛苦的事情 

序列化再探讨

    从以上技术的讨论中我们不难体会到,序列化是Java之所以能够出色地实现其鼓吹的两大卖点——分布式(distributed)和跨平台(OS independent)的一个重要基础。TIJ(即“Thinking in Java”)谈到I/O系统时,把序列化称为“lightweight persistence”——“轻量级的持久化”,这确实很有意思。

为什么叫做“序列”化?
    开场白里我说更习惯于把“Serialization”称为“序列化”而不是“串行化”,这是有原因的。介绍这个原因之前先回顾一些计算机基本的知识,我们知道现代计算机的内存空间都是线性编址的(什么是“线性”知道吧,就是一个元素只有一个唯一的“前驱”和唯一的“后继”,当然头尾元素是个例外;对于地址来说,它的下一个地址当然不可能有两个,否则就乱套了),“地址”这个概念推广到数据结构,就相当于“指针”,这个在本科低年级大概就知道了。注意了,既然是线性的,那“地址”就可以看作是内存空间的“序号”,说明它的组织是有顺序的,“序号”或者说“序列号”正是“Serialization”机制的一种体现。为什么这么说呢?譬如我们有两个对象a和b,分别是类A和B的实例,它们都是可序列化的,而A和B都有一个类型为C的属性,根据前面我们说过的原则,C当然也必须是可序列化的。

import java.io.*;
...
class A implements Serializable
{
  C c;
  ...
}

class B implements Serializable
{
  C c;
  ...
}

class C implements Serializable
{
  ...
}

A a;
B b;
C c1;
...

    注意,这里我们在实例化a和b的时候,有意让他们的c属性使用同一个C类型对象的引用,譬如c1,那么请试想一下,但我们序列化a和b的时候,它们的c属性在外部字节流(当然可以不仅仅是文件)里保存的是一份拷贝还是两份拷贝呢?序列化在这里使用的是一种类似于“指针”的方案:它为每个被序列化的对象标上一个“序列号”(serial number),但序列化一个对象的时候,如果其某个属性对象是已经被序列化的,那么这里只向输出流写入该属性的序列号;从字节流恢复被序列化的对象时,也根据序列号找到对应的流来恢复。这就是“序列化”名称的由来!这里我们看到“序列化”和“指针”是极相似的,只不过“指针”是内存空间的地址链,而序列化用的是外部流中的“序列号链”
    使用“序列号”而不是内存地址来标识一个被序列化的对象,是因为从流中恢复对象到内存,其地址可能就未必是原来的地址了——我们需要的只是这些对象之间的引用关系,而不是死板的原始位置,这在RMI中就更是必要,在两台不同的机器之间传递对象(流),根本就不可能指望它们在两台机器上都具有相同的内存地址。

更灵活的“序列化”:transient属性和Externalizable
    Serializable确实很方便,方便到你几乎不需要做任何额外的工作就可以轻松将内存中的对象保存到外部。但有两个问题使得Serializable的威力收到束缚:
    一个是效率问题,《Core Java 2》中指出,Serializable使用系统默认的序列化机制会影响软件的运行速度,因为需要为每个属性的引用编号和查号,再加上I/O操作的时间(I/O和内存读写差的可是一个数量级的大小),其代价当然是可观的。
    另一个困扰是“裸”的Serializable不可定制,傻乎乎地什么都给你序列化了,不管你是不是想这么做。其实你可以有至少三种定制序列化的选择。其中一种前面已经提到了,就是在implements Serializable的类里面添加私有的writeObject()和readObject()方法(这种Serializable就不裸了,),在这两个方法里,该序列化什么,不该序列化什么,那就由你说了算了,你当然可以在这两个方法体里面分别调用ObjectOutputStream.defaultWriteObject()和ObjectInputStream.defaultReadObject()仍然执行默认的序列化动作(那你在代码上不就做无用功了?呵呵),也可以用ObjectOutputStream.writeObject()和ObjectInputStream.readObject()方法对你中意的属性进行序列化。但虚拟机一看到你定义了这两个方法,它就不再用默认的机制了。
    如果仅仅为了跳过某些属性不让它序列化,上面的动作似乎显得麻烦,更简单的方法是对不想序列化的属性加上transient关键字,说明它是个“暂态变量”,默认序列化的时候就不会把这些属性也塞到外部流里了。当然,你如果定义writeObject()和readObject()方法的化,仍然可以把暂态变量进行序列化。题外话,像transientviolatefinallyassert这样的关键字初学者可能会不太重视,而现在有的公司招聘就偏偏喜欢问这样的问题
    再一个方案就是不实现Serializable而改成实现Externalizable接口。我们研究一下这两个接口的源代码,发现它们很类似,甚至容易混淆。我们要记住的是:Externalizable默认并不保存任何对象相关信息!任何保存和恢复对象的动作都是你自己定义的。Externalizable包含两个public的方法:

public void writeExternal(ObjectOutput out) throws IOException;
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

    乍一看这和上面的writeObject()和readObject()几乎差不多,但Serializable和Externalizable走的是两个不同的流程:Serializable在对象不存在的情况下,就可以仅凭外部的字节序列把整个对象重建出来;但Externalizable在重建对象时,先是调用该类的默认构造函数(即不含参数的那个构造函数)使得内存中先有这么一个实例,然后再调用readExternal方法对实例中的属性进行恢复,因此,如果默认构造函数中和readExternal方法中都没有赋值的那些属性,特别他们是非基本类型的话,将会是空(null)。在这里需要注意的是,transient只能用在对Serializable而不是Externalizable的实现里面

序列化与克隆
    从“可序列化”的递归定义来看,一个序列化的对象貌似对象内存映象的外部克隆,如果没有共享引用的属性的化,那么应该是一个深度克隆。关于克隆的话题有可以谈很多,这里就不细说了,有兴趣的话可以参考IBM developerWorks上的一篇文章:JAVA中的指针,引用及对象的clone

一点启示

    作为一个实际的应用,我在写那个简易的邮件客户端JExp的时候曾经对比过好几种保存Message对象(主要是几个关键属性和邮件的内容)到本地的方法,譬如XML、Properties等,最后还是选择了用序列化的方式,因为这种方法最简单, 大约可算是“学以致用”罢。这里“存取程序状态”其实只是一个引子话题罢了,我想说的是——就如同前面我们讨论的关于logging的话题一样——在Java面前对同一个问题你可以有很多种solution:熟悉文件操作的,你可能会觉得Properties、XML或Bean比较方便,然后又发现了还有Preferences这么一个东东,大概又会感慨“天外有天”了,等到你接触了很多种新方法以后,结果又会“殊途同归”,重新反省Serialization机制本身。这不仅是Java,科学也是同样的道理。

posted @ 2005-09-28 10:09 小海船 阅读(327) | 评论 (0)编辑 收藏

1。追加文件方法
          1.java.io.FileWriter 的构造函数中:FileWriter(File file, boolean append)
          Constructs a FileWriter object given a File object.
append - if true, then bytes will be written to the end of the file rather than the beginning

         2.类java.io.RandomAccessFile extends Object implements DataOutput, DataInput 中方法
              seek(long pos)将当前操作指针移到文件末尾。
2.关于隔行写入文件java.io.BufferedWriter类中的方法newLine()



posted @ 2005-09-27 14:00 小海船 阅读(1177) | 评论 (1)编辑 收藏

Log4j 学习笔记

ccjsmile (http://ijsp.net)

 

这是我在学习Log4j时做的一点笔记,希望对各位朋友有一点帮助。我的mail:ccjsmile@sohu.com,希望能与您进行讨论^_*

 

Log4j 是一个开放源码项目,它是一个日志管理程序。

Log4j的优点:

1.       方便的调试信息;

2.       日志以各种丰富的(主要是文件)形式保留,用于以后分析;

缺点:减慢程序运行速度.


(A) 其中,level 是日志记录的优先级,分为OFFFATALERRORWARNINFODEBUGALL或者您定义的级别。Log4j建议只使用四个级别,优先级从高到低分别是ERRORWARNINFODEBUG。通过在这里定义的级别,您可以控制到应用程序中相应级别的日志信息的开关。比如在这里定义了INFO级别,则应用程序中所有DEBUG级别的日志信息将不被打印出来。

appenderName就是指定日志信息输出到哪个地方。您可以同时指定多个输出目的地。

 

(B) 其中,Log4j提供的appender有以下几种:

org.apache.log4j.ConsoleAppender(控制台),

org.apache.log4j.FileAppender(文件),

org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件),org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件),

org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)

 

(C) 其中,Log4j提供的layout有以下几种:

org.apache.log4j.HTMLLayout(以HTML表格形式布局),

org.apache.log4j.PatternLayout(可以灵活地指定布局模式),

org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),

org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)

 

 

下面介绍一下log4jweb中应用的例子:

这是一个用于log4j初始化的servlet

 

package net.ijsp.log4j;

 

import org.apache.log4j.PropertyConfigurator;

import javax.servlet.http.HttpServlet;

import javax.servlet.ServletException;

 

public class InitLog4j extends HttpServlet {

 

  public  void init() throws ServletException  {

   PropertyConfigurator.configure("D:/resin/webapps/log4j/web-inf/classes/log4j.properties");

    System.out.println("ok");

  }

}

 

在上述文件中我们发现需要一个log4j.properties的文件,他的存放路径为:D:/resin/webapps/log4j/web-inf/classes/log4j.properties

这个properties的文件内容如下:

#log4j.properties

#Set root logger level to DEBUG and its only appender to A1.

log4j.rootLogger=INFO,A1

#A1 is set to be a ConsoleAppender.

#log4j.appender.A1=org.apache.log4j.ConsoleAppender

       log4j.appender.A1=org.apache.log4j.RollingFileAppender

       log4j.appender.A1.File=example11.log

#A1 uses PatternLayout

       log4j.appender.A1.layout=org.apache.log4j.PatternLayout

       log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c \n- %m%n\n"

 

log4j.logger.ltestlog4j=INFO,A2

log4j.appender.A2=org.apache.log4j.ConsoleAppender

       log4j.appender.A2.layout=org.apache.log4j.PatternLayout

       log4j.appender.A2.layout.ConversionPattern=%d [%t] %-5p %c \n- %m%n\n"

 

#log4j.appender.A1.MaxFileSize=1000KB

# Keep one backup file

#log4j.appender.A1.MaxBackupIndex=1

 

因为这是一个servlet文件,同时我们还要修改web.xml文件

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>

       <servlet>

              <servlet-name>log4jinit</servlet-name>

              <servlet-class>net.ijsp.log4j.InitLog4j</servlet-class>

              <load-on-startup>1</load-on-startup>

  </servlet>

</web-app>

 

下面这两个为测试文件:

package net.ijsp.log4j;

 

import org.apache.log4j.PropertyConfigurator;

import org.apache.log4j.Logger;

import javax.servlet.http.HttpServlet;

import javax.servlet.ServletException;

 

public class Test {

 

  public Test() {}

 

  static Logger logger =Logger.getRootLogger();

  static Logger logger1 = Logger.getLogger("ltestlog4j");

 

  public void t() {

    logger.error("sssssssssss");

    System.out.println(logger);

    logger1.error("kjdlfkj");

    System.out.println("ddddddddddddddd");

  }

}

 

 

<%@page import ="net.ijsp.log4j.*"%>

 

<%

Test t = new Test();

t.t();

%>

posted @ 2005-09-26 17:27 小海船 阅读(328) | 评论 (0)编辑 收藏


Last login: Fri Sep 23 14:37:23 2005 from 211.155.247.222
Sun Microsystems Inc.   SunOS 5.9       Generic May 2002
[iplanet@ec2 sunone]$ telnet 10.211.12.22
Trying 10.211.12.22...
Connected to 10.211.12.22.
Escape character is '^]'.


SunOS 5.9

login: oracle
Password:
Last login: Mon Sep 26 06:39:46 from 10.211.12.233
Sun Microsystems Inc.   SunOS 5.9       Generic May 2002
You have mail.
[oracle@db oracle]$ sqlplus cpf/cpf

SQL*Plus: Release 9.2.0.5.0 - Production on Mon Sep 26 09:24:44 2005

Copyright (c) 1982, 2002, Oracle Corporation.  All rights reserved.


Connected to:
Oracle9i Enterprise Edition Release 9.2.0.5.0 - 64bit Production
With the Partitioning, OLAP and Oracle Data Mining options
JServer Release 9.2.0.5.0 - Production

SQL> drop table bs_banksetting
  2  ;

Table dropped.

SQL> drop table bs_bankaccountinfo;

Table dropped.

SQL> drop table bs_clientsetting;

Table dropped.

SQL> drop table bs_countrysetting;

Table dropped.

SQL> drop table bs_currencysetting;

Table dropped.

SQL> exit
Disconnected from Oracle9i Enterprise Edition Release 9.2.0.5.0 - 64bit Production
With the Partitioning, OLAP and Oracle Data Mining options
JServer Release 9.2.0.5.0 - Production
[oracle@db oracle]$ ftp 211.155.247.197
Connected to 211.155.247.197.
220 fsdev FTP server ready.
Name (211.155.247.197:oracle): root
331 Password required for root.
Password:
230 User root logged in.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> cd bankportal
250 CWD command successful.
ftp> ls
200 PORT command successful.
150 Opening ASCII mode data connection for file list.
AcctDataSourceType.class
AcctDataSourceType.java
CurrencyMappingDAO_oracle.class
SettingData.dmp
account
addp.jsp
bankportal.properties
create_table.sql
datamaintain
example.xls
expbp0926.dmp
expbp1420.dmp
iTreasury-bankportal.ear
itreasury.properties
menubp.dmp
model.xls
query
v003.jsp
v005.jsp
226 Transfer complete.
316 bytes received in 0.016 seconds (19.33 Kbytes/s)
ftp> get expbp0926.dmp
200 PORT command successful.
150 Opening BINARY mode data connection for expbp0926.dmp (47104 bytes).
226 Transfer complete.
local: expbp0926.dmp remote: expbp0926.dmp
47104 bytes received in 0.16 seconds (292.70 Kbytes/s)
ftp> bye
221-You have transferred 47104 bytes in 1 files.
221-Total traffic for this session was 48051 bytes in 2 transfers.
221-Thank you for using the FTP service on fsdev.
221 Goodbye.
[oracle@db oracle]$ imp system/manager1 file=expbp0926.dmp fromuser=bp_cpf touser=cpf

Import: Release 9.2.0.5.0 - Production on Mon Sep 26 09:30:09 2005

Copyright (c) 1982, 2002, Oracle Corporation.  All rights reserved.


Connected to: Oracle9i Enterprise Edition Release 9.2.0.5.0 - 64bit Production
With the Partitioning, OLAP and Oracle Data Mining options
JServer Release 9.2.0.5.0 - Production

Export file created by EXPORT:V09.02.00 via conventional path

Warning: the objects were exported by BP_CPF, not by you

import done in ZHS16GBK character set and AL16UTF16 NCHAR character set
. importing BP_CPF's objects into CPF
. . importing table           "BS_BANKACCOUNTINFO"        150 rows imported
. . importing table               "BS_BANKSETTING"        107 rows imported
. . importing table             "BS_CLIENTSETTING"         27 rows imported
. . importing table            "BS_COUNTRYSETTING"         21 rows imported
. . importing table           "BS_CURRENCYSETTING"         53 rows imported
Import terminated successfully without warnings.
[oracle@db oracle]$

posted @ 2005-09-26 09:38 小海船 阅读(269) | 评论 (0)编辑 收藏

package property;

import java.io.*;
import java.util.*;

public class TemplateId {

 private static Properties p;

 private static final TemplateId pi = new TemplateId();

 public TemplateId() {
  // 从templateId.properties属性文件获得数据
  InputStream is = getClass()
    .getResourceAsStream("templateId.properties");
  p = new Properties();
  try {
   p.load(is);
  } catch (IOException ex) {
   ex.printStackTrace();
  }
 }

 // 此处的templateId就是templateId.properties属性文件中的templateId。
 public static String getTemplateId() {
  return pi.p.getProperty("templateId");
 }

 public static void main(String args[]) {
  System.out.println("templateId=" + getTemplateId()); // 测试调用
 }
}
templateId.properties文件内容:templateId=FFD4156506-3-2F8CAC7

posted @ 2005-09-21 17:51 小海船 阅读(348) | 评论 (0)编辑 收藏

        所谓热部署,就是在应用正在运行的时候升级软件,却不需要重新启动应用。对于Java应用程序来说,热部署就是在运行时更新Java类文件。在基于Java的应用服务器实现热部署的过程中,类装入器扮演着重要的角色。大多数基于Java的应用服务器,包括EJB服务器和Servlet容器,都支持热部署。类装入器不能重新装入一个已经装入的类,但只要使用一个新的类装入器实例,就可以将类再次装入一个正在运行的应用程序。

       由于类装入器担负着把代码装入JVM的重任,所以它的体系结构应当保证整个平台的安全性不会受到威胁。每一个类装入器要为它装入的类定义一个独立的名称空间,因此运行时,一个类由它的包名称和装入它的类装入器两者结合唯一地标识。

  在名称空间之外,类不可见,运行时不同名称空间的类之间有一种保护性的“隔离墙”,在Java 2向父类委托的类装入模式中,类装入器可以请求其父类装入器装入的类,因此类装入器需要的类不一定全部由它自己装入。

  在Java运行环境中,不同的类装入器从不同的代码库装入类,之所以要将各个类装入器代码库的位置分开,是为了便于给不同的代码库设定不同的信任级别。在JVM中,由bootstrap类装入器装入的类具有最高的信任级别,用户自定义类装入器代码库的信任级别最低。此外,类装入器可以把每一个装入的类放入一个保护域,保护域定义了代码执行时所拥有的权限。

  如果要以系统安全策略(一个java.security.Policy的实例)为基础定义代码的权限,定制类装入器必须扩展java.security.SecureClassLoad类,调用其defineClass方法,SecureClassLoad类的defineClass方法有一个java.security.CodeSource参数。defindClass方法从系统策略获得与CodeSource关联的权限,以此为基础定义一个java.security.ProtectionDomain。详细介绍该安全模型已经超出了本文的范围,请读者自行参阅有关JVM内部机制的资料。

 类装入器装入的最小执行单元是Java .class文件。Java .class文件包含Java类的二进制描述,其中有可执行的字节码以及该类用到的对其他类的引用,包括对Java标准API里面的类的引用。简单地说,类装入器首先找到要装入的Java类的字节码,读入字节码,创建一个java.lang.Class类的实例。做好这些准备之后,类就可以被JVM执行了。
 当JVM最初开始运行时,它里面不装入任何类。如果要求JVM执行一个程序,被执行的类首先装入,字节码执行期间会引用到其他类和接口,这些被引用到的类和接口随之也被装入。因此,有人把JVM的类装入方式称为“懒惰的”装入方式,即只有必须用到某个类时才会装入它(而不是预先装入各种可能用到的类),正因为如此,开始时JVM不必知道运行时要装入哪些类。在Java平台上,懒惰的装入方式是实现动态可扩展性机制的关键因素之一。在本文的后面,你将会看到通过实现一个定制的Java类装入器,我们可以为Java运行时环境加入许多有趣的功能
   一、委托模式
     Java 2运行时环境中有多个类装入器的实例,每一个类装入器的实例从不同的代码库装入Java类。例如,Java核心API类由bootstrap(或primordial)类装入器装入,应用程序的类由system(或application)类装入器装入。另外,应用程序可以自定义类装入器从指定的代码库装入类。Java 2定义了类装入器之间的父-子关系,每一个类装入器(bootstrap除外)都有一个父类装入器,形成一个由类装入器构成的树形结构,bootstrap类装入器是这个树形结构的根,因此bootstrap没有父类装入器。
 当客户程序请求类装入器装入某个类时,类装入器按照下面的算法装入一个类:
   首先执行一次检查,查看客户程序请求的类是否已经由当前的类装入器装入。如果是,则返回已装入的类,请求处理完毕。JVM缓冲了类装入器装入的所有类,已经装入的类不会被再次装入。

 如果尚未装入类,则装入类的请求被委托给父类装入器,这个操作发生在当前的类装入器尝试装入指定的类之前。委托装入类的操作一直向上传递,直至bootstrap类装入器,如前所述,bootstrap类装入器是处于树形结构的顶层,它不再有可委托的父类装入器。

 如果父类装入器未能装入指定的类,则当前的类装入器将尝试搜索该类。每一个类装入器有预定义的搜索和装入类的位置。例如,bootstrap类装入器搜索sun.boot.class.path系统属性中指定的位置(包括目录和zip、jar文件),system类装入器搜索的位置由JVM开始运行时传入的CLASSPATH命令行变量指定(相当于设置java.class.path系统属性)。如果找到了类,则类装入器将类返回给系统,请求处理完毕。
   
 ⑷ 如果找不到类,则抛出java.lang.ClassNotFoundException异常。
posted @ 2005-09-21 15:50 小海船 阅读(1467) | 评论 (0)编辑 收藏

在网上看见了这篇文章,作者以轻松的语言比喻了java的32种模式,有很好的启发作用。
        创建型模式
       
        1、FACTORY—追MM少不了请吃饭了,麦当劳的鸡翅和肯德基的鸡翅都是MM爱吃的东西,虽然口味有所不同,但不管你带MM去麦当劳或肯德基,只管向服务员说“来四个鸡翅”就行了。麦当劳和肯德基就是生产鸡翅的Factory
       
        工厂模式:客户类和工厂类分开。消费者任何时候需要某种产品,只需向工厂请求即可。消费者无须修改就可以接纳新产品。缺点是当产品修改时,工厂类也要做相应的修改。如:如何创建及如何向客户端提供。
       
        2、BUILDER—MM最爱听的就是“我爱你”这句话了,见到不同地方的MM,要能够用她们的方言跟她说这句话哦,我有一个多种语言翻译机,上面每种语言都有一个按键,见到MM我只要按对应的键,它就能够用相应的语言说出“我爱你”这句话了,国外的MM也可以轻松搞掂,这就是我的“我爱你”builder。(这一定比美军在伊拉克用的翻译机好卖)
       
        建造模式:将产品的内部表象和产品的生成过程分割开来,从而使一个建造过程生成具有不同的内部表象的产品对象。建造模式使得产品内部表象可以独立的变化,客户不必知道产品内部组成的细节。建造模式可以强制实行一种分步骤进行的建造过程。
       
        3、FACTORY METHOD—请MM去麦当劳吃汉堡,不同的MM有不同的口味,要每个都记住是一件烦人的事情,我一般采用Factory Method模式,带着MM到服务员那儿,说“要一个汉堡”,具体要什么样的汉堡呢,让MM直接跟服务员说就行了。
       
        工厂方法模式:核心工厂类不再负责所有产品的创建,而是将具体创建的工作交给子类去做,成为一个抽象工厂角色,仅负责给出具体工厂类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。
       
        4、PROTOTYPE—跟MM用QQ聊天,一定要说些深情的话语了,我搜集了好多肉麻的情话,需要时只要copy出来放到QQ里面就行了,这就是我的情话prototype了。(100块钱一份,你要不要)
       
        原始模型模式:通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的方法创建出更多同类型的对象。原始模型模式允许动态的增加或减少产品类,产品类不需要非得有任何事先确定的等级结构,原始模型模式适用于任何的等级结构。缺点是每一个类都必须配备一个克隆方法。
       
        5、SINGLETON—俺有6个漂亮的老婆,她们的老公都是我,我就是我们家里的老公Sigleton,她们只要说道“老公”,都是指的同一个人,那就是我(刚才做了个梦啦,哪有这么好的事)
       
        单例模式:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例单例模式。单例模式只应在有真正的“单一实例”的需求时才可使用。
       
        结构型模式
       
        6、ADAPTER—在朋友聚会上碰到了一个美女Sarah,从香港来的,可我不会说粤语,她不会说普通话,只好求助于我的朋友kent了,他作为我和Sarah之间的Adapter,让我和Sarah可以相互交谈了(也不知道他会不会耍我)
       
        适配器(变压器)模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口原因不匹配而无法一起工作的两个类能够一起工作。适配类可以根据参数返还一个合适的实例给客户端。
       
        7、BRIDGE—早上碰到MM,要说早上好,晚上碰到MM,要说晚上好;碰到MM穿了件新衣服,要说你的衣服好漂亮哦,碰到MM新做的发型,要说你的头发好漂亮哦。不要问我“早上碰到MM新做了个发型怎么说”这种问题,自己用BRIDGE组合一下不就行了
       
        桥梁模式:将抽象化与实现化脱耦,使得二者可以独立的变化,也就是说将他们之间的强关联变成弱关联,也就是指在一个软件系统的抽象化和实现化之间使用组合/聚合关系而不是继承关系,从而使两者可以独立的变化。
       
        8、COMPOSITE—Mary今天过生日。“我过生日,你要送我一件礼物。”“嗯,好吧,去商店,你自己挑。”“这件T恤挺漂亮,买,这条裙子好看,买,这个包也不错,买。”“喂,买了三件了呀,我只答应送一件礼物的哦。”“什么呀,T恤加裙子加包包,正好配成一套呀,小姐,麻烦你包起来。”“……”,MM都会用Composite模式了,你会了没有?
       
        合成模式:合成模式将对象组织到树结构中,可以用来描述整体与部分的关系。合成模式就是一个处理对象的树结构的模式。合成模式把部分与整体的关系用树结构表示出来。合成模式使得客户端把一个个单独的成分对象和由他们复合而成的合成对象同等看待。
       
        9、DECORATOR—Mary过完轮到Sarly过生日,还是不要叫她自己挑了,不然这个月伙食费肯定玩完,拿出我去年在华山顶上照的照片,在背面写上“最好的的礼物,就是爱你的Fita”,再到街上礼品店买了个像框(卖礼品的MM也很漂亮哦),再找隔壁搞美术设计的Mike设计了一个漂亮的盒子装起来……,我们都是Decorator,最终都在修饰我这个人呀,怎么样,看懂了吗?
       
        装饰模式:装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案,提供比继承更多的灵活性。动态给一个对象增加功能,这些功能可以再动态的撤消。增加由一些基本功能的排列组合而产生的非常大量的功能。
       
        10、FACADE—我有一个专业的Nikon相机,我就喜欢自己手动调光圈、快门,这样照出来的照片才专业,但MM可不懂这些,教了半天也不会。幸好相机有Facade设计模式,把相机调整到自动档,只要对准目标按快门就行了,一切由相机自动调整,这样MM也可以用这个相机给我拍张照片了。
       
        门面模式:外部与一个子系统的通信必须通过一个统一的门面对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。每一个子系统只有一个门面类,而且此门面类只有一个实例,也就是说它是一个单例模式。但整个系统可以有多个门面类。
       
        11、FLYWEIGHT—每天跟MM发短信,手指都累死了,最近买了个新手机,可以把一些常用的句子存在手机里,要用的时候,直接拿出来,在前面加上MM的名字就可以发送了,再不用一个字一个字敲了。共享的句子就是Flyweight,MM的名字就是提取出来的外部特征,根据上下文情况使用。
       
        享元模式:FLYWEIGHT在拳击比赛中指最轻量级。享元模式以共享的方式高效的支持大量的细粒度对象。享元模式能做到共享的关键是区分内蕴状态和外蕴状态。内蕴状态存储在享元内部,不会随环境的改变而有所不同。外蕴状态是随环境的改变而改变的。外蕴状态不能影响内蕴状态,它们是相互独立的。将可以共享的状态和不可以共享的状态从常规类中区分开来,将不可以共享的状态从类里剔除出去。客户端不可以直接创建被共享的对象,而应当使用一个工厂对象负责创建被共享的对象。享元模式大幅度的降低内存中对象的数量。
       
        12、PROXY—跟MM在网上聊天,一开头总是“hi,你好”,“你从哪儿来呀?”“你多大了?”“身高多少呀?”这些话,真烦人,写个程序做为我的Proxy吧,凡是接收到这些话都设置好了自动的回答,接收到其他的话时再通知我回答,怎么样,酷吧。
       
        代理模式:代理模式给某一个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理就是一个人或一个机构代表另一个人或者一个机构采取行动。某些情况下,客户不想或者不能够直接引用一个对象,代理对象可以在客户和目标对象直接起到中介的作用。客户端分辨不出代理主题对象与真实主题对象。代理模式可以并不知道真正的被代理对象,而仅仅持有一个被代理对象的接口,这时候代理对象不能够创建被代理对象,被代理对象必须有系统的其他角色代为创建并传入。
       
        行为模式
       
        13、CHAIN OF RESPONSIBLEITY—晚上去上英语课,为了好开溜坐到了最后一排,哇,前面坐了好几个漂亮的MM哎,找张纸条,写上“Hi,可以做我的女朋友吗?如果不愿意请向前传”,纸条就一个接一个的传上去了,糟糕,传到第一排的MM把纸条传给老师了,听说是个老处女呀,快跑!
       
        责任链模式:在责任链模式中,很多对象由每一个对象对其下家的引用而接
       
        起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,系统可以在不影响客户端的情况下动态的重新组织链和分配责任。处理者有两个选择:承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象所接受。
       
        14、COMMAND—俺有一个MM家里管得特别严,没法见面,只好借助于她弟弟在我们俩之间传送信息,她对我有什么指示,就写一张纸条让她弟弟带给我。这不,她弟弟又传送过来一个COMMAND,为了感谢他,我请他吃了碗杂酱面,哪知道他说:“我同时给我姐姐三个男朋友送COMMAND,就数你最小气,才请我吃面。”,:-(
       
        命令模式:命令模式把一个请求或者操作封装到一个对象中。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。命令模式允许请求的一方和发送的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否执行,何时被执行以及是怎么被执行的。系统支持命令的撤消。
       
        15、INTERPRETER—俺有一个《泡MM真经》,上面有各种泡MM的攻略,比如说去吃西餐的步骤、去看电影的方法等等,跟MM约会时,只要做一个Interpreter,照着上面的脚本执行就可以了。
       
        解释器模式:给定一个语言后,解释器模式可以定义出其文法的一种表示,并同时提供一个解释器。客户端可以使用这个解释器来解释这个语言中的句子。解释器模式将描述怎样在有了一个简单的文法后,使用模式设计解释这些语句。在解释器模式里面提到的语言是指任何解释器对象能够解释的任何组合。在解释器模式中需要定义一个代表文法的命令类的等级结构,也就是一系列的组合规则。每一个命令对象都有一个解释方法,代表对命令对象的解释。命令对象的等级结构中的对象的任何排列组合都是一个语言。
       
       
       
        16、ITERATOR—我爱上了Mary,不顾一切的向她求婚。
       
        Mary:“想要我跟你结婚,得答应我的条件”
       
        我:“什么条件我都答应,你说吧”
       
        Mary:“我看上了那个一克拉的钻石”
       
        我:“我买,我买,还有吗?”
       
        Mary:“我看上了湖边的那栋别墅”
       
        我:“我买,我买,还有吗?”
       
        Mary:“你的小弟弟必须要有50cm长”
       
        我脑袋嗡的一声,坐在椅子上,一咬牙:“我剪,我剪,还有吗?”
       
        ……
       
        迭代子模式:迭代子模式可以顺序访问一个聚集中的元素而不必暴露聚集的内部表象。多个对象聚在一起形成的总体称之为聚集,聚集对象是能够包容一组对象的容器对象。迭代子模式将迭代逻辑封装到一个独立的子对象中,从而与聚集本身隔开。迭代子模式简化了聚集的界面。每一个聚集对象都可以有一个或一个以上的迭代子对象,每一个迭代子的迭代状态可以是彼此独立的。迭代算法可以独立于聚集角色变化。
       
        17、MEDIATOR—四个MM打麻将,相互之间谁应该给谁多少钱算不清楚了,幸亏当时我在旁边,按照各自的筹码数算钱,赚了钱的从我这里拿,赔了钱的也付给我,一切就OK啦,俺得到了四个MM的电话。
       
        调停者模式:调停者模式包装了一系列对象相互作用的方式,使得这些对象不必相互明显作用。从而使他们可以松散偶合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用。保证这些作用可以彼此独立的变化。调停者模式将多对多的相互作用转化为一对多的相互作用。调停者模式将对象的行为和协作抽象化,把对象在小尺度的行为上与其他对象的相互作用分开处理。
       
        18、MEMENTO—同时跟几个MM聊天时,一定要记清楚刚才跟MM说了些什么话,不然MM发现了会不高兴的哦,幸亏我有个备忘录,刚才与哪个MM说了什么话我都拷贝一份放到备忘录里面保存,这样可以随时察看以前的记录啦。
       
        备忘录模式:备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捉住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。
       
        19、OBSERVER—想知道咱们公司最新MM情报吗?加入公司的MM情报邮件组就行了,tom负责搜集情报,他发现的新情报不用一个一个通知我们,直接发布给邮件组,我们作为订阅者(观察者)就可以及时收到情报啦
       
        观察者模式:观察者模式定义了一种一队多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使他们能够自动更新自己。
       
        20、STATE—跟MM交往时,一定要注意她的状态哦,在不同的状态时她的行为会有不同,比如你约她今天晚上去看电影,对你没兴趣的MM就会说“有事情啦”,对你不讨厌但还没喜欢上的MM就会说“好啊,不过可以带上我同事么?”,已经喜欢上你的MM就会说“几点钟?看完电影再去泡吧怎么样?”,当然你看电影过程中表现良好的话,也可以把MM的状态从不讨厌不喜欢变成喜欢哦。
       
        状态模式:状态模式允许一个对象在其内部状态改变的时候改变行为。这个对象看上去象是改变了它的类一样。状态模式把所研究的对象的行为包装在不同的状态对象里,每一个状态对象都属于一个抽象状态类的一个子类。状态模式的意图是让一个对象在其内部状态改变的时候,其行为也随之改变。状态模式需要对每一个系统可能取得的状态创立一个状态类的子类。当系统的状态变化时,系统便改变所选的子类。
       
        21、STRATEGY—跟不同类型的MM约会,要用不同的策略,有的请电影比较好,有的则去吃小吃效果不错,有的去海边浪漫最合适,单目的都是为了得到MM的芳心,我的追MM锦囊中有好多Strategy哦。
       
        策略模式:策略模式针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。策略模式把行为和环境分开。环境类负责维持和查询行为类,各种算法在具体的策略类中提供。由于算法和环境独立开来,算法的增减,修改都不会影响到环境和客户端。
       
        22、TEMPLATE METHOD——看过《如何说服女生上床》这部经典文章吗?女生从认识到上床的不变的步骤分为巧遇、打破僵局、展开追求、接吻、前戏、动手、爱抚、进去八大步骤(Template method),但每个步骤针对不同的情况,都有不一样的做法,这就要看你随机应变啦(具体实现);
       
        模板方法模式:模板方法模式准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。先制定一个顶级逻辑框架,而将逻辑的细节留给具体的子类去实现。
       
        23、VISITOR—情人节到了,要给每个MM送一束鲜花和一张卡片,可是每个MM送的花都要针对她个人的特点,每张卡片也要根据个人的特点来挑,我一个人哪搞得清楚,还是找花店老板和礼品店老板做一下Visitor,让花店老板根据MM的特点选一束花,让礼品店老板也根据每个人特点选一张卡,这样就轻松多了;
       
        访问者模式:访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由的演化。访问者模式使得增加新的操作变的很容易,就是增加一个新的访问者类。访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个的节点类中。当使用访问者模式时,要将尽可能多的对象浏览逻辑放在访问者类中,而不是放到它的子类中。访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员类。
posted @ 2005-09-20 20:06 小海船 阅读(291) | 评论 (0)编辑 收藏

作者:郎云鹏(dev2dev ID: hippiewolf)

摘要:虽然session机制在web应用程序中被采用已经很长时间了,但是仍然有很多人不清楚session机制的本质,以至不能正确的应用这一技术。本文将详细讨论session的工作机制并且对在Java web application中应用session机制时常见的问题作出解答。

目录:
一、术语session
二、HTTP协议与状态保持
三、理解cookie机制
四、理解session机制
五、理解javax.servlet.http.HttpSession
六、HttpSession常见问题
七、跨应用程序的session共享
八、总结
参考文档

一、术语session
在我的经验里,session这个词被滥用的程度大概仅次于transaction,更加有趣的是transaction与session在某些语境下的含义是相同的。

session,中文经常翻译为会话,其本来的含义是指有始有终的一系列动作/消息,比如打电话时从拿起电话拨号到挂断电话这中间的一系列过程可以称之为一个 session。有时候我们可以看到这样的话“在一个浏览器会话期间,...”,这里的会话一词用的就是其本义,是指从一个浏览器窗口打开到关闭这个期间 ①。最混乱的是“用户(客户端)在一次会话期间”这样一句话,它可能指用户的一系列动作(一般情况下是同某个具体目的相关的一系列动作,比如从登录到选购商品到结账登出这样一个网上购物的过程,有时候也被称为一个transaction),然而有时候也可能仅仅是指一次连接,也有可能是指含义①,其中的差别只能靠上下文来推断②。

然而当session一词与网络协议相关联时,它又往往隐含了“面向连接”和/或“保持状态”这样两个含义, “面向连接”指的是在通信双方在通信之前要先建立一个通信的渠道,比如打电话,直到对方接了电话通信才能开始,与此相对的是写信,在你把信发出去的时候你并不能确认对方的地址是否正确,通信渠道不一定能建立,但对发信人来说,通信已经开始了。“保持状态”则是指通信的一方能够把一系列的消息关联起来,使得消息之间可以互相依赖,比如一个服务员能够认出再次光临的老顾客并且记得上次这个顾客还欠店里一块钱。这一类的例子有“一个TCP session”或者 “一个POP3 session”③。

而到了web服务器蓬勃发展的时代,session在web开发语境下的语义又有了新的扩展,它的含义是指一类用来在客户端与服务器之间保持状态的解决方案④。有时候session也用来指这种解决方案的存储结构,如“把xxx保存在session 里”⑤。由于各种用于web开发的语言在一定程度上都提供了对这种解决方案的支持,所以在某种特定语言的语境下,session也被用来指代该语言的解决方案,比如经常把Java里提供的javax.servlet.http.HttpSession简称为session⑥。

鉴于这种混乱已不可改变,本文中session一词的运用也会根据上下文有不同的含义,请大家注意分辨。
在本文中,使用中文“浏览器会话期间”来表达含义①,使用“session机制”来表达含义④,使用“session”表达含义⑤,使用具体的“HttpSession”来表达含义⑥

二、HTTP协议与状态保持
HTTP 协议本身是无状态的,这与HTTP协议本来的目的是相符的,客户端只需要简单的向服务器请求下载某些文件,无论是客户端还是服务器都没有必要纪录彼此过去的行为,每一次请求之间都是独立的,好比一个顾客和一个自动售货机或者一个普通的(非会员制)大卖场之间的关系一样。

然而聪明(或者贪心?)的人们很快发现如果能够提供一些按需生成的动态信息会使web变得更加有用,就像给有线电视加上点播功能一样。这种需求一方面迫使HTML逐步添加了表单、脚本、DOM等客户端行为,另一方面在服务器端则出现了CGI规范以响应客户端的动态请求,作为传输载体的HTTP协议也添加了文件上载、 cookie这些特性。其中cookie的作用就是为了解决HTTP协议无状态的缺陷所作出的努力。至于后来出现的session机制则是又一种在客户端与服务器之间保持状态的解决方案。

让我们用几个例子来描述一下cookie和session机制之间的区别与联系。笔者曾经常去的一家咖啡店有喝5杯咖啡免费赠一杯咖啡的优惠,然而一次性消费5杯咖啡的机会微乎其微,这时就需要某种方式来纪录某位顾客的消费数量。想象一下其实也无外乎下面的几种方案:
1、该店的店员很厉害,能记住每位顾客的消费数量,只要顾客一走进咖啡店,店员就知道该怎么对待了。这种做法就是协议本身支持状态。
2、发给顾客一张卡片,上面记录着消费的数量,一般还有个有效期限。每次消费时,如果顾客出示这张卡片,则此次消费就会与以前或以后的消费相联系起来。这种做法就是在客户端保持状态。
3、发给顾客一张会员卡,除了卡号之外什么信息也不纪录,每次消费时,如果顾客出示该卡片,则店员在店里的纪录本上找到这个卡号对应的纪录添加一些消费信息。这种做法就是在服务器端保持状态。

由于HTTP协议是无状态的,而出于种种考虑也不希望使之成为有状态的,因此,后面两种方案就成为现实的选择。具体来说cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。同时我们也看到,由于采用服务器端保持状态的方案在客户端也需要保存一个标识,所以session机制可能需要借助于cookie机制来达到保存标识的目的,但实际上它还有其他选择。

三、理解cookie机制 
cookie机制的基本原理就如上面的例子一样简单,但是还有几个问题需要解决:“会员卡”如何分发;“会员卡”的内容;以及客户如何使用“会员卡”。

正统的cookie分发是通过扩展HTTP协议来实现的,服务器通过在HTTP的响应头中加上一行特殊的指示以提示浏览器按照指示生成相应的cookie。然而纯粹的客户端脚本如JavaScript或者VBScript也可以生成cookie。

而cookie 的使用是由浏览器按照一定的原则在后台自动发送给服务器的。浏览器检查所有存储的cookie,如果某个cookie所声明的作用范围大于等于将要请求的资源所在的位置,则把该cookie附在请求资源的HTTP请求头上发送给服务器。意思是麦当劳的会员卡只能在麦当劳的店里出示,如果某家分店还发行了自己的会员卡,那么进这家店的时候除了要出示麦当劳的会员卡,还要出示这家店的会员卡。

cookie的内容主要包括:名字,值,过期时间,路径和域。
其中域可以指定某一个域比如.google.com,相当于总店招牌,比如宝洁公司,也可以指定一个域下的具体某台机器比如www.google.com或者froogle.google.com,可以用飘柔来做比。
路径就是跟在域名后面的URL路径,比如/或者/foo等等,可以用某飘柔专柜做比。
路径与域合在一起就构成了cookie的作用范围。
如果不设置过期时间,则表示这个cookie的生命期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览器会话期的 cookie被称为会话cookie。会话cookie一般不存储在硬盘上而是保存在内存里,当然这种行为并不是规范规定的。如果设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie仍然有效直到超过设定的过期时间。

存储在硬盘上的cookie 可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存里的cookie,不同的浏览器有不同的处理方式。对于IE,在一个打开的窗口上按 Ctrl-N(或者从文件菜单)打开的窗口可以与原窗口共享,而使用其他方式新开的IE进程则不能共享已经打开的窗口的内存cookie;对于 Mozilla Firefox0.8,所有的进程和标签页都可以共享同样的cookie。一般来说是用javascript的window.open打开的窗口会与原窗口共享内存cookie。浏览器对于会话cookie的这种只认cookie不认人的处理方式经常给采用session机制的web应用程序开发者造成很大的困扰。

下面就是一个goolge设置cookie的响应头的例子
HTTP/1.1 302 Found
Location: http://www.google.com/intl/zh-CN/
Set-Cookie: PREF=ID=0565f77e132de138:NW=1:TM=1098082649:LM=1098082649:S=KaeaCFPo49RiA_d8; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com
Content-Type: text/html




这是使用HTTPLook这个HTTP Sniffer软件来俘获的HTTP通讯纪录的一部分




浏览器在再次访问goolge的资源时自动向外发送cookie

 


使用Firefox可以很容易的观察现有的cookie的值
使用HTTPLook配合Firefox可以很容易的理解cookie的工作原理。




IE也可以设置在接受cookie前询问

 


这是一个询问接受cookie的对话框。

四、理解session机制
session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。

当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否已包含了一个session标识 - 称为 session id,如果已包含一个session id则说明以前已经为此客户端创建过session,服务器就按照session id把这个 session检索出来使用(如果检索不到,可能会新建一个),如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个 session id将被在本次响应中返回给客户端保存。

保存这个session id的方式可以采用cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器。一般这个cookie的名字都是类似于SEEESIONID,而。比如weblogic对于web应用程序生成的cookie,JSESSIONID= ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764,它的名字就是 JSESSIONID。

由于cookie可以被人为的禁止,必须有其他机制以便在cookie被禁止时仍然能够把session id传递回服务器。经常被使用的一种技术叫做URL重写,就是把session id直接附加在URL路径的后面,附加方式也有两种,一种是作为URL路径的附加信息,表现形式为http://...../xxx;jsessionid= ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764
另一种是作为查询字符串附加在URL后面,表现形式为http://...../xxx?jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764
这两种方式对于用户来说是没有区别的,只是服务器在解析的时候处理的方式不同,采用第一种方式也有利于把session id的信息和正常程序参数区分开来。
为了在整个交互过程中始终保持状态,就必须在每个客户端可能请求的路径后面都包含这个session id。

另一种技术叫做表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session id传递回服务器。比如下面的表单
<form name="testform" action="/xxx">
<input type="text">
</form>
在被传递给客户端之前将被改写成
<form name="testform" action="/xxx">
<input type="hidden" name="jsessionid" value="ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764">
<input type="text">
</form>
这种技术现在已较少应用,笔者接触过的很古老的iPlanet6(SunONE应用服务器的前身)就使用了这种技术。
实际上这种技术可以简单的用对action应用URL重写来代替。

在谈论session机制的时候,常常听到这样一种误解“只要关闭浏览器,session就消失了”。其实可以想象一下会员卡的例子,除非顾客主动对店家提出销卡,否则店家绝对不会轻易删除顾客的资料。对session来说也是一样的,除非程序通知服务器删除一个session,否则服务器会一直保留,程序一般都是在用户做log off的时候发个指令去删除session。然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分session机制都使用会话cookie来保存session id,而关闭浏览器后这个 session id就消失了,再次连接服务器时也就无法找到原来的session。如果服务器设置的cookie被保存到硬盘上,或者使用某种手段改写浏览器发出的HTTP请求头,把原来的session id发送给服务器,则再次打开浏览器仍然能够找到原来的session。

恰恰是由于关闭浏览器不会导致session被删除,迫使服务器为seesion设置了一个失效时间,当距离客户端上一次使用session的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把session删除以节省存储空间。

五、理解javax.servlet.http.HttpSession
HttpSession是Java平台对session机制的实现规范,因为它仅仅是个接口,具体到每个web应用服务器的提供商,除了对规范支持之外,仍然会有一些规范里没有规定的细微差异。这里我们以BEA的Weblogic Server8.1作为例子来演示。

首先,Weblogic Server提供了一系列的参数来控制它的HttpSession的实现,包括使用cookie的开关选项,使用URL重写的开关选项,session持久化的设置,session失效时间的设置,以及针对cookie的各种设置,比如设置cookie的名字、路径、域, cookie的生存时间等。

一般情况下,session都是存储在内存里,当服务器进程被停止或者重启的时候,内存里的session也会被清空,如果设置了session的持久化特性,服务器就会把session保存到硬盘上,当服务器进程重新启动或这些信息将能够被再次使用, Weblogic Server支持的持久性方式包括文件、数据库、客户端cookie保存和复制。

复制严格说来不算持久化保存,因为session实际上还是保存在内存里,不过同样的信息被复制到各个cluster内的服务器进程中,这样即使某个服务器进程停止工作也仍然可以从其他进程中取得session。

cookie生存时间的设置则会影响浏览器生成的cookie是否是一个会话cookie。默认是使用会话cookie。有兴趣的可以用它来试验我们在第四节里提到的那个误解。

cookie的路径对于web应用程序来说是一个非常重要的选项,Weblogic Server对这个选项的默认处理方式使得它与其他服务器有明显的区别。后面我们会专题讨论。

关于session的设置参考[5] http://e-docs.bea.com/wls/docs70/webapp/weblogic_xml.html#1036869

六、HttpSession常见问题
(在本小节中session的含义为⑤和⑥的混合)


1、session在何时被创建
一个常见的误解是以为session在有客户端访问时就被创建,然而事实是直到某server端程序调用 HttpServletRequest.getSession(true)这样的语句时才被创建,注意如果JSP没有显示的使用 <% @page session="false"%> 关闭session,则JSP文件在编译成Servlet时将会自动加上这样一条语句 HttpSession session = HttpServletRequest.getSession(true);这也是JSP中隐含的 session对象的来历。

由于session会消耗内存资源,因此,如果不打算使用session,应该在所有的JSP中关闭它。

2、session何时被删除
综合前面的讨论,session在下列情况下被删除a.程序调用HttpSession.invalidate();或b.距离上一次收到客户端发送的session id时间间隔超过了session的超时设置;或c.服务器进程被停止(非持久session)

3、如何做到在浏览器关闭时删除session
严格的讲,做不到这一点。可以做一点努力的办法是在所有的客户端页面里使用javascript代码window.oncolose来监视浏览器的关闭动作,然后向服务器发送一个请求来删除session。但是对于浏览器崩溃或者强行杀死进程这些非常规手段仍然无能为力。

4、有个HttpSessionListener是怎么回事
你可以创建这样的listener去监控session的创建和销毁事件,使得在发生这样的事件时你可以做一些相应的工作。注意是session的创建和销毁动作触发listener,而不是相反。类似的与HttpSession有关的listener还有 HttpSessionBindingListener,HttpSessionActivationListener和 HttpSessionAttributeListener。

5、存放在session中的对象必须是可序列化的吗
不是必需的。要求对象可序列化只是为了session能够在集群中被复制或者能够持久保存或者在必要时server能够暂时把session交换出内存。在 Weblogic Server的session中放置一个不可序列化的对象在控制台上会收到一个警告。我所用过的某个iPlanet版本如果 session中有不可序列化的对象,在session销毁时会有一个Exception,很奇怪。

6、如何才能正确的应付客户端禁止cookie的可能性
对所有的URL使用URL重写,包括超链接,form的action,和重定向的URL,具体做法参见[6]
http://e-docs.bea.com/wls/docs70/webapp/sessions.html#100770

7、开两个浏览器窗口访问应用程序会使用同一个session还是不同的session
参见第三小节对cookie的讨论,对session来说是只认id不认人,因此不同的浏览器,不同的窗口打开方式以及不同的cookie存储方式都会对这个问题的答案有影响。

8、如何防止用户打开两个浏览器窗口操作导致的session混乱
这个问题与防止表单多次提交是类似的,可以通过设置客户端的令牌来解决。就是在服务器每次生成一个不同的id返回给客户端,同时保存在session里,客户端提交表单时必须把这个id也返回服务器,程序首先比较返回的id与保存在session里的值是否一致,如果不一致则说明本次操作已经被提交过了。可以参看《J2EE核心模式》关于表示层模式的部分。需要注意的是对于使用javascript window.open打开的窗口,一般不设置这个id,或者使用单独的id,以防主窗口无法操作,建议不要再window.open打开的窗口里做修改操作,这样就可以不用设置。

9、为什么在Weblogic Server中改变session的值后要重新调用一次session.setValue
做这个动作主要是为了在集群环境中提示Weblogic Server session中的值发生了改变,需要向其他服务器进程复制新的session值。

10、为什么session不见了
排除session正常失效的因素之外,服务器本身的可能性应该是微乎其微的,虽然笔者在iPlanet6SP1加若干补丁的Solaris版本上倒也遇到过;浏览器插件的可能性次之,笔者也遇到过3721插件造成的问题;理论上防火墙或者代理服务器在cookie处理上也有可能会出现问题。
出现这一问题的大部分原因都是程序的错误,最常见的就是在一个应用程序中去访问另外一个应用程序。我们在下一节讨论这个问题。

七、跨应用程序的session共享

常常有这样的情况,一个大项目被分割成若干小项目开发,为了能够互不干扰,要求每个小项目作为一个单独的web应用程序开发,可是到了最后突然发现某几个小项目之间需要共享一些信息,或者想使用session来实现SSO(single sign on),在session中保存login的用户信息,最自然的要求是应用程序间能够访问彼此的session。

然而按照Servlet规范,session的作用范围应该仅仅限于当前应用程序下,不同的应用程序之间是不能够互相访问对方的session的。各个应用服务器从实际效果上都遵守了这一规范,但是实现的细节却可能各有不同,因此解决跨应用程序session共享的方法也各不相同。

首先来看一下Tomcat是如何实现web应用程序之间session的隔离的,从 Tomcat设置的cookie路径来看,它对不同的应用程序设置的cookie路径是不同的,这样不同的应用程序所用的session id是不同的,因此即使在同一个浏览器窗口里访问不同的应用程序,发送给服务器的session id也可以是不同的。


  

根据这个特性,我们可以推测Tomcat中session的内存结构大致如下。


 

笔者以前用过的iPlanet也采用的是同样的方式,估计SunONE与iPlanet之间不会有太大的差别。对于这种方式的服务器,解决的思路很简单,实际实行起来也不难。要么让所有的应用程序共享一个session id,要么让应用程序能够获得其他应用程序的session id。

iPlanet中有一种很简单的方法来实现共享一个session id,那就是把各个应用程序的cookie路径都设为/(实际上应该是/NASApp,对于应用程序来讲它的作用相当于根)。
<session-info>
<path>/NASApp</path>
</session-info>

需要注意的是,操作共享的session应该遵循一些编程约定,比如在session attribute名字的前面加上应用程序的前缀,使得 setAttribute("name", "neo")变成setAttribute("app1.name", "neo"),以防止命名空间冲突,导致互相覆盖。


在Tomcat中则没有这么方便的选择。在Tomcat版本3上,我们还可以有一些手段来共享session。对于版本4以上的Tomcat,目前笔者尚未发现简单的办法。只能借助于第三方的力量,比如使用文件、数据库、JMS或者客户端cookie,URL参数或者隐藏字段等手段。

我们再看一下Weblogic Server是如何处理session的。


  

从截屏画面上可以看到Weblogic Server对所有的应用程序设置的cookie的路径都是/,这是不是意味着在Weblogic Server中默认的就可以共享session了呢?然而一个小实验即可证明即使不同的应用程序使用的是同一个session,各个应用程序仍然只能访问自己所设置的那些属性。这说明Weblogic Server中的session的内存结构可能如下


 

对于这样一种结构,在 session机制本身上来解决session共享的问题应该是不可能的了。除了借助于第三方的力量,比如使用文件、数据库、JMS或者客户端 cookie,URL参数或者隐藏字段等手段,还有一种较为方便的做法,就是把一个应用程序的session放到ServletContext中,这样另外一个应用程序就可以从ServletContext中取得前一个应用程序的引用。示例代码如下,

应用程序A
context.setAttribute("appA", session); 

应用程序B
contextA = context.getContext("/appA");
HttpSession sessionA = (HttpSession)contextA.getAttribute("appA"); 

值得注意的是这种用法不可移植,因为根据ServletContext的JavaDoc,应用服务器可以处于安全的原因对于context.getContext("/appA");返回空值,以上做法在Weblogic Server 8.1中通过。

那么Weblogic Server为什么要把所有的应用程序的cookie路径都设为/呢?原来是为了SSO,凡是共享这个session的应用程序都可以共享认证的信息。一个简单的实验就可以证明这一点,修改首先登录的那个应用程序的描述符weblogic.xml,把cookie路径修改为/appA 访问另外一个应用程序会重新要求登录,即使是反过来,先访问cookie路径为/的应用程序,再访问修改过路径的这个,虽然不再提示登录,但是登录的用户信息也会丢失。注意做这个实验时认证方式应该使用FORM,因为浏览器和web服务器对basic认证方式有其他的处理方式,第二次请求的认证不是通过 session来实现的。具体请参看[7] secion 14.8 Authorization,你可以修改所附的示例程序来做这些试验。

八、总结
session机制本身并不复杂,然而其实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器,服务器的经验当作普遍适用的经验,而是始终需要具体情况具体分析。

关于作者:
郎云鹏(dev2dev ID: hippiewolf),软件工程师,从事J2EE开发
电子邮件:langyunpeng@yahoo.com.cn
地址:大连软件园路31号科技大厦A座大连博涵咨询服务有限公司

参考文档:
[1] Preliminary Specification http://wp.netscape.com/newsref/std/cookie_spec.html
[2] RFC2109 http://www.rfc-editor.org/rfc/rfc2109.txt
[3] RFC2965 http://www.rfc-editor.org/rfc/rfc2965.txt
[4] The Unofficial Cookie FAQ http://www.cookiecentral.com/faq/
[5] http://e-docs.bea.com/wls/docs70/webapp/weblogic_xml.html#1036869
[6] http://e-docs.bea.com/wls/docs70/webapp/sessions.html#100770
[7] RFC2616 http://www.rfc-editor.org/rfc/rfc2616.txt
posted @ 2005-09-20 10:04 小海船 阅读(1453) | 评论 (4)编辑 收藏