標準 Java SE java.net.CookieManager 在 J2ME 完全缺失。若需維持會話狀態(購物車、登錄態),只能採用 URL 重寫——繁瑣且不安全。在 MIDP 上手寫 Cookie 引擎,可復用既有 Web 後端邏輯,大幅降低協議適配成本。
Set-Cookie: name=value; Path=/; Domain=.example.com; Expires=...Cookie: name=value(符合 Domain/Path/未過期)
+----------------+ +----------------+ +----------------+
| 1. 獲取 Cookie | --> | 2. 解析/存儲 | --> | 3. 發送 Cookie |
| HttpConnection | | Cookie Bean | | HttpConnection |
| getHeaderField | | + RMS 持久化 | | setRequestProp |
+----------------+ +----------------+ +----------------+
private static final String SET_COOKIE = "Set-Cookie";
private static final String SESSION_ID = "SessionID"; // 部分伺服器用此頭
public void extractCookies(HttpConnection conn, String url) {
for (int i = 0; ; i++) {
String key = conn.getHeaderFieldKey(i);
if (key == null) break;
if (SET_COOKIE.equalsIgnoreCase(key) || SESSION_ID.equalsIgnoreCase(key)) {
String cookieStr = conn.getHeaderField(i);
saveCookie(cookieStr, url);
}
}
}
public class Cookie implements Serializable {
private String name, value, path, domain;
private long expire; // 0 = session cookie
// 序列化至 RMS
public void serialize(DataOutputStream dos) throws IOException {
dos.writeUTF(name);
dos.writeUTF(value);
dos.writeUTF(path);
dos.writeUTF(domain);
dos.writeLong(expire);
}
// 從 RMS 反序列化
public static Cookie deserialize(DataInputStream dis) throws IOException {
Cookie c = new Cookie();
c.name = dis.readUTF();
c.value = dis.readUTF();
c.path = dis.readUTF();
c.domain = dis.readUTF();
c.expire = dis.readLong();
return c;
}
// 解析 Set-Cookie 字串
public static Cookie parse(String header, String requestUri) {
Cookie c = new Cookie();
StringTokenizer st = new StringTokenizer(header, ";");
while (st.hasMoreTokens()) {
String token = st.nextToken().trim();
int eq = token.indexOf('=');
if (eq == -1) continue; // Secure, HttpOnly 等無值屬性
String n = token.substring(0, eq).toLowerCase();
String v = token.substring(eq + 1);
if ("path".equals(n)) c.path = v;
else if ("expires".equals(n)) c.expire = parseDate(v); // 需實現 RFC 1123 日期解析
else if ("domain".equals(n)) c.domain = v;
else { c.name = n; c.value = v; } // 第一組 name=value 為實際數據
}
if (c.path == null || c.path.isEmpty()) c.path = extractPath(requestUri);
return c;
}
public boolean isExpired() { return expire > 0 && System.currentTimeMillis() > expire; }
public boolean matches(String reqDomain, String reqPath) { /* 域尾匹配 + 路徑前綴匹配 */ }
}
RecordStore,Key = domain#path#name,Value = 序列化字節陣列Hashtable,MIDlet 銷毀自動消失
public void attachCookies(HttpConnection conn, String url) {
String domain = extractDomain(url);
String path = extractPath(url);
StringBuilder cookieHeader = new StringBuilder();
// 1. 記憶體會話 Cookie
for (Cookie c : sessionCookies.values()) {
if (c.matches(domain, path)) appendCookie(cookieHeader, c);
}
// 2. RMS 持久 Cookie
RecordStore rs = RecordStore.openRecordStore("cookies", true);
for (int id : rs.getRecordIds()) {
Cookie c = Cookie.deserialize(new DataInputStream(
new ByteArrayInputStream(rs.getRecord(id))));
if (!c.isExpired() && c.matches(domain, path)) {
appendCookie(cookieHeader, c);
}
}
rs.closeRecordStore();
if (cookieHeader.length() > 0) {
conn.setRequestProperty("Cookie", cookieHeader.toString());
}
}
private void appendCookie(StringBuilder sb, Cookie c) {
if (sb.length() > 0) sb.append("; ");
sb.append(c.name).append('=').append(c.value);
}
在《Java ME 核心技術與最佳實踐》隨書 httpme 框架中,上述邏輯被封裝為 CookieManager 单例,攔截器模式自動處理:
// 使用範例
HttpRequest req = new HttpRequest("POST", "https://api.example.com/order");
req.addParam("itemId", "123");
HttpResponse rsp = HttpClient.execute(req); // 內部自動 extract/attach Cookie
| 場景 | 預期行為 |
|---|---|
| 登錄介面回應 Set-Cookie: JSESSIONID=abc; Path=/ | 後續請求自動帶上 Cookie: JSESSIONID=abc |
| 跨子域請求 (api.example.com → img.example.com) | Domain=.example.com 時自動共享 |
| Cookie 過期 | 自動不發送,RMS 記錄下次啟動清理 |
| Secure 標記 + HTTPS | 僅 HTTPS 請求附加 |
| HttpOnly 標記 | 不影響網絡層發送(僅限制腳本訪問,J2ME 無腳本引擎) |
在 J2ME 上實現完整 Cookie 引擎約需 300~500 行代碼,卻能無縫對接現有 Web 後端會話體系。關鍵在於:嚴格遵循 RFC 2965 域/路徑匹配規則、分離會話與持久存儲、RMS 空間定期回收。將其封裝為 HTTP 客戶端攔截器,業務代碼零感知,專注核心邏輯開發。