Retrofit實現持久化Cookie的三種方案
在上一文中,我們提到retrofit 2.0中如何實現非持久化cookie的兩種方案,但並未做過深的解釋。
現在我們重點關注JavaNetCookieJar實現非持久化cookie背後的原理。
話不多說,步入正題。
非持久化Cookie實現分析
首先來看上文中提到的非持久化cookie的實現:
public void setCookies(OkHttpClient.Builder builder) {
CookieManager cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
builder.cookieJar(new JavaNetCookieJar(cookieManager));
}
現在我們就以這段程式碼為起點來研究為什麼這幾行程式碼就實現了非持久化cookie呢?不難先發現此處實現cookie的關鍵就是cookieJar()
,我們就以該方法切入。
首先來看該方法的原始碼:
//設定cookie處理器,如果沒設定,則使用CookieJar.NO_COOKIES作為預設的處理器
public Builder cookieJar(CookieJar cookieJar) {
if (cookieJar == null) throw new NullPointerException("cookieJar == null" );
this.cookieJar = cookieJar;
return this;
}
通過該方法我們知道這裡的關鍵就在於CookieJar介面,來看看這個介面的定義:
public interface CookieJar {
//預設的cookie處理器,不接受任何cookie
CookieJar NO_COOKIES = new CookieJar() {
@Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
}
@Override public List<Cookie> loadForRequest(HttpUrl url) {
return Collections.emptyList();
}
};
//根絕cookie處理策略,儲存響應中的cookie
void saveFromResponse(HttpUrl url, List<Cookie> cookies);
//為請求新增cookie
List<Cookie> loadForRequest(HttpUrl url);
}
該介面非常簡單,提供了儲存和載入cookie的兩個方法loadForRequest(HttpUrl url)
及saveFromResponse(HttpUrl url,List<Cookie> cookies)
,這就意味這我們可以自行實現介面來實現對cookie管理。到這裡想必各位可能已經在想“我們自行實現該介面來將cookie儲存到本地,使用的時候再從本地讀取,這樣不久實現cookie持久化了麼?”,這當然沒問題,但我們先繼續往下看。
除此之外,該介面中存在一個預設的實現NO_COOKIES。
接下來,我們來看看該介面的另外一個實現類JavaNetCookieJar,它本質上只是java.net.CookieHandler的代理類。同樣,來看一下JavaNetCookieJar中的原始碼:
public final class JavaNetCookieJar implements CookieJar {
private final CookieHandler cookieHandler;
public JavaNetCookieJar(CookieHandler cookieHandler) {
this.cookieHandler = cookieHandler;
}
@Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
if (cookieHandler != null) {
List<String> cookieStrings = new ArrayList<>();
for (Cookie cookie : cookies) {//遍歷cookie集合
cookieStrings.add(cookie.toString());
}
Map<String, List<String>> multimap = Collections.singletonMap("Set-Cookie", cookieStrings);
try {
//具體的儲存工作交給cookieHandler去處理
cookieHandler.put(url.uri(), multimap);
} catch (IOException e) {
Internal.logger.log(WARNING, "Saving cookies failed for " + url.resolve("/..."), e);
}
}
}
@Override public List<Cookie> loadForRequest(HttpUrl url) {
// The RI passes all headers. We don't have 'em, so we don't pass 'em!
Map<String, List<String>> headers = Collections.emptyMap();
Map<String, List<String>> cookieHeaders;
try {
//從cookieHandler中取出cookie集合。
cookieHeaders = cookieHandler.get(url.uri(), headers);
} catch (IOException e) {
Internal.logger.log(WARNING, "Loading cookies failed for " + url.resolve("/..."), e);
return Collections.emptyList();
}
List<Cookie> cookies = null;
for (Map.Entry<String, List<String>> entry : cookieHeaders.entrySet()) {
String key = entry.getKey();
if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key))
&& !entry.getValue().isEmpty()) {
for (String header : entry.getValue()) {
if (cookies == null) cookies = new ArrayList<>();
cookies.addAll(decodeHeaderAsJavaNetCookies(url, header));
}
}
}
return cookies != null
? Collections.unmodifiableList(cookies)
: Collections.<Cookie>emptyList();
}
//將請求Header轉為OkHttp中HttpCookie
private List<Cookie> decodeHeaderAsJavaNetCookies(HttpUrl url, String header) {
List<Cookie> result = new ArrayList<>();
for (int pos = 0, limit = header.length(), pairEnd; pos < limit; pos = pairEnd + 1) {
//具體轉換過程在此不做展示
}
return result;
}
}
上面的程式碼非常簡單,其核心無非在於通過CookieHandler實現對cookie的儲存和取值。既然,真正的工作類是CookieHandler,那我們就重點關注CookieHandler.
public abstract class CookieHandler {
private static CookieHandler cookieHandler;
public synchronized static CookieHandler getDefault() {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(SecurityConstants.GET_COOKIEHANDLER_PERMISSION);
}
return cookieHandler;
}
public synchronized static void setDefault(CookieHandler cHandler) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(SecurityConstants.SET_COOKIEHANDLER_PERMISSION);
}
cookieHandler = cHandler;
}
public abstract Map<String, List<String>>
get(URI uri, Map<String, List<String>> requestHeaders)
throws IOException;
public abstract void
put(URI uri, Map<String, List<String>> responseHeaders)
throws IOException;
}
我們發現CookieHandler是個抽象類,其實現類就是我們上面在非持久化Cookie中用到的CookieManager,沒辦法,我們在此轉移焦點至CookieManager:
public class CookieManager extends CookieHandler
{
private CookiePolicy policyCallback;//cookie處理策略
private CookieStore cookieJar = null;//cookie儲存
public CookieManager() {
this(null, null);
}
//在這裡我們可以指定cookie儲存方式及cookie處理策略
public CookieManager(CookieStore store,
CookiePolicy cookiePolicy)
{
// use default cookie policy if not specify one
policyCallback = (cookiePolicy == null) ? CookiePolicy.ACCEPT_ORIGINAL_SERVER
: cookiePolicy;
// 如果沒有指定持久化方式,則預設使用記憶體持久化。
if (store == null) {
cookieJar = new InMemoryCookieStore();
} else {
cookieJar = store;
}
}
//指定cookie處理策略
public void setCookiePolicy(CookiePolicy cookiePolicy) {
if (cookiePolicy != null) policyCallback = cookiePolicy;
}
public CookieStore getCookieStore() {
return cookieJar;
}
//獲取cookie
public Map<String, List<String>>
get(URI uri, Map<String, List<String>> requestHeaders)
throws IOException
{
//... 省略多行程式碼
Map<String, List<String>> cookieMap =
new java.util.HashMap<String, List<String>>();
//... 省略多行程式碼
for (HttpCookie cookie : cookieJar.get(uri)) {
//... 省略多行程式碼
}
// apply sort rule (RFC 2965 sec. 3.3.4)
List<String> cookieHeader = sortByPath(cookies);
cookieMap.put("Cookie", cookieHeader);
return Collections.unmodifiableMap(cookieMap);
}
//儲存cookie
public void
put(URI uri, Map<String, List<String>> responseHeaders)
throws IOException
{
//... 省略多行程式碼
PlatformLogger logger = PlatformLogger.getLogger("java.net.CookieManager");
for (String headerKey : responseHeaders.keySet()) {
// RFC 2965 3.2.2, key must be 'Set-Cookie2'
// we also accept 'Set-Cookie' here for backward compatibility
if (headerKey == null
|| !(headerKey.equalsIgnoreCase("Set-Cookie2")
|| headerKey.equalsIgnoreCase("Set-Cookie")
)
)
{
continue;
}
for (String headerValue : responseHeaders.get(headerKey)) {
try {
List<HttpCookie> cookies;
try {
cookies = HttpCookie.parse(headerValue);
} catch (IllegalArgumentException e) {
// Bogus header, make an empty list and log the error
cookies = java.util.Collections.emptyList();
if (logger.isLoggable(PlatformLogger.Level.SEVERE)) {
logger.severe("Invalid cookie for " + uri + ": " + headerValue);
}
}
for (HttpCookie cookie : cookies) {
if (cookie.getPath() == null) {
//... 省略多行程式碼
cookie.setPath(path);
}
if (cookie.getDomain() == null) {
//... 省略多行程式碼
cookie.setDomain(host);
}
//... 省略多行程式碼
if (shouldAcceptInternal(uri, cookie)) {
cookieJar.add(uri, cookie);
}
}
} catch (IllegalArgumentException e) {
// invalid set-cookie header string
// no-op
}
}
}
}
}
在CacheManager中,需要重點關注的便是policyCallback和cookieJar兩個成員變數。分別來看看這二者做了什麼?
cookie處理策略
cookie的處理策略由CookiePolicy介面確定,該介面中預置了三種處理策略:ACCEPT_ALL,ACCEPT_NONE及ACCEPT_SERVER,分別用於接受所有cookie,不接受cookie,只接受初始伺服器的cookie。
cookie儲存
cookie的儲存則由CookieStore介面負責,該介面目前存在唯一的實現類InMemoryCookieStore,通過該實現類的名字也大概猜的出它將cookie存在記憶體中了,同樣我們簡單的瀏覽一下其原始碼:
class InMemoryCookieStore implements CookieStore {
private List<HttpCookie> cookieJar = null;
private Map<String, List<HttpCookie>> domainIndex = null;
private Map<URI, List<HttpCookie>> uriIndex = null;
// use ReentrantLock instead of syncronized for scalability
private ReentrantLock lock = null;
/**
* The default ctor
*/
public InMemoryCookieStore() {
cookieJar = new ArrayList<HttpCookie>();
domainIndex = new HashMap<String, List<HttpCookie>>();
uriIndex = new HashMap<URI, List<HttpCookie>>();
lock = new ReentrantLock(false);
}
//儲存cookie
public void add(URI uri, HttpCookie cookie) {
// pre-condition : argument can't be null
if (cookie == null) {
throw new NullPointerException("cookie is null");
}
lock.lock();
try {
// 首先移除已經存在的cookie
cookieJar.remove(cookie);
// 快取時間不為0
if (cookie.getMaxAge() != 0) {
cookieJar.add(cookie);
// 儲存到域索引中
if (cookie.getDomain() != null) {
addIndex(domainIndex, cookie.getDomain(), cookie);
}
//儲存到url索引中
if (uri != null) {
// add it to uri index, too
addIndex(uriIndex, getEffectiveURI(uri), cookie);
}
}
} finally {
lock.unlock();
}
}
}
到現在我們已經弄明白非持久化Cookie的原理,這裡用一張類圖來概括一下:
從設計的來說,這裡也充分體現了面向抽象程式設計,單一職責理念等。對這些設計原則不明白的可以看我以前寫的文章。
持久化Cookie實現
自定義InDiskCookieStore實現持久化Cookie
通過上面的分析,我們最終知道非持久化Cookie的儲存靠InMemoryCookieStore實現,除此之外,我們發現CookieManager中也可以指定另外的CookieStore的實現。這樣看來,我們只需要自定義InDiskCookieStore實現CookieStore,並將其設定給CookieManager就可以實現Cookie的持久化了。接下來,我們仿照inMemoryCookieStore來實現自己的InDiskCookieStore的即可。
不難發現這裡的重點在於如何持久化Cookie到本地。我們可以從響應頭中讀取set-cookie(可能有多個),然後拼接儲存在相關的配置檔案中,也可以直接序列化HttpCookie物件到本地。這裡我們採用序列化的方式實現,對Java序列化和反序列化不熟的同學可以自行補充。
檢視HttpCookie發現,該類並不支援序列化,哪該怎麼辦呢?最簡單的方式是為該類建立輔助類DiskHttpCookie,用來實現序列化和反序列化的過程:
public class DiskHttpCookie implements Serializable {
private static final long serialVersionUID = -6370968478600008500L;
private transient final HttpCookie mCookie;
private transient HttpCookie mClientCookie;
public DiskHttpCookie(HttpCookie cookie) {
mCookie = cookie;
}
public HttpCookie getCookie() {
HttpCookie cookie = mCookie;
if (mClientCookie != null) {
cookie = mClientCookie;
}
return cookie;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(mCookie.getName());
out.writeObject(mCookie.getValue());
out.writeObject(mCookie.getComment());
out.writeObject(mCookie.getCommentURL());
out.writeObject(mCookie.getDomain());
out.writeLong(mCookie.getMaxAge());
out.writeObject(mCookie.getPath());
out.writeObject(mCookie.getPortlist());
out.writeBoolean(mCookie.getSecure());
out.writeBoolean(mCookie.getDiscard());
out.writeInt(mCookie.getVersion());
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
String name = (String) in.readObject();
String value = (String) in.readObject();
mClientCookie = new HttpCookie(name, value);
mClientCookie.setComment((String) in.readObject());
mClientCookie.setCommentURL((String) in.readObject());
mClientCookie.setDomain((String) in.readObject());
mClientCookie.setMaxAge(in.readLong());
mClientCookie.setPath((String) in.readObject());
mClientCookie.setPortlist((String) in.readObject());
mClientCookie.setSecure(in.readBoolean());
mClientCookie.setDiscard(in.readBoolean());
mClientCookie.setVersion(in.readInt());
}
}
在DiskHttpCookie中,通過writeObject()和readObject()方法實現自定義序列化的過程。下面我們正式來看InDiskCookieStore的實現,此次我們參考了async-http-client的相關原始碼:
public class InDiskCookieStore implements CookieStore {
private static final String LOG_TAG = "InDiskCookieStore";
private static final String COOKIE_PREFS = "CookiePrefsFile";
private static final String COOKIE_NAME_PREFIX = "cookie_";
//在記憶體中,快取cookie,加快訪問速度
private final HashMap<String, ConcurrentHashMap<String, HttpCookie>> cookies;
//儲存cookie到本地
private final SharedPreferences cookiePrefs;
private ReentrantLock mLock = null;
public InDiskCookieStore(Context context) {
cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, 0);
mLock = new ReentrantLock(false);
cookies = new HashMap<>();
initMemoryCookie();
}
private void initMemoryCookie() {
// 載入本地cookie到記憶體中
Map<String, ?> prefsMap = cookiePrefs.getAll();
for (Map.Entry<String, ?> entry : prefsMap.entrySet()) {
if (((String) entry.getValue()) != null && !((String) entry.getValue()).startsWith(COOKIE_NAME_PREFIX)) {
String[] cookieNames = TextUtils.split((String) entry.getValue(), ",");
for (String name : cookieNames) {
String encodedCookie = cookiePrefs.getString(COOKIE_NAME_PREFIX + name, null);
if (encodedCookie != null) {
HttpCookie decodedCookie = decodeCookie(encodedCookie);
if (decodedCookie != null) {
if (!cookies.containsKey(entry.getKey()))
cookies.put(entry.getKey(), new ConcurrentHashMap<String, HttpCookie>());
cookies.get(entry.getKey()).put(name, decodedCookie);
}
}
}
}
}
}
@Override
public void add(URI uri, HttpCookie cookie) {
mLock.lock();
try {
String name = getCookieToken(uri, cookie);
if (!cookie.hasExpired()) {
if (!cookies.containsKey(uri.getHost()))
cookies.put(uri.getHost(), new ConcurrentHashMap<String, HttpCookie>());
cookies.get(uri.getHost()).put(name, cookie);
} else {//清除過期cookie
if (cookies.containsKey(uri.toString()))
cookies.get(uri.getHost()).remove(name);
}
// Save cookie into persistent store
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));
prefsWriter.putString(COOKIE_NAME_PREFIX + name, encodeCookie(new DiskHttpCookie(cookie)));
prefsWriter.commit();
} finally {
mLock.unlock();
}
}
protected String getCookieToken(URI uri, HttpCookie cookie) {
return cookie.getName() + cookie.getDomain();
}
@Override
public List<HttpCookie> get(URI uri) {
mLock.lock();
ArrayList<HttpCookie> ret = new ArrayList<HttpCookie>();
if (cookies.containsKey(uri.getHost()))
ret.addAll(cookies.get(uri.getHost()).values());
mLock.unlock();
return ret;
}
@Override
public boolean removeAll() {
mLock.lock();
try {
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
prefsWriter.clear();
prefsWriter.commit();
cookies.clear();
} finally {
mLock.unlock();
}
return true;
}
@Override
public boolean remove(URI uri, HttpCookie cookie) {
mLock.lock();
try {
String name = getCookieToken(uri, cookie);
if (cookies.containsKey(uri.getHost()) && cookies.get(uri.getHost()).containsKey(name)) {
cookies.get(uri.getHost()).remove(name);
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
if (cookiePrefs.contains(COOKIE_NAME_PREFIX + name)) {
prefsWriter.remove(COOKIE_NAME_PREFIX + name);
}
prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));
prefsWriter.commit();
return true;
} else {
return false;
}
} finally {
mLock.unlock();
}
}
@Override
public List<HttpCookie> getCookies() {
mLock.lock();
ArrayList<HttpCookie> ret = new ArrayList<HttpCookie>();
for (String key : cookies.keySet())
ret.addAll(cookies.get(key).values());
mLock.unlock();
return ret;
}
@Override
public List<URI> getURIs() {
mLock.lock();
ArrayList<URI> ret = new ArrayList<URI>();
for (String key : cookies.keySet())
try {
ret.add(new URI(key));
} catch (URISyntaxException e) {
e.printStackTrace();
}
mLock.unlock();
return ret;
}
//序列化DiskHttpCookie,並將其轉為字串
protected String encodeCookie(DiskHttpCookie cookie) {
if (cookie == null)
return null;
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
ObjectOutputStream outputStream = new ObjectOutputStream(os);
outputStream.writeObject(cookie);
} catch (IOException e) {
Log.d(LOG_TAG, "IOException in encodeCookie", e);
return null;
}
return byteArrayToHexString(os.toByteArray());
}
//通過cookie字串反序列化為HttpCookie物件
protected HttpCookie decodeCookie(String cookieString) {
byte[] bytes = hexStringToByteArray(cookieString);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
HttpCookie cookie = null;
try {
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
cookie = ((DiskHttpCookie) objectInputStream.readObject()).getCookie();
} catch (IOException e) {
Log.d(LOG_TAG, "IOException in decodeCookie", e);
} catch (ClassNotFoundException e) {
Log.d(LOG_TAG, "ClassNotFoundException in decodeCookie", e);
}
return cookie;
}
//位元組轉16進位制
protected String byteArrayToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte element : bytes) {
int v = element & 0xff;
if (v < 16) {
sb.append('0');
}
sb.append(Integer.toHexString(v));
}
return sb.toString().toUpperCase(Locale.US);
}
//16進位制轉位元組
protected byte[] hexStringToByteArray(String hexString) {
int len = hexString.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i + 1), 16));
}
return data;
}
}
此類一方面將cookie暫存在記憶體中,以便實現快速的訪問,另一方面會將cookie序列化在本地。在每次建立InDiskCookieStore時,會用本地的cookie初始化到快取中。
那麼如何使用呢?其實用和我們使用JavaNetCookieJar非常相似:
public void setCookies(OkHttpClient.Builder builder) {
CookieManager cookieManager = new java.net.CookieManager(new InDiskCookieStore(this),CookiePolicy.ACCEPT_ORIGINAL_SERVER);
builder.cookieJar(new JavaNetCookieJar(cookieManager));
}
通過這種方式,無需做其他的變化就實現了持久化Cookie。
自定義攔截器實現持久化Cookie
通過自定義攔截器實現Cookie持久化的思路我們已經在上一篇文章說過,就不多做解釋了,直接來看程式碼即可:
首先定義響應攔截器,該攔截器實現從response獲取set-cookie欄位的值,並將其儲存在本地。
public class SaveCookiesInterceptor implements Interceptor {
private static final String COOKIE_PREF = "cookies_prefs";
private Context mContext;
public SaveCookiesInterceptor(Context context) {
mContext = context;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
//set-cookie可能為多個
if (!response.headers("set-cookie").isEmpty()) {
List<String> cookies = response.headers("set-cookie");
String cookie = encodeCookie(cookies);
saveCookie(request.url().toString(),request.url().host(),cookie);
}
return response;
}
//整合cookie為唯一字串
private String encodeCookie(List<String> cookies) {
StringBuilder sb = new StringBuilder();
Set<String> set=new HashSet<>();
for (String cookie : cookies) {
String[] arr = cookie.split(";");
for (String s : arr) {
if(set.contains(s))continue;
set.add(s);
}
}
Iterator<String> ite = set.iterator();
while (ite.hasNext()) {
String cookie = ite.next();
sb.append(cookie).append(";");
}
int last = sb.lastIndexOf(";");
if (sb.length() - 1 == last) {
sb.deleteCharAt(last);
}
return sb.toString();
}
//儲存cookie到本地,這裡我們分別為該url和host設定相同的cookie,其中host可選
//這樣能使得該cookie的應用範圍更廣
private void saveCookie(String url,String domain,String cookies) {
SharedPreferences sp = mContext.getSharedPreferences(COOKIE_PREF, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
if (TextUtils.isEmpty(url)) {
throw new NullPointerException("url is null.");
}else{
editor.putString(url, cookies);
}
if (!TextUtils.isEmpty(domain)) {
editor.putString(domain, cookies);
}
editor.apply();
}
}
其次定義請求攔截器,如果該請求存在cookie,則為其新增到Header的Cookie中,程式碼如下:
public class AddCookiesInterceptor implements Interceptor {
private static final String COOKIE_PREF = "cookies_prefs";
private Context mContext;
public AddCookiesInterceptor(Context context) {
mContext = context;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request.Builder builder = request.newBuilder();
String cookie = getCookie(request.url().toString(), request.url().host());
if (!TextUtils.isEmpty(cookie)) {
builder.addHeader("Cookie", cookie);
}
return chain.proceed(builder.build());
}
private String getCookie(String url, String domain) {
SharedPreferences sp = mContext.getSharedPreferences(COOKIE_PREF, Context.MODE_PRIVATE);
if (!TextUtils.isEmpty(url)&&sp.contains(url)&&!TextUtils.isEmpty(sp.getString(url,""))) {
return sp.getString(url, "");
}
if (!TextUtils.isEmpty(domain)&&sp.contains(domain) && !TextUtils.isEmpty(sp.getString(domain, ""))) {
return sp.getString(domain, "");
}
return null;
}
}
最後我們將這兩個攔截器設定到OkHttpClient即可:
public void setCookies(OkHttpClient.Builder builder) {
builder.addInterceptor(new AddCookiesInterceptor(this));
builder.addInterceptor(new SaveCookiesInterceptor(this));
}
這樣,我們也實現了Cookie的持久化,再次證明了OkHttp的攔截器機制是如此方便。
和上面的方案相比,該方案需要額外新增兩個攔截器。對某些有程式碼潔癖的人來說,可能並不喜歡這種方案。
自定義CookieJar實現持久化Cookie
到現在我們已經實現了持久化Cookie的兩種方案。自定義CookieJar來實現Cookie持久化和以上兩種方案並無太大的區別,結合上一篇文章中通過CookieJar實現非持久化Cookie,你也可以很容易的搞定持久化Cookie,故不做詳細的說明了。
總結
今天我們從頭到位瞭解了Cookie持久化技術,並對不同的方案做了簡單的說明,希望對各位有所幫助。