2021-10-16

《Thinking in Java》 的時代眼淚

最近接了一個 Java 家教,目前在上 OO 概念的部份。因為本來就不是靠唸書學會 OO 的(而且也沒學得多好),加上閒來無事,所以拿起塵封已久的《Thinking in Java》中文版(2002 初版)來看一下有沒有啥遺漏的。

等等… 「(p.252) 利用 imports 來改變行為」?這是什麼鬼?書上是這樣寫的:

Java 並不具備 C 的「條件編譯(conditional compilation)」功能。此功能讓你得以切換開關,令程式產生不同行為,無須更動程式碼。Java 拿掉這個功能的原因,可能是因為此功能在 C 語言中多半被用來解決跨平台問題,也就是根據不同的平台編譯程式碼中的不同部份。由於 Java 本身的設計可自動跨越不同平台,所以應該不需要此功能。

不過條件編譯仍具有其他實用價值。除錯便是常用的用途之一:在開發過程開啟除錯功能,在出貨產品中關閉除錯功能。Allen Holub(www.holub.com)提出了以 packages 來模擬條件編譯的想法。根據這一想法,他在 Java 裡頭產生原本在 C 語言中極為有用的 assertion 機制。

基本上就是寫兩個叫做 Assert 的 class、擺在不同 package 下,要出貨的時候把 Assert 的 import 改成空殼 Assert 的路徑。

真的是時代的眼淚阿… [遠目]

其實中文版出版的同一年,Java 1.4 也出了,其中一個重大的改進就是導入 assert

這裡還有個不可靠的印象,就是當年有些人認為:「 assert 導入的太晚,現在用(誕生於 1997 年,正逐漸統治世界 XD) JUnit 就好了」

時至今日,JUnit 勉強算是有用一點,assert 則是從來沒用過,只有在追 GXT 的 source code 有看到過。至於「利用 imports 來改變行為」這事… 連我這種不碰 Spring 的咖小都知道應該要套個 DI 什麼的,而不是去動 import 這種鬼想法…

當然,現在回頭用吐槽的姿態看二十年前的概念實在沒什麼太大的意義,那時本來就還在盤古開天,連 design pattern 都還是浪頭上的顯學,殺個豬公也不是什麼太奇怪的事情,就只能說是時代的眼淚。

BTW… Allen Holub 的網站還在… 不知道當事人還記不記得這段黑歷史… wwwww

2021-08-20

Google Sheet API v3 升 v4

之前用這個網址格式取得 JSON 格式 Google Sheet 資料,一直相安無事:

https://spreadsheets.google.com/feeds/list/SHEET_ID/TAB_INDEX/public/values?alt=json

然後大概從 2021/8/15 開始,就有機率讀取不成功(對,有時候會成功 🙈),有的時候是炸 CORS 問題、有的時候是炸 404。

狗了一下的結果,應該就是 Google Sheet API v3 在 2021/8/2 停用所導致的。至於 v3 版跟應該還會繼續活著的 GData 有什麼關係、為什麼是「有機率掛掉」就… 不研究… [蓋牌][翻白眼]

經過幾天的摸索,如果只是單純要取得 JSON,那麼會遇到下面這幾件事情。

首先要去 Google Cloud Platform 搞一個 API 金鑰(以下簡稱 API_KEY)。如果之前沒有用過可能還要先開一個(看起來毫無意義的)專案然後才能產生… 😱

有了 API 金鑰之後,接下來是把網址改成這個格式:

https://sheets.googleapis.com/v4/spreadsheets/SHEET_ID/values/RANGE&key=API_KEY

這邊不再是用「第幾個工作表」(或是那個根本不知道怎麼來的 id 值)的方式,而是指定 RANGE。完整版的 RANGE 大概是「FOO!A1:D100」的長相,這表示要求「FOO」這個工作表的 A1~D100 這個 cell 範圍的資料。

實測之後(找不到文件… 🙈)發現允許下列幾種變形:

  • FOO:可以省略 cell 範圍。如此會回傳整個 FOO 的資料
  • A1:D100:可以省略工作表名稱。如此會回傳第一個工作表的指定 cell 範圍的資料

回傳的 JSON 也跟以前不一樣,v4 版變得很純:

{
  "range": "'FOO'!A1:D100",
  "majorDimension": "ROWS",
  "values": [
    ["ColumnA", "欄位B", "ColumnC", "欄位D"],
    ["第一筆A", "第一筆B", "第一筆C", "第一筆D"],
    [],
    ["", "第三筆B"]
}
  • v3 會把 sheet 的第一個 row 的值當成 JSON 的 key 值;v4 純粹就是給你一個二維字串陣列。(反正都能指定範圍了)
  • v3 遇到第一個空白 row 就會視為後面沒有資料停止輸出;v4 會在指定範圍內確定是否以下空白才停止輸出,其中夾雜的空白行會給一個空陣列。
  • 如果該 row 第 n 個 column 之後都沒有值,則該 row 對應的陣列長度只到 n - 1。1~n - 1 當中如果有 column 沒有值,會給空字串。

一樣是實測結果(還是找不到文件… 🙈),下列狀況會導致 server 給不是 200 的 HTTP status code:

  • 400:range 無法正常解析
  • 403:API_KEY 值不正確
  • 404:SHEET_ID 值不正確

最後,用 v4 版,sheet 可以不用做「發布到網路」這一個動作。

2021-05-15

Spring Data REST 初步心得

測試環境:

  • OpenJDK 14
  • Spring Boot 2.2.4

注意

這是在「REST 只有基本概念、Spring 碰不到一個月而且沒認真讀過文件」的情況下,在混亂的實做中得到的混亂廢話

因為 Spring 幾乎沒碰過,所以從頭 (明明就是鞋子) 開始講起。

Spring Boot 真的讚,pom.xml 加幾個 dependency,弄個 @SpringBootApplication 的 main class,就連 Tomcat(包含自動 reload)都幫你啟動完畢。瞬間把 Eclipse 那個越來越難用的 Servers view 給關了省的佔畫面(不過萬一要同時多個 webapp 運作可能還是… 算了到時候再說 XD)

@RestController 大概也是簡化的極限了,預設情況下連 return 值轉 JSON 變成 response 內容都自動幫你處理好,找不到可以抱怨的點(因為也沒寫過幾個… [逃])

Spring Data JPA 稍微棘手一點,自動掃 @Entity class 一直沒成功,然後又想搞可以自訂檔案位置(而不是設定檔寫死)的 H2,所以稍微花了一點時間。不過已經比想像中的還要簡單很多了。

撇開一些個人龜毛的部份,從 Spring 程度幾乎等於 0 的初學者(但是會基本 Maven、有用過 Hibernate)、到弄出一個 REST controller 來對 DB 做 CRUD,大概就是 10hr 以內的光景。當然,這裡說的是 POC 等級的程度。 是說我寫出來的東西好像也都只有 POC 等級

至於 Spring Data REST 嘛… 一句話就可以結案:「@Repository class 再加一個 @RepositoryRestResource(path = "foo")

立馬 /foo 就可以處理 GET / PUT… 了,就是這麼簡單~ 就是這麼神奇~

好,server side 收工!

才沒有這麼沒好… [眼神死]

第一個會炸到的問題是:「GET 的 response 要怎麼 unmarshal?」

不是直接把 List<Foo> 轉成 JSON 嗎?我也這麼希望,但就不是,而是長這樣(HATEOAS / HAL):

{
  "_embedded" : {
    "foos" : [ {
      "name" : "FooName",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/foo/2"
        },
        "location" : {
          "href" : "http://localhost:8080/foo/2"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/foo"
    },
    "profile" : {
      "href" : "http://localhost:8080/profile/foo"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 1,
    "totalPages" : 1,
    "number" : 0
  }
}

真正的資料是在 ._embedded.foos,這還好,不算啥大事,而且有分頁功能好棒棒。問題是裡頭的 Foo 結構沒有掛 @Id 的 field。雖然這個去狗一下可以找到解決辦法,但終究得花上一些功夫才有可能轉回 Foo instance… (沒有實際測試,好像還得設法讓 gwt-jackson 忽略 "_links" 才能正常運作… )

這也許是用 GWT 的人才會有的困擾,畢竟 GWT 的賣點之一就是 server / client 可以使用同一份 code。如果 client 用純 JS 或許就沒問題,反正本來 就是假的 OOP 就沒有 code 可以用,要抓 JSON 中的值也可以用字串隨便指到想要的位置…

目前的作法是直接跳過 @RepositoryRestResource 提供的 URI,自己另外寫一個吐單純 JSON 的 REST controller… 💃

第二個炸點就更麻煩了:「不會處理 @ManyToOne(其他沒測)的 field」。

假設前頭的 Foo 是這樣:

@Entity
public class Foo {
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;
	private String name;
	
	@ManyToOne
	private Wtf wtf;
}

GET 得到的 JSON 不會出現 wtf、POST 也會跳過這個 field 不處理,直接塞 null。

這樣設計有沒有道理?應該有,至少 GET 的傳輸量就可以少很多(都說 many to one 了嘛)。但是完全連轉都沒轉是不是合理?為什麼不像 ORM 時 wtf 會轉換成 WTF_ID column 那樣做 JSON marshal?(還是說有神秘的設定檔可以改這行為?)(沒有試著找過,因為連關鍵字都不知道怎麼下 XD)

總之,沒用到就沒事,只要有用到 ManyToOne 的話,Spring Data REST 大概就是爛給你看。如果是剛出生沒多久的 project 或許還改得動,不然自己手寫 REST controller 可能還比較實在… 😱

沒有意義的結論

  • Spring 真的強,強到「完全不敢想像背後程式是怎麼寫」的那種強
  • 為什麼 HATEOAS / HAL 會設計成這樣子 (之前接觸 FHIR 的時候好像也是這付德性,原本還以為是 FHIR 亂搞…)
  • 目前看起來 GWT 這邊沒有 library 可以對付 HATEOAS / HAL,不知道 JS 那邊… 算了,我不想知道
  • Spring Data REST (至少)還有 validate 等功能要 try… 😭

最後就是… 嗯… 我真的很懷念 GWT RPC。要是有時程壓力,我一定馬上回頭用 GWT RPC… [遠目]

2021-02-19

升版後 GetValueProvider 導致 GXT Grid 炸 NPE

經過緩慢且冗長的演進之後,開發環境終於逐步變成 Java 8 + GWT 2.8 + GXT 4.0…

然後昨天手賤去升級 Eclipse,結果重啟不能、抱怨 Java 8 不給用、得換到 Java 11(印象中)以後。把 JAVA_HOME 切到 OpenJDK 14.0.1 之後,換 GWT 罷工,理由是 GWT 2.8 只能用 Java 8。為了都使用同一個 JDK 的 無聊 理由,所以 GWT 就乾脆也升到 2.8.2,也就是 Java 14 + GWT 2.8.2 + GXT 4.0。

前面是前情提要,接下來終於進入到主角了:RQC

RQC (14e5fb7) 還停留在 Java 7 + GWT 2.7 + GXT 3.1 的狀態,而且那時 GoogleSheetToolkit 也還沒抽出去。所以得先處理開發環境升級…

然後就爆炸了… <囧>

目前確認的一個詭異炸點(也許還有其他的 😱 )是 SheetIdGrid.javagenColumnModel(),只要用到 GetValueProvider 就會在 attach 的時候炸 NPE(實際上是炸 AttachDetachException)。關鍵點是 GetValueProvider.getPath() 預設回傳 null,只要不用 GetValueProvider 或是 override getPath() 隨便回傳個空字串都行。

帳面上看起來似乎對症下藥了,但其實整件事很謎:

  • GetValueProvider 已經用 N 年、有 N 個 Grid 在用這玩意
  • 在另外一個 project(LATE)完全無法重現這個問題。
    • 這兩個 project 的差別就只有 RQC 原本是 Java 7 + GWT 2.7 + GXT 3.1,而 LATE 原本是 Java 8 + GWT 2.8 + GXT 4.0,其餘 pom.xml / gwt.xml 都(幾乎)一樣。

目前實在沒什麼想法 (X你X的,都無法重現了還能怎樣) ,只能就先留個紀錄備考。當然改變 GetValueProvider.getPath() 的回傳值是一個萬用解,不過那等到其他 project 也炸了再來考慮吧… 😭