OkHttp3實現Cookies管理及持久化
okHttp3正式版剛發布了沒幾天,正好重構之前的代碼,于是第一時間入坑了。對okHttp3的一些改變,會陸續寫下來,這是第一篇Cookies管理及持久化。
Cookies管理
OkHttp的源碼過于復雜,感興趣的同學可以自行閱讀,這里只針對 HttpEngineer 類進行分析,從字面意思即可看出這個類負責http請求的request、response等等操作的處理,而cookies管理也是隨著http請求的request、response來處理。
3.0之前
先看networkRequest方法,在里面通過client.getCookieHandler()函數獲得了CookieHandler對象,通過該對象拿到cookie并設置到請求頭里,請求結束后取得響應后通過networkResponse.headers()函數將請求頭獲得傳入receiveHeaders函數,并將取得的cookie存入getCookieHandler得到的一個CookieHandler對象中去
private Request networkRequest(Request request) throws IOException { Request.Builder result = request.newBuilder();//例行省略....
CookieHandler cookieHandler = client.getCookieHandler(); if (cookieHandler != null) { // Capture the request headers added so far so that they can be offered to the CookieHandler. // This is mostly to stay close to the RI; it is unlikely any of the headers above would // affect cookie choice besides "Host". Map<String, List<String>> headers = OkHeaders.toMultimap(result.build().headers(), null);
Map<String, List<String>> cookies = cookieHandler.get(request.uri(), headers); // Add any new cookies to the request. OkHeaders.addCookies(result, cookies);
}
//例行省略....
return result.build(); }</pre>
public void readResponse() throws IOException { //例行省略....receiveHeaders(networkResponse.headers());
//例行省略.... }</pre>
public void receiveHeaders(Headers headers) throws IOException { CookieHandler cookieHandler = client.getCookieHandler(); if (cookieHandler != null) { cookieHandler.put(userRequest.uri(), OkHeaders.toMultimap(headers, null)); } }CookieHandler對象是OkHttpClient類中的一個屬性,傳入了這個對象,那么OkHttp就會對cookie進行自動管理
private CookieHandler cookieHandler; public OkHttpClient setCookieHandler(CookieHandler cookieHandler) { this.cookieHandler = cookieHandler; return this; }public CookieHandler getCookieHandler() { return cookieHandler; }</pre>
OkHttpClient client = new OkHttpClient(); client.setCookieHandler(CookieHandler cookieHanlder);3.0之后
而在OkHttp3中,對cookie而言,新增了兩個類 Cookiejar 、 Cookie 兩個類,在了解這兩個類之前,先去看一下 HttpEngine 關于cookie管理的變化
private Request networkRequest(Request request) throws IOException { Request.Builder result = request.newBuilder();//例行省略.... List<Cookie> cookies = client.cookieJar().loadForRequest(request.url()); if (!cookies.isEmpty()) { result.header("Cookie", cookieHeader(cookies)); } //例行省略.... return result.build();
}</pre>
private String cookieHeader(List<Cookie> cookies) { StringBuilder cookieHeader = new StringBuilder(); for (int i = 0, size = cookies.size(); i < size; i++) { if (i > 0) { cookieHeader.append("; "); } Cookie cookie = cookies.get(i); cookieHeader.append(cookie.name()).append('=').append(cookie.value()); } return cookieHeader.toString(); }public void receiveHeaders(Headers headers) throws IOException { if (client.cookieJar() == CookieJar.NO_COOKIES) return;List<Cookie> cookies = Cookie.parseAll(userRequest.url(), headers); if (cookies.isEmpty()) return; client.cookieJar().saveFromResponse(userRequest.url(), cookies);
}</pre>
通過以上幾個關鍵方法,可以很明顯的感覺到作者的意圖了,為了更加自由定制化的cookie管理。其中 loadForRequest() 、 saveFromResponse() 這兩個方法最為關鍵,分別是在發送時向request header中加入cookie,在接收時,讀取response header中的cookie。現在再去看 Cookiejar 這個類,就很好理解了
public interface CookieJar { /* A cookie jar that never accepts any cookies. / CookieJar NO_COOKIES = new CookieJar() { @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) { }@Override public List<Cookie> loadForRequest(HttpUrl url) { return Collections.emptyList(); }
};
/**
- Saves {@code cookies} from an HTTP response to this store according to this jar's policy. *
- <p>Note that this method may be called a second time for a single HTTP response if the response
- includes a trailer. For this obscure HTTP feature, {@code cookies} contains only the trailer's
cookies. */ void saveFromResponse(HttpUrl url, List<Cookie> cookies);
/**
- Load cookies from the jar for an HTTP request to {@code url}. This method returns a possibly
- empty list of cookies for the network request. *
- <p>Simple implementations will return the accepted cookies that have not yet expired and that
{@linkplain Cookie#matches match} {@code url}. */ List<Cookie> loadForRequest(HttpUrl url); }</pre>
so!在OkHttpClient創建時,傳入這個CookieJar的實現,就能完成對Cookie的自動管理了
OkHttpClient client = new OkHttpClient.Builder() .cookieJar(new CookieJar() { private final HashMap<HttpUrl, List<Cookie>> cookieStore = new HashMap<>();
@Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
cookieStore.put(url, cookies);
}
@Override public List<Cookie> loadForRequest(HttpUrl url) {
List<Cookie> cookies = cookieStore.get(url); return cookies != null ? cookies : new ArrayList<Cookie>();
} }) .build();</pre>
Cookies持久化
對Cookies持久化的方案,與之前版本并無很大區別,還是參考 android-async-http 這個庫,主要參考其中兩個類:
-
PersistentCookieStore
-
SerializableHttpCookie
與之前版本的區別是要將對 java.net.HttpCookie 這個類的緩存處理換成對 okhttp3.Cookie 的處理,其他方面幾乎一樣。
廢話不多說了,直接上代碼
SerializableOkHttpCookies
主要做兩件事:
-
將Cookie對象輸出為ObjectStream
-
將ObjectStream序列化成Cookie對象
public class SerializableOkHttpCookies implements Serializable {
private transient final Cookie cookies; private transient Cookie clientCookies;
public SerializableOkHttpCookies(Cookie cookies) { this.cookies = cookies; }
public Cookie getCookies() { Cookie bestCookies = cookies; if (clientCookies != null) {
bestCookies = clientCookies;
} return bestCookies; }
private void writeObject(ObjectOutputStream out) throws IOException { out.writeObject(cookies.name()); out.writeObject(cookies.value()); out.writeLong(cookies.expiresAt()); out.writeObject(cookies.domain()); out.writeObject(cookies.path()); out.writeBoolean(cookies.secure()); out.writeBoolean(cookies.httpOnly()); out.writeBoolean(cookies.hostOnly()); out.writeBoolean(cookies.persistent()); }
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { String name = (String) in.readObject(); String value = (String) in.readObject(); long expiresAt = in.readLong(); String domain = (String) in.readObject(); String path = (String) in.readObject(); boolean secure = in.readBoolean(); boolean httpOnly = in.readBoolean(); boolean hostOnly = in.readBoolean(); boolean persistent = in.readBoolean(); Cookie.Builder builder = new Cookie.Builder(); builder = builder.name(name); builder = builder.value(value); builder = builder.expiresAt(expiresAt); builder = hostOnly ? builder.hostOnlyDomain(domain) : builder.domain(domain); builder = builder.path(path); builder = secure ? builder.secure() : builder; builder = httpOnly ? builder.httpOnly() : builder; clientCookies =builder.build(); } }</pre>
PersistentCookieStore
根據一定的規則去緩存或者獲取Cookie:
public class PersistentCookieStore { private static final String LOG_TAG = "PersistentCookieStore"; private static final String COOKIE_PREFS = "Cookies_Prefs";
private final Map<String, ConcurrentHashMap<String, Cookie>> cookies; private final SharedPreferences cookiePrefs;
-
public PersistentCookieStore(Context context) {
cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, 0);
cookies = new HashMap<>();
//將持久化的cookies緩存到內存中 即map cookies
Map<String, ?> prefsMap = cookiePrefs.getAll();
for (Map.Entry<String, ?> entry : prefsMap.entrySet()) {
String[] cookieNames = TextUtils.split((String) entry.getValue(), ",");
for (String name : cookieNames) {
String encodedCookie = cookiePrefs.getString(name, null);
if (encodedCookie != null) {
Cookie decodedCookie = decodeCookie(encodedCookie);
if (decodedCookie != null) {
if (!cookies.containsKey(entry.getKey())) {
cookies.put(entry.getKey(), new ConcurrentHashMap<String, Cookie>());
}
cookies.get(entry.getKey()).put(name, decodedCookie);
}
}
}
}
}
protected String getCookieToken(Cookie cookie) {
return cookie.name() + "@" + cookie.domain();
}
public void add(HttpUrl url, Cookie cookie) {
String name = getCookieToken(cookie);
//將cookies緩存到內存中 如果緩存過期 就重置此cookie
if (!cookie.persistent()) {
if (!cookies.containsKey(url.host())) {
cookies.put(url.host(), new ConcurrentHashMap<String, Cookie>());
}
cookies.get(url.host()).put(name, cookie);
} else {
if (cookies.containsKey(url.host())) {
cookies.get(url.host()).remove(name);
}
}
//講cookies持久化到本地
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
prefsWriter.putString(url.host(), TextUtils.join(",", cookies.get(url.host()).keySet()));
prefsWriter.putString(name, encodeCookie(new SerializableOkHttpCookies(cookie)));
prefsWriter.apply();
}
public List<Cookie> get(HttpUrl url) {
ArrayList<Cookie> ret = new ArrayList<>();
if (cookies.containsKey(url.host()))
ret.addAll(cookies.get(url.host()).values());
return ret;
}
public boolean removeAll() {
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
prefsWriter.clear();
prefsWriter.apply();
cookies.clear();
return true;
}
public boolean remove(HttpUrl url, Cookie cookie) {
String name = getCookieToken(cookie);
if (cookies.containsKey(url.host()) && cookies.get(url.host()).containsKey(name)) {
cookies.get(url.host()).remove(name);
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
if (cookiePrefs.contains(name)) {
prefsWriter.remove(name);
}
prefsWriter.putString(url.host(), TextUtils.join(",", cookies.get(url.host()).keySet()));
prefsWriter.apply();
return true;
} else {
return false;
}
}
public List<Cookie> getCookies() {
ArrayList<Cookie> ret = new ArrayList<>();
for (String key : cookies.keySet())
ret.addAll(cookies.get(key).values());
return ret;
}
/**
* cookies 序列化成 string
*
* @param cookie 要序列化的cookie
* @return 序列化之后的string
*/
protected String encodeCookie(SerializableOkHttpCookies 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());
}
/**
* 將字符串反序列化成cookies
*
* @param cookieString cookies string
* @return cookie object
*/
protected Cookie decodeCookie(String cookieString) {
byte[] bytes = hexStringToByteArray(cookieString);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Cookie cookie = null;
try {
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
cookie = ((SerializableOkHttpCookies) objectInputStream.readObject()).getCookies();
} catch (IOException e) {
Log.d(LOG_TAG, "IOException in decodeCookie", e);
} catch (ClassNotFoundException e) {
Log.d(LOG_TAG, "ClassNotFoundException in decodeCookie", e);
}
return cookie;
}
/**
* 二進制數組轉十六進制字符串
*
* @param bytes byte array to be converted
* @return string containing hex values
*/
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);
}
/**
* 十六進制字符串轉二進制數組
*
* @param hexString string of hex-encoded values
* @return decoded byte array
*/
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;
}
}</pre>
最終效果
完成對Cookie持久化之后,就可以對Cookiejar進行進一步修改了,最終效果:
/*** 自動管理Cookies */ private class CookiesManager implements CookieJar { private final PersistentCookieStore cookieStore = new PersistentCookieStore(getApplicationContext()); @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) { if (cookies != null && cookies.size() > 0) { for (Cookie item : cookies) { cookieStore.add(url, item); } } } @Override public List<Cookie> loadForRequest(HttpUrl url) { List<Cookie> cookies = cookieStore.get(url); return cookies; } }</pre>
Tips
在這樣做之前,嘗試了使用 Interceptor 和 NetWorkInterceptor 在Http請求request和response時,攔截響應鏈,加入對Cookie的管理。so!接下來可能會詳細介紹下 Interceptor 這個非常酷的實現。