摘要 |
客户端HTTP状态管理对于创建 需要与像基于网络浏览器的email或在线银行服务网络程序交互作用的Java应用程序是十分重要的。本文介绍了在Java中一个强大易用的客户端HTTP状态管理cookie库,这个库在固有的java.net工具箱中很少见。其中存在几种客户端HTTP状态管理APIs,它们提供了难于学习并没必要重新开发设计的函数方法。这篇文章中Cookie管理库尽量使用核心Java API类。 |
当在开发一个针对所有主要的internet邮件服务器(基于Web或其他类型)提供单点访问的通用邮件客户端时,我发现我的应用程序经常不得不作为一个小的网络浏览器与提供邮件服务的网站交互。 当开发XML网络服务以便于机器更容易访问网站时我总在需要网站交互时遇到困难。这些网站经常使用cookies进行状态管理及维护用户会话数据,在这两种情况,我意识到多数网站交互都涉及cookie操作。我也注意到虽然两种情况下的应用程序都执行cookie操作,但其逻辑处理较困难及不具有互换性。针对此限制,我从开发一个小型普通用途库出发致力于cookie操作。在这篇文章中我将与你分享这个库。 |
为了在运行中图解说明库,我建议使用基于Hotmail邮件检测器的控制台。此外,我从在J2ME平台上使用MIDP的移动设备观点探究了客户端状态管理。 |
Cookie基础 |
让我们从回答一些问题开始: |
什么是状态管理,为什么我们需要它? |
什么是cookies,它们怎样适应图片? |
要回答第一个问题,我们必须更精密地检测一下HTTP。HTTP是无国界协议,因为从网络服务器观点看所有HTTP请求都独立于先前请求。就是说每一个HTTP响应完全依赖于相应请求中包含的信息。当这种行为使网络服务执行更简单有效时,用它作为复杂网络应用的基础将更为合适。 |
状态管理机制克服了HTTP的一些限制并允许网络客户端及服务器端维护请求间的关系。在这种关系维持的期间叫做会话(session)。多数要求你登录的网络应用程序使用了会话及状态管理。购物推车应用程序使用状态管理控制所有标记为已购买项目的列表。状态管理能够使个别用户参数的入口及搜索引擎个性化定制。网络应用程序甚至能使用状态管理根据用户爱好兴趣定制网站内容。 |
Cookies影响着状态管理。Cookies是服务器在本地机器上存储的小段文本并随每一个请求发送至同一个服务器。IETF RFC 2965 HTTP State Management Mechanism 是通用cookie规范。网络服务器用HTTP头向客户端发送cookies,在客户终端,浏览器解析这些cookies并将它们保存为一个本地文件,它会自动将到同一服务器的任何请求缚上这些cookies。在这篇文章后面,我同义性地使用了cookie操作和状态管理术语。 |
如果你要找出你访问的哪个网站使用了cookies,可以试试这个简单的试验: |
注意: 只有当你觉得改变你的浏览器设置没什么问题并知道方法时才执行这个练习。 |
● 打开你常用的浏览器,我假设你使用的是Internet Explorer (IE) 5+或Netscape Navigator 4+。 |
● 使自动cookie操作无效: |
|
|
● 现在浏览你“收藏”中的站点,特别是当你检查你的网络邮件或进入在线电子商店时,要求你允许接收cookies的对话框会不断地向你轰来。 |
将上面的步骤恢复到你以前的初始设置,你也能看见哪些cookies被保存到了你的本地机器上(在警告应用之前): |
● 对于IE:使用“Windows资源管理器”或“我的电脑”浏览C:\Windows\Cookies文件夹,在这个文件夹中的所有文本文件都包含cookies。 |
● 对于Netscape Navigator: |
|
|
注意: 根据你安装的系统不同,使自动cookie操作无效及查看保存的cookies的步骤也可能不同。 |
现在你已经知道了一些基本知识,接下来我将阐述怎样将这些与Java联系起来。 |
|
jCookie结构 |
下面我将描述层及他们使用的不同的类。 |
层1 |
那些开发者多数都想进行透明cookie操作,这通常是使用层1的情形。在这个级别,你用Client类操作cookies。它有两个主要的方法: |
· public CookieJar getCookies(URLConnection urlConn): 这个方法从给出的URLConnection中析取cookies,将它们解析到Cookie对象,并作为一个CookieJar返回。 |
· public CookieJar setCookies(URLConnection urlConn, CookieJar cj): 这个方法从CookieJar中提取合适的Cookie对象并设置URLConnection的报头。 |
层0 |
这些开发者没有在使用层0的代码中深入就无法呼吸(包括我)。在这里,你可以通过使用cookie操作代码改变解析逻辑和安全规则。要这样做,首先实现CookieParser接口,它有以下四个方法: |
· public Header getCookieHeaders(CookieJar cj): 在CookieJar中转换Cookies为一报头以适合与一个HTTP请求一起发送。 |
· public boolean allowedCookie(Cookie c, URL url): 检查是否一个给出URL的请求能返回指定的Cookie。 |
· public CookieJar parseCookies(Header h, URL url): 在一个HTTP响应中将报头转换到一个Cookie对象的CookieJar中。 |
· public boolean sendCookieWithURL(Cookie c, URL url, boolean bRespectExpires): 检查是否给出的Cookie能被与给出URL的一个请求一起发送。 |
你能使用Client类的setCookieParser(CookieParser cp)方法去设置CookieParser实现。被库缺省使用的CookieParser是一个RFC 2965 cookie规范中的实现。 |
在层1,jCookie作为一个库;在层0,它成为一个API的基础。 |
jCookie用法 |
Client类在两个层都调用cookie操作逻辑。它提供了应用程序开发者的库架构。要使用jCookie库,按照下面这些步骤: |
· 从响应到请求检索cookies: |
|
· 和一个请求(假定一个CookieJar已被检索)一起发送cookies: |
|
下面的摘录显示了普通jCookie的用法。这个jCookie代码十分突出: |
import com.sonalb.net.http.cookie.*; |
import java.net.*; |
import java.io.*; |
... |
public class Example |
{ |
... |
public void someMethod() |
{ |
... |
URL url = new URL("http://www.site.com/"); |
HttpURLConnection huc = (HttpURLConnection) url.openConnection(); |
//在这里初始化HttpURLConnection. |
... |
huc.connect(); |
InputStream is = huc.getInputStream(); |
Client client = new Client(); |
CookieJar cj = client.getCookies(huc); |
//进行一些处理 |
... |
huc.disconnect(); |
// 执行另一请求 |
url = new URL("http://www.site.com/"); |
huc = (HttpURLConnection) url.openConnection(); |
client.setCookies(huc, cj); |
huc.connect(); |
... |
// 进行一些处理 |
} |
} |
上面的代码描述了jCookie API的两个方面: |
· 本地java.net对象的使用(HttpURLConnection)。 |
· 轻易地回收和发送cookies(单个方法调用)。 |
在实践中,上述代码已经能成功地维护两个请求间的会话。现在我们转换层的基本结构,让我们将jCookie与一些真实代码连接。 |
Hotmail新邮件检测器 |
为了阐明jCookie库的使用方便,我将在一个显示一个Hotmail账号新消息的发件人、主题及日期字段的应用程序中使用它。为了简单起见,应用程序在控制台显示这些信息。为了在Hotmail收件箱接收新消息,应用程序需要完成以下步骤: |
· 在登录表单中执行一个HTTP POST操作登录Hotmail。 |
· 为了到达主页,操作重定向及cookies。 |
· 检索收件箱的HTML页。 |
· 提取新消息的相关字段。 |
多数站点要求用户第一次通过一个表单执行一个HTTP POST 操作以完成登录过程。为了成功鉴定身份,POST的响应通常是一个带一些cookie报头的HTTP重定向。当重定向页被请求时cookies返回给服务器。 |
jCookie库包括一个很有用的类叫HTTPRedirectHandler,它管理当完成客户端cookie操作时操作重定向的普通任务。要使用这个类,首先要在一个未连接的HttpURLConnection中创建一个HTTPRedirectHandler实例,然后调用HTTPRedirectHandler实例的connect()方法去操作重定向及cookie。句柄从HTTP响应代码中确定是否运行成功。一旦进程完成,调用的类就检索表明最后一次请求的HttpURLConnection对象。CookieJar包含所有在能被检索的重定向过程中接收的cookies。Cookie操作逻辑存在于HTTPRedirectHandler的connect()方法中。让我们来看一看这个方法的代码。Cookie操作部份进行了注释: |
package com.sonalb.net.http; |
import com.sonalb.net.http.cookie.*; |
import java.net.*; |
import java.io.*; |
public class HTTPRedirectHandler |
{ |
... |
public HTTPRedirectHandler(HttpURLConnection huc) |
{ |
... |
} |
public void connect() throws IOException |
{ |
if(bConnected) |
{ |
throw new IllegalStateException("No can do. Already connected."); |
} |
int code; |
URL url; |
huc.setFollowRedirects(false); |
// 设置在Cookies中的检验 |
if(!cj.isEmpty()) |
{ |
client.setCookies(huc,cj); |
} |
is = huc.getInputStream(); |
// 从HttpURLConnection中提取Cookies并加到CookieJar中去 |
cj.addAll(Client.getCookies(huc)); |
while((code = huc.getResponseCode()) != successCode && maxRedirects > 0) |
{ |
if(code != 302) |
{ |
throw new IOException("Can't deal with this code (" + code + ")."); |
} |
is.close(); |
is = null; |
url = new URL(huc.getHeaderField("location")); |
huc.disconnect(); |
huc = null; |
huc = (HttpURLConnection) url.openConnection(); |
//和HTTP请求一起发送Cookies |
Client.setCookies(huc, cj); |
huc.setFollowRedirects(false); |
huc.connect(); |
is = huc.getInputStream(); |
//从响应中提取Cookies并加进jar中去 |
cj.addAll(Client.getCookies(huc)); |
maxRedirects--; |
} |
if(maxRedirects <= 0 && code != successCode) |
{ |
throw new IOException("Max redirects exhausted."); |
} |
bConnected = true; |
} |
//其他方法在这里出现 |
public void handleCookies(boolean b) |
{ |
... |
} |
public void setSuccessCode(int i) |
{ |
... |
} |
public void setCookieJar(CookieJar cj) |
{ |
... |
} |
public void addCookies(CookieJar cj) |
{ |
... |
} |
public CookieJar getCookieJar() |
{ |
... |
} |
public HttpURLConnection getConnection() |
{ |
... |
} |
public void setMaxRedirects(int i) |
{ |
... |
} |
} |
HotmailChecker应用程序使用HTTPRedirectHandler进行登录操作。应用程序从使用带有并发请求的HTTPRedirectHandler中检索CookieJar。HotmailChecker的相关部份显示如下。Hotmail细节和jCookie关联注释被突出显示: |
public boolean doLogin() throws Exception |
{ |
//对于HTTPS初始化JSSE |
System.getProperties().put("java.protocol.handler.pkgs","com.sun.net.ssl.internal.www.protocol"); |
java.security.Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider()); |
//创建HttpURLConnection并初始化 |
URL url = new URL("https://lc2.law13.hotmail.passport.com/cgi-bin/dologin"); |
HttpURLConnection huc = (HttpURLConnection) url.openConnection(); |
huc.setDoOutput(true); |
huc.setRequestMethod("POST"); |
huc.setRequestProperty("User-Agent","Mozilla/4.7 [en] (Win98; I)"); |
//发送登录表单字段 |
StringBuffer sb = new StringBuffer(); |
sb.append("login="); sb.append(URLEncoder.encode(user)); |
... |
OutputStream os = huc.getOutputStream(); |
os.write(sb.toString().getBytes("US-ASCII")); |
os.close(); |
//创建句柄并进行处理 |
HTTPRedirectHandler hrh = new HTTPRedirectHandler(huc); |
hrh.connect(); |
huc = hrh.getConnection(); |
//Microsoft有一个中间过渡页使用了一个刷新元标签以便于在HTTPS和HTTP间转换,这将防止安全 |
//警告弹出 |
//我们需要通过读取响应和解析URL手动取出URL |
BufferedReader br = new BufferedReader(new InputStreamReader(huc.getInputStream())); |
... |
//一旦我们有了主页的URL,我们就又使用HTTPRedirectHandler重定向并处理响应以校验正确的注 |
//册 |
url = new URL(homeUrl); |
huc = (HttpURLConnection) url.openConnection(); |
huc.setRequestProperty("User-Agent","Mozilla/4.7 [en] (Win98; I)"); |
hrh = new HTTPRedirectHandler(huc); |
hrh.setCookieJar(cj); |
hrh.connect(); |
... |
//保存Cookies用于以后的请求 |
cj.addAll(hrh.getCookieJar()); |
... |
return(bLoggedIn); |
} |
现在我们已经登录到Hotmail,我们请求收件箱页,在登录过程中已检索的Cookies中通过。一旦我们拥有了收件箱页,我们必须因为与新消息有关的信息而解析这个HTML。代替使用暴力的StringTokenizer检索这个信息,我们将用一个稍微文雅(既复杂的)方法调控XML。这种方法包括: |
· 将成形不好的HTML转换为well-formed HTML。 |
· 用DOM(文档对象模型)通过well-formed HTML去得到新消息的信息。 |
假如DOM、XML和well-formed 对你来说一窍不通,只要说我们把收件箱HTML转换成一个树状结构的对象并得到想要的信息就足够了。 |
要将成形不好的HTML转换成well-formed HTML,我们用一个可自由下载的组件JTidy工具和一个通用的处理器。ConvertBadHTMLToGood帮助类将成形不好的Hotmail HTML转换成well-formed HTML。相关代码显示如下: |
import java.io.*; |
import org.w3c.tidy.*; |
public class ConvertBadHTMLToGood |
{ |
... |
public ConvertBadHTMLToGood(Reader r) |
{ |
if(r == null) |
{ |
throw new IllegalArgumentException(); |
} |
inReader = r; |
} |
public Reader doConvert() throws IOException |
{ |
//初始化JTidy对象 |
Tidy tidy = new Tidy(); |
tidy.setXmlOut(true); |
tidy.setErrout(new PrintWriter(new StringWriter())); |
//JTidy解析器要求一个InputStream,对于我的知识来说这里没有直接的办法将一个Reader转换 |
//成一个InputStream。这个工作区代码没有字符编码安全,但还可以混过。 |
BufferedReader br = new BufferedReader(inReader); |
StringBuffer sb = new StringBuffer(); |
String line; |
while((line = br.readLine()) != null) |
{ |
sb.append(line); |
sb.append("\n"); |
} |
ByteArrayInputStream bais = new ByteArrayInputStream(sb.toString().getBytes("US-ASCII")); |
ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
//作一个将HTML转换well-formed HTML 的预备。 |
tidy.parse(bais, baos); |
//整理一些遗漏的JTidy得到能被“true-blue”XML解析器解析的输出。 |
FixEntities fe = new FixEntities(baos.toString()); |
return(fe.getFixedReader()); |
} |
一旦我们拥有了well-formed HTML,我们就用XML解析的Java API(JAXP)去转换well-formed HTML 成一个DOM树并通过树得到新消息的表单、主题及日期字段。我将忽略一些代码而向你展示如何使用HotmailChecker: |
import com.sonalb.net.http.cookie.*; |
... |
public class HotmailChecker |
{ |
public static void main(String args[]) throws Exception |
{ |
if(args.length != 2) |
{ |
usage(); |
System.exit(0); |
} |
String uname = args[0]; |
String pass = args[1]; |
HotmailChecker hmc = new HotmailChecker(uname,pass); |
if(!hmc.doLogin()) |
{ |
System.out.println("Could not login to Hotmail."); |
System.exit(0); |
} |
Vector newMessages = hmc.getNewMessages(); |
if(newMessages == null) |
{ |
System.out.println("No NEW Messages."); |
return; |
} |
System.out.println("You have " + newMessages.size() + " NEW Messages"); |
System.out.println("---------------------------------------------"); |
Iterator iter = newMessages.iterator(); |
//HMMessage封装了一个Hotmail消息 |
HMMessage hm; |
while(iter.hasNext()) |
{ |
hm = (HMMessage) iter.next(); |
System.out.println(" From: " + hm.getFrom()); |
System.out.println(" Subject: " + hm.getSubject()); |
System.out.println("Sent Date: " + hm.getSentDate()); |
System.out.println("---------------------------------------------"); |
} |
} |
static void usage() |
{ |
System.out.println("\nUsage: java HotmailChecker <Hotmail Username> <Password>"); |
} |
//实例变量和方法从这里开始 |
... |
public HotmailChecker(String username, String password) |
{ |
... |
} |
public boolean doLogin() throws Exception |
{ |
... |
} |
public Vector getNewMessages() throws Exception |
{ |
... |
} |
... |
} |
你可以从Resources下载完全功能的HotmailChecker及相关类。 |
jCookieMicro与J2ME结合 |
注意: 这部分假设已经对J2ME至少有一点熟悉。 |
前面,我曾提起用jCookieMicro库在J2ME平台的移动设备上建立与网络应用程序交互的客户系统的可能性。我仍然在开发jCookieMicro库。它的结构及用法将与jCookie库类似,除了URLConnection,jCookieMicro库将用MIDP HttpConnection对象。这部分描述了在移动应用程序(在我们的案例MIDlets中)使用成熟的cookie操作的好处。 |
让我们先练习在J2ME应用程序中状态管理常用的方法。诺基亚论坛一篇命名为“A Brief Introduction to Networked MIDlets”(2002年三月)的论文描述了一个方法。论文提出作为一个URL重写机制的变异工作的机制:一个在网络服务器上的servlet站点操作所有的商务逻辑和使用通常的HTTP报头传送状态信息要胜于cookies。MIDlet简单地作为一个用户界面,传递用户输入到servlet并显示结果。(更多的关于在J2ME应用程序上的状态管理的URL重写及其他方法,请读“Track Wireless Sessions with J2ME/MIDP”,Michael Juntao Yuan和Ju Long著(JavaWorld,2002年四月).) |
使用上述方法,这有与此讨论相关的应用程序的解决方法,一是象一个小型网络浏览器一样与网络服务器或应用程序交互: |
· MIDlet从用户那里收集相关输入(比如,一个Hotmail用户名和密码) |
· MIDlet传送输入到servlet |
· Servlet用输入与网络服务器或应用程序交互(比如,Hotmail网站) |
· Servlet传送结果到 MIDlet(比如,一个新消息列表) |
· MIDlet向用户显示结果 |
在上述解决方法中,通用HTTP报头维护一个MIDlet和servlet间的会话。因此,servlet和MIDlet 都包含执行会话管理的逻辑。这证明前面讲过的不受欢迎的同一原因:通用代码很容易被破坏,甚至成为常规管理变化所带来的必然结果,比如服务器升级。这种方法的另一个缺点:它要求有一个在目标网络应用程序(如Hotmail)和移动应用程序之间的中间件。 |
对于上述方法你可以用两种办法替代jCookieMicro: |
1. 将商务逻辑转移到移动应用程序上并完全消除中间servlet。在移动客户系统上用jCookieMicro进行会话管理。 |
2. 将商务逻辑保持在中间servlet上,但除去通用报头,并用jCookieMicro进行透明坚固的会话管理。 |
修改已存在的应用程序第二种方法证明更适合。第一种方法导致成本的降低和移动应用程序开发更轻松,因为它除去了服务器端资源的开销。下面的应用程序使用了第一种方法: |
· MIDlet从用户处收集相关输入 |
· MIDlet直接连接到目标网络服务器并与之交互 |
· MIDlet向用户显示结果 |
第一种方法同时也消除了将商务逻辑保持在servlet的如下一些缺点: |
· 一个servlet容器故障会导致整个应用程序离线,即使目标网络服务仍在运行。 |
· 应用程序被限制仅作为servlets展开。 |
在移动应用程序中进行状态管理的另一个方法在Sun无线Java开发者的一篇不依赖风俗权威的文章“Session Handling in MIDP”(2002年一月)中有描述,但包括在移动应用程序中写操作cookies的通用代码。前面关于写通用代码的缺点的讨论及使用jCookie(Micro)的优势请看上述命为“在Java中的状态管理”部分)。 |
自从PJAE提供连同一些Java2类的完整JDK 1.1.8平台,即使在工作中的jCookieMicro,今天你也能在PersonalJava Application Environment (PJAE)下的应用程序中使用jCookie库。。 |
jCookie局限性 |
jCookie库还有一些局限性: |
· 当cookie解析逻辑及安全标准插入到已存在的API(用CookieParser)时,没有一个对于核心数据结构和Cookie类有用的机制。 |
· 没有作性能测试。 |
你可以从http://jcookie.sourceforge.net/得到jCookie最近的版本以及项目源代码。有一些项目是为未来版本计划的包括: |
· 雅加达项目log4J logging API的使用提供了用户可配置的记录和调试。 |
· 用一个用户定义的控制器可简单修改jCookie行为,这可以决定解析一单个cookie是否失败将导致致命错误。 |
这些及其他部分的执行大量依赖于你的反馈。请在SourceForge.net的jCookie项目站点上使用邮件列表、bug追踪、特征请求等等。 |
取得cooking |
这里提到的jCookie库能帮助减轻客户端应用程序状态开发的难度。作为前面曾提到的,其他库的执行类似于函数,但这些结构已和存在的本地java.net API远无关系。另外,没有API/库单独从事cookie操作。其他的库将cookie操作合并作为一个完整的Java 网络客户结构的一部份,结果,使用这些库涉及了整个新的学习体系。 |
jCookie库接近于存在的java.net对象。在普遍的URLConnection或HttpConnection两个方法调用中状态管理十分简单。你可以通过一个有用的HTTPRedirectHandler类使得状态管理更简单。在处理客户端应用程序开发者时jCookie努力把浏览器作为cookie管理器。这能成功走多远只能由你的反响决定。我将感激任何你所分享的提示或建议。 |