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(); }
public void readResponse() throws IOException { //例行省略.... receiveHeaders(networkResponse.headers()); //例行省略.... }
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; }
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(); }
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); }
通過以上幾個關鍵方法,可以很明顯的感覺到作者的意圖了,為了更加自由定製化的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); }
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();
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(); } }
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; } }
最終效果
完成對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; } }
Tips
在這樣做之前,嘗試了使用 Interceptor 和 NetWorkInterceptor 在Http請求request和response時,攔截響應鏈,加入對Cookie的管理。so!接下來可能會詳細介紹下 Interceptor 這個非常酷的實現。
相關文章
- Redis 設計與實現 (三)--持久化Redis持久化
- GO實現Redis:GO實現Redis的AOF持久化(4)GoRedis持久化
- Multipath實現LUN裝置名稱的持久化持久化
- 利用Kubernetes實現容器的持久化儲存持久化
- Redis資料持久化—RDB持久化與AOF持久化Redis持久化
- 使用Spring Data JPA實現持久化層的簡化開發Spring持久化
- Go 實現 Raft 第四篇:持久化和調優GoRaft持久化
- Sentinel 實戰-規則持久化持久化
- Kafka實戰-資料持久化Kafka持久化
- webpack 持久化快取實踐Web持久化快取
- jmeter學習指南之管理CookiesJMeterCookie
- BloomFilter 原理,實現及優化OOMFilter優化
- Redis的兩種持久化方式-快照持久化(RDB)和AOF持久化Redis持久化
- 搞懂Redis RDB和AOF持久化及工作原理Redis持久化
- k8s使用glusterfs實現動態持久化儲存K8S持久化
- k8s使用ceph實現動態持久化儲存K8S持久化
- EMQX+HStreamDB 實現物聯網流資料高效持久化MQ持久化
- redis系列:RDB持久化與AOF持久化Redis持久化
- J2EE持久層持久化上下文的傳播以及會話(Conversation)實現持久化會話
- synchronized實現原理及鎖優化synchronized優化
- 用非常硬核的JAVA序列化手段實現物件流的持久化儲存Java物件持久化
- LaravelZero 從零實現區塊鏈(三)資料持久化與 CLILaravel區塊鏈持久化
- Redis學習 RDB和AOF兩種持久化介紹以及實現Redis持久化
- 面試官:Redis如何實現持久化的、主從哨兵又是什麼?面試Redis持久化
- Redis 持久化Redis持久化
- Redis - 持久化Redis持久化
- redisaof持久化Redis持久化
- ehcache持久化持久化
- Docker 持久化Docker持久化
- redis持久化Redis持久化
- [Redis]持久化Redis持久化
- 城商行容器雲平臺應用場景及持久化儲存實踐持久化
- 使用容器化塊儲存OpenEBS在K3s中實現持久化儲存持久化
- Redis 資料持久化方案的介紹及應用Redis持久化
- linux iscsi multipath多路徑及名稱持久化配置Linux持久化
- Java Redis系列2 (redis的安裝與使用+redis持久化的實現))JavaRedis持久化
- springboot+quartz以持久化的方式實現定時任務Spring Bootquartz持久化
- 基於 RocksDB 實現高可靠、低時延的 MQTT 資料持久化MQQT持久化