➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
網頁安全React JS 網頁開發筆記(一)

Retrofit HTTP Client

Table of contents

1 關於 Retrofit

Retrofit 係一個 HTTP client 既 Java library,可以幫我地做到 HTTP call,有啲似 Spring Cloud OpenFeign,都係行 declarative approach,透過 interface methods 既 annotations 以及 parameters 黎定義 HTTP 請求。

2 動手使用 Retrofit 下載資料

2.1 需求情境

試想像我地而家想寫一個程式,可以幫我地喺 Jumbo Computer 網站獲取電腦零件報價資訊。
Jumbo Computer 既網址係:https://shoponjc.com/quote.php
透過 Chrome developer tools 既 Network 分頁,我地可以見到,當我地喺頁面選擇左「產品類型」之後,Jumbo Computer 會喺 XHR 分類下有個 getprod.php 既 HTTP POST request,而 response body 係 JSON 格式。

2.2 分析

2.2.1 必填的 session form data

我地可以見到 Jumbo Computer 拎資料用既 HTTP POST request 係需要一個 session 既 form data entry。
經 Postman 一試,我地會發現如果個 HTTP request 缺少左 session,又或者個 session 值係我地自己改出黎既話,都會收到 error response,咁係因為 Jumbo Computer 既 server 會驗證呢個 session 值。
至於呢個 session 值要點樣先可以拎到?實驗話畀我地知,呢個 session 值好似唔會 expire,咁即係我地可以直接用已知既 session 值。不過,穩陣起見,我地都係睇下佢係點樣攞到或者計到呢個 session 值比較好。

2.2.2 如何取得新的 session

喺 Chrome developer tools 既 Application 分頁下面,我地可以見到 Local Storage 分類下,shoponjc.com 有一個 KeySession ID 既 entry。
另外,喺 Chrome developer tools 既 Sources 分頁下面,我地可以見到 shoponjc.com 有一個 JavaScript 檔叫 quo.js?d83scwsoejkx6,裡面有以下一段:
if (window.localStorage) { storage = window.localStorage; storage.setItem("Version", "1.0"); storage.setItem("Session ID", document.getElementById("sid").value); }
咁即係我地可以用 sid 作為 DOM element ID 黎喺個網頁上搵 DOM element,而結果如下:
<input type="hidden" id="sid" value="wpjfi557nqf75" />
解讀: 每當我地瀏覽呢個網頁既時候,佢 server 會返回一個帶有隨機 session ID 既 DOM element 既 HTML 檔,然後 JavaScript 會令 Local Storage 裡面多左一個名為 Session ID 既 entry。
另外,我地都可以從個 JavaScript 檔見到個 session 值係黎自於 this.$refs.sid。因為個網頁係用 Vue.js library 黎寫,而 this.$refs.sid 就即係指緊 DOM element ID 為 sid 既 DOM element。
1var vm = new Vue({ 2 // ... 省略 500 幾行 3 mounted: function () { 4 this.session = this.$refs.sid.value; 5 // ... 省略幾行 6 }, 7});
解讀: 喺個 sid DOM element mount 左之後,Vue.js 既 sessionthis.session)就會變成 sid DOM element 既 value
再睇下佢係點樣組裝 POST request 同 call 出去:
1actualSearch: function (pnum) { 2 /* 3 if (this.ptype == '') { 4 alert("Please select 產品類型 Product Type first."); 5 return; 6 } 7 if (this.ptypeval.length > 40) { 8 alert("關鍵字 Key words are too long, please re-enter first."); 9 return; 10 } 11 */ 12 if (this.searchFired == false) { 13 this.searchFired = true; 14 var form_data = new FormData(); 15 form_data.append('session', this.session); 16 form_data.append('sort', this.nsort); 17 form_data.append('link', this.linkpicks); 18 form_data.append('ptype', this.ptype); 19 form_data.append('pmin', this.range[0]); 20 form_data.append('pmax', this.range[1]); 21 form_data.append('pwords', this.ptypeval); 22 form_data.append('prow', this.pagerow); 23 form_data.append('pnum', pnum); 24 ajax({ 25 method: 'post', 26 url: 'getprod.php', 27 data: form_data 28 }).then(response => { 29 this.searchFired = false; 30 this.$nextTick(function () { 31 let json = response.data; 32 this.havedata = false; 33 if (json.status == 'Error') { 34 alert("No product found!!"); 35 } else { 36 this.plist = json.plist; 37 if (json.plist.length >= 10) 38 this.havedata = true; 39 } 40 }); 41 }); 42 } 43},
解讀: 可以見到拎資料既 POST request 既 session 係黎自於 Vue.js 既 sessionthis.session)。
成件事就係咁簡單!我地每次用 jsoup 下載個新既 HTML 落黎就可以拎到新既 session ID 值。

2.3 添加 Maven dependencies

我地會用到以下 Maven dependencies,當中包括 jsoup:
1<!-- ##### jsoup ##### --> 2<dependency> 3 <groupId>org.jsoup</groupId> 4 <artifactId>jsoup</artifactId> 5 <version>1.13.1</version> 6</dependency> 7 8<!-- ##### retrofit ##### --> 9<dependency> 10 <groupId>com.squareup.retrofit2</groupId> 11 <artifactId>retrofit</artifactId> 12 <version>2.9.0</version> 13</dependency> 14<dependency> 15 <groupId>com.squareup.retrofit2</groupId> 16 <artifactId>converter-scalars</artifactId> 17 <version>2.9.0</version> 18</dependency> 19<dependency> 20 <groupId>com.squareup.retrofit2</groupId> 21 <artifactId>converter-gson</artifactId> 22 <version>2.9.0</version> 23</dependency>

2.4 寫 Java code

ProductsResponse.java
1@Data 2@Accessors(chain = true) 3@FieldDefaults(level = PRIVATE) 4public class ProductsResponse { 5 String status; 6 String session; 7 8 @SerializedName("plist") 9 List<Product> products; 10}
JumboClient.java
1public interface JumboClient { 2 3 @FormUrlEncoded 4 @POST("getprod.php") 5 Call<ProductsResponse> getProductsByCategory( 6 @Field("session") String session, 7 @Field("sort") Integer sortType, 8 @Field("link") Boolean link, 9 @Field("ptype") Integer productType, 10 @Field("pmin") Long priceMin, 11 @Field("pmax") Long priceMax, 12 @Field("pwords") String productKeywords, 13 @Field("prow") Long pageRowCount, 14 @Field("pnum") Long pageNum 15 ); 16}
RetrofitHelper.java
1public final class RetrofitHelper { 2 3 private static RetrofitHelper retrofitHelper; 4 5 private RetrofitHelper() { 6 7 } 8 9 public static RetrofitHelper getInstance() { 10 return retrofitHelper==null ? (retrofitHelper = new RetrofitHelper()) 11 : retrofitHelper; 12 } 13 14 public JumboClient createJumboClient() { 15 return createRetrofit("https://shoponjc.com").create(JumboClient.class); 16 } 17 18 private Retrofit createRetrofit(String baseUrl) { 19 return new Retrofit.Builder() 20 .baseUrl(baseUrl) 21 .client(new OkHttpClient.Builder() 22 .readTimeout(10, SECONDS) 23 .connectTimeout(10, SECONDS) 24 .build()) 25 .addConverterFactory(ScalarsConverterFactory.create()) 26 .addConverterFactory(GsonConverterFactory.create(new GsonBuilder().setLenient().create())) 27 .build(); 28 } 29}
Main.java
1@Slf4j 2public class Main { 3 4 public static void main(String[] args) throws Exception { 5 6 final JumboClient jumboClient = RetrofitHelper.getInstance().createJumboClient(); 7 8 // 拎資料 9 jumboClient.getProductsByCategory( 10 getSessionIdWithJsoup(), // sessionId 11 3, // sortType 12 true, // link 13 5, // productType 14 1L, // priceMin 15 200000L, // priceMax 16 "", // productKeywords 17 10000L, // pageRowCount 18 0L // pageNum 19 ).execute().body(); 20 } 21 22 private static String getSessionIdWithJsoup() { 23 24 try { 25 final String html = Jsoup.connect("https://shoponjc.com/quote.php").get().html(); 26 27 final Matcher matcher = Pattern.compile("<input type=\"hidden\" id=\"sid\" ref=\"sid\" value=\"(?<sessionId>\\w+)\"").matcher(html); 28 final String sessionId = matcher.find() ? matcher.group("sessionId") : null; 29 30 return sessionId; 31 } catch (Exception e) { 32 log.error("Failed to download Jumbo webpage.", e); 33 } 34 35 return null; 36 } 37}